Effective Java (3rd Editin) 读书笔记:2 所有对象共有的方法

2 所有对象共有的方法

Item 10:重写 equals 方法时遵守通用协同

不需要重写 equals() 方法的情况:

  1. 类的每一个实例都认为是不同。比如 Thread 这种代表活跃的实体而不是值
  2. 不需要“逻辑相等”的判断。比如 Pattern 不需要检查内嵌的正则表达式是否相等
  3. 父类已经重写了合适的 equals() 方法。比如,AbstractList 等的子类
  4. private 或 package 访问权限的类。包外无法访问,不需要

需要重写 equals() 方法的情况(大多数 value class):

  1. 只比较是否是堆中的同一个对象不够,还需要判断“逻辑相等”
  2. 父类没有合适的 equals() 方法

有一些 value class 不需要重写,因为它们能确保每一个值最多只存在一个对象,比如 Enum 类型。

Object 类的 API 文档注释中,说明了 equals() 方法的通用协同。

继承一个可实例化的类并增加新属性的时候,必然会违反 equals 协同

public class Point {
    private final int x, y;
    // ...
    @Override public boolean equals(Object o) {
        if (!(o instance of Point)) return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}

public class ColorPoint extends Point{
    private final Color color;
    // ...
    @Override public boolean equals(Object o) { // 违反了对称性
        if (!(o instance of ColorPoint)) return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}

Point 对象可能等于某个 ColorPoint 对象,但是反之不成立,所以违反了对称性。比如 java.util.Date 和它的子类 java.sql.Timestamp 之间就存在这个问题,它们不能混合比较,否则会产生混乱,而这个问题只能靠程序员在使用时自己注意。

解决上述问题的一个妥协方法是,用组合代替继承

public class ColorPoint {
    private final Point point;
    private final Color color;
    // ...
    public Point asPoint() { return point; }
    @Override public boolean equals(Object o) {
        if (!(o instance of ColorPoint)) return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

重写 equals() 方法的建议:

  1. 使用 == 检查是否是堆中的同一个对象
  2. 使用 instanceOf 检查对象类型,并考虑了 null 的情况
  3. 将参数对象强制类型转换为正确类型
  4. 对于对象中的每一个“有意义”的字段属性,检查是否和测试对象相等,元素数据类型可以用 ==,float 和 double 用包装类的 compare() 静态方法,数组使用 Arrays.equals() 方法,引用类型使用考虑了 null 的 Objects.equals() 方法。
  5. 最后,不要忘了,重写 equals 方法时也要重写 hashCode 方法(Item 11)

例如,String 的例子:

public boolean equals(Object anObject) {
    if (this == anObject) { 
        return true;
    }
    if (anObject instanceof String) { 
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

Item 11:重写 equals 方法时也要重写 hashCode 方法

如果不做到这一条的话,在使用 HashMap 和 HashSet 等会使用哈希值的集合类时,会出现异常。

Object 的 API 文档中同样规定了 hashCode 的通用协同:

  1. 一个进程中,同一个对象多次调用 hashCode 方法,如果 equal 方法中比较的字段属性没有修改的话,它应该返回相同的 int 值
  2. equals 方法返回 true 的两个对象,hashCode 方法返回相等的 int 值
  3. equals 方法返回 false 的两个对象,不要求但是墙裂建议 hashCode 方法返回不相等的 int 值

重写 equals 方法时没有重写 hashCode 方法的话,很可能会违反上述协同的第二条。

为了尽可能满足第三条规定,这个有一个生成哈希值的建议:

  1. 声明 int 变量 result,初始化为你的对象的第一个重要字段的哈希值 c ,计算方法如 2.a.(“重要字段”是指会影响 equals 比较的字段)

  2. 对象中剩下的每个重要字段,都按如下处理:

    a. 计算得到该字段的 int 类型哈希值 c :

    • ⅰ. 字段是原始数据类型,使用包装类的 hashCode 静态函数来计算
    • ⅱ. 字段是对象引用,如果 equals 方法中在比较此字段时会递归调用它的 equals 方法,那么这里也要递归调用它的 hashCode 方法。如果字段为 null,使用 0
    • ⅲ. 字段是数组,把数组中的每个重要元素当作字段,使用 2.a. 中的规则计算每个字段的哈希值,使用 2.b. 中的规则组合这些值。如果数组没有重要元素,使用一个常量(最好不要是 0);如果数组中每个元素都重要,使用 Arrays.hashCode

    b. 将 2.a. 中计算得到的哈希值 c 按照如下规则组合进 result 中:result = 31 * result + c

  3. 返回 result

之所以会用乘数因子 31 ,是因为它是素数而且 31 * i == (i << 5) - i,JVM 会自动完成此优化。

// java.lang.String 的 hashCode
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) { // 懒加载
        char val[] = value;
		// 重要字段为数组
        for (int i = 0; i < value.length; i++) { 
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

Item 12:总是重写 toString 方法

设计良好的 toString 方法使得对象使用起来很惬意,也方便 debug 过程。尽可能使 toString 方法返回对象中的所有重要信息。如果你决定在 toString 的 API 文档中指明返回格式的话,其他程序员可能为此量身定做一些持久化的对象存储和解析的工具,这可能会限制未来的类的更新。

比如,AbstractMap 的 toString 方法,规定了返回格式为 {k1=v1, k2=v2} ,或 {}

/**
 * Returns a string representation of this map.  The string representation
 * consists of a list of key-value mappings in the order returned by the
 * map's <tt>entrySet</tt> view's iterator, enclosed in braces
 * (<tt>"{}"</tt>).  Adjacent mappings are separated by the characters
 * <tt>", "</tt> (comma and space).  Each key-value mapping is rendered as
 * the key followed by an equals sign (<tt>"="</tt>) followed by the
 * associated value.  Keys and values are converted to strings as by
 * {@link String#valueOf(Object)}.
 *
 * @return a string representation of this map
 */

Item 13:重写 clone 方法要慎重

immutable 类不需要提供 clone 方法。

ArrayList 的 clone 方法:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

使用父类的 super.clone(),只是简单的字段赋值,原始数据类型和 immutable 字段不会出问题,但是对于 mutable 的对象引用类型的字段,拷贝前后,引用的是同一个堆中的对象,因此需要额外的拷贝工作。

clone 方法最大的亮点是对数组拷贝的实现,它会新建一个新数组,并进行元素的赋值,和 Arrays.copyOf 的效果一样。

另一个大问题是,clone 方法中不要调用子类可能会重写的方法,否则会导致子类还为拷贝完成,就执行了自己的方法,有安全隐患,因此调用的方法最好有 final 修饰,比如 HashMap 的 clone 方法中调用了 final 类型的 putMapEntries 方法。

@Override
public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

考虑到 clone 方法的安全隐患,当你需要拷贝的功能时,最好考虑提供拷贝构造器或拷贝工厂方法,它们有如下优点:

  1. 没有 clone 的种种注意事项
  2. 不会和 final 字段冲突
  3. 不需要多余的 try-catch 语句
  4. 不需要强制类型转换
  5. 支持基于接口的拷贝

如下是 HashMap 中基于 Map 接口的拷贝构造器:

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

Item 14:考虑实现 Comparable 接口

Comparable 的 API 文档中指明了 compareTo 方法的通用协同(sgn 函数将负数、0、正数转换为 -1、0、1):

  1. sgn(x.compareTo(y)) == - sgn(y.compareTo(x)),前者抛出异常的充要条件是后者也抛出异常
  2. transitive:x.compareTo(y) > 0 && y.compateTo(z) > 0,可推导出 x.compareTo(z) > 0
  3. x.compareTo(y) == 0,可推导出 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  4. 需要补充的是,不要求但是墙裂推荐,(x.compareTo(y) == 0) == (x.equals(y)),不满足这条要求的 compareTo 方法必须在 API 文档中声明:“Note: This class has a natural ordering that is inconsistent with equals.”

和 equals 方法类似,继承一个可实例化的类并增加新属性的时候,必然会违反 compareTo 协同 。解决方法也是一样,使用组合代替继承

协同的第四项需要特别注意,比如 BigDecimal 类就不满足此建议,有如下效果:

Set<BigDecimal> treeSet = new TreeSet<BigDecimal>();
Set<BigDecimal> hashSet = new HashSet<BigDecimal>();
treeSet.add(new BigDecimal("1.0"));
hashSet.add(new BigDecimal("1.0"));
treeSet.add(new BigDecimal("1.00"));
hashSet.add(new BigDecimal("1.00"));
System.out.println(treeSet.size() == 1); // true
System.out.println(hashSet.size() == 2); // true

这是因为排序集合类(如 TreeSet,TreeMap)使用 compareTo 来判断元素是否相等,而一般集合类使用 equals 来判断元素是否相等。

原始数据类型的比较,避免使用 < > 符号,或者减法(可能溢出),建议使用包装类的 compare 静态方法,或者用 Comparator 接口的链式构造方法(会有少量性能损耗,但可读性强且简洁)。

private static final Comparator<PhoneNumber> COMPARATOR = 
    Comparator.compaingInt((PhoneNumber pn) -> pn.areaCode)
    	.thenComparingInt(pn -> pn.prefix)
    	.thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPATATOR.compare(this, pn);
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值