欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

2.对于所有对象都通用的方法_EJ

程序员文章站 2022-07-01 17:27:16
第8条: 覆盖equals时请遵守通用约定 我们在覆盖equals方法时,必须遵守它的通用约定: 1.自反性。对于任何非null的引用值x,x.equals(x)必须返回true; 2.对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须 ......

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

我们在覆盖equals方法时,必须遵守它的通用约定:

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

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

一个“不区分大小写”字符串的例子:

public class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s){
        if(s == null){
            throw new NullPointerException();
        }
        this.s = s;
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof CaseInsensitiveString){
            return s.equalsIgnoreCase(((CaseInsensitiveString)obj).s);
        }
        if(obj instanceof String){
            return s.equalsIgnoreCase((String)obj);
        }
        return false;
    }
    public static void main(String[] args){
        CaseInsensitiveString s1 = new CaseInsensitiveString("xxx");
        String s2 = "xxx";
        System.out.println(s1.equals(s2));  //true
        System.out.println(s2.equals(s1));  //false
    }
}

该例子明显违反了对称性。

3.传递性。对于任何非null的引用值x,y和z。如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equas(z)也必须返回true。

一个违反对称性的例子:

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;
    }
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result  + x;
        result = 31 * result  + y;
        return result;
    }
}
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 Point)){
            return false;
        }
        //if obj is a normal Point, do a color-blind comparison
        if(!(obj instanceof ColorPoint)){
            return obj.equals(this);
        }
        return super.equals(obj) && ((ColorPoint)obj).color == color;
    }
    public static void main(String[] args){
        ColorPoint p1 = new ColorPoint(1,3, Color.RED);
        Point p2 = new Point(1,3);
        ColorPoint p3 = new ColorPoint(1,3, Color.BLUE);
        System.out.println(p1.equals(p2));  //true
        System.out.println(p2.equals(p3));    //true
        System.out.println(p1.equals(p3));  //false
    }
}

enum Color{
    RED, GREEN, BLUE;
}

上面的例子提供了对称性,但却牺牲了传递性。我们无法在扩展可实例化类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

4.一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用到的信息没有被改变,多次调用x.equas(y)就会一致地返回true或false;

5.非空性。对于任何非null的引用值x,x.equas(null)必定会返回false。

综上,实现高质量equals方法的诀窍:

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

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

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

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

String类中的equals方法:

    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;
    }

 

第9条: 覆盖equals时总要覆盖hashCode方法

首先明确一个概念,两个对象使用equals返回true,则它们的hashCode也一定相等;如果两个对象的hashCode相等,则它们的equals则不一定相等。

考虑一个简单的PhoneNumber类:

public 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, 9999, "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 obj) {
        if(obj == this){
            return true;
        }
        if(!(obj instanceof PhoneNumber)){
            return false;
        }
        PhoneNumber pn = (PhoneNumber) obj;
        return pn.lineNumber == lineNumber
            && pn.prefix == prefix
            && pn.areaCode == areaCode;
    }
    public static void main(String[] args){
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(new PhoneNumber(707, 867, 5039), "Kevin");
        String name = m.get(new PhoneNumber(707, 867, 5039)); //如果PhoneNumber类不实现hashCode方法,则返回null
        System.out.println(name);
    }
}

main方法测试返回为null,这是因为该类没有实现hashCode方法,导致两个相等的实例具有不同的散列码。put方法把电话号码对象存放在一个散列通中,而get方法却在另外一个桶中查找这个电话号码。即使两个对象正好被放在一个桶中,get方法也必定会返回为null,因为hashMap有项优化,可以把每个项有关的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。所以需要提供一个hashCode方法:

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

再次用main方法测试,返回“Kevin”。

 

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

虽然java.lang.Object提供了一个toString方法,但返回的字符串通常不是用户希望的信息。应该返回一个“简洁的,信息丰富,并且易于阅读的表达形式”。在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息譬如之前电话号码的例子:

@Override
    public String toString() {
        return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
    }

 

第11条: 谨慎地覆盖clone

 Cloneable并没有包含任何方法,那到底有什么作用呢?它决定了Object中受保护clone方法的实现行为,如果一个类实现了cloneable,Object的clone方法就返回该对象中的逐域拷贝,否则就会抛出cloneNotSupportedException异常。clone带来的问题很多,所以可以不用clone方法尽量不用。clone方法也分浅复制和深复制,这里分别举点例子。

