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

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

equals 業務 父親 face 使用 訪問 his () 而是

閱讀目錄

  • 建議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     @Override
28     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方法的自反性原則:對於任何非空引用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.equalsIgnoreCase方法自然報錯,此處就是為了說明覆寫equals方法遵循的一個原則---

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

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

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