13.使類和成員的可訪問性最小化
- 儘可能的使每個類或者成員不被外界訪問
- 對於頂層類,接口,只有兩種訪問級別: 包級私有(
package-private
)和公有(public
) - 對於成員,有四種訪問級別(
private
,package-private
,protect
,public
) - 如果一個類只對一個類可見,則應該將其定義爲私有的內部類,而沒必要
public
的類都應該定義爲package private
- 子類的訪問級別不允許低於父類的訪問級別.
小結
- 應該始終儘可能的降低可訪問性
- 除了公有靜態
final
域外,公有類
都不應該包含非公有域.
14.在共有類中使用訪問方法,而非共有域
示例
class Point {
public double x; //
public double y;
}
- 如果是公有類的時候,應該使用私有成員,並提供setter方法(除非不可變域)
- 包級私有,或內部類,直接暴露則沒有本質的錯誤.
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
15.使可變性最小
建議
- 不提供可以改變本對象狀態的方法
- 保證類不會被擴展(防止子類改變)
- 使所有的域都是
final
的 - 使所有的域都稱爲
private
的 - 確保對於任何可變組件的互斥訪問
示例:
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
示例中除了標準的
Object
方法之外,運算操作都是創建新的實例,而不改變原來對象
,這種做法被稱爲 函數的(functional)做法 .
提示:
- 不可變對象比較簡單
- 不可變對象本質上是線程安全的
- 不僅可以共享對象,甚至可以共享不可變對象的內部信息.[
BigInteger
內部的內部數組] - 不可變對象爲其他對象提供了 大量的構件(
building blocks
)[map key
和set element
]
缺點
- 不可變對象對於每個不同的值 都需要創建一個單獨的對象.
- 由於1的問題.導致多步操作會有性能問題.這種問題的解決辦法就是
提供一個公有的可變配套類
,如String
和StringBuilder
.
保持不可變性
爲了保持不被子類化,可以使用final
修飾.除此之外,還有一種方法:
- 讓類的構造器變成
private/package-private
,添加靜態工廠
來代替公有構造器
,見[第一條] - 使用靜態工廠方法,具體實現類可以有多個,還能進行
object cache
[第一條]
小結
- 堅決不要爲每個
get
方法編寫一個 對應的set
方法,除非有很好的理由. - 如果類不可做成
不可變的
,也該儘量限制其可變性
(final 域
) - 當實現
Serializable
,一定要實現readObject/readResolve
方法,或者使用ObjectOutputStream.writeUnshared/ObjectInputStream.readUnshared
案例: TimeTask : 可變,但狀態空間被有意的設計的很小.
16.複合優於繼承
簡介
專爲
繼承設計的
,包內部 繼承
非常安全,是代碼重用
的有效手段.
對於具體類
進行跨越包邊界
的繼承是危險
的.
繼承缺點
- 打破
封裝性
,子類依賴父類實現細節,父類改變,子類會遭到破壞
比如:HashSet
記錄添加元素個數
的方法,addAll
內部調用了add
,導致程序錯誤.
- 在後續升級版本中,如果父類新增了與子類
相同簽名,返回值不同
的方法,子類無法通過編譯
,如果新增了簽名和返回值都相同
的方法,則會發生覆蓋
複合方式
- 新類中增加一個
引用現在類
的私有域
,通過轉發
實現
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {this.s = s;}
public boolean add(E e) {return s.add(e);}
}
缺點
- 不適用於callback frameworks,在回調框架中,需要將自身傳遞給調用對象來回調.
而被包裝起來的對象並不知道 它被包裝的 外面的對象, 因此導致回調失敗.
繼承和符合選擇問題
- 繼承應該在is-a的場景中使用
- 繼承除了會繼承父類的API功能,也會繼承父類的設計缺陷,而組合則可以隱藏成員類的設計缺陷,通過轉發暴露部分
API
17.要麼爲繼承而設計,要麼禁用繼承
簡介
- 一個類必須在文檔中說明,每個可重寫的方法,在該類的實現中的哪些地方會被調用。調用時機、順序、結果產生的影響,包括多線程、初始化等情況。
- 被繼承類應該通過謹慎選擇
protected的方法或成員
,來提供一些hook
,以便能進入到它的內部工作流程中.如:java.util.AbstractList::removeRange
- 構造器不能調用可被覆蓋的方法,因爲
父類構造函數比子類構造函數先執行
,導致子類重寫方法比子類構造器先執行
- 如果實現了
Serializable/Cloneable
,無論是clone
還是readObject
都不可以調用可複寫方法
- 如果實現了
Serializable
,readResolve/writeReplace
必須是protected
,而非private
,否則子類會忽略.
總結
- 爲繼承而設計的類,對這個類會有一些實質性的限制.
- 不是爲繼承而設計的類,應禁止子類化.
18.接口優於抽象類
簡介
Java 語言提供了兩種機制: 接口和抽象類,
Java是單繼承,多實現的,因此抽象類作爲類型定義受到了很大的限制.
- 通過接口,現有類可以很容易的被更新
- 接口是
mixin
(混合類型)的理想選擇(提供某些可供選擇的行爲) - 接口允許我們構建非層次結構的框架(利用接口多繼承特性)
public interface Singer{}
public interface Writer{}
public interface SingerWriter extends Singer ,Writer{}
有效的避免了繼承導致的
臃腫的類層次
(組合爆炸).
注意事項
- 對每個重要接口都提供一個
抽象的骨架實現類
,把接口和抽象類
的優點結合起來 - 通過把對接口的實現轉發到
一個擴展了骨架實現的內部私有實例
上,稱作模擬多重實現
,示例:Map.Entry
與AbstractMapEntry
- 骨架類是爲了繼承而設計的[參考第17條規範]
- 還有一種簡單實現,
AbstractMap.SimpleEntry
(可能是基本實現
,可能是空實現
)
區別
- 抽象類演變比接口方便,可以在後續版本中新增新的方法,並提供默認實現,現有實現類都提供這個新的方法,接口則不行
小結
- 接口一旦發行,並廣泛實現後,想要改變幾乎不可能.
- 通過接口定義類型,可以允許多實現(多繼承)
- 但是演進需求大於靈活性、功能性時,抽象類更合適
- 提供接口時,提供一個骨架實現,同時審慎考慮接口設計
19.接口只用於定義類型
簡介
接口充當了可以引用這個類的實例類型
Type
.
不良使用
常量接口: 如:
public interface Constants{
static final int AAA = 0;
}
這種接口,會使用戶糊塗,並使接口污染,如果這個類被修改了,
意味着,在後續版本中,不再需要這些常量了,它依然必須實現這個接口.
合理方案
- 只在當前類和接口中導出這些常量.
如:Integer.MAX_VALUE
- 不可實例化的工具類中導出常量,配合靜態導入(
static import
) - 接口應該 只被用來定義類型,而不是定義常量.
20.類層次優於標籤類
概述
標籤類,就是在內部定義一個tag
變量,由其控制功能的轉換,如下類型
class Figure {
enum Shape {RECTANGLE, CIRCLE};
// Tag field - the shape of this figure
final Shape shape;
}
標籤類過於冗長,容易出錯
解決方案
通過子類化
解決.爲每種標籤都定義具體的子類.
示例代碼: Figure
abstract class Figure {}
class Rectangle extends Figure {}
class Circle extends Figure {}
好處:
- 代碼簡單清除,沒有樣板代碼
- 所有域都是
final
的,在初始化構造器時就初始化數據域 - 多個程序員可以獨立的擴展類層次結構
建議
當編寫包含顯示標籤域的代碼時,應考慮標籤是否可以取消.
21.用函數對象表示策略
簡介
允許程序把
調用特殊函數的能力
存儲起來並傳遞這種能力.這種機制通常允許函數調用者傳入第二個參數
來指定自己的行爲.定義一種對象,它的方法
執行其他對象
上的操作,這樣的類被稱爲函數對象
.如Compare
接口的實現類.這種
對象實例
,可以稱爲其他對象
的具體策略
這種策略類,沒有
狀態
,沒有域
,所有實例都是等價的,因此最好用單例
來實現,如:
//第一種
Arrays.sort(new short[]{}, new Comparator<String>() {
@Override public int compare(String o1, String o2) {
return 0;
}
});
//第二種
class StringLengthComparator implements Comparator<String> {
private StringLengthComparator() {
}
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
@Override public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
總結
- 函數指針的主要途徑是實現策略模式
- 需要聲明一個接口來表示該策略(如
Comparator<T>
,一般是泛型接口) - 當一個具體的策略只使用一次時,使用
匿名內部類
- 當一個具體策略被
重複使用
時,使用單例
來實現
22.優先考慮靜態成員類
簡介
有四種嵌套類: 靜態成員類,非靜態成員類,匿名類與局部類.除了第一種,其他三種都被稱爲內部類.
靜態成員
- 靜態成員類是最簡單的一種嵌套類,可以看成是一種恰好被聲明在另一個類的內部的普通類. 和
其他靜態成員
規則一致. - 靜態成員類的一種常見用法是作爲公有的輔助類.僅當與外部類一起使用時纔有意義.
- 可以在外圍實例之外獨立存在,用於表示(封裝)外部類的一些成員.
非靜態成員
- 每個實例都隱含着外圍類的一個實例關聯.(內存泄漏一般也是由此引起),持有強引用
- 不可以在外圍實例之外獨立存在
- 如果內部類不需要引用外部類的成員和方法,則一定要將其定義爲
static
,避免空間/時間開銷,避免內存泄漏
匿名類
- 沒有名字,不是外圍類的成員,在使用的時候被聲明和實例化
- 有諸多限制,不能
instanceof
,無法擴展一個類或者接口. - 通常像
Runnable
,Thread
在內部類中使用居多
局部類
- 局部類是使用最少的類,在任何可以
聲明局部變量地方都可以聲明局部變量.
- 是否static取決於其定義的上下文
- 可以在作用域內重複使用
- 不能有static成員
總結
如果成員類每個實例都需要一個指向其外圍實例的引用,用非靜態
否則,用靜態