Effective Java——類和接口 第四章 類和接口

本系列文章是總結Effective Java文章中我認爲最重點的內容,給很多沒時間看書的朋友以最短的時間看到這本書的精華。
第一篇《Effective Java——創建和銷燬對象》
第二篇《Effective Java——對於所有對象都通用的方法》

第四章 類和接口

第13條:使類和成員的可訪問性最小化

該條規則儘可能使每個類或者成員不被外界訪問,只對外暴露有用的API接口且永遠支持它。
開發系統每個模塊之前的實現細節全部隱藏,只是通過API去掉用,那麼會大大增加這個系統的穩定性和並行開發能力,每個模塊都可以單獨的運行、調試、測試。
四種訪問級別按照訪問性的遞增順序如下:

  1. 私有的(private)——只要在聲明該成員的頂層類內部纔可以訪問。
  2. 包級私有(package-private)——聲明該成員的包內部任何類都可以訪問這個成員。又被成爲“缺省(default)訪問級別”,如果沒有爲成員指定訪問修飾符,就採用這個訪問級別。
  3. 受保護(protected)——聲明該成員的包內部任何類和該類的子類都可以訪問這個成員。
  4. 共有的(public)——任何地方都可以訪問。

protected和public都屬於導出API的一部分,需要永久維護。

實例域決不能是共有的
如果域是非final得,或者是一個指向可變對象final引用,那麼一旦這個域成爲共有的,就放棄了對存儲在這個域中的值進行限制的能力。
用代碼說話:

public static final class ClassA{
       //非final
        public String string = "A";
        //可變對象final
        public final StringBuilder stringBuilder = new StringBuilder();
}
//在外部都可以修改這個域,所以就放棄了對存儲在這個域中的值進行限制的能力。
ClassA classA = new ClassA();
classA.string = "B";
classA.stringBuilder.append("BBBB");

長度非零的數組總是可變的
用代碼說話:

//類暴露這個字段,客戶端程序員可以任意修改數組中的值
public static final Integer[] VALUES = {0,1,2,3,4,5,6,7,8,9};
//解決方法1
public static final Integer[] PRIVATE_VALUES = {0,1,2,3,4,5,6,7,8,9};
public static final List<Integer> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//解決方法2
public static final Integer[] PRIVATE_VALUES = {0,1,2,3,4,5,6,7,8,9};
public static final Integer[] values(){
            return PRIVATE_VALUES.clone();
}

第14條:在公有類中使用訪問方法而非公有域

這條說的很簡單:
如果類可以在它所在的包的外部進行訪問,就提供訪問方法。
如果類是包級私有的,或者是私有嵌套類,直接暴露他的數據域並沒有本質的錯誤。
用代碼說話:

//如下類是不符合規範的
public class Point{
        public double x;
        public double y;
}
//需要提供getter和setter方法
public class Point{
        public double x;
        public double y;
        public double getX() {
            return x;
        }
        public void setX(double x) {
            this.x = x;
        }
        public double getY() {
            return y;
        }
        public void setY(double y) {
            this.y = y;
        }
}

第15條:使可變性最小化

不可變類只是其實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例的時候就提供,並在對象的整個生命週期內固定不變。例如StringBigInteger,BigDecimal,基本類型的包裝類。
使類不可變的五條原則:
1.不要提供任何會修改對象狀態的方法。
2.保證類不會被擴展。防止子類化:final修飾類,private構造方法
3.使所有的域都成爲私有的。
4.確保對於任何可變組件的互斥訪問。如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用。並且永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法(accessor)中返回該對象對的引用。在構造器、訪問方法和readObject方法中請使用保護性拷貝(defensive copy)技術。
函數的:類的方法進行運算之後返回一個新創建的實例,而不是修改這個實例。

