Java 設計類和接口的八條優秀實踐清單

本文結合《Effective Java》第四章《類和接口》和自己的理解及實踐,講解了設計Java類和接口的優秀指導原則,文章發佈於專欄Effective Java,歡迎讀者訂閱。


清單1:使類和成員的可訪問性最小化

這個原則,其實就是我們常說的“封裝”,也是軟件設計的基本原則之一。

類之間,隱藏內部數據和實現細節,只通過API進行通信。

信息隱藏的好處:模塊可獨立開發測試優化,並行開發,降低大型系統的風險等。


清單2: final不一定不可變

很多人容易把final跟不可變劃上等號,但是,final限制的只是引用不可變,

也就是說,一個final數組,你不能把它指向另一個數組,但是你可以修改數組的元素。

看下面這段代碼,TestFinal提供了一個final的數組,然後以爲final了就無敵了,自以爲是的加了public修飾符

public class TestFinal {
	public static final String[] VALUES = {"1","2","3"};
}

接着,Test類來調用了

public class Test {
	public static void main(String[] args) {
		String[] arr = {"1","2","3"};
//		TestFinal.VALUES = arr; // cannot be assigned because of final
		TestFinal.VALUES[0] = "11"; // but u can change the sub item
	}
}

它修改了TestFinal的final數組的角標爲0的元素,而且還修改成功了。

那麼,要怎樣做,才能既對外提供這個數組的訪問權限,又讓外界不能修改數組的子元素呢?

一種方法是使用Collections.unmodifiableList暴露一個不可修改的List

public class TestFinal {
	private static final String[] PRIVATE_VALUES = {"1","2","3"};
	public static final List<String> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
}

這樣外界在修改的時候會拋出java.lang.UnsupportedOperationException

另一種方法是提供一個get方法,返回一個clone對象

public class TestFinal {
	private static final String[] PRIVATE_VALUES = {"1","2","3"};
	public String[] getValues()
	{
		return PRIVATE_VALUES.clone();
	}
}

清單3 使類的可變性最小化

不可變類是實例不能被修改的類,這種類具有天然的線程安全特性,不需要同步,也不需要進行保護性拷貝。

設計一個不可變類的四條規則:

1) 不提供任何修改對象狀態的方法

2) 把類聲明爲final,保證不被擴展

3) 把所有的域都聲明爲final,這樣可以更清楚的表明意圖

4) 使所有域都是private

不可變類唯一的缺點就是,對於每個不同的值都需要創建一個單獨的對象,性能差。比如String,因此,對於不可變類,我們一般都會提供一個可變配套類,比如String對應的可變配套類就是StringBuilder和StringBuffer。


清單4 複合優先於繼承

繼承有一個天然的缺陷,子類依賴於超類的特定功能,和清單1所提到的封裝相違背,而包裝模式的複合,則可以解決這個問題。

關於這條清單的詳細說明,請讀者移步到專欄的另一篇文章,Java繼承的天然缺陷和替代方案


清單5 要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承

既然繼承有清單4所講的缺陷,那麼就不要輕易提供繼承的能力。

禁止繼承的兩種方法:

1)把類聲明爲final

2)構造器私有或者包級私有


清單6 構造器不能調用可被覆蓋的方法

爲直觀說明這個原則,下面舉個例子:

有個類違法了這個原則:

public class Super {
    // Broken - constructor invokes an overridable method
    public Super() {
        overrideMe();
    }
    public void overrideMe() {
    }
}

然後下面這個子類覆蓋了overrideMe方法:

import java.util.*;

public final class Sub extends Super {
    private final Date date; // Blank final, set by constructor

    Sub() {
        date = new Date();
    }

    // Overriding method invoked by superclass constructor
    @Override public void overrideMe() {
        System.out.println(date);
    }

    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

由於超類的構造器會在子類構造器之前執行,因此會有兩次打印,而且第一次打印的是null,因爲父類構造器先於子類構造器執行,如果這裏調用了date的方法,那麼就會導致NullPointer異常。


清單7 類層次優於標籤類

在面向過程的編碼中,常常會使用標籤,當標籤等於某個值的時候,是一種代碼邏輯,當標籤等於另一個值的時候,執行另一套邏輯。

而這種標籤的方式,在面向對象的Java裏面,都應該被抽取爲超類和子類。

舉個簡單的例子,下面是一個標籤類,可以表示圓形或者矩形:

class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // Tag field - the shape of this figure
    final Shape shape;

    // These fields are used only if shape is RECTANGLE
    double length;
    double width;

    // This field is used only if shape is CIRCLE
    double radius;

    // Constructor for circle
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // Constructor for rectangle
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
          case RECTANGLE:
            return length * width;
          case CIRCLE:
            return Math.PI * (radius * radius);
          default:
            throw new AssertionError();
        }
    }
}

可以看到,代碼裏充斥這各種枚舉和條件語句,一旦要新增類型,修改時很容易遺漏。

用Java面向對象的思維,改造一下:

// Class hierarchy replacement for a tagged class
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    double area() { return Math.PI * (radius * radius); }
}

class Rectangle extends Figure {
    final double length;
    final double width;

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

改造後的代碼,簡單清楚,而且很容易擴展。


清單8 接口優先於抽象類

接口和抽象類都可以讓實現或者繼承它們的類,具有某些特定的函數模板。

和抽象類相比,接口具有以下優勢:

1)一個類可以實現多個接口,但是卻只能繼承一個類。想一下,假如Comparable接口當初被設計爲一個抽象類了,那由於Java的單繼承的特點,我們很多客戶端的代碼就都無法做到Comparable了。

2)接口可以實現非層次結構的類型框架

清單7裏講到了層次結構,但是,我們常常會遇到非層次結構的類型,比如歌唱家和作曲家,這倆就是非層次結構的,因爲有的歌唱家本身也是作曲家。這就只能用接口來實現了,因爲Java給了接口一個特權——接口可以多繼承。

你可以這樣做:

public interface Singer {
	String sing();
}

public interface Singer {
	String sing();
}

public interface SingerSongwriter extends Singer, SongWriter {

}

當然,抽象類也有它的優勢:

1)抽象類可以包含一些方法的具體實現,接口不行。 如果使用接口,一般都要提供一個骨架實現類,客戶端可以去繼承這個骨架實現類來使用方法的具體實現。

2)抽象類的演變比接口的演變要容易得多。抽象類可以隨意添加新的方法,但是接口不行,一旦接口新增了方法,之前實現了這個接口的類就無法編譯通過。


總結一下:

接口通常是定義允許多個實現的類型的最佳選擇。但是,當演進的容易性被更重視,或者說,後續修改的可能性更大時,這種情況下,就應該使用抽象類。


以上八條清單,希望可以給你帶來幫助。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章