本節我們繼續討論覆寫equals的問題,這次我們編寫一個員工Employee類繼承Person類,這很正常,員工也是人嘛,而且在JavaBean中繼承也很多見,代碼如下:
1 public class Employee extends Person { 2 private int id; 3 4 public Employee(String _name, int _id) { 5 super(_name); 6 id = _id; 7 } 8 9 public int getId() { 10 return id; 11 } 12 13 public void setId(int id) { 14 this.id = id; 15 } 16 17 @Override 18 public boolean equals(Object obj) { 19 if (obj instanceof Employee) { 20 Employee e = (Employee) obj; 21 return super.equals(obj) && e.getId() == id; 22 } 23 return false; 24 } 25 26 } 27 28 class Person { 29 private String name; 30 31 public Person(String _name) { 32 name = _name; 33 } 34 35 public String getName() { 36 return name; 37 } 38 39 public void setName(String name) { 40 this.name = name; 41 } 42 43 @Override 44 public boolean equals(Object obj) { 45 if (obj instanceof Person) { 46 Person p = (Person) obj; 47 if (null == p.getName() || null == name) { 48 return false; 49 } else { 50 return name.equalsIgnoreCase(p.getName()); 51 } 52 } 53 return false; 54 } 55 }
員工類增加了工號ID屬性,同時也覆寫了equals方法,只有在姓名和ID都相同的情況下才表示同一個員工,這是為了避免一個公司中出現同名同姓員工的情況。看看上面的代碼,這裡的條件已經相當完善了,應該不會出錯了,那我們測試一下,代碼如下:
1 public static void main(String[] args) { 2 Employee e1 = new Employee("張三", 100); 3 Employee e2 = new Employee("張三", 1000); 4 Person p1 = new Person("張三"); 5 System.out.println(p1.equals(e1)); 6 System.out.println(p1.equals(e2)); 7 System.out.println(e1.equals(e2)); 8 }
上面定義了兩個員工和一個社會閒雜人員,雖然他們同名同姓,但肯定不是同一個,輸出都應該是false,但運行之後結果為: true true false
很不給力呀,p1竟然等於e1,也等於e2,為什麼不是同一個類的兩個實例竟然也會相等呢?這很簡單,因為p1.equals(e1)是調用父類Person的equals方法進行判斷的,它使用的是instanceof關鍵字檢查e1是否是Person的實例,由於兩者村子繼承關系,那結果當然是true了,相等也就沒有任何問題了,但是反過來就不成立了,e1和e2是不可能等於p1,這也是違反對稱性原則的一個典型案例。
更玄的是p1與e1、e2相等,但e1和e2卻不相等,似乎一個簡單的符號傳遞都不能實現,這才是我們分析的重點:e1.equals(e2)調用的是子類Employee的equals方法,不僅僅要判斷姓名相同,還要判斷Id相同,兩者工號是不同的,不相等也是自然的了。等式不傳遞是因為違反了equals的傳遞性原則,傳遞性原則指的是對於實例對象x、y、z來說,如果x.equals(y)返回true,y.equals(z)返回true,那麼x.equals(z)也應該返回true。
這種情況發生的關鍵是父類引用了instanceof關鍵字,它是用來判斷一個類的實例對象的,這很容易讓子類鑽空子。想要解決也很簡單,使用getClass來代替instanceof進行類型判斷,Person的equals方法修改後如下所示:
@Override public boolean equals(Object obj) { if (null != obj && obj.getClass() == this.getClass()) { Person p = (Person) obj; if (null == p.getName() || null == name) { return false; } else { return name.equalsIgnoreCase(p.getName()); } } return false; }
當然,考慮到Employee也有可能被繼承,也需要把它的instanceof修改為getClass。總之,在覆寫equals時建議使用getClass進行類型判斷,而不要使用instanceof。
覆寫equals方法必須覆寫hasCode方法,這條規則基本上每個Javaer都知道,這也是JDK的API上反復說明的,不過為什麼要則這麼做呢?這兩個方法之間什麼關系呢?本建議就來解釋該問題,我們先看看代碼:
public class Client48 { public static void main(String[] args) { // Person類的實例作為map的key Map<Person, Object> map = new HashMap<Person, Object>() { { put(new Person("張三"), new Object()); } }; // Person類的實例作為List的元素 List<Person> list = new ArrayList<Person>() { { add(new Person("張三")); } }; boolean b1 = list.contains(new Person("張三")); boolean b2 = map.containsKey(new Person("張三")); System.out.println(b1); System.out.println(b2); } }
代碼中的Person類與上一建議的Person相同,equals方法完美無缺。在這段代碼中,我們在聲明時直接調用方法賦值,這其實也是一個內部匿名類,現在的問題是b1和b2值是否都為true?
我們先來看b1,Person類的equals覆寫了,不再判斷兩個地址相等,而是根據人員的姓名來判斷兩個對象是否相等,所以不管我們的new Person("張三")產生了多少個對象,它們都是相等的。把張三放入List中,再檢查List中是否包含,那結果肯定是true了。
接下來看b2,我們把張三這個對象作為了Map的鍵(Key),放進去的是張三,檢查的對象還是張三,那應該和List的結果相同了,但是很遺憾,結果為false。原因何在呢?
原因就是HashMap的底層處理機制是以數組的方式保存Map條目的(Map Entry)的,這其中的關鍵是這個數組的下標處理機制:依據傳入元素hashCode方法的返回值決定其數組的下標,如果該數組位置上已經有Map條目,並且與傳入的值相等則不處理,若不相等則覆蓋;如果數組位置沒有條目,則插入,並加入到Map條目的鏈表中。同理,檢查鍵是否存在也是根據哈希碼確定位置,然後遍歷查找鍵值的。
接著深入探討,那對象元素的hashCode方法返回的是什麼值呢?它是一個對象的哈希碼,是由Object類的本地方法生成的,確保每個對象有一個哈希碼(也是哈希算法的基本要求:任意輸入k,通過一定算法f(k),將其轉換為非可逆的輸出,對於兩個輸入k1和k2,要求若k1=k2,則必須f(k1)=f(k2),但也允許k1 != k2 , f(k1)=f(k2)的情況存在)。
那回到我們的例子上,由於我們沒有覆寫hashCode方法,兩個張三對象的hashCode方法返回值(也就是哈希碼)肯定是不相同的了,在HashMap的數組中也找不到對應的Map條目了,於是就返回了false。
問題清楚了,修改也很簡單,在Person類中重寫一下hashCode方法即可,代碼如下:
class Person{ @Override public int hashCode() { return new HashCodeBuilder().append(name).toHashCode(); } }
其中HashCodeBuilder是org.apache.commons.lang.builder包下的一個哈希碼生成工具,使用起來非常方便,大家可以直接項目中集成(為何不直接寫hashCode方法?因為哈希碼的生成有很多種算法,自己寫麻煩,事兒又多,所以必要的時候才取"拿來主義",不重復造輪子是最好的辦法。)
為什麼要覆寫toString方法,這個問題很簡單,因為Java提供的默認toString方法不友好,打印出來看不懂,不覆寫不行,看這樣一段代碼:
public class Client49 { public static void main(String[] args) { System.out.println(new Person("張三")); } } class Person { private String name; public Person(String _name) { name = _name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
輸出結果是:Perso@188edd79.如果機器不同,@後面的內容也會不同,但格式都是相同的:類名+@+hashCode,這玩意是給機器看的,人哪能看懂呀!這就是因為我們沒有覆寫Object類的toString方法的緣故,修改一下,代碼如下:
@Override public String toString() { return String.format("%s.name=%s", this.getClass(),name); }
如此即就可以在需要的時候輸出調試信息了,而且非常友好,特別是在bean流行的項目中(一般的Web項目就是這樣),有了這樣的輸出才能更好地debug,否則查找錯誤就有點麻煩!當然,當bean的屬性較多時,自己實現就不可取了,不過可以直接使用apache的commons工具包中的ToStringBuilder類,簡潔,實用又方便。可能有人會說,為什麼通過println方法打印一個對象會調用toString方法?那是源於println的打印機制:如果是一個原始類型就直接打印,如果是一個類類型,則打印出其toString方法的返回值,如此而已。同時現在IDE也很先進,大家debug時也可查看對象的變量,但還是建議大家覆寫toString方法,這樣調試會更方便哦。
Java中有一個特殊的類:package-info類,它是專門為本包服務的,為什麼說它特殊,主要體現在三個方面:
package-info類還有幾個特殊的地方,比如不可以繼承,沒有接口,沒有類間關系(關聯、組合、聚合等)等,Java中既然有這麼特殊的一個類,那肯定有其特殊的作用了,我們來看看它的特殊作用,主要表現在以下三個方面:
class PkgClazz { public void test() { } } class PkgConstant { static final String PACKAGE_CONST = "ABC"; }
注意以上代碼是放在package-info.java中的,雖然它沒有編寫package-info的實現,但是package-info.class類文件還是會生成。通過這樣的定義,我們把一個包需要的常量和類都放置在本包下,在語義上和習慣上都能讓程序員更適應。
創建package-info,也可以利用IDE工具如下圖:
解釋了這麼多,總結成一句話:在需要用到包的地方,就可以考慮一下package-info這個特殊類,也許能起到事半功倍的作用。
很久很久以前,在java1.1的年代裡,我們經常會看到System.gc這樣的調用---主動對垃圾進行回收,不過,在Java知識深入人心後,這樣的代碼就逐漸銷聲匿跡了---這是好現象,因為主動進行垃圾回收是一個非常危險的動作。
之所以危險,是因為System.gc要停止所有的響應,才能檢查內存中是否存在可以回收的對象,這對一個應用系統來說風險極大,如果是一個Web應用,所有的請求都會暫停,等待垃圾回收器執行完畢,若此時堆內存(heap)中的對象少的話還可以接受,一但對象較多(現在的web項目是越做越大,框架、工具也越來越多,加載到內存中的對象當然也就更多了),這個過程非常耗時,可能是0.01秒,也可能是1秒,甚至20秒,這就嚴重影響到業務的運行了。
例如:我們寫這樣一段代碼:new String("abc"),該對象沒有任何引用,對JVM來說就是個垃圾對象。JVM的垃圾回收器線程第一次掃描(掃描時間不確定,在系統不繁忙的時候執行)時給它貼上一個標簽,說"你是可以回收的",第二次掃描時才真正的回收該對象,並釋放內存空間,如果我們直接調用System.gc,則是說“嗨,你,那個垃圾回收器過來檢查一下有沒有垃圾對象,回收一下”。瞧瞧看,程序主動找來垃圾回收器,這意味著正在運行的系統要讓出資源,以供垃圾回收器執行,想想看吧,它會把所有的對象都檢查一遍,然後處理掉那些垃圾對象。注意哦,是檢查每個對象。
不要調用System.gc,即使經常出現內存溢出也不要調用,內存溢出是可分析的,是可以查找原因的,GC可不是一個好招數。