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

來源:互聯網
上載者:User

標籤:setname   社會   處理機   語義   array   底線   公司   www   文檔   

閱讀目錄

  • 建議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     @Override18     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     @Override44     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]如果機器不同,@後面的內容也會不同,但格式都是相同的:類名[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檔案中即可,而且在很多開源項目中也採用了此方法,比如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可不是一個好招數。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.