转载---编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议47~51)

阅读目录

回到顶部

建议47:在equals中使用getClass进行类型判断

  本节我们继续讨论覆写equals的问题,这次我们编写一个员工Employee类继承Person类,这很正常,员工也是人嘛,而且在JavaBean中继承也很多见,代码如下:

 1 public class Employee extends Person {
 2     private int id;
 3
 4     public Employee(String _name, int _id) {
 5         super(_name);
 6         id = _id;
 7     }
 8
 9     public int getId() {
10         return id;
11     }
12
13     public void setId(int id) {
14         this.id = id;
15     }
16
17     @Override
18     public boolean equals(Object obj) {
19         if (obj instanceof Employee) {
20             Employee e = (Employee) obj;
21             return super.equals(obj) && e.getId() == id;
22         }
23         return false;
24     }
25
26 }
27
28 class Person {
29     private String name;
30
31     public Person(String _name) {
32         name = _name;
33     }
34
35     public String getName() {
36         return name;
37     }
38
39     public void setName(String name) {
40         this.name = name;
41     }
42
43     @Override
44     public boolean equals(Object obj) {
45         if (obj instanceof Person) {
46             Person p = (Person) obj;
47             if (null == p.getName() || null == name) {
48                 return false;
49             } else {
50                 return name.equalsIgnoreCase(p.getName());
51             }
52         }
53         return false;
54     }
55 }

  员工类增加了工号ID属性,同时也覆写了equals方法,只有在姓名和ID都相同的情况下才表示同一个员工,这是为了避免一个公司中出现同名同姓员工的情况。看看上面的代码,这里的条件已经相当完善了,应该不会出错了,那我们测试一下,代码如下:  

1 public static void main(String[] args) {
2         Employee e1 = new Employee("张三", 100);
3         Employee e2 = new Employee("张三", 1000);
4         Person p1 = new Person("张三");
5         System.out.println(p1.equals(e1));
6         System.out.println(p1.equals(e2));
7         System.out.println(e1.equals(e2));
8     }

  上面定义了两个员工和一个社会闲杂人员,虽然他们同名同姓,但肯定不是同一个,输出都应该是false,但运行之后结果为: true  true  false

  很不给力呀,p1竟然等于e1,也等于e2,为什么不是同一个类的两个实例竟然也会相等呢?这很简单,因为p1.equals(e1)是调用父类Person的equals方法进行判断的,它使用的是instanceof关键字检查e1是否是Person的实例,由于两者村子继承关系,那结果当然是true了,相等也就没有任何问题了,但是反过来就不成立了,e1和e2是不可能等于p1,这也是违反对称性原则的一个典型案例。

  更玄的是p1与e1、e2相等,但e1和e2却不相等,似乎一个简单的符号传递都不能实现,这才是我们分析的重点:e1.equals(e2)调用的是子类Employee的equals方法,不仅仅要判断姓名相同,还要判断Id相同,两者工号是不同的,不相等也是自然的了。等式不传递是因为违反了equals的传递性原则,传递性原则指的是对于实例对象x、y、z来说,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

  这种情况发生的关键是父类引用了instanceof关键字,它是用来判断一个类的实例对象的,这很容易让子类钻空子。想要解决也很简单,使用getClass来代替instanceof进行类型判断,Person的equals方法修改后如下所示: 

@Override
    public boolean equals(Object obj) {
        if (null != obj && obj.getClass() == this.getClass()) {
            Person p = (Person) obj;
            if (null == p.getName() || null == name) {
                return false;
            } else {
                return name.equalsIgnoreCase(p.getName());
            }
        }
        return false;
    }

  当然,考虑到Employee也有可能被继承,也需要把它的instanceof修改为getClass。总之,在覆写equals时建议使用getClass进行类型判断,而不要使用instanceof。

回到顶部

建议48:覆写equals方法必须覆写hashCode方法

覆写equals方法必须覆写hasCode方法,这条规则基本上每个Javaer都知道,这也是JDK的API上反复说明的,不过为什么要则这么做呢?这两个方法之间什么关系呢?本建议就来解释该问题,我们先看看代码:

