類和接口
一、使類和成員的可訪問性最小化
首先我們要了解一個 軟件設計基本原則:封裝
模塊隱藏所有的實現(xiàn)細節(jié),只通過API進行模塊間通信
為什么要這樣設計呢?
- 有效的接觸系統(tǒng)模塊之間的耦合:各模塊獨立開發(fā),測試,優(yōu)化,使用,理解,修改。
- 提高軟件的重用性:因為模塊基本只依賴所使用的環(huán)境
- 降低了構建大型系統(tǒng)的風險:即使整個系統(tǒng)不可用,但是有些獨立的模塊仍可用
接下來我們得先了解一下Java提供的訪問控制機制:
訪問修飾符 | 說明 |
---|---|
private(私有的) | 只能在聲明該成員的的頂層類內(nèi)部才可以訪問,當然包括嵌套內(nèi)部類 |
package-private(包級私有的,也就是不加修飾符) | 聲明該成員的包內(nèi)部的任何類都可以訪問該成員(這是默認的訪問級別) |
protected(受保護的) | 聲明該成員的類的子類可以訪問該成員,聲明該成員的包內(nèi)的任何類均能訪問該成員 |
public(共有的) | 任何地方都能訪問該成員 |
可訪問性的能力:private < package private < protected < public
規(guī)范一:盡可能使得每個類或者成員不被外界訪問
也就是說盡可能的降低成員的可訪問性的能力。提供越大的可訪問性的能力就必須付出更更多的精力甚至是無法控制的破壞。
需要注意的是:
- 從父類覆蓋過來的方法不能將訪問性變得更小
- 實現(xiàn)接口的時候,實現(xiàn)的方法都是共有的,因為接口中聲明的方法默認就是共有的
- 做測試的時候可以將私有成員提升到包級私有,但是我們可以讓測試作為被測試的包的一部分,從而可以訪問到包的私有成員
受保護的成員如果作為API的一部分,那么我們必須提供支持
規(guī)范二:實例域絕不能是共有
非final域或者指向可變對象的final引用是共有的就放棄了強制這個對象的不可變的能力。而且被外部隨意的訪問并修改可能操作意想不到災難性后果。因為final引用本身不可以改變,但是引用的對象卻是可以修改的。
對于靜態(tài)域提升為共有的的時候同樣會有以上的問題。尤其是 public static final xxx xxx的域。
因此: 包含共有可變域的類并不是線程安全的,類具有共有的靜態(tài)final數(shù)組域(也可以是指向可變對象的引用)或者返回這種域的方法幾乎總是錯誤
常見的處理方法:
//存在安全漏洞的寫法
public static final Thing[] VALUES = {......};
//改進方法1:
private static final Thing[] PRIVATE_VALUES = {......};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//改進方法2:
private static final Thing[] PRIVATE_VALUES = {......};
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
二、在共有類中使用方法訪問而不是公有域
這應該是面向對象程序設計的一個準則之一
我們先來看個例子
//直接將數(shù)據(jù)域暴露
public class Point{
public double x;
public double y;
}
//數(shù)據(jù)域變成私有,提供公有訪問方法
public class Point{
private double x;
private double y;
public Point(double x,double y){
this.x = x;
this.y = y;
}
public double getX(){
//可以做一些邏輯處理,外部使用不用關系內(nèi)部實現(xiàn)的邏輯,只關注返回的結果
return x;
}
public double getY(){
return y;
}
public void setX(double x){
this.x = x;
}
public void setY(double y){
this.y = y;
}
//當內(nèi)部需求有改變的時候在共有訪問方法中進行邏輯處理
}
有些特例:在包級私有的類中,或者是私有類的嵌套類,直接暴露它的數(shù)據(jù)域并沒有本質的錯誤
總結:無論類是可變還是不可變,包級私有還是私有類的嵌套類,我們都應該使用共有方法訪問私有數(shù)據(jù)域,這樣可以將危害見到最小。
三、使可變性最小化
先來了解一下 不可變類:每個實例中包含的所有信息都必須在創(chuàng)建該實例的時候提供,并且在對象的整個生命周期內(nèi)固定不變
不可變類的一些優(yōu)點:易于設計、實現(xiàn)、使用。不易出錯,更安全。
成為不可變類的遵循的五個準則:
- 不提供任何會改變對象狀態(tài)的方法
- 保證類不會被拓展(防止子類化帶來的破壞)
- 使所有的域都成為final域(顯示強調(diào)不可變性)
- 使所有的域都成為私有的(防止客戶端直接訪問數(shù)據(jù)域或者引用域,使用共有方法進行獲取,但是要保證引用域的安全性)
- 確保對于任何可變組件的互斥訪問(存在可變對象的域的時候,確保客戶端使用者無法獲取指向這些對象的引且不使用客戶端提供的對象引用初始化這樣的域,也不從任何方法返回它的引用,考慮使用保護性拷貝)
public final class Complex{ ////final 類不能子類化
private final double re;
private final double im;
public Complex(double re,double im){
this.re = re;
this.im = im;
}
//另一種防止子類化的方法:
/*
private Complex(double re,double im){
this.re = re;
this.im = im;
}
public static Complex newInstance(double re,double im){
return new Complex(re,im);
}
*/
//共有方法獲取數(shù)據(jù)域,但是沒有顯示提供修改數(shù)據(jù)的方法
public double realPart(){
return re;
}
public double imaginaryPart(){
return im;
}
//每次操作都會返回一個新的不可變對象
public Complex add(Complex c){
return new Complex(re + c.re, im + c.im);
}
}
從以上可以得到一個不可變類的特性: 不可變對象本質是線程安全的,不要求同步,且可以被自由的共享
因為可以被自由的共享,我們可以將頻繁使用的實例提供靜態(tài)final常量:
public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE = new Complex(1,0);
而且不需要進行保護性拷貝(前提是沒有存在指向對象的引用域),也不應該提供clone方法。
除了共享對象本身,對象內(nèi)的信息也可能被共享
例如:BigInteger中用一個int表示符號位,一個int[]表示數(shù)值,negate方法只是改變符號為的int,而數(shù)值指向同一個int[],但是可以表示正負兩個數(shù)。
因為每種狀態(tài)都需要一個不可變的對象實例,但是對象的反復創(chuàng)建是有性能代價的,尤其是一個步驟中可能需要很多不同數(shù)值的對象,但是步驟結束都將被廢棄。所以可以使用靜態(tài)工廠方法代替構造器,同時對頻繁使用的對象進行緩存,避免重復生成。因此不可變的最大缺點就是: 對于每個不同的值都需要一個單獨的對象
對于依賴BigInteger和BigDecimal,因為他們的方法有可能被覆蓋,所以不可變類中包含他們的話要提供保護性拷貝:
public static BigInteger safeInstance(BigInstance val){
if(val.getInstance != BigInteger.class)
return new BigInteger(val.toByteArray());
return val;
}
對于前面提及的五個不可變類的準則,可以適當放松,例如不可變類中存在一個或者多個非final域提供緩存設計功能的實現(xiàn),進而減少高昂的開銷。
對于實現(xiàn)了Serializable接口且不可變類中含有指向可變對象的域,那么需要提供readObject,readResolve或者使用ObjectOutputStream.readUnshared 和ObjectOutputStream.writeUnshared ,避免存在攻擊者從不可變類構建可變實例。
總結:如果類不可能設計成不可變類,那也應該盡可能限制它的可變性。同時需要權衡不可變類與性能要求。
四、復合優(yōu)于繼承
集成作為實現(xiàn)代碼重用的有力手段,但是也破壞了封裝性
繼承帶來的缺點是:
- 子類依賴于父類的特定功能實現(xiàn)細節(jié),父類的改變可能會破壞子類,使得子類非常脆弱。
- 對于override動作,后期的演變過程可能存在一下問題:
- 后續(xù)在父類提供了一個新方法,而子類又添加了一個方法名與參數(shù)一致而返回類型不一致的方法,會導致編譯錯誤
- 子類新提供的方法與父類方法形成覆蓋,前期是訪問父類的方法,現(xiàn)在就會訪問子類覆蓋父類的方法,如果兩種實現(xiàn)并不一致,將可能導致錯誤。
為了避免上述的問題可以考慮使用 復合 而不是繼承。
復合是將一個類作為新類的組件,實際表現(xiàn)就是新類的一個域。
//此處ForwardingSet僅是作為一個包裝類
public class ForwardingSet<E> implement Set<E>{
private final Set<E> set; //復合:將正真操作數(shù)據(jù)的Set最為封裝類的一個組件(域)
public ForwardingSet(Set<E> set){
this.set = set;
}
public boolean add(E e){ return set.add(e);}
public boolean addAll(Collection<? extends E> c){ return set.addAll(c);}
//....省略了其他的實現(xiàn)Set接口的方法,實質也是同上面的add方法一樣,只是直接調(diào)用set的對應方法
}
//一個可以返回添加到Set中的元素的總數(shù)的類
public class CountSet<E> extends ForwardingSet<E>{
private int count = 0;
//此處的特點是將其他任何Set的實現(xiàn)封裝成一個可以對添加的元素計數(shù)的Set
public CountSet(Set<E> set){
super(s);
}
@Override public boolean add(E e){
count++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c){
count+=c.size();
return super.addAll(c);
}
public int getCount(){
return count;
}
}
以上我們需要注意的是ForwardingSet作為包裝類,使用了Decorator(裝飾者)模式,而不是Delegation(委托)模式,委托模式的一個特點時將包裝對象本身傳遞給被包裝的對象。
此外需要注意的是被包裝的對象(上面的示例時Set)不適合在回調(diào)框架中使用,因為回調(diào)把被包裝對象本身的引用傳遞給其他對象,后續(xù)調(diào)用引用會造成,越過封裝類的實現(xiàn)。
總結:
- 只有確實存在“is-a”關系,也就是子類正真是父類的子類型(從概念與邏輯上),才適合繼承,否則應該使用復合。
- 如果子類與父類在不同的包中,且父類并不是為了繼承而設計,會暴露繼承的脆弱性(參考上述關于繼承的缺點)。
五、要么為了繼承而設計,并提供文檔,要么就禁止繼承
為了繼承而設計的類提供文檔說明,這樣的文檔是:精確描述覆蓋方法所帶來的影響,也就是明確它可覆蓋的方法的自用性。對于構造器或者共有、受保護的的方法指明對可覆蓋方法的使用情況,以及帶來的影響。
- 為了繼承而設計類的時候,考慮應該暴露那些受保護的方法或者域(唯一方法就是多編寫子類測試,是否出現(xiàn)難以使用或者出現(xiàn)安全問題等)
- 對于將要發(fā)布的為了繼承而設計的類,自用模式以及受保護的方法和域中隱含的實現(xiàn)都是對外(類使用者)有永久承諾的。因此后期對其進行性能提升等改進都會時比較困難的。(唯一辦法還是編寫子類測試)
- 構造器不能調(diào)用可覆蓋的方法,無論是直接還是間接的調(diào)用。(因為子類覆蓋的方法會在子類構造器運行之前被調(diào)用,前提時構造器中對該方法有調(diào)用)
- 為了繼承而設計的類需要實現(xiàn)Cloneable、Serializable接口的時候,clone(),readObject()相當于構造器,因此不能在clone(),readObject()中調(diào)用可覆蓋的方法,無論是直接還是間接的調(diào)用。
- 對于普通類不需要被安全的子類化,那么就禁止子類化(1. final類;2.私有構造器;3.提供靜態(tài)工廠方法替代構造器)
- 某些情況禁止普通類進行繼承可能會為使用帶來許多不便,此時應該保證這些類能消除可覆蓋方法的自用性帶來的影響(可以將可覆蓋方法的代碼邏輯移到私有輔助方法中,覆蓋方法調(diào)用對應的自由輔助方法)
六、接口優(yōu)于抽象類
先來了解一下接口與抽象類的聯(lián)系與區(qū)別:
接口 | 抽象類 |
---|---|
所有方法都是未實現(xiàn)的 | 可以同時存在未實現(xiàn)方法和實現(xiàn)方法 |
可繼承(public interface A extends B,C,D) | 可繼承,如果繼承類未實現(xiàn)所有抽象方法,那么該類必須仍然是抽象類 |
屬性域默認時public final 的 | 屬性域與普通類一樣 |
可以同時implement多個接口 | 智能通過extends實現(xiàn)單繼承 |
不存在構造器 | 存在構造器 |
此外JDK 1.8中interface中允許有默認方法,也就是實現(xiàn)類如果不提供實現(xiàn)邏輯,將使用interface中默認的實現(xiàn)邏輯
接口與抽象類的一個重要區(qū)別是: 為實現(xiàn)抽象類定義的類型,類必須成為抽象類的子類,而實現(xiàn)接口的類只需要實現(xiàn)接口所有定義的抽象方法,且不存在類的層次限制
因此接口帶來的好處是:
-
現(xiàn)有類可以很容易的實現(xiàn)更新,通過實現(xiàn)新的接口
例如實現(xiàn)Comparable接口讓類可以輕松的在結合框架中使用排序的功能,限于抽象類的單繼承的限制,通過抽象類實現(xiàn)新加功能會破壞類的層級性
-
接口是定義混合類型的理想選擇
例如實現(xiàn)Comparable接口的不同類之間可以進行比較,而對于抽象類而言很難找到適合的地方讓不同的類都繼承同一個抽象類
-
接口允許我們構造非層次結構的類型框架
對于一個類需要實現(xiàn)多種功能,或者不同的類需要實現(xiàn)其中幾種功能,而每個功能有一個接口進行約束。
public interface Songer{ void song(); } public interface SongWriter{ Song writeSong(); } pulbic interface SongerWriter extends Songer,SongWrite{ void act(); }
在JDK 1.8之前interface不提供默認方法,我們可以提供一個骨架抽象實現(xiàn)(實際上是一個實現(xiàn)了部分主要接口方法的抽象類)。例如Java集合框架中的AbstractList等。骨架實現(xiàn)類時相對比較簡單的實現(xiàn),確定那些時最基本的方法,然后其他的方法提供默認實現(xiàn)。需要注意的是骨架實現(xiàn)類是要被繼承的,所以同樣要注意繼承帶來的問題,并提供良好的文檔。
然而接口同樣存在著痛點:在版本演變中很難通過新添加接口方法進行功能添加,但是抽象類可以很輕松搞定這種事,因此發(fā)布的接口在發(fā)布之前就應該通過嚴密的測試。因為一個接口發(fā)布出去并被廣泛使用后,對其進行修改幾乎時不可能的。
七、接口只適用于定義類型
當接口被類實現(xiàn)之后,該接口就充當可以應用該類實例的類型。例如:
List<String> strList = new ArrayList();
strList.add("bug");
另一種特殊情況就是 常量接口,這種接口僅包含常量而沒有其他的抽象方法。
public interface EarthConstants{
double EARTH_RADIUS = XXXXX.XXXXX;
}
但是這是一種不良接口,會導致使用者對接口含義的模糊,而且使用選擇了實現(xiàn)該接口,那么后續(xù)不在需要這些常量的時候,我們不能刪除該接口。
對于常量的定義,如果和類或者關系接口很密切,可以使用工具類來定義:
public final class EarthConstants{
private EarthConstants(){}
public static final double EARTH_RADIUS = XXXXX.XXXXX;
}
八、類層次優(yōu)于標簽類
首先我們看一下下面的標簽類:
class Figure{
//枚舉類型作為標簽
enum Shape{RECTANGLE,CIRCLE};
final Shape shape;
//Rectangle使用
double lenght;
double width;
//Circle使用
double radius;
//通過不同的構造函初始化不一樣的類型
Figure(double radius){
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double width,double length){
shape = Shape.RECTANGLE;
this.width = width;
this.length = length;
}
//根據(jù)標簽計算圖形的面積
double area(){
switch(shape){
case Shape.CIRCLE:
return Math.PI * radius * radius;
case Shape.RECTANGLE:
return width * length;
}
}
}
以上的這個標簽類僅是設置了兩個標簽,但是可以發(fā)現(xiàn)其中充斥者樣板代碼,而且會有很多判斷語句,標簽域。此外數(shù)據(jù)域。如果標簽很多的時候構造器可能相互會很近似,易導致出錯。總的來說就是標簽類會導致: 代碼冗長。容易出錯,且效率低下
而變換成類層次后:
abstract class Shape{
abstact double area();
}
class Rectangle extends Shape{
final double width;
final double length;
public Rectangle(double width,double length){
this.width = width;
this.length = length;
}
public double area(){
return width * lenght;
}
}
class Circle extends Shape{
final double radius;
pulbic Circle(double radius){
this.radius = radius;
}
public double area(){
return Math.PI * radius * radius;
}
}
類層次使得類型之間的關系更明了,層次結構清晰,特定類型只含有需要的數(shù)據(jù)域,另一個好處就是更加便于拓展。
class Square extends Rectangle{
public Square(doubel side){
super(side,side);
}
}
九、用函數(shù)對象表示策略
在C語言中可以通過函數(shù)的參數(shù)可以是一個指向函數(shù)的指針,該指針可以完成一些特定的策略,而在Java中可以通過對象引用實現(xiàn)類似的功能。
我們可以看一下Java.Util中的Comparator接口:
public intarface Comparator<T>{
//接收兩個泛型參數(shù),返回一個整型,t1>t2返回大于0的整數(shù),t1==t2返回0,t1<t2返回小于0的整數(shù)
public int compara(T t1,T t2);
}
//
int arr[] = new int[]{....};
Arrays.sort(arr,new Comparator<Integer>(){
public int compara(Integer i1,Integer i2){
return ....//實現(xiàn)具體的策略邏輯
}
});
//上面使用匿名內(nèi)部類的方式生成一個策略,那么每次調(diào)用都會新生成一個新的實例
//對于重復執(zhí)行的策略,可以將其保存為一個靜態(tài)域。
class Host{
private static class IntCmp implement<Integer>{
public int compara(Integer i1,Integer i2){
return ....;
}
}
public static final Comparator<Integer> INT_COMPARATOR = new IntCmp();
}
Arrays.sort(arr,Host.INT_COMPARATOR);
十、優(yōu)先考慮靜態(tài)成員類
首先理解嵌套類(nested class)的概念:定義在另一個類內(nèi)部的類,它的目的時為它的外圍類提供服務。
嵌套類的種類:靜態(tài)成員類(static member class)、非靜態(tài)成員類(nonstatic member class)、匿名類(anonymous class)、局部類(local class)。
除了靜態(tài)內(nèi)部類外其他三種都稱為內(nèi)部類(inner class)
-
靜態(tài)成員類
- 能訪問外部類的所有成員
- 屬于外部類的一個靜態(tài)成員,遵守類靜態(tài)成員的訪問規(guī)則(private則只能在外部類的內(nèi)部使用)
- 不持有外部類的實例
- 不能直接調(diào)用外部類的非靜態(tài)方法
- 常用做為公有的輔助類
-
非靜態(tài)成員類
- 每個非靜態(tài)內(nèi)部類持有一個外部類的實例(外部類調(diào)用非靜態(tài)成員類的構造器的時候建立實例關聯(lián)關系)
- 可以直接調(diào)用外部類的方法
- 常見用法是:定義一個Adapter,他允許外部類的實例被看作是另一個不相關的類的實例。例如Map中的KeySet、entrySet
- 應為每個非靜態(tài)成員類都與外部類的一個實例關聯(lián),可能導致外部類實例符合垃圾回收卻仍需要保留著
- 非靜態(tài)成員類如果是導出的API的一部分,那么后續(xù)將不可以修改成靜態(tài)成員類
-
匿名類
- 沒有類名,不能使用繼承,實現(xiàn)接口等特性
- 只有在使用的時候才會初始化,并與外部類實例建立聯(lián)系
- 可以出現(xiàn)在代碼中的任何允許表達式出現(xiàn)地方
- 必須保持簡短才能保證程序良好的閱讀性
- 常見使用:創(chuàng)建對象過程:Thead、Runnable,靜態(tài)工廠方法內(nèi)部
-
局部類
任何可以聲明局部變量的地方都可聲明局部類
同樣遵循作用域的規(guī)則
與成員類類似,右類名,可以被重復使用
只有在非靜態(tài)環(huán)境中使用才會與外部類實例關聯(lián)
-
不能包含靜態(tài)成員
?