Effective Java-類和接口

本部分內容用來指導程序員怎樣才能設計出更加有用、健壯、靈活的類和接口。
內容導圖如下:

類和接口

1.使類和成員的可訪問性最小化

封裝是面向對象思想的特性之一,它的基本思想就是就是僅暴露必要的接口來和其他對象交互,隱藏其內部數據和具體實現細節。設計良好的模塊會隱藏所有的實現細節,把它的API與它的實現清晰地隔離開來,模塊間只通過它們的API進行通信,一個模塊不需要知道其它模塊內部是如何工作的。它可以有效地解除組成系統的各模塊之間的耦合關系,使得這些模塊可以獨立地開發、測試、優化、使用、理解和修改。
Java語言提供了private、package-private、protected、public四種訪問級別,用來對類、接口和成員進行訪問控制。原則是盡可能地使每個類或成員不被外界訪問,換言之,盡可能在最小的訪問級別完成軟件功能的實現
信息隱藏的另一個重要原因就是程序員必須為每個公開的API和成員負責,它們必須得到永遠的支持。它們也代表了類對于某個實現細節的公開承諾。因為無法預估和控制其他程序員如何使用公開的API和成員,所有這些公開的信息在整個軟件的生命周期內都必須始終如一地保持其原有的功能,暴露的越多,需要維護的就越多,會對軟件后續的維護、迭代帶來高昂的成本,甚至會壓垮整個軟件系統。
最后,包含公有可變域的類也不是線程安全的,在多線程模式下,可能會造成類的狀態不一致,從而導致災難性的后果。

//公開域會造成無法控制的訪問,也會造成對象狀態的不一致
public int value = 1;
public static int VALUE = 1; 

//OBJECTS是不可變的,但它所指的對象卻是可變的,這點特別容易被忽視,造成安全漏洞
public static final Thing[] VALUES = {...};

//修改辦法一
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

//修改辦法二
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

應始終盡可能地降低可訪問性。在仔細設計了一個最小的API之后,應該防止把任何散亂的類、接口和成員變成API的一部分。除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域,并且要確保公有靜態final域所引用的對象都是不可變的。

?? 我們生活的世界實際就是一個物質的、面向對象的世界,生活中的衣食住行都要與一個個的具體對象實例打交道。買衣服不必知曉衣服是如何縫制出來的,僅關心衣服所呈現的外在特性是否滿足個人需求及審美;吃飯不必知道如何烹飪,只需要知道菜品是否合乎自己的口味需求;住宿也不需要我們知道房屋是如何建造的,只需要看它提供的條件是否滿足我們的需求;開車也不用先弄清楚車是怎樣生產制造的,只要能根據車輛提供的控制接口正確操作就可以了。封裝的思想隱藏于我們生活的方方面面,也許正是因為它無處不在,反而最容易被人們忽視。
?? 封裝于軟件模塊而言,就是僅提供它要提供給外界模塊使用的API接口,實現這些API接口所需要的數據和其他實現細節是隱藏于模塊內部的,外界模塊不關心、不必要知曉這些API是如何實現的,模塊間僅僅通過固定的API接口進行交互就可以了。滿足這樣設計的模塊就會容易替換、修改,更加靈活。舉例來講,我們常會找人辦事,實際是要找具備能辦成此事(對應于模塊API)這種能力的人,他可能直接就能解決事情,也可能還需委托多個中間人,具體如何操作,一般我們是不關心的。

2.在公有類中使用方法而非公有域

有時候可能會編寫一些退化類用來集中實例域:

class Point {
    public double x;
    public double y;
}

這種類的數據域是可以被直接訪問的,沒有提供封裝的功能。如果不改變API,就無法改變它的數據表示法(更改命名),也無法強加任何約束條件。在域被訪問的時候,無法采取任何輔助的行為。面向對象的設計會使用公有訪問方法來代替:

class Point {
    private double x;
    private double 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;
    }
}

