1. 程式人生 > >Thinking in Java——第十章-內部類

Thinking in Java——第十章-內部類

本文是對《Thinking in Java》第十章內容的整理。書中有些地方略顯晦澀,我加上了自己淺薄的理解。

文章結構
1、介紹了建立 內部類以及內部類的普遍特性
2、介紹了局部內部類、匿名內部類、巢狀類
3、分析了內部類的最大優點
4、內部類的繼承問題
5、內部類標誌符

可以將一個類的定義放在另一個類的定義內部,這就是內部類
學會內部類可以使你的程式碼更優雅

建立內部類

建立內部類的方式就如你想的一樣,把類的定義放在外圍類裡面

程式碼

public class Parcel1 {
    class Contents {
        private int
i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public void ship(String dest) { Contents c = new Contents(); Destination d = new
Destination(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel1 p = new Parcel1(); p.ship("Tasmania"); } }
/**Output
Tasmania
*/

當我們在ship()方法內使用內部類時,與使用普通類並沒有什麼不同。在這裡區別只是內部類的名字是巢狀在Parcel1裡面的。不過你會看到,這並不是唯一的區別

更經典的情況是,外部類有一個方法,該方法返回一個指向內部類的引用,就像下面的to()和Contents()

方法。如下

程式碼

public class Parcel2 {
    class Contents {
        private int i = 11;
        public int value() { return  i; }
    }
    class Destination {
        private String label;
        Destination(String whereTo) {
            label = whereTo;
        }
        String readLabel() { return label; }
    }
    public Contents contents() {
        return new Contents();
    }
    public Destination to(String s) {
        return new Destination(s);
    }
    public void ship(String dest) {
        Contents c = contents();
        Destination d = to(dest);
        System.out.println(d.readLabel());
    }
    public static void main(String[] args) {
        Parcel2 p = new Parcel2();
        p.ship("Tasmania");
        Parcel2 q = new Parcel2();
        Parcel2.Contents c = q.contents();
        Parcel2.Destination d = q.to("Borneo");
    }
}
/**Output
Tasmania
*/

我們看到在main方法中建立內部類物件時,需要具體的指明這個物件的型別OutClassName.InnerClassName

連結到外部類

到目前為止,內部類似乎還只是一種名字隱藏和組織程式碼的模式。這很有用,但絕對不是重點。它還有另一個用途,當生成一個內部類的物件時,此物件與製造它的外圍物件之間就有了一種聯絡,所以它能訪問其外圍物件的所有成員,而不需要任何特殊條件

我們通過實現迭代器來內部類是如何訪問外圍物件的成員的

interface Selector {
    boolean end();
    Object current();
    void next();
}
public class Sequence {
    private Object[] items;
    private int next = 0;
    public Sequence(int size) { items = new Object[size]; }
    public void add(Object x) {
        if(next < items.length) items[next++] = x;
    }
    private class SequenceSelector implements Selector{
        private int i = 0;
        public boolean end() { return i == items.length; }
        public Object current() { return items[i]; }
        public void next() { if(i < items.length) ++i; }
    }
    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for(int i = 0; i < 10; ++i) sequence.add(Integer.toString(i));
        Selector selector = sequence.selector();
        while(!selector.end()) {
            System.out.print(selector.current() + " ");
            selector.next();
        }
    }
}
/**Output
0 1 2 3 4 5 6 7 8 9 
*/

內部類自動擁有對其外圍類所有成員的訪問權。這是如何做到的呢?當某個外圍類的物件建立了一個內部類物件時,此內部類物件必定會祕密的捕獲一個指向那個外圍類物件的引用。然後在你訪問此外圍類的成員時,就是用那個引用來選擇外圍類的成員。

檢視.class檔案內容就可以看到這個外圍類物件[this$0]

這裡寫圖片描述
當你訪問內部類物件時就會用到這個物件[this$0]
這裡寫圖片描述
如果不使用內部類那應該怎樣實現該功能呢?繼承Selector介面。這相當於宣告”Sequence是一個Selector“。但是我如果想再提供一個反向遍歷的迭代器呢?再繼承一個reverseSelector介面嗎?那這裡就有了讓人迷惑的“is-a“關係,同時你可能需要修改兩個介面中衝突的方法的名字。

然而,用內部類卻簡單無比。只有內部類有這種靈活性

使用.this和.new

如果你需要生成對外部類物件的引用,可以使用外部類的名字後面緊跟.this

程式碼

public class DoThis {
    void f() { System.out.println("DotThis.f()"); }
    public class Inner {
        public DoThis outer() { return DoThis.this; }
    }
    public Inner inner() { return new Inner(); }
    public static void main(String[] args) {
        DoThis dt = new DoThis();
        DoThis.Inner dti = dt.inner();
        dti.outer().f();
    }
}
/**
DotThis.f()
*/

如果想直接建立內部類物件可以使用.new, 如下

public class DotNew {
    public class Inner {}
    public static void main(String[] args) {
        DotNew dn = new DotNew();
        DotNew.Inner dni = dn.new Inner();
    }
}

內部類與向上轉型

當將內部類向上轉型為其基類,尤其是轉型成為一個介面時,內部類就有了用武之地。這是因為此內部類——某個介面的實現——能夠完全不可見,並且不可用,所得到的只是指向基類或介面的引用,所以能夠很好的隱藏實現細節

程式碼

class Parcel4 {
    private class PContents implements Contents{
        private int i = 11;
        @Override
        public int value() { return i; }
    }
    protected class PDestination implements Destination{
        private String label;
        public PDestination(String  whereTo) { label= whereTo;}
        @Override
        public String readLabel() { return label; }
    }
    public Destination destination(String s) {
        return new PDestination(s);
    }
    public Contents contents() { return new PContents();}
}
public class TestParcel {
    public static void main(String[] args) {
        Parcel4 p = new Parcel4();
        Contents c = p.contents();
        Destination d = p.destination("Tamania");
    }
}

因為PContentsprivate的,所以這種方式可以 完全阻止依賴於型別的編碼,Parcel4.PContents pc = p.new PContents();是不被允許的。所有Pcontent 物件的引用都必須 是Contents介面,所以肯定沒有依賴於型別的編碼。並且它完全隱藏了實現的細節。

在方法和作用域內的內部類

在一個 方法裡面或者在任意的作用域內定義內部類,這麼做有兩個理由
1、如前所示,你實現了某型別的介面,於是可以建立並返回對其的引用
2、你要解決一個複雜的問題,想建立一個類來輔助你的解決方案,但是又不希望這個類是公共可用的

下面的例子展示了在方法中的作用域內,建立一個完整的類,這被稱作區域性內部類

public class Parcel5 {
    public Destination destination(String s) {
        class PDestination implements Destination {
            private String label;
            public PDestination(String whereTo){ label = whereTo; }
            @Override
            public String readLabel() { return label; }
        }
        return new PDestination(s);
    }
    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        Destination d = p.destination("Tasmania");
    }
}

