EffectiveJava学习笔记(三)

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

类具有特有的逻辑相等的概念,且超类没有覆盖equals方法时应该覆盖equals方法,例如integer、String这种“值类”。

但是有一种值值类无需覆盖equals,即实例受控,每个值最多只存在一个对象的类,比如枚举类,这种类逻辑相同和对象相同是同一回事,所以Object的equals方法等她与逻辑的equals。

equals必须满足四种等价关系:自反性、对称性、传递性、一致性,并且对于非null的x,x.equals(null)永远返回false.

对称性:

CaseString类equals方法比较不区分大小写的字符串,问题在与CaseString的equals方法知道普通类型的字符串,但是String的equals方法并不知道CaseString,比较时违背了对称性。
public class EqualsDemo {

    public static void main(String[] args) {

        CaseString caseString = new CaseString("Case");
        String string = "case";
        System.out.println(caseString.equals(string));
        System.out.println(string.equals(caseString));
    }
}

final class CaseString {
    private final String s;

    public CaseString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseString) {
            return s.equalsIgnoreCase(((CaseString) obj).s);
        }
        if (obj instanceof String) {
            return s.equalsIgnoreCase((String) obj);
        }
        return false;
    }
}

 解决方式:equals方法中去除与String类型比较的部分。

@Override
    public boolean equals(Object obj) {
        return obj instanceof CaseString && s.equalsIgnoreCase(((CaseString) obj).s);
    }

传递性:

  子类增加的信息会影响equals的比较结果,我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。但是可以在抽象类的子类增加新的组件且不违反equals的规定。

一致性:

  如果两个对象相等,他们就要一直保持相等,所以不要使equals方法依赖不可靠的资源。

非空性:

  指所有的对象都不能等于null,大多数情况下一个非空对象equals null,都会返回false(返回true的情况我还没有想到),很多类的equals方法中为了避免抛出NPE,会先判读入参是否为null,其实这是没有必要的,因为equals方法在比较之前,必须是呀instanceof判断Object类型的入参是否为比较类型子类的对象,如果instanceof的第一个操作数是null,那么不管第二个操作数是什么类型,都会返回false,所以不需要显式的null检查。

ps:

1.对于非float和double类型的基本数据类型,可以使用==比较,对于float和double类型,可以使用Float.compare(x,y)和Double.compare(x,y),因为存在Float.NAN,-0.0f之类的常量,如果使用Float.equals()或Double.equals()会对基本类型进行装箱操作,降低性能.

2.不要将equals方法参数中Object对象替换成其他类型的对象,如果替换的话,它将不会重写Object.equals()而是重载。

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

在每个覆盖了equals方法的类中都要覆盖hashCode方法,否则这种类对象元素的散列集合(如HashMap、HashSet)将无法正常运行。

例:


public class EqualsDemo {

public static void main(String[] args) {

CaseString caseString = new CaseString("Case");
String string = "case";
CaseString caseString1 = new CaseString("Case");
System.out.println(caseString.equals(caseString1));
HashMap<CaseString, Integer> map = new HashMap<>();
map.put(caseString, 1);
map.put(caseString1, 2);
System.out.println(map);
CaseString caseString3 = new CaseString("test");
CaseString caseString4 = new CaseString("test");
map.put(caseString3, 3);
System.out.println(map.get(caseString4));

}
}

final class CaseString {
private final String s;

public CaseString(String s) {
this.s = Objects.requireNonNull(s);
}

@Override
public boolean equals(Object obj) {
return obj instanceof CaseString && s.equalsIgnoreCase(((CaseString) obj).s);
}
}
 

输出:

equals和HashCode比较规则:

  equals相等->hashCode一定相等

  hashCode相等->equals不一定相等

  hashCode不等->equals一定不相等

所以在比较时,会优先比较hashCode,hashCode相等后再比较equals,所以不同的对象生成不同的hash值,会提高比较的性能。

  第12条:始终要覆盖toString方法

返回值的关注的信息,易于调试。

  第13条:谨慎地覆盖clone

Cloneable接口是一个标记接口,表示实现了这个接口的类可以被克隆,object的clone方法返回该对象的逐级拷贝,否则抛出CloneNotSupportedException异常。实现cloneable接口的类是为了提供一个功能适当的公有clone方法,clone方法无需调用构造器就可以创建对象。

clone方法约定:

1. x.clone != x

2. x.clone.getClass == x.getClass

3. x.clone.equals(x) == true

  对于不可变的类,永远不应该提供clone方法,因为会激发不必要的克隆。

  clone方法相当于另一个构造器;必须确保原始的对象和克隆的对象不会互相伤害。如果对象包含的域中,引用了可变的对象,并且只实现简单的clone,会导致修改原始的实例会破坏克隆对象中的约束条件。

public class CloneDemo {

    public static void main(String[] args) throws CloneNotSupportedException {

        Clo clo = new Clo();
        System.out.println(Arrays.toString(clo.getArray()));
        Clo cloCopy = clo.clone();
        System.out.println(Arrays.toString(cloCopy.getArray()));
        clo.getArray()[0] = 9;
        System.out.println(Arrays.toString(cloCopy.getArray()));
    }

}

class Clo implements Cloneable {

    private int[] array;

    public Clo() {
        this.array = new int[]{1, 2, 3};
    }

    public int[] getArray() {
        return array;
    }

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

输出:

 可以看到修改原始对象数组的值,同时影响了克隆后对象的数组的值,为了使clone方法正常工作,应递归的调用clone,clone方法修改为如下:

  @Override
    protected Clo clone() throws CloneNotSupportedException {
        Clo clo =  (Clo) super.clone();
        clo.array = array.clone();
        return clo;
    }

输出:

   ps:要注意,如果array的类型是final的上述方法无法正常工作,因为clone方法是禁止给final域赋新值的。

   克隆负责对象的最后方法是,先调用super.clone,然后爸结果对象中的所有域设为初始状态,再调用高层方法重新赋值,同时要注意,不要在clone方法的构造过程中,调用被子类覆盖的方法,如果调用了在子类中覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先被执行,可能会导致克隆对象和原始对象的不一致。

  线程安全的类的clone方法也必须做到同步。

  对象拷贝的更好的方法是实现一个拷贝构造器或拷贝工厂:

  public Yum(Yum yum){ ... }

  public static Yum newInstance (Yum yum){ ... }

    第14条:考虑实现Comparable接口

  对排序敏感的类可以kaol实现Compareable接口

  compareTo约定:

  • 确保所有的x,y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
  • 比较关心可传递:x.compareTo(y) > 0,y.compareTo(z) > 0  --> x.compareTo(z) > 0
  • 确保x.compareTo(y) == 0 --> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • 强烈建议 (x.compareTo(y) == 0) == (x.equals(y)),如果遵守这一条,那么compareTo方法所施加的顺序关系就被认为与equlas一致。

 

posted @ 2020-01-05 16:45  _绵绵  阅读(314)  评论(0)    收藏  举报