如果類可以在它所在的包外進行訪問,就提供訪問方法,以保留將來改變該類的內部表示法的靈活性。如果公有類暴露了它的數據域,要想在將來改變其內部表示法是不可能的,因為公有域的客戶端代碼已經遍布各處了。
如果類是包級私有的,或者是私有的嵌套類,直接暴露它的數據域是可以接受的。對它的修改是不會對客戶端代碼造成影響的。

公有類永遠都不應該暴露可變的域。

3.使可變性最小化

不可變類指的是實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例的時候就提供,并在對象的整個生命周期內固定不變。Java平臺類庫中包含許多不可變的類,有String、BigInteger、BigDecimal、基本類型包裝類。
存在不可變類有很多理由:不可變的類比可變類更加易于設計、實現和使用,它們不易出錯,且更加安全。

為了使類不可變,要遵循下面5條原則:

  1. 不要提供任何會修改對象狀態的方法
  2. 保證類不會被擴展
  3. 使所有的域都是final的
  4. 使所有的域都成為私有的
  5. 確保對于任何可變組件的互斥訪問

    如果類具有指向可變對象的域,必須確保該類的客戶端無法獲得指向這些對象的引用。永遠不要用客戶端提供的對象來初始化這樣的域,也不要從任何訪問方法中返回該對象的引用。在構造器、訪問方法和readObject方法中使用保護性拷貝技術。

不可變類的優點:

  1. 不可變對象比較簡單
  2. 不可變對象本質上是線程安全的,它們不要求同步
  3. 不可變對象可以被自由地共享,不需要進行保護性拷貝
  4. 不可變對象為其他對象提供了大量的構件

不可變類的缺點:

  1. 對于每個不同的值都需要一個單獨的對象

    創建這種對象的代價可能很高,特別是對于大型對象的情形。對于大型的對象,最好的辦法是提供一個公有的可變配套類,如String的公有配套類StringBuilder。

為了確保不可變性,類絕對不允許自身被子類化。除了“使類成為final的”這種方法外,還可以讓類的所有構造器都變成私有的或者包級私有的,并添加公有的靜態工廠來代替公有的構造器。

public class Complex {
    private final double re;
    private final double im;

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    ...
}

堅決不要為每個get方法編寫一個相應的set方法。除非有很好的理由要讓類成為可變的類,否則就應該是不可變的。
不可變類唯一的缺點是在特定情況下存在潛在的性能問題。當把較大的值對象做成不可變類時,若其會對性能造成影響,可以為它提供相應的可變配套類。
如果類不能被做成不可變的,仍然要盡可能地限制它的可變性。降低對象可以存在的狀態數,可以更容易地分析該對象的行為,同時降低出錯的可能性。
構造器應該創建完全初始化的對象,并建立起所有的約束關系。不要在構造器或靜態工廠之外再提供公有的初始化方法,除非有令人信服的理由必須要那樣做。不應該提供“重新初始化”方法。

4.復合優先于繼承

繼承是實現代碼復用的有力手段,但使用不當會導致軟件變得很脆弱。在包的內部使用繼承是非常安全的,在包的內部,子類和超類的實現都處在同一個程序員的控制之下。對于專門為了繼承而設計,且有很好的文檔說明的類來說,使用繼承也是非常安全的。但是,對普通的具體類進行跨越包邊界的繼承,是非常危險的。
繼承打破了封裝性。子類依賴于超類中特定功能的實現細節。超類的實現有可能會隨著發行版本的不同而有所變化,如果真的發生了變化,子類可能會遭到破壞。因此,子類必須要跟著超類的更新而演變,除非超類是專門為了擴展而設計的,且具有很好的文檔說明。
下面的例子說明了繼承的危險性:

//本來是為了統計HashSet自創建以來曾經添加了多少個元素,但結果卻是不正確的
//原因就是HashSet中addAll()的實現依賴于add()
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, 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;
    }
}

