2.對於所有對象都通用的方法_EJ
第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); } }
2.對於所有對象都通用的方法_EJ