public class Client48 {
    public static void main(String[] args) {
        // Person类的实例作为map的key
        Map<Person, Object> map = new HashMap<Person, Object>() {

            {
                put(new Person("张三"), new Object());
            }
        };
        // Person类的实例作为List的元素
        List<Person> list = new ArrayList<Person>() {
            {
                add(new Person("张三"));
            }
        };
        boolean b1 = list.contains(new Person("张三"));
        boolean b2 = map.containsKey(new Person("张三"));
        System.out.println(b1);
        System.out.println(b2);

    }
}

  代码中的Person类与上一建议的Person相同,equals方法完美无缺。在这段代码中,我们在声明时直接调用方法赋值,这其实也是一个内部匿名类,现在的问题是b1和b2值是否都为true?

  我们先来看b1,Person类的equals覆写了,不再判断两个地址相等,而是根据人员的姓名来判断两个对象是否相等,所以不管我们的new Person("张三")产生了多少个对象,它们都是相等的。把张三放入List中,再检查List中是否包含,那结果肯定是true了。

  接下来看b2,我们把张三这个对象作为了Map的键(Key),放进去的是张三,检查的对象还是张三,那应该和List的结果相同了,但是很遗憾,结果为false。原因何在呢?

  原因就是HashMap的底层处理机制是以数组的方式保存Map条目的(Map Entry)的,这其中的关键是这个数组的下标处理机制:依据传入元素hashCode方法的返回值决定其数组的下标,如果该数组位置上已经有Map条目,并且与传入的值相等则不处理,若不相等则覆盖;如果数组位置没有条目,则插入,并加入到Map条目的链表中。同理,检查键是否存在也是根据哈希码确定位置,然后遍历查找键值的。

  接着深入探讨,那对象元素的hashCode方法返回的是什么值呢?它是一个对象的哈希码,是由Object类的本地方法生成的,确保每个对象有一个哈希码(也是哈希算法的基本要求:任意输入k,通过一定算法f(k),将其转换为非可逆的输出,对于两个输入k1和k2,要求若k1=k2,则必须f(k1)=f(k2),但也允许k1  != k2 , f(k1)=f(k2)的情况存在)。

  那回到我们的例子上,由于我们没有覆写hashCode方法,两个张三对象的hashCode方法返回值(也就是哈希码)肯定是不相同的了,在HashMap的数组中也找不到对应的Map条目了,于是就返回了false。

  问题清楚了,修改也很简单,在Person类中重写一下hashCode方法即可,代码如下: 

class Person{

   @Override
    public int hashCode() {
        return new HashCodeBuilder().append(name).toHashCode();
    }   

}

  其中HashCodeBuilder是org.apache.commons.lang.builder包下的一个哈希码生成工具,使用起来非常方便,大家可以直接项目中集成(为何不直接写hashCode方法?因为哈希码的生成有很多种算法,自己写麻烦,事儿又多,所以必要的时候才取"拿来主义",不重复造轮子是最好的办法。)

回到顶部

建议49:推荐覆写toString方法

  为什么要覆写toString方法,这个问题很简单,因为Java提供的默认toString方法不友好,打印出来看不懂,不覆写不行,看这样一段代码: 

public class Client49 {
    public static void main(String[] args) {
        System.out.println(new Person("张三"));
    }
}

class Person {
    private String name;

