第十章 內部類
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 {@Overridepublic 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 定義在方法中和任意作用域內部的類
上面所展示的一些內部類的定義都是普通內部類的定義,如果我想在一個方法中或者某個作用域內定義一個內部類該如何編寫呢?
你可能會考慮這幾種定義的思路:
- 我想定義一個內部類,它實現了某個接口,我定義內部類是為了返回接口的引用
- 我想解決某個問題,并且這個類又不希望它是公共可用的,顧名思義就是封裝起來,不讓別人用
- 因為懶...
以下是幾種定義內部類的方式:
- 一個在方法中定義的類(局部內部類)
- 一個定義在作用域內的類,這個作用域在方法的內部(成員內部類)
- 一個實現了接口的匿名類(匿名內部類)
- 一個匿名類,它擴展了非默認構造器的類
- 一個匿名類,執行字段初始化操作
- 一個匿名類,它通過實例初始化實現構造
- 定義在方法內部的類又被稱為局部內部類
public class Parcel5 {private Destination destination(String s){class PDestination implements Destination{String label;public PDestination(String whereTo){label = whereTo;}@Overridepublic 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;@Overridepublic 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;@Overridepublic 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方法,實現子類的執行邏輯@Overridepublic 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;@Overridepublic String readLabel() {return label;}};}public static void main(String[] args) {Parcel9 p9 = new Parcel9();System.out.println(p9.destination("pen").readLabel());}
}
Java編程思想p198中說如果給字段進行初始化操作,那么形參必須是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);}@Overridepublic 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()方法變成靜態的,會編譯出錯的原因:
Java編程思想p201頁講到:如果不需要內部類與其外圍類之前產生關系的話,就把內部類聲明為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的字段@Overridepublic int getValue() {return value;}// 編譯不報錯,靜態內部類PContents可以訪問靜態屬性bValuepublic int f(){return bValue;}}// 普通內部類private class PDestination implements Destination {@Overridepublic 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編譯出錯的原因是 靜態方法不能直接訪問非靜態內部類,而需要通過創建外圍類的對象來訪問普通內部類。
10.5.2 接口內部的類
納尼?接口內部只能定義方法,難道接口內部還能放一個類嗎?可以!
正常情況下,不能在接口內部放置任何代碼,但是嵌套類作為接口的一部分,你放在接口中的任何類默認都是public和static的。因為類是static的,只是將嵌套類置于接口的命名空間內,這并不違反接口的規則,你甚至可以在內部類實現外部類的接口,不過一般我們不提倡這么寫
public interface InnerInterface {void f();class InnerClass implements InnerInterface {@Overridepublic void f() {System.out.println("實現了接口的方法");}public static void main(String[] args) {new InnerClass().f();}}// 不能在接口中使用main方法,你必須把它定義在接口的內部類中
// public static void main(String[] args) {}
}
輸出: 實現了接口的方法
10.5.3 內部類實現多重繼承
在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();}@Overridepublic void f() {System.out.println("InheritInner.f()");}/** 加上@Override 會報錯,因為BaseInnerClass 中沒有g()方法* 這也是為什么覆寫一定要加上Override注解的原因,否則默認是本類* 中持有的方法,會造成誤解,程序員以為g()方法是重寫過后的。@Overridepublic 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
內部類的優點:1、封裝部分代碼,當你創建一個內部類的時候,該內部類默認持有外部類的引用;2、內部類具有一定的靈活性,無論外圍類是否繼承某個接口的實現,對于內部類都沒有影響;3、內部類能夠有效的解決多重繼承的問題。