另一個危險的原因來源于overriding動作。如果在擴展一個類的時候,僅僅是添加新的方法,而不覆蓋現有的方法,你可能會認為這是安全的。但是,如果超類在后續的發行版本中獲得了一個新的方法,并且不幸的是,你給子類提供了一個簽名相同但返回類型不同的方法,這樣的子類將無法通過編譯。
采用復合則可以避免上述問題。復合不依賴于現有類的實現細節,即使現有的類添加了新的方法,也不影響新的類。
只有當子類真正是超類的子類型時,才適合繼承。換言之,對于兩個類A和B,只有當兩者之間確實存在“is-a”關系的時候,類B才應該擴展類A。
如果在適合于使用復用的地方使用了繼承,則會不必要地暴露實現細節。這樣得到的API會把你限制在原始的實現上,永遠限定了類的性能。更嚴重的是,由于暴露了內部的細節,客戶端就有可能直接訪問這些內部細節,導致語義上的混淆或直接導致錯誤發生。

在復用代碼的時候,優先考慮復合而非繼承,只有當子類和超類確實存在子類型關系時,使用繼承才是恰當的。即便如此,如果子類和超類處于不同的包中,并且超類并不是為了繼承而設計的,那么繼承將會導致脆弱性,為了避免這種脆弱性,可以用復用來代替繼承。

5.要么為繼承而設計,并提供文檔,要么就禁止繼承

對不是為了繼承而設計、并且沒有文檔說明的“外來”類進行子類化是很危險的。
對于為了繼承而設計的類,該類的文檔必須要精確地描述覆蓋每個方法所帶來的影響。即該類必須有文檔說明它可覆蓋的方法的自用性。對于每個公有的或受保護的方法或構造器,它的文檔必須指明該方法或構造器調用了哪些可覆蓋的方法,是以什么順序調用的,每個調用的結果又是如何影響后續的處理過程的。
另外,為了使程序員能夠編寫出更加有效的子類,而無需承受不必要的痛苦,類必須通過某種形式提供適當的鉤子(hook),以便能夠進入到它的內部工作流程中。
對于為了繼承而設計的類,唯一的測試方法就是編寫子類。經驗表明,3個子類通常就足以測試一個可擴展的類
在為了繼承而設計有可能被廣泛使用的類時,必須要意識到,對于文檔中所說明的自用模式,以及對于其protected方法和域中所隱含的實現策略,你實際上已經做出了永久性的承諾。這些承諾使得你在后續的版本中提高這個類的性能或者增加新功能都變得非常困難,甚至不可能。因此,必須在發布類之前首先編寫子類對類進行測試
為了允許繼承,類還必須遵守其他一些約束:構造器決不能調用可被覆蓋的方法,無論是直接調用還是間接調用。如果違反了這條規則,很有可能導致程序失敗。
對于普通的具體類,它們既不是final的,也不是為了子類化而設計和編寫文檔的,繼承它們是非常危險的,因為每次對這種類進行修改,從這個類擴展得到的客戶類都有可能遭到破壞。對這個問題的最佳解決方案是:對于那些并非為了安全地進行子類化而設計和編寫文檔的類,要禁止子類化。有兩種方法可以禁止子類化:

  1. 把這個類聲明為final的
  2. 把所有的構造器都變成私有的,或者包級私有的,并增加一些公有的靜態工廠來替代構造器。

如果禁止繼承可能會帶來不便,或者認為必須允許從這樣的類繼承,一種合理的辦法是確保這個類永遠不會調用它的任何可覆蓋的方法,并在文檔中說明這一點。換言之,完全消除這個類中可覆蓋方法的自用特性。這樣做之后,就可以創建“能夠安全地進行子類化”的類。覆蓋方法將永遠也不會影響其他任何方法的行為。
你可以消除類中可覆蓋方法的自用特性,而不改變它的行為。將每個可覆蓋方法的代碼體移到一個私有的“輔助方法”中,并且讓每個可覆蓋的方法調用它的私有輔助方法,然后直接調用私有的輔助方法來代替可覆蓋方法,去除可覆蓋方法的自用調用。

