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

來源:互聯網
上載者:User

標籤:alt   需要   tput   javabean   不能   input   any   部分   基於   

閱讀目錄

  • 建議41:讓多重繼承成為現實
  • 建議42:讓工具類不可執行個體化
  • 建議43:避免對象的淺拷貝
  • 建議44:推薦使用序列化對象的拷貝
  • 建議45:覆寫equals方法時不要識別不出自己
  • 建議46:equals應該考慮null值情景
回到頂部建議41:讓多重繼承成為現實

  在Java中一個類可以多重實現,但不能多重繼承,也就是說一個類能夠同時實現多個介面,但不 能同時繼承多個類。但有時候我們確實需要繼承多個類,比如希望擁有多個類的行為功能,就很難使用單繼承來解決問題了(當然,使用多繼承是可以解決的)。幸 運的是Java中提供的內部類可以曲折的解決此問題,我們來看一個案例,定義一個父親、母親介面,描述父親強壯、母親溫柔的理想情形,代碼如下: 

public interface Father {    public int strong();}interface Mother {    public int kind();}

  其中strong和kind的傳回值表示強壯和溫柔的指數,指數越高強壯和溫柔也就越高,這與遊戲中設定人物的屬性是一樣的,我們繼續開看父親、母親這兩個實現:  

class FatherImpl implements Father {    // 父親的強壯指數為8    @Override    public int strong() {        return 8;    }}class MotherImpl implements Mother {    // 母親的溫柔指數為8    @Override    public int kind() {        return 8;    }}

  父親的強壯指數為8,母親的溫柔指數也為8,門當戶對,那他們生的兒子和女兒一定更優秀了,我們看看兒子類,代碼如下:

class Son extends FatherImpl implements Mother {    @Override    public int strong() {        // 兒子比父親強壯        return super.strong() + 1;    }    @Override    public int kind() {        return new MotherSpecial().kind();    }    private class MotherSpecial extends MotherImpl {        @Override        public int kind() {            // 兒子的溫柔指數降低了            return super.kind() - 1;        }    }}

  兒子繼承自父親,變得比父親更強壯了(覆寫父類strong方法),同時兒子也具有母親的優點,只是 溫柔指數降低了。注意看,這裡構造了MotherSpecial類繼承母親類,也就是獲得了母親類的行為和方法,這也是內部類的一個重要特性:內部類可以 繼承一個與外部類無關的類,保證了內部類的獨立性,正是基於這一點,多重繼承才會成為可能。MotherSpecial的這種內部類叫做成員內部類(也叫 作執行個體內部類,Instance Inner Class),我們再來看看女兒類,代碼如下:

class Daughter extends MotherImpl implements Father {    @Override    public int strong() {        return new FatherImpl() {            @Override            public int strong() {                //女兒的強壯指數降低了                return super.strong() - 2;            }        }.strong();    }}

  女兒繼承了目前的溫柔指數,同時又覆寫了父親的強壯指數,不多解釋。注意看覆寫的strong方法,這裡是建立了一個匿名內部類(Anonymous Inner Class)來覆寫父類的方法,以完成繼承父親行為的功能。

  多重繼承指的是一個類可以同時從多與一個的父親那裡繼承行為與特徵,按照這個定義,我們的兒子類、女兒類都實現了從父親和母親那裡繼承所有的功能,應該屬於多重繼承,這完全歸功於內部類,大家在需要用到多重繼承的時候,可以思考一下內部類。

   在現實生活中,也確實存在多重繼承的問題,上面的例子說後人繼承了父親也繼承了母親的行為和特徵,再比如我國的特產動物"四不像"(學名麋鹿),其外 形" 似鹿非鹿,似馬非馬,似牛非牛,似驢非驢 ",這你想要用單繼承實現就比較麻煩了,如果用多繼承則可以很好地解決此問題:定義鹿、馬、牛、驢 四個類,然後建立麋鹿類的多個內部類,繼承他們即可。   

回到頂部建議42:讓工具類不可執行個體化

  Java項目中使用的工具類非常多,比如JDK自己的工具類 java.lang.Math、java.util.Collections等都是我們經常用到的。工具類的方法和屬性都是靜態,不需要產生執行個體即可訪 問,而且JDK也做了很好的處理,由於不希望被初始化,於是就設定了建構函式private的存取權限,表示出了類本身之外,誰都不能產生一個執行個體,我們 來看一下java.lang.Math代碼:

public final class Math {    /**     * Don‘t let anyone instantiate this class.     */    private Math() {}}