PDestinationdestination方法的一部分,而不是Parcel5的一部分。所以 destination之外不能訪問這個類

下面的例子展示瞭如何在任意一個作用域中嵌入內部類

public class Parcel6 {
    private void internalTracking(boolean b) {
        if(b) {
            class TrackingSlip {
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip() { return id; }
            }
            TrackingSlip trackingSlip = new TrackingSlip("slip");
            String s = trackingSlip.getSlip();
        }
    }
}

if語句的作用域中建立內部類

匿名內部類

下面的程式碼看起來有點奇怪

public class Parcel7 {
    public Contents contents() {
        return new Contents() {
            private int i = 11;
            @Override
            public int value() { return i; }
        };
    }
}

contents方法將返回值的生成與表示這個返回值得類的定義結合在一起!另外,這個類是沒有名字的。
上述匿名內部類的語法是下述形式的簡化形式

public class Parcel7b {
    class MyContents implements Contents{
        private int i = 11;
        public int value() { return i; }
    }
    public Contents contents() { return new MyContents(); }

    public static void main(String[] args) {
        Parcel7b p = new Parcel7b();
        Contents c = p.contents();
    }
}

上面的程式碼是使用的無參構造器,下面的程式碼展示的是如果你需要一個有參構造器,該怎麼辦

public class Wrapping {
    private int i;
    public Wrapping(int x){ i = x; }
    public int value() { return i; }
}
public class Parcel8 {
    public Wrapping wrapping(int x) {
        return new Wrapping(x) {
            public int value() { return super.value() * 47; }
        };
    }

    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);
    }
}

只需簡單的傳遞合適的引數給基類的構造器就可以,這裡是將x傳進new Wrappint(x)。儘管Wrapping只是一個具有某個 具體 實現的普通類,但它還是 被其匯出類當過公共“介面”使用。

在匿名類中定義欄位時,還可以進行初始化操作

public class Parcel9 {
    public Destination destination(final String dest) {
        return new Destination() {
            private String label = dest;
            @Override
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel9 p = new Parcel9();
        Destination d = p.destination("Tasmania");
    }
}

如果定義一個匿名內部類,並且希望它使用一個在其外部定義的物件,那麼其引數引用必須是final

巢狀類

如果不需要內部類物件與其外圍類物件之間有聯絡,那麼可以將內部類宣告為static 這通常成為巢狀類

public class Parcel11 {
    private static class ParcelContents implements Contents{
        private int i = 11;
        public int value() { return i; }
    }
    protected static class ParcelDestination
        implements Destination {
        private String label;
        private ParcelDestination(String whereTo) {
            label = whereTo;
        }
        public String readLabel() { return label; }
    }
    public static Destination destination(String s) { return new ParcelDestination(s); }
    public static Contents contents() {  return new ParcelContents(); }

