【Effective java 学习】第三章:对于所有对象都通用的方法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/baidu_37378518/article/details/81176583

第八条:覆盖equals是请遵守通用约定

满足下列四个条件之一,就不需要覆盖equals方法:

  1. 类的每个实例本质上都已唯一的。不包括代表值的类,如:Integer,String等,Object提供的equals方法就够用了

  2. 不关心是否提供了“逻辑相等”的测试功能。对于Random类,用户只关心函数返回的随机数,不会关心产生的两个随机数是不是相等,所以对其进行equal方法覆盖将没有意义

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

  4. 类是私有的或是包级私有的,并且确定它的equals方法永远不会被调用。同时为了防止该equals被调用,可以如此覆盖:

    @Override
    public boolean equals(Object obj) {
    throw new AssertionError();
    }

    注:此处原文是 类是私有的或包级私有的,可以确定它的equals方法永远不会被调用 。但翻译的不太合适,原文为

    The class is private or package-private, and you are certain that its equals method will never be invoked.

如果类有自己的“逻辑相等”的概念,通常属于“值类”的情形,且超类还没有覆盖equals以实现期望的行为,此时就需要对euqals进行覆盖。但对于“每个值最多只存在一个对象”的类即单例模式实现的类则不需要覆盖。

在覆盖equals方法时,需要遵守其通用约定:

  1. 自反性 。对于任何非null的引用值x,x.equals(x)必须==true。

  2. 对称性 。对于任何非null的引用值x和y,当且仅当y.equals(x)==true时,x.equals(y)必须==true。

    扫描二维码关注公众号,回复: 4816465 查看本文章
    class CaseInsensitiveString {
       private final String s;
    
       public CaseInsensitiveString(String s) {
           if (s == null){
               throw new NullPointerException();
           }
           this.s = s;
       }
    
       //违反了对称性
       //企图与与普通的String对象进行互操作,但是String类中的equals方法并不知道这个类
       //一旦违反了对称性,当其他对象面对你的对象时,行为是无法知道的
       @Override
       public boolean equals(Object obj) {
           if (obj instanceof CaseInsensitiveString){
               return s.equalgnoreCase(((CaseInsensitiveString) obj).s);
           }
           if (obj instanceof String){
               return s.equalsIgnoreCase((String) obj);
           }
           return false;
       }
    }
  3. 传递性 。对于任何非null的引用值x, y和z,如果x.equals(y)==true,并且y.equals(z)==true, 那么对于x.equals(z)也必须==true。

  4. 一致性 。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回同样的结果。

    可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。无论类是否不可变,都不要使用equals方法依赖于不可靠的资源。

  5. 对于任何非null的引用值x,x.equals(null)必须==false。

实现高质量equals方法的诀窍,下列每一项都是基于前一项:

  1. 使用==操作符检查“参数是否为这个对象的引用”。性能优化

  2. 使用instanceof操作符检查“参数是否为正确的类型”

  3. 把参数转换成正确的类型

  4. 对于该类中的每个“关键”域,检查参数中的域是否对该对象中对应的域相匹配

    1. 对于float,使用Float.compare;对于double,使用Double.compare

    2. 有些对象引用域包含null可能是合法的,使用下面的方式避免NullPointException

      (field == null ? o.field == null : field.equals(o.field))
    3. 域的比较可能会影响到equals性能。应先比较最有可能不一致的域,或是开销最低的域

  5. 当你编写完成了equals方法后,问三个问题:是否是对称的、传递的、一致的。同时还需编写测试单元来检验。另外两个特性通常会自动满足。

最后的告诫:

  1. 覆盖equals时总要覆盖覆盖hashCode
  2. 不要企图让equals方法过于智能
  3. 不要将equals声明中的Object对象替换为其他的类型

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

在覆盖equals方法时,如果不覆盖hashCode方法,会导致该类无法与HashMap、HashSet和HashTable一起正常运作。相等的对象必须具有相等的散列码。

class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 999, "line number");
        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 obj) {
        if (obj == this){ //使用 == 操作符检查 “参数是否为这个对象的引用”
            return true;
        }
        if (!(obj instanceof PhoneNumber)){//使用instanceof检查“参数是否为正确的类型”
            return false;
        }
        PhoneNumber pn = (PhoneNumber) obj;//转换为正确的类型
        //为了获得最佳性能,先比较最有可能不一样的域
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }
}

在执行下列操作时

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5360), "jenny");

//执行下列操作时,虽然已经重写这个值类的equals方法
//但由于前后两个对象的hashCode值不同,所以不会执行我们预期的操作
m.get(new PhoneNumber(707, 867, 5360));

一个好的散列函数通常倾向于“为不想等的对象产生不想等的散列码”。下面是一种简单的解决办法:

  1. 把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中

  2. 对于对象中每个关键域 f (知equals方法中涉及的每个域),完成以下步骤:

    1. 为该域计算int类型的散列码c:

      1. 如果该域是boolean类型,则计算( f ? 1 : 0)
      2. 如果该域是byte、char、short或者int类型,计算(int)f
      3. 如果该域是long类型,则计算(int)(f ^ (f >>> 32))
      4. 如果该域是float类型,则计算Float.floatToIntBits(f)
      5. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照2计算
      6. 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常熟,但通常是0)
      7. 如果该域是一个数组,则要把每一个元素当作单独的域来处理。
    2. 按照下面额公式,把上面步骤中计算的散列码c合并到result中:

      result = 31 * result + c;
    3. 返回result

    4. 编写单元测试来检验