為繼承而設計的類,要提供文檔說明,并在發布前編寫子類進行測試;
對于普通的類,要盡可能地禁止其子類化,可以將其聲明為final或使用靜態方法來代替構造器;
對于無法禁止子類化的類,要消除這種類中可被子類覆蓋的方法的自用性,即在父類中使用的方法,要確保其在子類中不可被覆蓋。

6.接口優先于抽象類

Java語言提供了接口和抽象類兩種機制來定義多個實現的類型。由于Java只允許單繼承,抽象類作為類型定義受到了極大的限制。
使用接口定義類型的好處如下:

  1. 現有的類可以很容易被更新,以實現新的接口類型
  2. 接口是定義mixin(混合類型)的理想選擇
  3. 接口允許我們構造非層次結構的類型框架
public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(boolean hit);
}

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

使用接口定義類型并不妨礙為程序員提供實現上的幫助。通過對你導出的每個重要接口都提供一個抽象的骨架實現類,可以把接口和抽象類的優點結合起來。接口的作用仍然是定義類型,但骨架類實現接管了所有與接口實現相關的工作。
骨架類實現的美妙之處在于:它們為抽象類提供了實現上的幫助,但又不強加“抽象類被用作類型定義時”所特有的嚴格限制。對于接口的大多數實現來講,擴展骨架類實現是一個好的選擇,但并不是必需的。如果預置的類無法擴展骨架實現類,這個類始終可以手工實現這個接口。
骨架類可以被用來實現模擬多重繼承。利用接口定義類型,實現接口的類可以把對接口方法的調用,轉發到一個內部私有類的實例上,這個內部私有類擴展了骨架實現類。
下圖是Java容器框架圖,利用接口和骨架類,Joshua為我們實現了強大而又靈活的各個容器類。

Java集合類圖

編寫骨架實現類的步驟:

  1. 認真研究接口,確定哪些方法是最為基本的,其他方法則可以根據它們來實現
  2. 將上述基本方法定義成骨架實現類中的抽象方法
  3. 在骨架實現類中實現接口中其他的方法
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
    public abstract K getKay();
    public abstract V getValue();
    
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }


    @Override
    public int hashCode() {
        return hashCode(getKay()) ^ hashCode(getValue());
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return true;
        }
        if (!(obj instanceof Map.Entry)) {
            return false;
        }
        Map.Entry<?, ?> arg = (Map.Entry)obj;
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
    }
    
    private static boolean equals(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }
    
    private static int hashCode(Object obj) {
        return obj == null ? 0 : obj.hashCode();
    }
}

骨架實現類是為了繼承的目的而設計的,應該提供相應的文檔說明。
抽象類定義多個實現類型的優勢在于:抽象類的演變比接口的演變要容易很多。如果在后續的發行版本中,希望在抽象類中增加新的方法,始終可以增加具體方法,它包含合理的默認實現,則該抽象類的所有實現類都將獲得這個新的方法。而在公共接口中增加方法,就會破壞實現這個接口的所有現有的類。
設計公有接口要非常謹慎,因為接口一旦被公開發行,并且已被廣泛實現,再想改變這個接口幾乎是不可能的了。在發行接口的時候,最好的做法是:在接口被“凍結”之前,盡可能讓更多的程序員用盡可能多的方式來實現這個接口。這樣有助于在可以改正缺陷的時候就發現它們

接口通常是定義允許多個實現的最佳途徑。如果你導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。應該盡可能謹慎地設計所有的公有接口,并通過編寫多個實現來對它們進行全面的測試。

7.接口只用于定義類型

