1. 程式人生 > >轉載---編寫高質量代碼:改善Java程序的151個建議(第3章:類、對象及方法___建議47~51)

轉載---編寫高質量代碼:改善Java程序的151個建議(第3章:類、對象及方法___建議47~51)

閱讀 aer 集中 記事本 哈希算法 讀者 堆內存 ref 類型判斷

閱讀目錄

  • 建議47:在equals中使用getClass進行類型判斷
  • 建議48:覆寫equals方法必須覆寫hashCode方法
  • 建議49:推薦覆寫toString方法
  • 建議50:使用package-info類為包服務
  • 建議51:不要主動進行垃圾回收
回到頂部

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

}
技術分享

  輸出結果是:[email protected],@後面的內容也會不同,但格式都是相同的:類名+@+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文件中即可,而且在很多開源項目中也采用了此方法,[email protected]@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可不是一個好招數。

轉載---編寫高質量代碼:改善Java程序的151個建議(第3章:類、對象及方法___建議47~51)