    public Person(String _name) {
        name = _name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

  输出结果是:[email protected]如果机器不同,@后面的内容也会不同,但格式都是相同的:类名[email protected]+hashCode,这玩意是给机器看的,人哪能看懂呀!这就是因为我们没有覆写Object类的toString方法的缘故,修改一下,代码如下:

@Override
    public String toString() {
        return String.format("%s.name=%s", this.getClass(),name);
    }

  如此即就可以在需要的时候输出调试信息了,而且非常友好,特别是在bean流行的项目中(一般的Web项目就是这样),有了这样的输出才能更好地debug,否则查找错误就有点麻烦!当然,当bean的属性较多时,自己实现就不可取了,不过可以直接使用apache的commons工具包中的ToStringBuilder类,简洁,实用又方便。可能有人会说,为什么通过println方法打印一个对象会调用toString方法?那是源于println的打印机制:如果是一个原始类型就直接打印,如果是一个类类型,则打印出其toString方法的返回值,如此而已。同时现在IDE也很先进,大家debug时也可查看对象的变量,但还是建议大家覆写toString方法,这样调试会更方便哦。

回到顶部

建议50:使用package-info类为包服务

  Java中有一个特殊的类:package-info类,它是专门为本包服务的,为什么说它特殊,主要体现在三个方面:

  1. 它不能随便创建:在一般的IDE中,Eclipse、package-info等文件是不能随便被创建的,会报"Type name is notvalid"错误,类名无效。在Java中变量定义规范中规定如下字符是允许的:字母、数字、下划线,以及那个不怎么写的$符号,不过中划线可不在之列,那么怎么创建这个文件呢?很简单,用记事本创建一个,然后拷贝进去再改一下就成了,更直接的办法就是从别的项目中拷贝过来。
  2. 它服务的对象很特殊:一个类是一类或一组事物的描述,比如Dog这个类,就是描述"阿黄"的,那package-info这个类描述的是什么呢?它总是要有一个被描述或陈述的对象吧,它是描述和记录本包信息的。
  3. package-info类不能有实现代码:package-info类再怎么特殊也是 一个类,也会被编译成 package-info.class,但是在package-info.java文件不能声明package-info类。   

  package-info类还有几个特殊的地方,比如不可以继承,没有接口,没有类间关系(关联、组合、聚合等)等,Java中既然有这么特殊的一个类,那肯定有其特殊的作用了,我们来看看它的特殊作用,主要表现在以下三个方面:

  • 声明友好类和包内访问常量:这个比较简单,而且很实用,比如一个包中有很多内部访问的类或常量,就可以统一放到package-info类中,这样很方便,便于集中管理,可以减少友好类到处游走的情况,代码如下:

class PkgClazz {
        public void test() {
        }
    }

    class PkgConstant {
        static final String PACKAGE_CONST = "ABC";
    }

  注意以上代码是放在package-info.java中的,虽然它没有编写package-info的实现,但是package-info.class类文件还是会生成。通过这样的定义,我们把一个包需要的常量和类都放置在本包下,在语义上和习惯上都能让程序员更适应。

  • 为在包上提供注解提供便利:比如我们要写一个注解(Annotation),查看一下包下的对象,只要把注解标注到package-info文件中即可,而且在很多开源项目中也采用了此方法,比如struts2的@namespace、hibernate的@FilterDef等.
  • 提供包的整体注释说明:如果是分包开发,也就是说一个包实现了一个业务逻辑或功能点或模块或组件,则该包需要一个很好的说明文档,说明这个包是做什么用的,版本变迁历史,与其他包的逻辑关系等,package-info文件的作用在此就发挥出来了,这些都可以直接定义到此文件中,通过javadoc生成文档时,会吧这些说明作为包文档的首页,让读者更容易对该包有一个整体的认识。当然在这点上它与package.html的作用是相同的,不过package-info可以在代码中维护文档的完整性,并且可以实现代码与文档的同步更新。  

  创建package-info,也可以利用IDE工具如下图:

  

解释了这么多,总结成一句话:在需要用到包的地方,就可以考虑一下package-info这个特殊类,也许能起到事半功倍的作用。

回到顶部

建议51:不要主动进行垃圾回收

  很久很久以前,在java1.1的年代里,我们经常会看到System.gc这样的调用---主动对垃圾进行回收,不过,在Java知识深入人心后,这样的代码就逐渐销声匿迹了---这是好现象,因为主动进行垃圾回收是一个非常危险的动作。

  之所以危险,是因为System.gc要停止所有的响应,才能检查内存中是否存在可以回收的对象,这对一个应用系统来说风险极大,如果是一个Web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(heap)中的对象少的话还可以接受,一但对象较多(现在的web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了),这个过程非常耗时,可能是0.01秒,也可能是1秒,甚至20秒,这就严重影响到业务的运行了。

  例如:我们写这样一段代码:new String("abc"),该对象没有任何引用,对JVM来说就是个垃圾对象。JVM的垃圾回收器线程第一次扫描(扫描时间不确定,在系统不繁忙的时候执行)时给它贴上一个标签,说"你是可以回收的",第二次扫描时才真正的回收该对象,并释放内存空间,如果我们直接调用System.gc,则是说“嗨,你,那个垃圾回收器过来检查一下有没有垃圾对象,回收一下”。瞧瞧看,程序主动找来垃圾回收器,这意味着正在运行的系统要让出资源,以供垃圾回收器执行,想想看吧,它会把所有的对象都检查一遍,然后处理掉那些垃圾对象。注意哦,是检查每个对象。

  不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出是可分析的,是可以查找原因的,GC可不是一个好招数。

时间: 2024-12-24 10:26:54

转载---编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议47~51)的相关文章

转载-------编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议1~5)

阅读目录 建议1:不要在常量和变量中出现易混淆的字母 建议2:莫让常量蜕变成变量 建议3:三元操作符的类型务必一致 建议4:避免带有变长参数的方法重载 建议5:别让null值和空值威胁到变长方法              The reasonable man adapts himself to the world; The unreasonable one persists in trying to adapt the world himself. 明白事理的人使自己适应世界:不明事理的人想让世

转载--编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议16~20)