String s = "string test";
String rep = s.replace("str","");
System.out.println(s);//打印結果string test
System.out.println(rep); //打印結果ing test
對比發現字符串實例`s`調用`replace`之後打印結果還是原來的沒變,表示沒有修改當前實例內部狀態,而是重新創建一個實例,查看字符串實例`rep`的打印結果可以證明這點。

過程的或者命令式的:類的方法進行運算之後不產生新的實例,導致當前實例內部狀態發生了改變。

函數的

優點:

  1. 不可變對象比較簡單,只有一種狀態創建時期的狀態,在整個生命週期內永遠不再發生變化。
  2. 不可變對象本質上是線程安全的,不需要同步。
  3. 不可變對象可以被自由的共享。無需提供clone方法或者拷貝構造器。但是String是個反例它仍然具有拷貝構造器。
  4. 不僅可以共享不可變對象,還可以共享它們的內部信息。例如:BigInteger用int類型表示符號,用int數組表示數值。negate方法方法產生一個新的實例數值一樣但是符號相反,在內部它並不需要拷貝數組,新建的實例也指向原來的數組來優化內存。
    缺點:
    每個不同的值都會創建一個單獨的對象。如果頻繁調用會造成內存緊張或者頻繁GC影響系統的性能。
    解決方法:
  5. 將頻繁用到的值提供靜態final常量。
  6. 提供靜態工廠方法,把頻繁被請求的實例緩存起來。從而降低內存佔用和垃圾回收的成本。
  7. 提供可變配套類,例如;String的可變配套類爲StringBuilder
不允許被子類化方案
1. 用final關鍵字修飾類
2. 將類的所有構造器都聲明爲private

用代碼說話:

 public class Complex{
        private final double re;
        private final double im;
        //構造器聲明爲private 
        private Complex(double re,double im){
            this.re = re;
            this.im = im;
        }
        public static final Complex valueOf(double re,double im){
            return new Complex(re,im);
        }
}

這種方式相對於第一種方式更爲靈活,優點:

  1. 它允許使用多個實現類。
  2. 使用靜態工廠方式創建對象有非常多的好處,可以增加緩存對象的能力,避免重載構造方法造成的功能不清晰。(可以會看本本書第一條規則)
    用代碼說話:
public static class Complex{
        private final double re;
        private final double im;
        private static SubComplex subComplex = null;
        private Complex(double re,double im){
            this.re = re;
            this.im = im;
        }
        //每個不同的功能用不同的靜態方法,避免構造方法重載
        public static final Complex valueOf(double re,double im){
            return new Complex(re,im);
        }
        //可以對頻繁創建的對象進行緩存
        public static final Complex valueOfSub(double re,double im){
            if(null == subComplex){
                subComplex =  new SubComplex(re,im);
            }
            return subComplex;
        }
        //允許子類化
        private static class SubComplex extends Complex{
            private SubComplex(double re, double im) {
                super(re, im);
            }
        }
}
不可變類實現Serializable接口

如果選擇讓不可變類實現Serializable接口,那麼會涉及到一下幾個方法,如下代碼解釋

public static class CustomSerializable implements Serializable{
        public static final CustomSerializable INSTANCE = new CustomSerializable();
        private String name;
        private String age;
        public String getName() { return name;}
        public void setName(String name) { this.name = name;}
        public String getAge() {return age; }
        public void setAge(String age) {this.age = age;}
        //如下兩個方法可以自定義序列化對象那些字段可以序列化,那些對象不可以序列化
        private void writeObject(ObjectOutputStream out) throws IOException {
//            out.defaultWriteObject();
            out.writeObject(age);
        }
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//            in.defaultReadObject();
            age = (String) in.readObject();
        }
        //實際上就是用readResolve()中返回的對象直接替換在反序列化過程中創建的對象。
        private Object readResolve() throws ObjectStreamException {
            return INSTANCE;
        }
        @Override
        public String toString() {
            return "CustomSerializable{" +
                    "name='" + name + '\'' +
                    ", age='" + age + '\'' +
                    '}';
        }
}

