Java中內部類的騷操作

Java極客技術發表於2019-07-30

 

10.1 如何定義內部類

如程式碼10.1-1 所示

public class Parcel1 {    public class Contents{        private int value = 0;        public int getValue(){            return value;        }    }}

 

這是一個很簡單的內部類定義方式,你可以直接把一個類至於另一個類的內部,這種定義Contents類的方式被稱為內部類

那麼,就像程式碼10.1-1所展示的,程式設計師該如何訪問Contents中的內容呢?

如程式碼10.1-2 所示

 

public class Parcel1 {    public class Contents{        private int value = 0;        public int getValue(){            return value;        }    }    public Contents contents(){        return new Contents();    }    public static void main(String[] args) {        Parcel1 p1 = new Parcel1();        Parcel1.Contents pc1 = p1.contents();        System.out.println(pc1.getValue());    }}

 

輸出結果: 0

就像上面程式碼看到的那樣,你可以寫一個方法來訪問Contents,相當於指向了一個對Contents的引用,可以用外部類.內部類這種定義方式來建立一個對於內部類的引用,就像Parcel1.Contents pc1 = p1.contents();所展示的,而pc1 相當於持有了對於內部類Contents的訪問許可權。

現在,我就有一個疑問,如果10.1-2 中的contents方法變為靜態方法,pc1還能訪問到嗎?

 

編譯就過不去,那麼為什麼會訪問不到呢?請看接下來的分析。

10.2 連結到外部類的方式

看到這裡,你還不明白為什麼要採用這種方式來編寫程式碼,好像只是為了裝逼?或者你覺得重新定義一個類很麻煩,乾脆直接定義一個內部類得了,好像到現在並沒有看到這種定義內部類的方式為我們帶來的好處。請看下面這個例子10.2-1

 

public class Parcel2 {    private static int i = 11;    public class Parcel2Inner {        public Parcel2Inner(){            i++;        }        public int getValue(){            return i;        }    }    public Parcel2Inner parcel2Inner(){        return new Parcel2Inner();    }    public static void main(String[] args) {        Parcel2 p2 = new Parcel2();        for(int i = 0;i < 5;i++){            p2.parcel2Inner();        }        System.out.println("p2.i = " + p2.i);    }}

 

輸出結果: 16

當你建立了一個內部類物件的時候,此物件就與它的外圍物件產生了某種聯絡,如上面程式碼所示,內部類Parcel2Inner 是可以訪問到Parcel2中的i的值的,也可以對這個值進行修改。

那麼,問題來了,如何建立一個內部類的物件呢?程式設計師不能每次都寫一個方法返回外部類的物件吧?見程式碼10.2-2

 

public class Parcel3 {    public class Contents {        public Parcel3 dotThis(){            return Parcel3.this;        }        public String toString(){            return "Contents";        }    }    public Parcel3 contents(){        return new Contents().dotThis();    }    public String toString(){        return "Parcel3";    }    public static void main(String[] args) {        Parcel3 pc3 = new Parcel3();        Contents c = pc3.new Contents();        Parcel3 parcel3 = pc3.contents();        System.out.println(pc3);        System.out.println(c);        System.out.println(parcel3);    }}

 

輸出: Parcel3 Contents Parcel3

如上面程式碼所示,Parcel3內定義了一個內部類Contents,內部類中定義了一個方法dotThis(),這個方法的返回值為外部類的物件,在外部類中有一個contents()方法,這個方法返回的還是外部類的引用。

10.3 內部類與向上轉型

本文到現在所展示的都是本類持有內部類的訪問許可權,那麼,與此類無關的類是如何持有此類內部類的訪問許可權呢?而且內部類與向上轉型到底有什麼關係呢? 如圖10.3-1

