本文結合《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)抽象類的演變比接口的演變要容易得多。抽象類可以隨意添加新的方法,但是接口不行,一旦接口新增了方法,之前實現了這個接口的類就無法編譯通過。
總結一下:
接口通常是定義允許多個實現的類型的最佳選擇。但是,當演進的容易性被更重視,或者說,後續修改的可能性更大時,這種情況下,就應該使用抽象類。
以上八條清單,希望可以給你帶來幫助。