try{
            INSTANCE.setName("aaaaaaaa");
            INSTANCE.setAge("30");
            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/person.out");
            ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
            oout.writeObject(INSTANCE); // 保存單例對象
            oout.close();
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
            Object newPerson = oin.readObject();
            oin.close();
            Log.e("TAG",newPerson.toString());
            Log.e("TAG",String.valueOf(INSTANCE == newPerson));
        }catch (Exception e){
            e.printStackTrace();
        }
//打印結果
04-19 22:09:49.644 22007-22007/? E/TAG: CustomSerializable{name='aaaaaaaa', age='30'}
04-19 22:09:49.644 22007-22007/? E/TAG: true

這篇文章講解的非常詳細:http://developer.51cto.com/art/201202/317181.htm

第16條:複合優先於繼承

在包的內部使用繼承是非常安全的,子類和超類的實現都處在同一個程序員的控制下。對於專門爲了繼承而設計、並且具有很好文檔說明的類來說繼承也是非常安全的。然而對於普通類進行跨域包邊界的繼承則是非常危險的
繼承打破了封裝性,子類依賴其超類中特定功能的實現。如果超類的實現隨着發佈的新版本發生了變化,那麼子類有可能會遭到破壞,即使他的代碼完全沒有改變。
如下例子:
檢測一個Set從創建依賴一共增加了多少個元素:如下代碼:

public static class InstrumentedHashSet<E> extends HashSet<E>{
        private int addCount = 0;
        public InstrumentedHashSet(){}
        public InstrumentedHashSet(int intCap,float loadFactor){
            super(intCap,loadFactor);
        }
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
}
 InstrumentedHashSet hashSet = new InstrumentedHashSet();
hashSet.addAll(Arrays.asList("aaa","bbb","ccc"));
Log.e("TAG","count : " + hashSet.getAddCount());

如上我們期望返回3,但實際上返回6。出現這種情況就是因爲HashSet.addAll內部調用了add()方法,所以總共增加了6。
子類的功能需要依賴於父類的內部實現。由於父類的內部實現是不對外承諾的,不能保證java的每個版本內部實現都一樣,所以不能保證子類的功能一定是正確的。
使用“複合”來解決這個問題,也就是包裝器模式
如下代碼:

public static class InstrumentedHashSet<E> extends ForwardingSet<E>{
        private int addCount = 0;
        public InstrumentedHashSet(Set<E> set) {
            super(set);
        }
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
    }
//包裝器類非常穩固它不依賴於任何類的實現細節,包裝器中的方法稱爲轉發方法
    public  static class ForwardingSet<E> implements Set<E>{
        private Set<E> mSet;
        public ForwardingSet(Set<E> set){
            mSet = set;
        }
        @Override
        public int size() {
            return mSet.size();
        }
        @Override
        public boolean add(E e) {
            return mSet.add(e);
        }
        @Override
        public boolean addAll(@NonNull Collection<? extends E> c) {
            return mSet.addAll(c);
        }
    //此處省略好多轉發方法,因爲篇幅有限
}
//而且所有繼承`Set`接口的對象都可以用這個包裝器類來實現增加對象計數功能
InstrumentedHashSet hashSet = new InstrumentedHashSet(new HashSet());
InstrumentedHashSet treeSet = new InstrumentedHashSet(new TreeSet());

如上代碼包裝類不依賴任何類的實現細節,只是通過轉發方法來實現類的功能,非常完美的解決了這個問題,而且所有繼承Set接口的對象都可以用這個包裝器類來實現增加對象計數功能。
包裝器模式也叫裝飾模式動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比生成子類更爲靈活。

第17條:要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承

專門爲繼承設計的類,該類的文檔必須精確地描述每個方法所帶來的影響,換句話說,該類必須有文檔說明它的可覆蓋的方法的自用性。對於每個共有的或受保護的方法或者構造器,它的文檔必須指明該方法或者構造器調用了那些可覆蓋的方法,是以什麼順序調用的,每個調用的結果又是如何響應後續的處理過程的。類必須在文檔中說明,在哪些情況下它會調用可覆蓋方法。

