Effective Java——对于所有对象都通用的方法

Android.jpg

本系列文章是总结Effective Java文章中我认为最重点的内容,给很多没时间看书的朋友以最短的时间看到这本书的精华。
第一篇《Effective Java——创建和销毁对象》

第三章对于所有对象都通用的方法

目录.png

第8条:覆盖equals时请遵守通用约定

符合以下条件则不需要覆盖equals情况

类的每个实例本质上都是唯一的

对于代表活动实体而不是值(value)的类。例如:Thread,他只关注该类能完成的任务或者功能,而不是像Integer关心他中存在的值(value),对于Thread这样的类Object提供的默认equals方法完全能够满足要求。

不关心类是否提供了“逻辑相等”的测试功能

例如:Random类,如果对他进行覆盖equals方法将毫无意义,使用者根本不关心多个Random实例产生的随机数是否相等。
逻辑相等:实例内存在的值是否相等,例如:Integer的两个实例用equals来判断是否逻辑相等。

超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的

集合类:Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

类是私有的或是包级私有的,可以确定他的equals方法永远不会被调用

私有的或是包级私有的类不可能对外部提供。

需要覆盖equals方法

对于“值类(value class)”来说,它们需要判断“逻辑相等”,且超类还没有覆盖equals实现期望的行为,这时我们需要覆盖equals方法。
值类:例如,IntegerDate,仅仅表示一个值,我们在调用equals时希望知道它们逻辑上是否相等,而不是想了解他们是否指向同一个对象。

覆盖equals遵守的约定

自反性

对于任何非null的引用值x,x.equals(x)必须返回true

对称性

对于任何非null的引用值x和y,如果x.equals(y)返回true,则y.equals(x)也必须返回true

传递性

对于任何非null的引用值x、y和z,如果x.equals(y)返回truey.equals(z)返回truex.equals(z)也必须返回true

一致性

对于任何非null的引用值x和y,只要equals方法内部比较所用到的字段内容没有被修改,那么多次调用x.equals(y)就会一致地返回true,或者false。

非空性

所有比较的对象都不为null,如下代码:

 @Override
public boolean equals(Object o){
    if(null == o){
        return false;
    }
......
}

其实如上代码根本不必要我们只需要用instanceof来判断参数类型,如果参数时null那么instanceof一定会返回false,如下代码:

@Override
public boolean equals(Object o){
    if(!(o instanceof MyClass)){
        return false;
    }
......
}

编写equals规则

1. 使用==操作符检查“参数是否为这个对象的引用”
2. 使用instanceof操作符检查“参数是否为正确的类型”
3. 把参数转换成正确的类型
4. 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配

在这里要多说几点:
对于float字段,要用Float.compare方法来比较,对于double字段,则使用Double.compare方法,由于存在着Float.NaN、-0.0f以及类似的double常量。
为了获得equals方法的最佳性能,应该最先比较最有可能不一致的字段,或者开销最低的字段。

5. 当编写完成equals方法之后,检查他是否满足,对称,传递,一致性

如下代码:

public class TestClass{
    private String field;
    @Override
    public boolean equals(Object obj) {
        //第一步
        if(this == obj){
            return true;
        }
        //第二步
        if(!(obj instanceof TestClass)){
            return false;
        }
        //第三步
        TestClass testClass = (TestClass) obj;
        //第四步
         return null == this.field ? null == testClass.field : this.field.equals(testClass.field);
    }
}

编写equals的忠告

  1. 覆盖equals时一定要覆盖hashCode方法
  2. 要尽量让equals方法简单一些
  3. 不要将equals方法声明中的参数转换为其他类型

第9条:覆盖equals时总要覆盖hashCode

hashCode约定

  1. 在同一个实例对象中,只要equals方法用到的信息没有被修改,那么这个对象多次调用hashCode方法应该返回结果相同。
  2. 如果两个对象根据equals方法比较是相等的,那么这两个对象hashCode方法产生的结果也必须相等。
  3. 如果两个对象根据equals方法比较是不相等的,那么这两个对象hashCode方法产生的结果有可能相等,也有可能不相等。如果能实现不相等,可以提高散列表的性能。