  之所以要將"Don‘t let anyone instantiate this class." 留下來,是因為Math的建構函式設定為了private:我就是一個工具類,我只想要其它類通過類名來訪問,我不想你通過執行個體對象來訪問。這在平台型或 架構項目中已經足夠了。但是如果已經告訴你不要這麼做了,你還要產生一個Math對象執行個體來訪問靜態方法和屬性(Java的反射是如此的發達,修改個構造 函數的存取權限易如反掌),那我就不保證正確性了,隱藏問題隨時都有可能爆發!那我們在項目中有沒有更好地限制辦法呢?有,即不僅僅設定成private 許可權,還拋出異常,代碼如下: 

class UtilsClazz{    public UtilsClazz(){        throw new Error("Don‘t instantiate "+getClass());    }}

  如此,才能保證一個工具類不會執行個體化,並且保證了所有的訪問都是通過類名來進行的。需要注意的一點是,此工具類最好不要做整合的打算,因為如果子類可以執行個體化的話,就要調用父類的建構函式,可是父類沒有可以被訪問的建構函式,於是問題就會出現。 

  注意:如果一個類不允許執行個體化,就要保證"平常" 渠道都不能執行個體它。 

回到頂部建議43:避免對象的淺拷貝

   我們知道一個類實現了Cloneable介面就表示它具備了被拷貝的能力。如果在覆寫clone()方法就會完全具備拷貝能力。拷貝是在記憶體中啟動並執行, 所以在效能方面比直接通過new產生的對象要快很多,特別是在大對象的產生上,這會使得效能的提升非常顯著。但是對象拷貝也有一個比較容易忽略的問題:淺 拷貝(Shadow Clone,也叫作影子拷貝)存在對象屬性拷貝不徹底的問題。我們來看這樣一段代碼:  

