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

来源:http://www.cnblogs.com/selene/archive/2016/09/15/5865784.html
-Advertisement-
Play Games

建議47:在equals中使用getClass進行類型判斷 本節我們繼續討論覆寫equals的問題,這次我們編寫一個員工Employee類繼承Person類,這很正常,員工也是人嘛,而且在JavaBean中繼承也很多見,代碼如下: 員工類增加了工號ID屬性,同時也覆寫了equals方法,只有在姓名和 ...


建議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可不是一個好招數。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • js代碼: ...
  • 使用SendMessage向另一進程發送WM_COPYDATA消息 Send端: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; u ...
  • R軟體功能非常強大,可以很好的進行各類統計,並能輸出圖形。下麵介紹一種R語言和C#進行通信的方法,並將R繪圖結果顯示到WinForm UI界面上。 1 前提準備 安裝R軟體,需要安裝32位的R軟體,64位的調用會報錯。另外就是講R添加到電腦環境變數中。 打開R軟體,安裝包 scatterplot3d ...
  • 經過四天的努力終於將SqlSugar ORM 成功支持ORACLE資料庫 優點: 1、高性能,達到原生最高水準,比SqlHelper性能要高,比Dapper快30% 比EF快50% 2、支持多種資料庫 ,sql版本更新最快,其它會定期更新,可以在多種資料庫用一種編程方式 3、支持.net Core ...
  • 接觸Asp.net boilerplate 一段時間,一次同事將他的代碼添加到zero項目模板中,他將路由配置成他的頁面,目的是要讓zero項目登錄成功之後跳轉到他的頁面,可是通過fiddler監視請求報了一個錯誤 後來得知CSRF,翻閱下ABP Documents,瞭解下 介紹 (CSRF)跨站點 ...
  • 簡單類型參數 Example 1: Sending a simple parameter in the Url [RoutePrefix("api/values")] public class ValuesController : ApiController { // http://localhos... ...
  • 簡單的說,C#已經內置了一些類,我們可以利用這些類來訪問資料庫。在這裡,我們假設讀者已經熟悉SqlServer資料庫或者其它資料庫(我以後也會補上相關內容)。我們如何來實現這項技術呢?大致可以分為三個步驟:1、連接資料庫 2、設置操作/命令 3、執行操作。現分述如下: 1、連接資料庫 連接資料庫我們 ...
  • 最近在看《演算法導論》這本書,在練習題當中發現了這樣的一個問題:使用二分查找法來實現插入排序,由於之前的內容當中有講解二分法的遞歸實現,所以在這便將它們結合起來希望解決這個問題。閑話不多說了,直接上代碼: 演算法思路很簡單,無非是將原來的線性查找被排序元素的合適的位置的部分換成了使用二分法來查找合適的位 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...