當類實現接口時,接口就充當了可以引用這個類的實例的類型。類實現了接口,就表明客戶端可以讓這個類的實例執行接口定義的動作。為了任何其他的目的而定義接口是不恰當的。
其中,常量接口模式是對接口的不良使用:

public interface PhysicalConstants {
    static final double AVOGANDROS_NUMBER = 6.02214199e23;
    static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    static final double ELECTRON_MASS = 9.10938188e-31;
}

類在內部使用某些常量,純粹是實現細節。實現常量接口,會導致把這樣的實現細節泄露到該類的導出API中。更糟糕的是它代表了一種承諾,如果將來的版本中這個類被修改了,不再需要使用這些常量了,它仍必須實現這個接口,以確保二進制兼容性。
導出常量,通常有三種處理方法:

  1. 如果常量與某個現有類或者接口緊密相關,就應該把常量添加到這個類或接口中
  2. 如果常量被看作枚舉類型的成員,就應該使用枚舉類型
  3. 將常量定義在不可實例化的工具類中
public class PhysicalConstants {

    private PhysicalConstants() {}

    public static final double AVOGANDROS_NUMBER = 6.02214199e23;
    public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    public static final double ELECTRON_MASS = 9.10938188e-31;
}

JDK1.5及其以后版本在使用這些靜態常量時,可使用靜態導入機制來簡化調用。

接口應該只被用來定義類型,它們不應該被用來導出常量。

8.類層次優于標簽類

有時候可能會遇到帶有兩種甚至更多種風格的實例的類,并包含表示實例風格的標簽域。

class Figure {
    enum Shape {
        RECTANGLE,
        CIRCLE
    }

    final Shape shape;

    double length;
    double width;

    double radius;

    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    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可以采用子類型化來構建類型層次,以此替換標簽類。為將標簽類轉變成類層次,具體做法如下:

  1. 定義一個抽象類,將標簽類中依賴標簽值的方法定義在抽象類中
  2. 為每種原始標簽類都定義根類的具體子類
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;
    }
}

類層次結構避免了標簽類所有的缺點,更重要的是它反映了類型之間本質上的層次關系,有助于增強靈活性,并可提供編譯時檢查,且非常容易擴展。

標簽類很少有適用的時候。當你要編寫一個包含顯式標簽域的類時,應該考慮一下,這個標簽類是否可以被取消,是否可以使用類層次來代替。當你遇到一個包含標簽域的現有類時,應考慮將它重構到一個層次結構中去。

9.用函數對象表示策略

有些語言支持函數指針、代理、lambda表達式,或者支持類似的機制,允許程序把“調用特殊函數的能力”存儲起來并傳遞這種能力。這種機制通常用于允許函數的調用者通過傳入第二個函數,來指定自己的行為。
Java沒有提供函數指針,但可以用對象引用實現同樣的功能。可以定義這樣一種對象:它的方法執行其他對象(這些對象被顯式傳遞給這些方法)上的操作。如果一個類僅僅導出一個這樣的方法,它的實例實際上就等同于一個指向該方法的指針。這樣的實例被稱為函數對象。

/*
* StringLengthComparator導出一個帶兩個字符串參數的方法
*指向它對象的引用可以被當作是一個指向該比較器的函數指針,它可以在任意一對字符串上調用
*/
class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

作為典型的具體策略類,StringLengthComparator是無狀態的,它沒有域,所以這個類的所有實例在功能上都是相互等價的。因此,它作為一個Singleton是非常合適的,可以節省不必要的對象創建開銷:

class StringLengthComparator {
    private StringLengthComparator() {}