在散列码的计算过程中。可以把冗余域(该域的值可以通过其他域值计算出来)排除在外。同时必须排除equals比较中没有用到的域。

对于PhoneNumber类,可以这样覆盖其hashCode方法

@override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

如果一个类是不可变的,并且计算散列码的开销也比较大,可以考虑把散列码缓存在对象内部。如果这种类型的大多数对象会被用作散列键,就应该在创建实例的时候计算散列码,否则,可以选择直到hashCode被第一次调用的时候才初始化。

不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。

第十条:始终要覆盖toString

建议所有的子类都覆盖这个方法。

在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。同时决定是否在文档中指定返回值的格式,对于值类,建议这么做,同时再提供一个相匹配的静态工厂或者构造器,以便于程序员可以很容易的在对象和它的字符串表示法之间转换,例如:BigInteger、BigDecimal和绝大多数的基本类型包装类。

但不足之处在于,如果该类被广泛使用,一旦指定格式,即必须始终坚持这种格式,如果在将来的发行版本中改变,就会破坏代码和数据。如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或者改进格式。

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

第十一条:谨慎地覆盖clone

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

在克隆对象时,如果每个域包含一个基本类型的值,或者包含一个指向不可变对象的引用,那么不需要再做进一步处理。如果对象中包含的域引用了可变的对象,此时就需要使用深拷贝。

实际上,clone方法就是另一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。clone架构与引用可变对象的final域的正常用法是不相兼容的,因为一个域被final修饰后,就无法再调用clone方法对其进行克隆(即赋值)。

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

覆盖版本的clone方法如果是公有的,就应该将Object中的clone抛出的CloneNotSupportException进行try-catch处理,因为这样会使得覆盖版本中的clone使用起来更加轻松。如果专门为了继承而设计的类,就应该模拟Object.clone的行为,这样使得子类具有实现或不实现Cloneable接口的自由。

如果用线程安全的类实现Cloneable接口,要使得clone方法也有很好的同步。

另一个实现对象拷贝的好办法是提供一个拷贝构造器,或者拷贝工厂,比起Cloneable/clone有以下优势:

  • 不依赖域某一种很有风险、语言之外的对象创建机制
  • 不要求遵守尚未指定好文档的规范
  • 不会与final域的正常使用发生冲突
  • 不会抛出不必要的手贱异常
  • 不需要进行类型转换
  • 可以带一个参数,参数类型是通过该类实现的接口。假设你有一个HashSet,并且希望把他拷贝成一个TreeSet,使用转换构造器:new TreeSet(s)

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

类实现该接口,就表明它的实例具有内在的排序关系,可以跟许多泛型算法以及依赖于该接口的集合实现进行协作,java平台类库中的所有值类都实现了该借口。

将这个对象与指定对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或正整数。若由于指定对象类型无法比较,则抛出ClassCastException。

说明(sgn为符号函数):

  • 必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(也暗示着,当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才必须抛出异常)
  • 必须确保此关系可传递
  • 必须确保x.compareTo(y) == 0 暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应明确予以说明。推荐使用的说法:“注意:该类具有内在排序功能,但与equals不一致”

    违反compareTo约定的类也会破坏其他依赖于比较关系的类,例如TreeSet和TreeMap,以及Collections和Array。

告诫:无法在用新的值组建扩展可实例化的类时的同时保持compareTo约定,除非愿意放弃面向对象的抽象优势。 如果想为一个实现了Comparable接口的类增加值组建,要编写一个不相关的类,其中包含第一个类的一个实例。

如果遵守上述“说明”中的最后一条,那么由compareTo所施加的顺序关系就被认为“与equals一致”,如果违反这条规则,就是“与equals不一致”,如果不一致,仍然能正常工作,但如果一个有序集合包含了该类元素,该集合可能就无法遵守相应集合接口(Collection, Set, Map)的通用约定。因为,这些接口的通用约定是按照equals来定义的,但是有序集合使用了由compareTo来定义。例如,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)在HashSet中会两个都存在,但是在TreeSet中只存在一个。

Comparable接口是参数化的,而且comparable方法是静态的类型,不必进行类型检查,也不必对它的参数进行类型转换。如果参数不合适,甚至无法编译。

如果一个类用多个关键域,那么必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果,则整个比较结束。

public int compareTo(PhoneNumber pn){
        if (areaCode < pn.areaCode)
            return -1;
        if (areaCode > pn.areaCode)
            return 1;
        // area code are equals, compare prefixes
        if (prefix < pn.prefix)
            return -1;
        if (prefix > pn.prefix)
            return 1;
        //area code and prefixes are equals, compare line number
        if (lineNumber < pn.lineNumber)
            return -1;
        if (lineNumber > pn.lineNumber)
            return 1;
        //all fields are equals
        return 0;
    }

如果compareTo的约定没有指定返回值的大小,而只是指定了返回值的符号,可以对上述代码进行简化

    public int compareTo(PhoneNumber 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;
    }

但这种简化方法,除非确定相关的域不会为负值,或者更一般的情况:最小和最大的可能域值之差小于或者等于INTEGER.MAX_VALUE。

猜你喜欢

转载自blog.csdn.net/baidu_37378518/article/details/81176583
今日推荐