 1 public class Person implements Cloneable { 2     public static void main(String[] args) { 3         // 定義父親 4         Person f = new Person("父親"); 5         // 定義大兒子 6         Person s1 = new Person("大兒子", f); 7         // 小兒子的資訊時通過大兒子拷貝過來的 8         Person s2 = s1.clone(); 9         s2.setName("小兒子");10         System.out.println(s1.getName() + " 的父親是 " + s1.getFather().getName());11         System.out.println(s2.getName() + " 的父親是 " + s2.getFather().getName());12     }13     // 姓名14     private String name;15     // 父親16     private Person father;17 18     public Person(String _name) {19         name = _name;20     }21 22     public Person(String _name, Person _parent) {23         name = _name;24         father = _parent;25     }26 27     @Override28     public Person clone() {29         Person p = null;30         try {31             p = (Person) super.clone();32         } catch (CloneNotSupportedException e) {33             e.printStackTrace();34         }35         return p;36 37     }38     39   /*setter和getter方法略*/40 }

   程式中我們描述了這樣一個情境:一個父親,有兩個兒子,大小兒子同根同種,所以小兒子的對象就通過拷貝大兒子的對象來產生,運行輸出結果如下:

    大兒子 的父親是 父親
           小兒子 的父親是 父親

  這很正確,沒有問題。突然有一天,父親心血來潮想讓大兒子去認個乾爹,也就是大兒子的父親名稱需要重新設定一下,代碼如下:

    public static void main(String[] args) {        // 定義父親        Person f = new Person("父親");        // 定義大兒子        Person s1 = new Person("大兒子", f);        // 小兒子的資訊時通過大兒子拷貝過來的        Person s2 = s1.clone();        s2.setName("小兒子");        //認乾爹        s1.getFather().setName("乾爹");        System.out.println(s1.getName() + " 的父親是 " + s1.getFather().getName());        System.out.println(s2.getName() + " 的父親是 " + s2.getFather().getName());    }

  大兒子重新設定了父親名稱,我們期望的輸出結果是:將大兒子的父親名稱修改為乾爹,小兒子的父親名稱保持不變。運行一下結果如下:

    大兒子 的父親是 乾爹
           小兒子 的父親是 乾爹

  怎 麼回事,小兒子的父親也成了"乾爹"?兩個兒子都木有了,這老子估計被要被氣死了!出現這個問題的原因就在於clone方法,我們知道所有類都繼承自 Object,Object提供了一個對象拷貝的預設方法,即頁面代碼中 的super.clone()方法,但是該方法是有缺陷的,他提供的是一種淺拷貝,也就是說它並不會把對象的所有屬性全部拷貝一份,而是有選擇的拷貝,它 的拷貝規則如下:

  1. 基本類型:如果變數是基本類型,則拷貝其值。比如int、float等
  2. 對 象:如果變數是一個執行個體對象,則拷貝其地址引用,也就是說此時拷貝出的對象與原有對象共用該執行個體變數,不受存取權限的控制,這在Java中是很瘋狂的,因 為它突破了存取權限的定義:一個private修飾的變數,竟然可以被兩個不同的執行個體對象訪問,這讓java的存取權限體系情何以堪。
  3. String字串:這個比較特殊,拷貝的也是一個地址,是個引用,但是在修改時,它會從字串池(String pool)中重建新的字串,原有的字串對象保持不變,在此處我們可以認為String是一個基本類型。

  明白了這三個原則,上面的例子就很清晰了。小兒子的對象是通過大兒子拷貝而來的,其父親是同一個人,也就是同一個對象,大兒子修改了父親的名稱後,小兒子也很跟著修改了——於是,父親的兩個兒子都沒了。其實要更正也很簡單,clone方法的代碼如下: 

public Person clone() {        Person p = null;        try {            p = (Person) super.clone();            p.setFather(new Person(p.getFather().getName()));        } catch (CloneNotSupportedException e) {            e.printStackTrace();        }        return p;    }

  然後再運行,小兒子的父親就不會是乾爹了,如此就實現了對象的深拷貝(Deep Clone),保證拷貝出來的對象自成一體,不受"母體"影響,和new產生的對象沒有什麼區別。

  注意:淺拷貝只是Java提供的一種簡單拷貝機制,不便於直接使用。

回到頂部建議44:推薦使用序列化對象的拷貝

    上一建議說了對象的淺拷貝問題,試下Cloneable介面就具備了拷貝能力,那我們開思考這樣一個問題:如果一個項目中有大量的對象是通過拷貝產生 的,那我們該如何處理呢?每個類都系而一個clone方法,並且還有深拷貝?想想這是何等巨大的工作量呀!是否有更好的方法呢?

  其實,可以通過序列化方式來處理,在記憶體中通過位元組流的拷貝來實現,也就是把母對象寫到一個位元組流中,再從位元組流中將其讀出來,這樣就可以重建一個對象了,該新對象與母對象之間不存在引用共用的問題,也就相當於深拷貝了一個對象,代碼如下:  

 1 import java.io.ByteArrayInputStream; 2 import java.io.ByteArrayOutputStream; 3 import java.io.IOException; 4 import java.io.ObjectInputStream; 5 import java.io.ObjectOutputStream; 6 import java.io.Serializable; 7  8 public final class CloneUtils { 9     private CloneUtils() {10         throw new Error(CloneUtils.class + " cannot instance ");11     }12 13     // 拷貝一個對象14     public static <T extends Serializable> T clone(T obj) {15         // 拷貝產生的對象16         T cloneObj = null;17         try {18             // 讀取對象位元組資料19             ByteArrayOutputStream baos = new ByteArrayOutputStream();20             ObjectOutputStream oos = new ObjectOutputStream(baos);21             oos.writeObject(cloneObj);22             oos.close();23             // 分配記憶體空間,寫入原始對象,產生新對象24             ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());25             ObjectInputStream ois = new ObjectInputStream(bais);26             // 返回新對象, 並做類型轉換27             cloneObj = (T) ois.readObject();28             ois.close();29         } catch (ClassNotFoundException e) {30             e.printStackTrace();31         } catch (IOException e) {32             e.printStackTrace();33         }34         return cloneObj;35 36     }37 38 }

   此工具類要求被拷貝的對象實現了Serializable 介面,否則是沒辦法拷貝的(當然,使用反射是另一種技巧),上一建議中的例子只是稍微修改一下即可實現深拷貝,代碼如下

public class Person implements Serializable{    private static final long serialVersionUID = 4989174799049521302L;        /*刪除掉clone方法,其它代碼保持不變*/}

   被拷貝的類只要實現Serializable這個標誌性介面即可,不需要任何實現,當然serialVersionUID常量還是要加上去的,然後我們就可以通過CloneUtils工具進行對象的深拷貝了,用詞方法進行對象拷貝時需要注意兩點:

  1. 對象的內部屬性都是可序列化的:如果有內部屬性不可序列化,則會拋出序列化異常,這會讓調試者很納悶,產生一個對象怎麼回出現序列化異常呢?從這一點考慮,也需要把CloneUtils工具的異常進行細化處理。
  2. 注 意方法和屬性的特殊修飾符:比如final,static變數的序列化問題會被引入對象的拷貝中,這點需要特別注意,同時 transient變數(瞬態變數,不進行序列化的變數)也會影響到拷貝的效果。當然,採用序列化拷貝時還有一個更簡單的方法,即使用Apache下的 commons工具包中SerializationUtils類,直接使用更加簡潔.
回到頂部建議45:覆寫equals方法時不要識別不出自己

   我們在寫一個JavaBean時,經常會覆寫equals方 法,其目的是根據商務規則判斷兩個對象是否相等,比如我們寫一個Person類,然後根據姓名判斷兩個執行個體對象是否相同時,這在DAO(Data Access Objects)層是經常用到的。具體操作時先從資料庫中獲得兩個DTO(Data Transfer Object,資料轉送對象),然後判斷他們是否相等的,代碼如下: 

public class Person {    private String name;    public Person(String _name) {        name = _name;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }        @Override    public boolean equals(Object obj) {        if(obj instanceof Person){            Person p = (Person) obj;            return name.equalsIgnoreCase(p.getName().trim());        }        return false;    }}

  覆寫的equals方法做了多個校正,考慮到Web上傳遞過來的對象有可能輸入了前後空格,所以用trim方法剪下了一下,看看代碼有沒有問題,我們寫一個main:

public static void main(String[] args) {        Person p1= new Person("張三");        Person p2= new Person("張三  ");        List<Person> list= new ArrayList<Person>();        list.add(p1);        list.add(p2);        System.out.println("列表中是否包含張三:"+list.contains(p1));            System.out.println("列表中是否包含張三:"+list.contains(p2));    }

  上面的代碼產生了兩個Person對象(注意p2變數中的那個張三後面有一個空格),然後放到list中,最後判斷list是否包含了這兩個對象。看上去沒有問題,應該列印出兩個true才對,但是結果卻是:

   列表中是否包含張三:true
       列表中是否包含張三:false  

   剛剛放到list中的對象竟然說沒有,這太讓人失望了,原因何在呢?list類檢查是否包含元素時時通過調用對象的equals方法來判斷的,也就是說 contains(p2)傳遞進去,會依次執行p2.equals(p1),p2.equals(p2),只有一個返回true,結果都是true,可惜 的是比較結果都是false,那問題出來了:難道

p2.equals(p2)因為false不成?

  還真說對了,p2.equals(p2)確實是false,看看我們的equals方法,它把第二個參數進行了剪下!也就是說比較的如下等式:

"張三  ".equalsIgnoreCase("張三");

  注意前面的那個張三,是有空格的,那結果肯定是false了,錯誤也就此產生了,這是一個想做好事卻辦成了 "壞事" 的典型案例,它違背了equlas方法的自反性原則:對於任何非Null 參考x,x.equals(x)應該返回true,問題直到了,解決非常簡單,只要把trim()去掉即可。注意解決的只是當前問題,該equals方法還存在其它問題。

回到頂部建議46:equals應該考慮null值情景

  繼續45建議的問題,我們解決了覆寫equals的自反性問題,是不是就完美了呢?在把main方法重構一下:  

public static void main(String[] args) {        Person p1= new Person("張三");        Person p2= new Person(null);
  /*其它部分沒有任何修改,不再贅述*/}

  很小的改動,大家肯定曉得了運行結果是包"null 指標"異常。原因也很簡單:null.equalsIgnoreCase方法自然報錯,此處就是為了說明覆寫equals方法遵循的一個原則---

  對稱性原則:對於任何引用x和y的情形,如果x.equals(y),把麼y.equals(x)也應該返回true。

  解決也很簡單,前面加上非空判斷即可,很簡單,就不貼代碼了。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.