    public static void main(String[] args) {
        Contents c = contents();
        Destination d = destination("Tasmina");
    }
}

巢狀類沒有了之前我們看到的特殊的this引用,這使得它類似於一個static方法

介面內部的類

正常情況下,不能在介面中放置任何程式碼,但巢狀類可以作為介面的一部分,你放到介面中的任何類都自動是publicstatic的。所以這並不違反介面的規則。你甚至可以在內部類中實現其外圍介面,如下

public interface ClassInInterface {
    void howdy();
    class Test implements ClassInInterface{
        public void howdy() {
            System.out.println("Howdy");
        }
        public static void main(String[] args) {
            new Test().howdy();
        }
    }
}
/**Output
Howdy
*/

如果你想要建立某些公共程式碼,使得他們可以被某個介面的所有不同實現所共用,那麼使用介面內部的巢狀類會顯得很方便

為什麼需要內部類

使用內部類最吸引人的原因是每個內部類都能繼承自 一個(介面)的實現,所以無論外圍類是否繼承了 某個介面的實現,對於內部類沒有影響內部類有效的實現了”多重繼承”,也就是說內部類允許繼承 多個非介面型別。

下面看一個例子

class D {}
abstract class E{}
class Z extends D {
    E makeE() {
        return new E() {

        };
    }
}
public class MultiImplementation {
    static void takesD(D d) {}
    static void takesE(E e) {}

    public static void main(String[] args) {
        Z z = new Z();
        takesD(z);
        takesE(z.makeE());
    }
}

閉包與回撥
閉包是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自於建立它的作用域 。通過這個定義,可以看出內部類是面向物件的閉包,因為它不僅包含外圍類物件(建立內部類的作用域)的資訊,還自動 擁有一個指向此外圍類物件的引用,在此作用域內,內部類有權操作所有的成員,包括是private成員

Java最引人爭議的問題之一就是 ,人們認為Java應該包含某種類似指標的機制,以允許回撥。通過回撥,物件能攜帶一些資訊,這些資訊允許它在稍後的 某個時刻呼叫初試的物件。稍後 我們會看到這是一個非常有用的概念。如果回撥時通過指標實現的,那麼就只能希望程式設計師不會誤用該指標。然而,Java更小心仔細,所以 沒有在語言中包括指標

通過內部類提供閉包的功能是優良的解決方案,它比指標更加靈活、安全

見程式碼

interface Incrementable {
    void increment();
}
class Callee1 implements Incrementable {
    private int i = 0;
    public void increment(){ ++i; System.out.println(i); }
}
class MyIncrement {
    public void increment() { System.out.println("Other operation"); }
    static void f(MyIncrement mi) { mi.increment(); }
}
class Callee2 extends MyIncrement {
    private int i = 0;
    public void increment() {
        super.increment();
        i++;
        System.out.println(i);
    }
    private class Closure implements Incrementable {
        @Override
        public void increment() {
            Callee2.this.increment();
        }
    }
    Incrementable getCallbackReference() {
        return new Closure();
    }
}
class Caller {
    private Incrementable callbackReference;
    Caller(Incrementable cbh) { callbackReference = cbh; }
    void go() { callbackReference.increment(); }
}
public class Callbacks {
    public static void main(String[] args) {
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        MyIncrement.f(c2);
        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallbackReference());
        caller1.go();
        caller1.go();
        caller2.go();
        caller2.go();
    }
}
/**Output
Other operation
1
1
2
Other operation
2
Other operation
3
*/

內部類的繼承

因為內部類的構造器必須 連線到外圍類的引用,所以在繼承內部類時必須初始化那個指向外圍類的”祕密的引用”,而在匯出類中不再存在可以連線的預設物件。要解決這個問題,必須使用特殊的語法才可以

class WithInner {
    class Inner{}
}
public class InheritInner extends WithInner.Inner{
    //!InheritInner() {} Wot't compile
    InheritInner(WithInner wi){
        wi.super();
    }

    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner ii = new InheritInner(wi);
    }
}

內部類可以被覆蓋嗎

不可以,見例項

class Egg {
    private Yolk y;
    protected class Yolk {
        public Yolk() { System.out.println("Egg.Yolk()"); }
    }
    public Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}
public class BigEgg extends Egg{
    public class Yolk{
        public Yolk() { System.out.println("BigEgg.Yolk"); }
    }

    public static void main(String[] args) {
        new BigEgg();
    }
}

當然,如果明確的繼承某個內部類也是可以 的

public void Yolk extend Egg.Yolk{

}

區域性內部類

區域性內部類和匿名內部類仍然可以訪問當前程式碼塊的常量以及外圍類的成員,可以自己用區域性內部類或匿名內部類去實現之前的迭代器試試

內部類標誌符

由於每個類 都會產生一個.class檔案,其中 包含了如何建立該型別的物件的全部資訊,內部類當然不例外。內部類的命名有嚴格的規則:外圍類的名字+“$”+內部類的名字。如果內部類是匿名的則會用一個數字代替內部類的名字