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

本章主要讲的是如何覆盖一些非final的Object方法:
equals/hashCode/toString/clone方法

第八条、覆盖equals时请遵守通用约定

1. 不覆盖equals方法的条件(满足其一即可):

  • 类的每个实例本质上都是唯一的。代表的是活动实体而不是值,例如Thread;

  • 不关心类是否提供逻辑相等的测试功能。如java.util.Random;

  • 父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的。例如:大部分的Set实现都从AbstractSet继承equals实现。

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

2. 什么时候应该覆盖equals方法:

如果类具有自己特有的逻辑相等概念,且其父类还没有覆盖equals以实现其期望的行为,则需要覆盖equals方法。
通常属于“值类”的情形,仅仅是表示值的类,如Integer和Date。这样做可以使得这个类的实例可以被用作映射表的键key,或者集合set的元素。

3. Object中的equals规范(等价关系equivalence relation):

  • 自反性(reflexive):x.equals(x)必须返回true,x非空。

  • 对称性(symmetric):当且仅当x.equals(y)返回true时,y.equals(x)必须返回true。x,y非空。

  • 传递性(transitive):x.equals(y)返回true,且y.equals(z)返回true,那么x.equals(z)也必须返回为true。 x,y,z非空。

  • 一致性(consistent):只要比较操作在对象中所用的信息没有被修改,则多次调用的结果会一致。

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

4. 等价关系的一个基本问题:

我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。除非愿意放弃面向对象的抽象带来的优势。(利用复合来解决类似的问题)

5. 实现高质量equals方法的诀窍:

  • 使用==操作符检查”参数是非为这个对象的引用“,如果是,则返回true;

  • 使用instanceof操作否检查”参数是否为正确的类型“;

  • 把参数转换成正确的类型;

  • 对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。域的比较顺序可能影响到equals方法的性能,应最先比较最有可能不一致的域或者是开销最小的域。

  • 完成后检查是否符合:对称性,传递性和一致性。

  • 覆盖equals时总要覆盖hashCode方法。

  • 不要企图使equals方法过于智能。

  • 不要将equals声明中的Object对象替换为其他的类型。原因是这个方法并没有覆盖Object.equals!参数类型不正确。在原有的equals方法的基础上,重载了一个强类型的equals方法。@Override注解在这里面就起作用了。


第九条、覆盖equals时总要覆盖hashCode

1. 在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。

否则会违反Object.hashCode的通用约定,导致该类无法结合所有基于散列的集合一起正常工作(HashMap、HashSet和HashTable)。

2. Object约束中hashCode的约定:

  • 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

  • 如果两个对象根据equals(Object)方法比较是相等的,则调用这两个对象中任意一个对象的hashCode方法都必须产生相同的整数结果。

  • 如果两个对象根据equals(Object)方法比较是不相等的,hashCode不一定产生不同的整数结果。

3. 一个好的散列函数“为不相等的对象产生不相等的散列码”。

理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值,要想完全达到这种理想的情形是非常困难的,下面有一种简单的解决方法:

  • 把某个非零的常数值(如17),保存在一个名为result的int类型的变量中;
  • 对于对象中每个关键域f(equals方法中涉及),完成以下步骤:
    • 为该域计算int类型的散列码c
      • boolean类型计算 :(f?1:0)
      • byte/char/short或者int类型计算: (int)f
      • long类型计算:(int)(f^(f>>>32))
      • float类型计算:Float.floatToIntBits(f)
      • double类型计算:Double.doubleToLongBits(f),然后按照long类型计算
      • 该域是一个对象引用,针对范式调用hashCode,再进行判断。
      • 该域是一个数组,Arrays.hashCode方法
    • 按照下面的公式,把上步中计算的散列码合并到result中:(使得散列值依赖于域的顺序)
      • result = 31 * result + c ;
  • 返回result

下面是一个实例

import java.util.HashMap;
import java.util.Map;

/**
 * Created by laneruan on 2017/6/6.
 */
public class PhoneNumberHashCode{
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumberHashCode(int areaCode,int prefix,int lineNumber){
        rangeCheck(areaCode,999,"area code");
        rangeCheck(prefix,999,"prefix");
        rangeCheck(lineNumber,999,"lineNumber");
        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }
    private static void rangeCheck(int arg,int max,String name){
        if(arg < 0 || arg > max){
            throw new IllegalArgumentException(name+": "+arg);
        }
    }
    @Override
    public boolean equals(Object o){
        if(o == this)
            return true;
        if(!(o instanceof PhoneNumberHashCode))
            return false;
        PhoneNumberHashCode pnhc = (PhoneNumberHashCode) o;
        return pnhc.lineNumber == lineNumber &&
                pnhc.prefix == prefix &&
                pnhc.areaCode == areaCode;
    }
    @Override
    public int hashCode(){
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
    //如果没有hashCode方法,会出现如下问题
//    Map<PhoneNumber,String> map = new HashMap<PhoneNumber,String>();
//    map.put(new PhoneNumber(701,867,5309),"Jenny");
//    map.get(new PhoneNumber(707,867,5309)) 返回为null
    
    //Lazy initialized,cached hashCode
    private volatile int hashCode;
    public int hashCode2(){
        int result = hashCode;
        if(result == 0){
            result = 17;
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
        }
        return result;
    }
}

4. 如果一个类是不可变的,且计算散列码的开销比较大,可以考虑把散列码缓存在对象内部,而不是每次请求都重新计算散列码。


第十条、始终要覆盖toString