    public static final StringLengthComparator INSTANCE = new StringLengthComparator();

    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

為了把StringLengthComparator實例傳遞給方法,需要適當的參數類型。使用StringLengthComparator并不好,因為客戶端將無法傳遞任何其他的比較策略。因此,在設計具體的策略類時,需要定義一個策略接口:

public interface Comparator<T> {
    public int compare(T t1, T t2);
}

具體的策略類往往使用匿名類聲明:

Arrays.sort(stringArray, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

使用匿名類時,將會在每次執行調用時創建一個新的實例。如果它被重復執行,可以考慮將一個函數對象存儲到一個私有的靜態final域里,并重用它。這樣做另外一個好處是:可以為這個函數對象取一個有意義的域名稱。

//通過公有靜態final域導出具體實現策略
class Host {
    private static class StrLenCmp implements Comparator<String> {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

    public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}

函數指針的主要用途就是實現策略模式。為了在Java中實現這種模式,要聲明一個接口來表示該策略,并且為每個具體策略聲明一個實現了該接口的類。當一個具體策略只被用來使用一次時,通常使用匿名類來聲明和實例化這個具體策略類。當一個具體策略是設計用來重復使用的時候,它的類通常被實現為私有的靜態成員類,并通過公有的靜態final域被導出,其類型為該策略接口。

10.優先考慮靜態成員類

嵌套類是指被定義在另一個類的內部的類。嵌套類存在的目的應該只是為它的外圍類提供服務。如果嵌套類將來可能會用于其它的某個環境中,它就應該被設計為頂層類。
嵌套類有四種:靜態成員類、非靜態成員類、匿名類、局部類。
可以把靜態成員類看作是普通的類,只是被聲明在另一個類的內部而已,它可以訪問外部類的所有成員,包括那些聲明為私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。如果它被聲明為私有的,它就只能在外圍類的內部才可以被訪問。靜態成員類的一種常見用法是作為公有的輔助類,僅當與它的外部類一起使用時才有意義。
非靜態成員類的每個實例都隱含著與外圍類的一個外圍實例相關聯。在非靜態成員類的實例方法內部,可以調用外圍實例上的方法,或者利用修飾過的this構造獲得外圍實例的引用。
當非靜態成員類的實例被創建的時候,它和外圍實例之間的關聯關系也隨之被建立起來,這種關聯關系以后不能被修改。
非靜態成員類的一個常見用法是定義一個Adapter,它允許外部類的實例被看作是另一個不相關的類的實例。

//非靜態成員類實現了Set的集合視圖
public class MySet<E> extends AbstractSet<E> {
    ...

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

如果聲明成員類不要求訪問外圍類實例,就要始終使用靜態成員類。因為非靜態成員類每個實例都包含一個額外的指向外圍對象的引用,保存這份引用要消耗時間和空間,并且會導致外圍實例在符合垃圾回收時仍得以保留。
匿名類沒有名字,它在使用的同時被聲明和實例化,由于匿名類出現在表達式中,它們必須保持簡短--大約10行或者更少些--否則就會影響程序的可讀性。
匿名類的常見用法:

  1. 動態地創建函數對象,如匿名的Comparator實例
  2. 創建過程對象,如Runnable、Thread、TimeTask實例
  3. 用在靜態工廠方法的內部
static List<Integer> intArrayAsList(final int[] a) {
    if(a == null) {
        throw new NullPointerException();
    }
    return new AbstractList<Integer>() {
        public Integer get(int i) {
            return a[i];
        }

        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;
            return oldVal;
        }

        public int size() {
            return a.length;
        }
    };
}

局部類是四種嵌套類中用的最少的類,在任何可以聲明局部變量的地方都可以聲明局部類,并且局部類也遵守同樣的作用域規則。它有名字,可被重復地使用,它不能包含靜態成員,必須非常簡短,以免影響可讀性。

四種嵌套類都有自己的用途。如果一個嵌套類需要在單個方法之外仍然可見,或者它太長了,不適合放在方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類聲明為非static的;否則就聲明為static的。如果這個嵌套類屬于一個方法的內部,而你只需要在一個地方創建實例,并且已經有了一個預置的類型可以說明這個類的特征,就要把它做成匿名類;否則,就做成局部類。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容