public interface Animal {    void eat();}public class Parcel4 {    private class Dog implements Animal {        @Override        public void eat() {            System.out.println("啃骨頭");        }    }    public Animal getDog(){        return new Dog();    }    public static void main(String[] args) {        Parcel4 p4 = new Parcel4();        //Animal dog = p4.new Dog();        Animal dog = p4.getDog();        dog.eat();    }}

 

輸出: 啃骨頭

這個輸出大家肯定都知道了,Dog是由private修飾的,按說非本類的任何一個類都是訪問不到,那麼為什麼能夠訪問到呢? 仔細想一下便知,因為Parcel4 是public的,而Parcel4是可以訪問自己的內部類的,那麼Animal也可以訪問到Parcel4的內部類也就是Dog類,並且Dog類是實現了Animal介面,所以getDog()方法返回的也是Animal類的子類,從而達到了向上轉型的目的,讓程式碼更美妙。

10.4 定義在方法中和任意作用域內部的類

上面所展示的一些內部類的定義都是普通內部類的定義,如果我想在一個方法中或者某個作用域內定義一個內部類該如何編寫呢? 你可能會考慮這幾種定義的思路:

1.我想定義一個內部類,它實現了某個介面,我定義內部類是為了返回介面的引用

2.我想解決某個問題,並且這個類又不希望它是公共可用的,顧名思義就是封裝起來,不讓別人用

3.因為懶...

以下是幾種定義內部類的方式:

•一個在方法中定義的類(區域性內部類)•一個定義在作用域內的類,這個作用域在方法的內部(成員內部類)•一個實現了介面的匿名類(匿名內部類)•一個匿名類,它擴充套件了非預設構造器的類•一個匿名類,執行欄位初始化操作•一個匿名類,它通過例項初始化實現構造定義在方法內部的類又被稱為區域性內部類

public class Parcel5 {        private Destination destination(String s){            class PDestination implements Destination{                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 p5 = new Parcel5();            Destination destination = p5.destination("China");            System.out.println(destination.readLabel());        }}

輸出 : China

如上面程式碼所示,你可以在編寫一個方法的時候,在方法中插入一個類的定義,而內部類中的屬性是歸類所有的,我在寫這段程式碼的時候很好奇,內部類的執行過程是怎樣的,Debugger走了一下發現當執行到p5.destination("China")的時候,先會執行return new PDestination(s),然後才會走PDestination的初始化操作,這與我們對其外部類的初始化方式是一樣的,只不過這個方法提供了一個訪問內部類的入口而已。 注: 區域性內部類的定義不能有訪問修飾符

一個定義在作用域內的類,這個作用域在方法的內部

public class Parcel6 {        // 吃椰子的方法        private void eatCoconut(boolean flag){            // 如果可以吃椰子的話            if(flag){                class Coconut {                    private String pipe;                    public Coconut(String pipe){                        this.pipe = pipe;                    }                    // 喝椰子汁的方法                    String drinkCoconutJuice(){                        System.out.println("喝椰子汁");                        return pipe;                    }                }                // 提供一個吸管,可以喝椰子汁                Coconut coconut = new Coconut("用吸管喝");                coconut.drinkCoconutJuice();            }            /**             * 如果可以吃椰子的話,你才可以用吸管喝椰子汁             * 如果不能接到喝椰子汁的指令的話,那麼你就不能喝椰子汁             */            // Coconut coconut = new Coconut("用吸管喝");            // coconut.drinkCoconutJuice();        }        public static void main(String[] args) {            Parcel6 p6 = new Parcel6();            p6.eatCoconut(true);        }}

輸出: 喝椰子汁

如上面程式碼所示,只有程式設計師告訴程式,現在我想吃一個椰子,當程式接收到這條命令的時候,它回答好的,馬上為您準備一個椰子,並提供一個吸管讓您可以喝到新鮮的椰子汁。程式設計師如果不想吃椰子的話,那麼程式就不會為你準備椰子,更別說讓你喝椰子汁了。

一個實現了匿名介面的類

我們都知道介面是不能被例項化的,也就是說你不能return 一個介面的物件,你只能是返回這個介面子類的物件,但是如果像下面這樣定義,你會不會表示懷疑呢?

public interface Contents {    int getValue();}public class Parcel7 {    private Contents contents(){        return new Contents() {            private int value = 11;            @Override            public int getValue() {                return value;            }        };    }    public static void main(String[] args) {        Parcel7 p7 = new Parcel7();        System.out.println(p7.contents().getValue());    }}

輸出 : 11

為什麼能夠返回一個介面的定義?而且還有 {},這到底是什麼鬼? 這其實是一種匿名內部類的寫法,其實和上面所講的內部類和向上轉型是相似的。也就是說匿名內部類返回的new Contents()其實也是屬於Contents的一個實現類,只不過這個實現類的名字被隱藏掉了,能用如下的程式碼示例來進行轉換:

public class Parcel7b {    private class MyContents implements Contents {        private int value = 11;        @Override        public int getValue() {            return 11;        }    }    public Contents contents(){        return new MyContents();    }    public static void main(String[] args) {        Parcel7b parcel7b = new Parcel7b();        System.out.println(parcel7b.contents().getValue());    }}

輸出的結果你應該知道了吧~! 你是不是覺得這段程式碼和 10.3 章節所表示的程式碼很一致呢?

一個匿名類,它擴充套件了非預設構造器的類

如果你想返回一個帶有引數的構造器(非預設的構造器),該怎麼表示呢?

public class WithArgsConstructor {    private int sum;    public WithArgsConstructor(int sum){        this.sum = sum;    }    public int sumAll(){        return sum;    }}public class Parcel8 {    private WithArgsConstructor withArgsConstructor(int x){        // 返回WithArgsConstructor帶引數的構造器,執行欄位初始化        return new WithArgsConstructor(x){            // 重寫sumAll方法,實現子類的執行邏輯            @Override            public int sumAll(){                return super.sumAll() * 2;            }        };    }    public static void main(String[] args) {        Parcel8 p8 = new Parcel8();        System.out.println(p8.withArgsConstructor(10).sumAll());    }}

以上WithArgsConstructor 中的程式碼很簡單,定義一個sum的欄位,構造器進行初始化,sumAll方法返回sum的值,Parcel8中的withArgsConstructor方法直接返回x的值,但是在這個時候,你想在返回值上做一些特殊的處理,比如你想定義一個類,重寫sumAll方法,來實現子類的業務邏輯。 Java程式設計思想198頁中說 程式碼中的“;”並不是表示內部類結束,而是表示式的結束,只不過這個表示式正巧包含了匿名內部類而已。

一個匿名類,它能夠執行欄位初始化

上面程式碼確實可以進行初始化操作,不過是通過構造器執行欄位的初始化,如果沒有帶引數的構造器,還能執行初始化操作嗎? 這樣也是可以的。

public class Parcel9 {    private Destination destination(String dest){        return new Destination() {            // 初始化賦值操作            private String label = dest;            @Override            public String readLabel() {                return label;            }        };    }    public static void main(String[] args) {        Parcel9 p9 = new Parcel9();        System.out.println(p9.destination("pen").readLabel());    }}

如果給欄位進行初始化操作,那麼形參必須是final的,如果不是final,編譯器會報錯,這部分提出來質疑,因為我不定義為final,編譯器也沒有報錯。 我考慮過是不是private的問題,當我把private 改為public,也沒有任何問題。

我不清楚是中文版作者翻譯有問題,還是經過這麼多Java版本的升級排除了這個問題,我沒有考證原版是怎樣寫的,這裡還希望有知道的大牛幫忙解釋一下這個問題。

一個匿名類,它通過例項初始化實現構造

public abstract class Base {    public Base(int i){        System.out.println("Base Constructor = " + i);    }    abstract void f();}public class AnonymousConstructor {    private static Base getBase(int i){        return new Base(i){            {                System.out.println("Base Initialization" + i);            }            @Override            public void f(){                System.out.println("AnonymousConstructor.f()方法被呼叫了");            }        };    }    public static void main(String[] args) {        Base base = getBase(57);        base.f();    }}

輸出: Base Constructor = 57 Base Initialization 57 AnonymousConstructor.f()方法被呼叫了

這段程式碼和 "一個匿名類,它擴充套件了非預設構造器的類" 中屬於相同的範疇,都是通過構造器實現初始化的過程。

10.5 巢狀類

10.4 我們介紹了6種內部類定義的方式,現在我們來解決一下10.1 提出的疑問,為什麼contents()方法變成靜態的,會編譯出錯的原因:

如果不需要內部類與其外圍類之前產生關係的話,就把內部類宣告為static。這通常稱為巢狀類,也就是說巢狀類的內部類與其外圍類之前不會產生某種聯絡,也就是說內部類雖然定義在外圍類中,但是確實可以獨立存在的。巢狀類也被稱為靜態內部類。 靜態內部類意味著: (1)要建立巢狀類的物件,並不需要其外圍類的物件 (2)不能從巢狀類的物件中訪問非靜態的外圍類物件

程式碼示例 10.5-1

public class Parcel10 {    private int value = 11;    static int bValue = 12;    // 靜態內部類    private static class PContents implements Contents {        // 編譯報錯,靜態內部類PContents中沒有叫value的欄位        @Override        public int getValue() {            return value;        }        // 編譯不報錯,靜態內部類PContents可以訪問靜態屬性bValue        public int f(){            return bValue;        }    }    // 普通內部類    private class PDestination implements Destination {        @Override        public String readLabel() {            return "label";        }    }    // 編譯不報錯,因為靜態方法可以訪問靜態內部類    public static Contents contents(){        return new PContents();    }    // 編譯報錯,因為非靜態方法不能訪問靜態內部類    public Contents contents2(){        Parcel10 p10 = new Parcel10();        return p10.new PContents();    }    // 編譯不報錯,靜態方法可以訪問非靜態內部類    public static Destination destination(){        Parcel10 p10 = new Parcel10();        return p10.new PDestination();    }    // 編譯不報錯,非靜態方法可以訪問非靜態內部類    public Destination destination2(){        return new PDestination();    }}

由上面程式碼可以解釋,10.1編譯出錯的原因是 靜態方法不能直接訪問非靜態內部類,而需要通過建立外圍類的物件來訪問普通內部類。

介面內部的類

納尼?介面內部只能定義方法,難道介面內部還能放一個類嗎?可以! 正常情況下,不能在介面內部放置任何程式碼,但是巢狀類作為介面的一部分,你放在介面中的任何類預設都是public和static的。因為類是static的,只是將巢狀類置於介面的名稱空間內,這並不違反介面的規則,你甚至可以在內部類實現外部類的介面,不過一般我們不提倡這麼寫

 

public interface InnerInterface {    void f();    class InnerClass implements InnerInterface {        @Override        public void f() {            System.out.println("實現了介面的方法");        }        public static void main(String[] args) {            new InnerClass().f();        }    }    // 不能在介面中使用main方法,你必須把它定義在介面的內部類中//    public static void main(String[] args) {}}

 

輸出: 實現了介面的方法

內部類實現多重繼承

在Java中,類與類之間的關係通常是一對一的,也就是單項繼承原則,那麼在介面中,類與介面之間的關係是一對多的,也就是說一個類可以實現多個介面,而介面和內部類結合可以實現"多重繼承",並不是說用extends關鍵字來實現,而是介面和內部類的對多重繼承的模擬實現。

參考chenssy的文章 http://www.cnblogs.com/chenssy/p/3389027.html 已經寫的很不錯了。

 

public class Food {    private class InnerFruit implements Fruit{        void meakFruit(){            System.out.println("種一個水果");        }    }    private class InnerMeat implements Meat{        void makeMeat(){            System.out.println("煮一塊肉");        }    }    public Fruit fruit(){        return new InnerFruit();    }    public Meat meat(){        return new InnerMeat();    }    public static void main(String[] args) {        Food food = new Food();        InnerFruit innerFruit = (InnerFruit)food.fruit();        innerFruit.meakFruit();        InnerMeat innerMeat = (InnerMeat) food.meat();        innerMeat.makeMeat();    }}

輸出: 種一個水果 煮一塊肉

10.6 內部類的繼承

內部類之間也可以實現繼承,與普通類之間的繼承相似,不過不完全一樣。

public class BaseClass {    class BaseInnerClass {        public void f(){            System.out.println("BaseInnerClass.f()");        }    }    private void g(){        System.out.println("BaseClass.g()");    }}/** *  可以看到,InheritInner只是繼承自內部類BaseInnerClass,而不是外圍類 *  但是預設的構造方式會報編譯錯誤, *  必須使用類似enclosingClassReference.super()才能編譯通過 *  用來來說明內部類與外部類物件引用之間的關聯。 * */public class InheritInner extends BaseClass.BaseInnerClass{    // 編譯出錯//    public InheritInner(){}    public InheritInner(BaseClass bc){        bc.super();    }    @Override    public void f() {        System.out.println("InheritInner.f()");    }    /*    * 加上@Override 會報錯,因為BaseInnerClass 中沒有g()方法    * 這也是為什麼覆寫一定要加上Override註解的原因,否則預設是本類    * 中持有的方法,會造成誤解,程式設計師以為g()方法是重寫過後的。    @Override    public void g(){        System.out.println("InheritInner.g()");    }*/    public static void main(String[] args) {        BaseClass baseClass = new BaseClass();        InheritInner inheritInner = new InheritInner(baseClass);        inheritInner.f();    }}

輸出:InheritInner.f()

10.7 內部類的覆蓋

關於內部類的覆蓋先來看一段程式碼:

public class Man {    private ManWithKnowledge man;    protected class ManWithKnowledge {        public void haveKnowledge(){            System.out.println("當今社會是需要知識的");        }    }    // 我們想讓它輸出子類的haveKnowledge()方法    public Man(){        System.out.println("當我們有了一個孩子,我們更希望他可以當一個科學家,而不是網紅");        new ManWithKnowledge().haveKnowledge();    }}// 網紅public class InternetCelebrity extends Man {    protected class ManWithKnowledge {        public void haveKnowledge(){            System.out.println("網紅是當今社會的一種病態");        }    }    public static void main(String[] args) {        new InternetCelebrity();    }}

輸出:當我們有了一個孩子,我們更希望他可以當一個科學家,而不是網紅 當今社會是需要知識的

我們預設內部類是可以覆蓋的。所以我們想讓他輸出 InternetCelebrity.haveKnowledge() ,來實現我們的猜想,但是卻輸出了ManWithKnowledge.haveKnowledge()方法。 這個例子說明當繼承了某個外圍類的時候,內部類並沒有發生特別神奇的變化,兩個內部類各自獨立,都在各自的名稱空間內。

10.8 關於原始碼中內部類的表示

由於每個類都會產生一個.class 檔案,包含了建立該型別物件的全部資訊 同樣的,內部類也會生成一個.class 檔案 表示方法為: OneClass$OneInnerClass

後記: 內部類屬於Java語言的高階特性,內部類在實際的應用場景比較少,內部類可以讓程式碼變的更加優雅,但是對於平常開發的話用處不是很大,畢竟不是所有同事都能瞭解全面內部類的特性和用法,所以在平常開發中儘量避免使用內部類,加大維護成本。內部類應該在設計階段考慮使用,你應該區分內部類和介面的應用場景,就算用不到,你也get到了一項新技能,就算裝逼也能落落大方的不是嗎?

10.9 內部類的優點

1、封裝部分程式碼,當你建立一個內部類的時候,該內部類預設持有外部類的引用;

2、內部類具有一定的靈活性,無論外圍類是否繼承某個介面的實現,對於內部類都沒有影響;

3、內部類能夠有效的解決多重繼承的問題。

 


 

Java 極客技術公眾號,是由一群熱愛 Java 開發的技術人組建成立,專注分享原創、高質量的 Java 文章。如果您覺得我們的文章還不錯,請幫忙讚賞、在看、轉發支援,鼓勵我們分享出更好的文章。

關注公眾號,大家可以在公眾號後臺回覆“部落格園”,免費獲得作者 Java 知識體系/面試必看資料。

相關文章