  1. java.lang.Object提供的toString方法的一个实现,但通常不是用户希望看到的:包含一个类名称,一个@符号接着是散列码的无符号十六进制表示法。“简洁的、但信息丰富且易于阅读的表达形式,建议所有的子类都覆盖这一方法。”当对象被传递给println,printf,字符串联操作符+、assert或者被调试器打印出来时,toString会自动调用

  2. 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。

  3. 无论是否指定格式,都用该在文档中明确表明你的意图;都应为toString返回中包含的所有信息,提供一种编程式的访问途径。


第十一条、谨慎地覆盖clone

1. Cloneable接口的目的是作为对象的一个mixin接口,表明这个对象允许克隆

其主要的缺陷在于缺少一个clone方法,Object的clone方法是受保护的,如果不加借助于反射,就不能因为一个对象实现了Cloneable接口,就可以调用clone方法。

这个接口的作用在于决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportException。

这样的副作用是:无需调用构造器就可以创建对象

2. java.lang.Object关于Clone方法的约定:

创建和返回该对象的一个拷贝,这个拷贝的精确含义取决于该对象的类。

一般的来说是:对于任何对象x,表达式x.clone() != x 为true 且 x.clone().getClass() == x.getClass() 为true,x.clone().equals(x)为true,但这些都不是绝对的要求。

拷贝对象往往会创建它的类的一个实例,但它同时也会要求拷贝内部的数据结构,过程中没有调用构造器!

3. 实际上,对于实现了Cloneable接口的类,我们总是期望它也提供一个功能适当的公有的clone方法。

从super.clone()中得到的对象可能会接近最终要返回的对象,也可能相差甚远,这取决于类的本质。如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么可能不需要再做进一步处理。

如果对象中包含的域引用了可变的对象,使用上述简单的clone实现可能会出现灾难性的后果。一般需要在这些可变的对象中递归地调用clone。实际上clone方法相当于另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariant)。clone架构与引用可变对象的final域的正常用法是不相兼容的。

克隆复杂对象的最后一种方法是:先调用super.clone(),然后把结果中的所有域都设置成它们的空白状态,然后调用高层的方法来重新产生对象的状态。

总结下来:所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone,此公有方法首先调用super.clone,然后修正任何需要修正的域。那么真的有必要这么复杂吗?很少!

4. 我们平时最好提供某些其他的途径来代替对象拷贝,或者根本不提供这样的功能。

比如对于不可变类,支持对象拷贝意义不大。

另一种提供对象拷贝的好方法是提供一个拷贝构造器(copy constructor)或拷贝工厂。这种做法优势很大。所以请谨慎地覆盖clone。
如:public Yum(Yum yum);public static Yum newInstance(Yum yum)

第十二条、考虑实现Comparable接口

  1. compareTo方法没有在Object中声明,它是Comparable接口中唯一的方法。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,它还是个泛型

  2. 类实现了Comparable接口,就表明它的实例具有内在的排序关系,为实现了Comparable接口的对象数组进行排序可以直接Arrays.sotr(a)。对存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也同样简单。一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。事实上,Java平台类库中所有的值类都实现了Comparable接口。

  3. 什么时候考虑实现Comparable接口:正在编写一个值类,它具有非常明显的内在排序关系,比如按照字母排序、数值排序或者年代排序,那就应该坚决考虑实现这个接口:

     public interface Comparable<T>{
         int compareTo(T t);
     }
    
  4. compareTo方法的通用约定:将这个对象与指定的对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数、零或者正整数、如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。符号sgn(表达式)表示数学中的signum函数,根据表达式的值为负数、零和正值,分别返回-1、0或1。

    • 实现者必须确保所有的x和y都满足:sgn(x.compareTo(y)) == -sign(y.compareTo(x))。也暗示着:当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)抛出异常。
    • 实现者还必须确保这个比较关系是可传递的:(x.compareTo(y)>0&&y.compareTo(z)>0)暗示着x.compareTo(z)>0
    • 实现者必须确保x.compareTo(y)==0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
    • 强烈建议(x.compareTo(y)==0) == (x.equals(y))。如果违反这个条件请予以说明:”注意:该类具有内在的排序功能,但与equals不一致。“ 违反compareTo约定的类会破坏依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索和排序算法。
  5. 如果一个类有多个关键域,那么按什么样的顺序来比较这些域是非常关键的,必须从最关键的域开始,逐步进行到所有的重要域。
    下面是关于电话类的一个实例:

     @Override
     public int compareTo(PhoneNumberHashCode pn){
         //这个方法得确信相关的域不会为负值,防止溢出。
         int areaCodeDiff = areaCode - pn.areaCode;
         if(areaCodeDiff != 0)
             return areaCodeDiff;
         int prefixDiff = prefix - pn.prefix;
         if(prefixDiff != 0)
             return prefixDiff;
         return lineNumber-pn.lineNumber;
     }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容