【Effective Java】10~14 equals,hashcode,toString,clone,Comparable

506 阅读6分钟

看完《Effective Java》感觉写的相当好,中间大部分都以作为参考手册,故整理出来。结合框架源码,jdk源码,自己的demo,给大家更多的示例,便于大家理解。每一条的介绍为个人理解,如果有任何不对,希望指出。对于博客有任何建议也希望给出建议。

10.覆盖 equals 时请遵守通用约定

equals设计的初衷时满足以下4点

  • 每个实例唯一
  • 类没有必要提供逻辑相等的测试功能,Object的equals已经足够
  • 超类已经覆盖了equals,对于子类同样适用
  • 类如果是私有的,确保它的equals永远不会被调用 如果你确实需要覆盖equals,需要满足5个属性
  • 自反性:a=a
  • 对称型:a=b => b=a
  • 传递性:a=b,b=c => a=c
  • 一致性:多次调用返回结果一致
  • 非空性:任何值不等于null
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Point)){
            return false;
        }

        Point p = (Point)obj;
        return p.x == x && p.y == y;
    }
}
public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

     @Override
     public boolean equals(Object obj) {

        if(!(obj instanceof ColorPoint)){
            return false;
        }
         return super.equals(obj) && ((ColorPoint) obj).color == color;
      }
}
public class TestDemo {
    public static void main(String[] args) {
        Point p = new Point(1,2);
        ColorPoint cp = new ColorPoint(1,2, Color.RED);
        //true
        System.out.println(p.equals(cp));
        //false
        System.out.println(cp.equals(p));
    }
}

书中举了如上例子,说明了自己编写equals的过程中可能导致的传递型问题,这个在jdk中也有对应的例子java.util.Date java.sql.Timestamp这两个类就有类似的问题。可以用下面这段代替instanceof,效果会好点。

if(obj == null || obj.getClass() != getClass()){
    return false;
}

作者给出了几个提高equals的诀窍

  1. 使用==检测是否为这个引用(double,float不适用,这两个使用Float.compare,Double.compare,但是保险起见还是推荐BigDecimal)
  2. 使用instanceof 检测是否参数是否为正确类型
  3. 把参数转换成正确类型
  4. 对关键变量查看是否匹配 最后作者给出了几个忠告
  5. 覆盖equals时总要覆盖hashcode
  6. 不要让equals过于智能
  7. 不要让参数Object替换成其他类型 个人总结:尽量不要自己乱写equals,尽量使用ide自动生成的,各个方面都替你考虑到了。不过应该也没人会自己手写equals吧
public class EqualsDemo {
    private Integer val1;
    private Integer val2;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EqualsDemo that = (EqualsDemo) o;
        return Objects.equals(val1, that.val1) &&
                Objects.equals(val2, that.val2);
    }

    @Override
    public int hashCode() {
        return Objects.hash(val1, val2);
    }
}

11.覆盖 equals 时总要覆盖 hash Code

这个不会有人不知道吧。网上博客应该也有一堆了。大致是如下这几个原则。

  • 如果ab相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode()(来自于廖雪峰)
  • 如果ab不相等,那么a.equals(b)一定为false,则a.hashCode()b.hashCode()尽量不要相等。(来自于廖雪峰)
  • hashCode相等对象不一定相等。 因为hashcode其实是个映射函数,所以是有可能不同对象得到同一个值的,这时候就会用到equals进行比较。

hashcode还是非常重要的,最著名的应该就是HashMap里面对hash的用法,简直牛逼。 这里,

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//低16与高16位异或,增加复杂度
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        //主要就是这里,n是2倍数,-1,就是从右向左全是1,再进行上面混淆过的hash值与运算,自动落入范围内
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

主要就是这里tab[i = (n - 1) & hash]主要就是这里,n是2倍数,-1,就是从右向左全是1高位全是0,再进行上面混淆过的hash值与运算,由于高位全是0,与的结果全是0,低位全是1,进行与操作能保证在范围内。

由于扩容全是2的倍数,此时再进行比较只要把链表遍历,比较高位那个值的不同。这hashcode运用的,简直了。所以,hashcode十分重要,不光是对象的标识,而且会参与运算。

12.始终要覆盖 toString

这个就没啥好说的了,基本现在生成一个类,必须带上toString,因为,大部分都是错误都是通过日志的方式,如果不覆盖就是com.mountain.monk.chapter3.Point@300ffa5d这种形式,完全无法调试,哪个参数有问题都不知道。

13.谨慎覆盖clone

Cloneable中没有任何方法,如果一个类实现了Cloneable接口,Object的clone方法返回该对象的逐域拷贝,如果一个类未实现Cloneable接口,则该对象就会抛出CloneNotSupportedException异常。

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

clone主要目的就是会获取一个对象的拷贝,但是和原对象不同。相当于省略了new一个对象,然后一个一个成员变量的set了。但记住它是个浅拷贝,之前有过一次经历,我想要值相同但是引用完全不同的一个对象,由于clone是浅拷贝,引用始终是同一个。

public class CloneA {

}
@Data
public class CloneB implements Cloneable, Serializable {
    private CloneA cloneA;



    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public static void main(String[] args) throws CloneNotSupportedException {

    CloneB b=new CloneB();
    CloneA a=new CloneA();
    b.setCloneA(a);
    CloneB b1=(CloneB) b.clone();
    System.out.println(b1.getCloneA()==a);//true

    String s=JSON.toJSONString(b);
    CloneB b2= JSON.parseObject(s,CloneB.class);
    System.out.println(b2.getCloneA()==a);//false

    CloneB b3=new CloneB();
    BeanUtils.copyProperties(b,b3);
    System.out.println(b3.getCloneA()==a);//true

}

由于clone是浅拷贝,后面也慢慢的通过BeanUtils.copyProperties(Object,Object);代替了,如果硬是想要clones深拷贝需要,内部引用对象也实现Cloneable,然后改写下clone方法如下所示

public class CloneA implements Cloneable{
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
@Data
public class CloneB implements Cloneable, Serializable {
    private CloneA cloneA;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        CloneB cloneB=(CloneB) super.clone();
        cloneB.setCloneA((CloneA) cloneB.getCloneA().clone());
        return cloneB;
    }

//    @Override
//    protected Object clone() throws CloneNotSupportedException {
//        return super.clone();
//    }
}

但是如果这么麻烦,还不如JSON序列化,反序列话了。 总的来说,clone我平常开发中基本没用到过,感觉十分鸡肋

14.考虑实现Comparable接口

基本上和Arrays.sort连用,要满足对称性和传递性,和equals一致,比较常用,到了java8可以和lambda表达式连用,简化写法。 十分简单常用,就不写例子了。