編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議47~51)

阿赫瓦里發表於2016-09-15

建議47:在equals中使用getClass進行型別判斷

   本節我們繼續討論覆寫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。

建議48:覆寫equals方法必須覆寫hashCode方法

 覆寫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方法?因為雜湊碼的生成有很多種演算法,自己寫麻煩,事兒又多,所以必要的時候才取"拿來主義",不重複造輪子是最好的辦法。)

建議49:推薦覆寫toString方法

   為什麼要覆寫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方法,這樣除錯會更方便哦。

建議50:使用package-info類為包服務

   Java中有一個特殊的類:package-info類,它是專門為本包服務的,為什麼說它特殊,主要體現在三個方面:

  1. 它不能隨便建立:在一般的IDE中,Eclipse、package-info等檔案是不能隨便被建立的,會報"Type name is notvalid"錯誤,類名無效。在Java中變數定義規範中規定如下字元是允許的:字母、數字、下劃線,以及那個不怎麼寫的$符號,不過中劃線可不在之列,那麼怎麼建立這個檔案呢?很簡單,用記事本建立一個,然後拷貝進去再改一下就成了,更直接的辦法就是從別的專案中拷貝過來。
  2. 它服務的物件很特殊:一個類是一類或一組事物的描述,比如Dog這個類,就是描述"阿黃"的,那package-info這個類描述的是什麼呢?它總是要有一個被描述或陳述的物件吧,它是描述和記錄本包資訊的。
  3. package-info類不能有實現程式碼:package-info類再怎麼特殊也是 一個類,也會被編譯成 package-info.class,但是在package-info.java檔案不能宣告package-info類。   

   package-info類還有幾個特殊的地方,比如不可以繼承,沒有介面,沒有類間關係(關聯、組合、聚合等)等,Java中既然有這麼特殊的一個類,那肯定有其特殊的作用了,我們來看看它的特殊作用,主要表現在以下三個方面:

  • 宣告友好類和包內訪問常量:這個比較簡單,而且很實用,比如一個包中有很多內部訪問的類或常量,就可以統一放到package-info類中,這樣很方便,便於集中管理,可以減少友好類到處遊走的情況,程式碼如下:
class PkgClazz {
        public void test() {
        }
    }
    
    class PkgConstant {
        static final String PACKAGE_CONST = "ABC";
    }

  注意以上程式碼是放在package-info.java中的,雖然它沒有編寫package-info的實現,但是package-info.class類檔案還是會生成。通過這樣的定義,我們把一個包需要的常量和類都放置在本包下,在語義上和習慣上都能讓程式設計師更適應。

  • 為在包上提供註解提供便利:比如我們要寫一個註解(Annotation),檢視一下包下的物件,只要把註解標註到package-info檔案中即可,而且在很多開源專案中也採用了此方法,比如struts2的@namespace、hibernate的@FilterDef等.
  • 提供包的整體註釋說明:如果是分包開發,也就是說一個包實現了一個業務邏輯或功能點或模組或元件,則該包需要一個很好的說明文件,說明這個包是做什麼用的,版本變遷歷史,與其他包的邏輯關係等,package-info檔案的作用在此就發揮出來了,這些都可以直接定義到此檔案中,通過javadoc生成文件時,會吧這些說明作為包文件的首頁,讓讀者更容易對該包有一個整體的認識。當然在這點上它與package.html的作用是相同的,不過package-info可以在程式碼中維護文件的完整性,並且可以實現程式碼與文件的同步更新。  

  建立package-info,也可以利用IDE工具如下圖:

  

解釋了這麼多,總結成一句話:在需要用到包的地方,就可以考慮一下package-info這個特殊類,也許能起到事半功倍的作用。

建議51:不要主動進行垃圾回收

   很久很久以前,在java1.1的年代裡,我們經常會看到System.gc這樣的呼叫---主動對垃圾進行回收,不過,在Java知識深入人心後,這樣的程式碼就逐漸銷聲匿跡了---這是好現象,因為主動進行垃圾回收是一個非常危險的動作。

  之所以危險,是因為System.gc要停止所有的響應,才能檢查記憶體中是否存在可以回收的物件,這對一個應用系統來說風險極大,如果是一個Web應用,所有的請求都會暫停,等待垃圾回收器執行完畢,若此時堆記憶體(heap)中的物件少的話還可以接受,一但物件較多(現在的web專案是越做越大,框架、工具也越來越多,載入到記憶體中的物件當然也就更多了),這個過程非常耗時,可能是0.01秒,也可能是1秒,甚至20秒,這就嚴重影響到業務的執行了。

  例如:我們寫這樣一段程式碼:new String("abc"),該物件沒有任何引用,對JVM來說就是個垃圾物件。JVM的垃圾回收器執行緒第一次掃描(掃描時間不確定,在系統不繁忙的時候執行)時給它貼上一個標籤,說"你是可以回收的",第二次掃描時才真正的回收該物件,並釋放記憶體空間,如果我們直接呼叫System.gc,則是說“嗨,你,那個垃圾回收器過來檢查一下有沒有垃圾物件,回收一下”。瞧瞧看,程式主動找來垃圾回收器,這意味著正在執行的系統要讓出資源,以供垃圾回收器執行,想想看吧,它會把所有的物件都檢查一遍,然後處理掉那些垃圾物件。注意哦,是檢查每個物件。

  不要呼叫System.gc,即使經常出現記憶體溢位也不要呼叫,記憶體溢位是可分析的,是可以查詢原因的,GC可不是一個好招數。

相關文章