例如:java.util.AbstractCollectionpublic boolean remove(Object o)這個方法的註釋,非常清楚。
源碼註釋片段:該實現遍歷整個集合來查找制定元素,如果找到該元素,將會利用迭代器的remove方法將之從集合中刪除,注意,如果該集合的iterator方法返回的迭代器沒有實現remove方法,改實現就會拋出UnsupportOperationException
該文檔清楚地說明了,覆蓋iterator方法將會影響remove方法的行爲。而且,它確切的描述了iterator返回的Iterator的行爲將會怎樣影響remove方法的行爲。

  1. 對於爲了繼承而設計的類,唯一的測試方法就是編寫子類。
  2. 爲了繼承而設計的類的構造器決不允許調用可被覆蓋的方法,如果實現了CloneableSerializable接口,就應該意識到clonereadObject,方法在行爲上非常類似構造器,所以無論是clone還是readObject都不可以調用可覆蓋方法,不管是直接還是間接。
  3. 不可變類禁止繼承
  4. 繼承一個類消除可覆蓋方法的自用性,而不改變它的行爲。將每個可覆蓋方法的代碼體移到一個私有的“輔助方法”中,並且讓每個可覆蓋的方法調用它的私有輔助方法。然後,用“直接調用可覆蓋方法的私有輔助方法”來代替“可覆蓋方法的每個自用調用”。

第18條:接口優於抽象類

  1. 現有類可以很容易被更新,以實現新的接口。例如,當Comparable接口引入Java平臺時,會更新許多現有類,以實現Comparable接口。一般來說無法更新現有類實現新的抽象類,由於Java只允許單繼承,所以會破壞類的層級關係,這樣做非常危險。
  2. 接口是定義mixin(混合類型)的理想選擇。mixin是指這樣的類型:類除了實現他的“基本類型”之外,還可以實現這個mixin類型,以表示它提供了某些可供選擇的行爲。例如,實現Comparable這個接口類的實例,除了表示他本身的類型外,還可以表示,它的實例可以和任何其他實現這個接口的類型的實例相比較。
  3. 接口允許我們構造非層次結構的的類的框架。主要說的就是接口可以多重繼承,一個類可以同時實現多個接口,實現多種功能。
  4. 包裝類模式,接口使得安全地增強類的功能成爲可能。如果使用抽象類,那麼只能使用繼承手段來增加功能。
  5. 骨架類。接口定義類型,骨架實現類接管了所有與接口實現相關的工作。例如,AbstractList、AbstractMap等。可以自行查看源碼。
  6. 模擬多重繼承,實現了這個接口的類可以把對於接口方法的調用,轉發到一個內部類私有類的實力上,這個內部私有類擴展了骨架實現類。
  7. 抽象類的演變比接口的演變要容易得多。如果在後續版本中,希望在抽象類中增加新的方法,始終可以增加具體的方法,並且可以包含具體的默認實現。然後,該抽象類的所有現有實現都將提供這個新的方法。對於接口是行不通的。
  8. 接口一旦被公開發行,並且已被廣泛實現,在想改變這個接口幾乎是不可能的。它會影響所有實現這個接口的類。但是實現這個接口骨架類的類不會受到影響,因爲可以直接在骨架類中增加新的方法。最佳途徑,爲每個藉口都實現一個骨架類。
  9. 如果演變比靈活性更加重要的情況下,應該使用抽象類而非接口。因爲抽象類在後期非常容易添加方法。

第19條:接口只用於定義類型

  1. 避免常量接口。接口沒有任何方法,只包含靜態的final域。
  2. 如果這些常量最好被看做枚舉類型,就應該使用枚舉常量
  3. 如果這些常量與某個現有的類或者接口機密相關,就應該把這些常量添加到這個接口或者類中。例如,Java平臺中的Integer.MIN_VALUEInteger.MAX_VALUE
  4. 使用不可實例化工具類來導出這些常量。例如,final類,或者private構造方法類