public static final class PhoneNumber{
        private final int areaCode;
        private final int prefix;
        private final int lineNumber;
        public PhoneNumber(int areaCode, int prefix, int lineNumber){
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNumber = lineNumber;
        }
        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;
            }
            if(!(obj instanceof PhoneNumber)){
                return false;
            }
            PhoneNumber phoneNumber = (PhoneNumber) obj;
            return this.areaCode == phoneNumber.areaCode
                    && this.prefix == phoneNumber.prefix
                    && this.lineNumber == phoneNumber.lineNumber;
        }
    }

如上代码只覆盖了equals方法没有覆盖hashCode,当我们把PhoneNumber应用到Map集合上如下代码:

Map<PhoneNumber, String > map = new HashMap<>();
map.put(new PhoneNumber(010,1111,2222),"Jenny");
        
String value = map.get((new PhoneNumber(010,1111,2222));

我们期望value=Jenny,实际上返回的null
这是由于有两个PhoneNumber实例,第一个实例用于put插入到HashMap中,第二个实例用于getHashMap中获取,由于没有覆盖hashCode方法所以两个实例的hashCode是不相等的。因此第一个实例根据自己的散列码来保存数据,第二个实力根据自己的散列码来获取数据,由于两个散列码不相等所以get方法返回为null
对于HashMap如何根据key来保存value查看这篇文章,说的非常想详细.
解决这个问题就是在PhoneNumber类中根据上面的约定来覆盖hashCode方法,
举例如下代码:

@Override
public int hashCode() {
      return lineNumber;
}

这段代码只是举个例子,具体的哈希值的计算非常复杂这里就不做讲解了。哈希值计算的好坏直接影响HashMap的效率,有兴趣可以在网上查查。

第10条:始终要覆盖toString

  1. 对于任何类都推荐覆盖toString方法,尤其是“值类”。
  2. toString返回的字符串描述了这个类的所有有用的信息,如下代码:
 @Override
public String toString() {
    return "PhoneNumber{" +
               "areaCode=" + areaCode +
               ", prefix=" + prefix +
               ", lineNumber=" + lineNumber +
                '}';
}
  1. toString的返回值格式进行约束。例如返回字符串在文档中约束为Json格式,那么无论何种情况都不能修改它的返回格式。如果修改了,那么之前代码中对这个返回值进行解析的代码都会报错。
  2. toString的返回值格式不进行约束。在文档中不限定他的返回格式,返回格式是可变的,有可能是Json有可能是Xml形式。如果代码中用到解析toString返回格式的代码其正确性就应该由使用者来保证了。
  3. 对于toString中用到的有用信息,类中都要有相关的方法来返回值。避免使用者来解析格式不固定的toString方法的返回值.
    如下代码:
 @Override
public String toString() {
    return "PhoneNumber{" +
               "areaCode=" + areaCode +
               ", prefix=" + prefix +
               ", lineNumber=" + lineNumber +
                '}';
}
//对于上面toString方法用到的信息都需要提供getX方法来获取他们的值
public int getAreaCode() {
    return areaCode;
}
public int getPrefix() {
    return prefix;
}
public int getLineNumber() {
    return lineNumber;
}

第9条:谨慎的覆盖clone

设计模式的原型模式可以由Objectclone方法来实现。

实现clone
  1. 当前对象实现Cloneable接口。
  2. 当前对象在public Object clone()方法中调用super.clone得到当前类的一个对象。前提是父类实现的clone方法没有问题,也就是所有字段(可变字段)都已经clone好了(深拷贝)。
  3. 把克隆对象浅拷贝的字段指向该对象clone得到的地址空间。
    如下代码:
public class E implements Cloneable {
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class C implements Cloneable {
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class D implements Cloneable {
    public E e = new E();
    @Override
    public Object clone() throws CloneNotSupportedException {
        D d = (D) super.clone();
        d.e = (E) this.e.clone();
        return d;
    }
}

public class B implements Cloneable{
    public D d = new D();
    @Override
    public Object clone() throws CloneNotSupportedException {
        B b = (B) super.clone();
        b.d = (D) this.d.clone();
        return b;
    }
}

public class A extends B implements Cloneable {
    private static String TAG = "aaaaa";
    private int count = 100;
    public C c = new C();
    @Override
    public Object clone() throws CloneNotSupportedException {
        A a = (A) super.clone();
        a.c = (C) this.c.clone();
        return a;
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        System.out.println("main");
        A a = new A();
        A a1 = (A) a.clone();
        System.out.println("a  : " + a.toString());
        System.out.println("a1 : " + a1.toString());
        System.out.println("a.c  : " + a.c);
        System.out.println("a1.c : " + a1.c);
    }
}
//打印信息
a  : com.example.test.A@723279cf
a1 : com.example.test.A@10f87f48
a.c  : com.example.test.C@b4c966a
a1.c : com.example.test.C@2f4d3709
深拷贝

以上代码实现了深拷贝,每一个可变字段都调用clone方法产生一个最新的对象,每个类都实现了Cloneable接口并且覆盖了public Object clone()方法。

浅拷贝

将上述代码稍微修改就可以变成浅拷贝

public class A extends B implements Cloneable {
    private static String TAG = "aaaaa";
    private int count = 100;
    public C c = new C();
    @Override
    public Object clone() throws CloneNotSupportedException {
        A a = (A) super.clone();
 //如果类中的可变字段没有调用如下代码,那么这个类就是浅拷贝
//        a.c = (C) this.c.clone();
        return a;
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        System.out.println("main");
        A a = new A();
        A a1 = (A) a.clone();
        System.out.println("a  : " + a.toString());
        System.out.println("a1 : " + a1.toString());
        System.out.println("a.c  : " + a.c);
        System.out.println("a1.c : " + a1.c);
    }
}
//打印信息如下:
a  : com.example.test.A@6e0be858
a1 : com.example.test.A@61bbe9ba
a.c  : com.example.test.C@610455d6
a1.c : com.example.test.C@610455d6

以上是浅拷贝,在A类的public Object clone()方法中没有对可变对象C进行拷贝,导致打印的信息中C对象的地址是相同的。

代替clone的方案

写代码很少覆盖Cloneable接口,说两种代替方案

  1. 拷贝构造器
    例如HashMap类的构造方法public HashMap(Map<? extends K, ? extends V> map);就是一个拷贝构造器
  2. 拷贝工厂方法
    例如public static Yum newInstance(Yum yum);

最好不要覆盖Cloneable接口,使用clone方法。

第12条:考虑实现Comparable接口

实现这个接口主要是为了对象之间的排序,Java平台类库中的所有“值类”都实现了这个接口。
规则:
将这个对象与指定对象进行比较。当该对象小于、等于、大于指定对象的时候,分别返回负数、零、正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。
如下代码:

public class PhoneNumber implements Comparable<PhoneNumber> {
    public final int area;
    public final int phone;
    public PhoneNumber(int area, int phone) {
       this.area = area;
       this.phone = phone;
    }
    @Override
    public int compareTo(@NonNull PhoneNumber pn) {
        if (area < pn.area) {
            return -1;
        }
        if (area > pn.area) {
            return 1;
        }
        if (phone < pn.phone) {
            return -1;
        }
        if (phone > pn.phone) {
            return 1;
        }
        return 0;
    }
    @Override
    public String toString() {
        return "PhoneNumber{" +
                "area=" + area +
                ", phone=" + phone +
                '}';
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        PhoneNumber[] phoneNumbers = {
                new PhoneNumber(100,300),
                new PhoneNumber(200,500),
                new PhoneNumber(50,10),

        };
        List<PhoneNumber> phoneNumberList = Arrays.asList(phoneNumbers);
        System.out.println(phoneNumberList);
        Collections.sort(phoneNumberList);
        System.out.println(phoneNumberList);
    }
}
打印日志
[PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}, PhoneNumber{area=50, phone=10}]
[PhoneNumber{area=50, phone=10}, PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容