阅读目录 建议16:易变业务使用脚本语言编写 建议17:慎用动态编译 建议18:避免instanceof非预期结果 建议19:断言绝对不是鸡肋 建议20:不要只替换一个类 回到顶部 建议16:易变业务使用脚本语言编写 Java世界一直在遭受着异种语言的入侵,比如PHP,Ruby,Groovy.Javascript等,这些入侵者都有一个共同特征:全是同一类语言-----脚本语言,它们都是在运行期解释执行的.为什么Java这种强编译型语言会需要这些脚本语言呢?那是因为脚本语言的三大特征,如下所示:

转载--编写高质量代码:改善Java程序的151个建议(第4章:字符串___建议52~55)

阅读目录 建议52:推荐使用String直接量赋值 建议53:注意方法中传递的参数要求 建议54:正确使用String.StringBuffer.StringBuilder 建议55:注意字符串的位置 回到顶部 建议52:推荐使用String直接量赋值 一般对象都是通过new关键字生成的,但是String还有第二种生成方式,也就是我们经常使用的直接声明方式,这种方式是极力推荐的,但不建议使用new String("A")的方式赋值.为什么呢?我们看如下代码: public class

转载---编写高质量代码:改善Java程序的151个建议(第2章:基本类型___建议21~25)

阅读目录 建议21:用偶判断,不用奇判断 建议22:用整数类型处理货币 建议23:不要让类型默默转换 建议24:边界还是边界 建议25:不要让四舍五入亏了一方 不积跬步,无以至千里: 不积小流,无以成江海. ---荀子<劝学篇> 回到顶部 建议21:用偶判断,不用奇判断 判断一个数是奇数还是偶数是小学里的基本知识,能够被2整除的整数是偶数,不能被2整除的数是奇数,这规则简单明了,还有什么可考虑的?好,我们来看一个例子,代码如下: 1 import java.util.Scanner; 2 3

转载--编写高质量代码:改善Java程序的151个建议(第5章:数组和集合___建议65~69)

阅读目录 建议65:避开基本类型数组转换列表陷阱 建议66:asList方法产生的List的对象不可更改 建议67:不同的列表选择不同的遍历算法 建议68:频繁插入和删除时使用LinkList 建议69:列表相等只关心元素数据 回到顶部 建议65:避开基本类型数组转换列表陷阱 我们在开发中经常会使用Arrays和Collections这两个工具类和列表之间转换,非常方便,但也有时候会出现一些奇怪的问题,来看如下代码: 1 public class Client65 { 2 public stat

编写高质量代码改善java程序的151个建议——[1-3]基础?亦是基础

原创地址:   http://www.cnblogs.com/Alandre/  (泥沙砖瓦浆木匠),需要转载的,保留下! Thanks The reasonable man adapts himself to the world;the unreasonable one persists in trying to adapt the world to himself. -萧伯纳 相信自己看得懂就看得懂了,相信自己能写下去,我就开始写了.其实也简单-泥沙砖瓦浆木匠 Written In The

编写高质量代码改善java程序的151个建议——[110-117]异常及Web项目中异常处理

原创地址:http://www.cnblogs.com/Alandre/(泥沙砖瓦浆木匠),需要转载的,保留下! 文章宗旨:Talk is cheap show me the code. 大成若缺,其用不弊.大盈若冲,其用不穷.  <道德经-老子>最完满的东西,好似有残缺一样,但它的作用永远不会衰竭:最充盈的东西,好似是空虚一样,但是它的作用是不会穷尽的 Written In The Font 摘要: 异常处理概述 学习内容: 建议110: 提倡异常封装 建议111: 采用异常链传递异常 建议

编写高质量代码改善java程序的151个建议——导航开篇

2014-05-16 09:08 by Jeff Li 前言 系列文章:[传送门] 下个星期度过这几天的奋战,会抓紧java的进阶学习.听过一句话,大哥说过,你一个月前的代码去看下,惨不忍睹是吧.确实,人和代码一样都在成长,都在变好当中.有时候只是实现功能的编程,长进不了呀. 博客提供的好处就可以交流,讨论的学习方法你们应该知道. 在这里,我会陆陆续续的进行对<编写高质量代码改善java程序的151个建议>看法,希望大家点击交流. 正文 看这本书原因   1.项目做的只是实现功能,然而没有好好

编写高质量代码:改善Java程序的151个建议 --[52~64]

编写高质量代码:改善Java程序的151个建议 --[52~64] 推荐使用String直接量赋值 Java为了避免在一个系统中大量产生String对象(为什么会大量产生,因为String字符串是程序中最经常使用的类型),于是就设计了一个字符串池(也叫作字符串常量池,String pool或String Constant Pool或String Literal Pool),在字符串池中容纳的都是String字符串对象,它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,