第20條:層次優先於標籤類

標籤類
public static class Figure{
       enum Shape{ RECTANGLE, CIRCLE};
        
        final Shape shape;
        
        double lenght;
        double width;
        
        double radius;
        
        Figure(double radius){
            shape = Shape.CIRCLE;
            this.radius = radius;
        }

        Figure( double lenght, double width){
            this.shape = Shape.RECTANGLE;
            this.lenght = lenght;
            this.width = width;
        }
        
        double area(){
            switch (shape){
                case RECTANGLE: {
                    return lenght * width;
                }
                case CIRCLE:{
                    return Math.PI*(radius*radius);
                }
                default:{return -1;}
            }
        }
}

如上代碼被稱爲標籤類。他有很多缺點:

  1. 充斥着樣本代碼,包括枚舉聲明、標籤域(final Shape shape;)以及條件語句。
  2. 單個類中存在了多個實現,通過標籤域進行區分,破壞了可讀性。
  3. 內存佔用增加,因爲實例承擔着屬於其他風格的不相關的域
  4. 無法給標籤類增加風格,除非修改源代碼。如果一定要添加風格,就必須記得給每個條件語句都添加一個條件,否則會運行失敗。
    一句話,標籤類過於冗長,容易出錯,並且效率低下。
類層次
public static abstract class Figure{
        abstract double area();
    }

    public static class Circle extends Figure{
        final double radius;
        public Circle(double radius){
            this.radius = radius;
        }

        @Override
        double area() {
            return Math.PI*(radius*radius);
        }
}
public static class Rectangle extends Figure {

        final double length;
        final double width;

        public Rectangle(double length, double width){
            this.length = length;
            this.width = width;
        }
        
        @Override
        double area() {
            return lenght * width;
        }
}

如上代碼被稱爲類層次,他糾正了標籤類的所有缺點:

  1. 代碼簡單清除,沒有樣板代碼。
  2. 每個類型都有自己的類,這些類都沒有收到不想管數據域的拖累。
  3. 所有域都是final
  4. 杜絕了switch case這種形式的語句,防止擴展時忘記寫case造成的運行失敗。
  5. 多個程序員可以獨立的擴展層次結構,並且不用訪問根類的遠代碼就能相互操作。6. 在不修改源文件的情況下增加類型。

標籤類儘量少用,需要被類層次來代替。

第21條:用函數對象表示策略

用函數對象表示策略模式

  1. 函數對象:如果一個類只導出一個方法,那麼他的實例就相當於指向該方法的一個指針,這樣的實例被稱爲函數對象
  2. 策略模式(Strategy Pattern):一個類的行爲或其算法可以在運行時更改。這種類型的設計模式屬於行爲型模式。

策略模式的典型應用就是java.util.Collections.sort(List<T> list, Comparator<? super T> c)
查看源碼:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
        // BEGIN Android-changed: Compat behavior for apps targeting APIs <= 25.
        // list.sort(c);
        int targetSdkVersion = VMRuntime.getRuntime().getTargetSdkVersion();
        if (targetSdkVersion > 25) {
            list.sort(c);
        } else {
            // Compatibility behavior for API <= 25. http://b/33482884
            if (list.getClass() == ArrayList.class) {
                Arrays.sort((T[]) ((ArrayList) list).elementData, 0, list.size(), c);
                return;
            }

            Object[] a = list.toArray();
            Arrays.sort(a, (Comparator) c);
            ListIterator<T> i = list.listIterator();
            for (int j = 0; j < a.length; j++) {
                i.next();
                i.set((T) a[j]);
            }
        }
        // END Android-changed: Compat behavior for apps targeting APIs <= 25.
}