浅复制:

public class Stack implements Cloneable{
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack(){
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e){
        elements[size++] = e;
    }
    public Object pop(){
        if(size == 0){
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;  //清空过期引用,不然会导致内存泄漏
        return result;
    }
    private void ensureCapacity(){
        if(elements.length == size){
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
    @Override
    protected Stack clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        Stack result = (Stack) super.clone();
//        result.elements = elements.clone();
        return result;
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        Stack s = new Stack();
        PhoneNumber pn = new PhoneNumber(111, 222, 3333);
        s.push(pn);
        Stack s2 = (Stack) s.clone();
        System.out.println(s.pop());  // (111) 222-3333
        System.out.println(s2.pop()); //  null 浅复制,s和s2拥有相同的elements引用,s的pop方法清空了过期引用,所以s2的pop方法返回null
    }
}

深复制:

public class HashTable {
    private Entry[] buckets;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    private static class Entry{
        final Object key;
        Object value;
        Entry next;
        Entry(Object key, Object value, Entry next){
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //recursively copy the linked list headed by this Entry
        //递归方法针对列表中的每个元素,它都要消耗一段内存空间,如果链表比较长,则容易导致栈溢出
        //可以用下面的迭代方式进行对象的深复制
//        Entry deepCopy(){
//            return new Entry(key, value, next == null ? null : next.deepCopy());
//        }
        
        //iteratively copy the linked list headed by this Entry
        Entry deepCopy(){
            Entry result = new Entry(key, value, next);
            for(Entry p = result; p.next != null; p = p.next){
                p.next = new Entry(p.next.key, p.next.value, p.next.next);
            }
            return result;
        }
    }
    public HashTable(){
        buckets = new Entry[DEFAULT_INITIAL_CAPACITY];
    }
    
    //Broken - results in shared internal state!
    /*@Override
    protected HashTable clone() throws CloneNotSupportedException {
        HashTable result = (HashTable) super.clone();
        result.buckets = buckets.clone();
        return result;
    }*/
    @Override
    protected HashTable clone() throws CloneNotSupportedException {
        HashTable result = (HashTable) super.clone();
        result.buckets = new Entry[buckets.length];
        for(int i = 0; i < buckets.length; i++){
            if(buckets[i] != null){
                result.buckets[i] = buckets[i].deepCopy();
            }
        }
        return result;
        
    }
}

 

 

第12条: 考虑实现Comparable接口

关于Comparable接口其中只有一个方法——compareTo。此方法和equals有类似之处,不过它所表达的含义相比equals要更多。equals通常是比较两个值是否相等,相等返回true,不相等返回false。compareTo则约定为第1对象若“大于”第2个对象则返回整数,“等于”则返回0,“小于”则返回负数,compareTo能约定更为复杂的“比较”,例如比较两个字符串进行字典序的比较,str = “abc”, str2 = “abd”,str.equals(str2)返回false,而str.compareTo(str2)则返回正数。compareTo与equals一样同样需要遵守自反性、对称性、传递性。同样有一个强烈的建议就是compareTo应该返回和equals方法相同的结果,但如果不一致,也不是不可以,就是最好能在注释中写明两个方法返回的结果不同。

CompareTo方法中域的比较是顺序的比较,而不是等同性的比较,比较对象引用域可以递归调用compareTo方法,如果一个域没有实现Comparable接口,或者你需要一个非标准的排序方式,就可以用一个显示的comparator来代替。或者编写自己的comparator,或者使用已有的comparator。

public class CaseInsensitiveString2 implements Comparable<CaseInsensitiveString2>{
    private final String s;
    public CaseInsensitiveString2(String s){
        if(s == null){
            throw new NullPointerException();
        }
        this.s = s;
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof CaseInsensitiveString2){
            return s.equalsIgnoreCase(((CaseInsensitiveString2)obj).s);
        }
        if(obj instanceof String){
            return s.equalsIgnoreCase((String)obj);
        }
        return false;
    }
    public static void main(String[] args){
        CaseInsensitiveString2 s1 = new CaseInsensitiveString2("xxx");
        String s2 = "xxx";
        System.out.println(s1.equals(s2));  //true
        System.out.println(s2.equals(s1));  //false
    }
    @Override
    public int compareTo(CaseInsensitiveString2 o) {
        // TODO Auto-generated method stub
        return String.CASE_INSENSITIVE_ORDER.compare(s, o.s);
    }
}