由源碼可見這是一個比較方法,具體的比較策略是由函數的參數決定的,也就是Comparator<? super T> c這個參數。所有實現Comparator接口的對象都可以作爲這個比較方法的策略。

查看Comparator源碼:

public interface Comparator<T> {
      int compare(T o1, T o2);
      boolean equals(Object obj);
}

如上代碼,這就是策略接口,實現這個接口,實現int compare(T o1, T o2);方法,就可以定義自己的策略了。實現這個接口需要使用函數對象的形式。

第22條:優先考慮靜態成員類

嵌套類:被定義在另一個類內部的類。嵌套類存在的目的應該只是爲了他的外圍類提供服務。
嵌套類四種:靜態成員類非靜態成員類匿名類局部類

1. 靜態成員類,靜態成員類使用static定義的內部類,由於靜態成員類內部沒有保存外圍類的實例,所以它只能訪問外圍類的靜態成員變量或者方法。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性。靜態成員的一個典型應用——建造者模式,如下代碼:
public class TestBuilder {
    private final String mName; 
    private TestBuilder(Builder builder){
        this.mName = builder.mName;
    }
    public static final class  Builder{
        private String mName;
        public Builder setName(String name){
            this.mName = name;
            return this;
        }        
        public TestBuilder builder(){
            return new TestBuilder(this);
        }
    }
    public static final void main(){
        TestBuilder testBuilder = new TestBuilder
                .Builder()
                .setName("heiheihei")
                .builder();
    }
}
2. 非靜態成員類,和靜態類唯一區別是它的定義去掉static關鍵字。非靜態成員類的每個實例都隱含着外圍類的一個外圍實例。

優點:它可以訪問外圍類的所有成員變量和方法。
缺點:

  1. 創建非靜態成員類浪費內存,並且增加了時間開銷(由於隱含着外圍類的實例)。
  2. 會導致外圍類實例在符合垃圾回收時仍然得以保留,出現內存泄漏(例如Android中Handler的內存泄漏)。

典型應用:ArrayList.class中的Iterator非靜態內部類,可以自行查看源碼。

  1. 想要創建非靜態成員類實例必須先創建外圍類的實例,enclosingInstance.new MembnerClass()
  2. 如果嵌套類的實例可以在它外圍類的實例之外獨立存在,這個嵌套類必須是靜態成員類。
  3. 如果聲明成員類不需要訪問外圍實例,就要始終定義成靜態成員類。
3. 匿名類,沒有名字,不是外圍類的一個成員。
  1. 它並不與其他成員一起被聲明,而是在使用的同時被聲明和實例化。
  2. 匿名類可以出現在代碼中任何允許存在表達式的地方。
  3. 匿名類出現在非靜態環境中,纔有外圍實例。
  4. 匿名類中不能有任何靜態成員變量,無論是否在靜態環境中。
  5. 匿名類除了在它們被聲明的時候之外,是無法將他們實例化的。
  6. 不能執行instanceof測試,或者做任何需要命名類的其他事情。
  7. 匿名類無法實現接口、無法繼承類。

典型應用

  1. 動態的創建函數對象,例如:java.util.Collections.sort(List<T> list, Comparator<? super T> c)方法中的函數對象。
  2. 創建過程對象例如:RunnableThreadTimerTask
  3. 靜態工廠內部例如:18條中創建的匿名骨架類。
3. 局部類,在任何“可以聲明局部變量”的地方,都可以聲明局部類。

局部類與其他三種嵌套類一樣有一些共同屬性,只是它聲明的位置不太一樣。

總結:

  1. 嵌套類需要在單個方法之外仍然可見,或者太長,就應該定義成成員類。
  2. 如果成員類每個實例都需要指向一個外圍類的引用,就應該定義成非靜態成員類,否則就應該定義成靜態成員類。
  3. 假設嵌套類屬於一個方法內部,如果你只需要在一個地方創建實例,並且已經有一個預置的類型可以說明這個類的特徵,就要把它做成匿名類,否則,就做成局部類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章