Java程序員必備基礎(chǔ):內(nèi)部類解析
前言
整理了一下內(nèi)部類的相關(guān)知識(shí),算是比較全,比較基礎(chǔ)的,希望大家一起學(xué)習(xí)進(jìn)步。
一、什么是內(nèi)部類?
在Java中,可以將一個(gè)類的定義放在另外一個(gè)類的定義內(nèi)部,這就是內(nèi)部類。內(nèi)部類本身就是類的一個(gè)屬性,與其他屬性 定義方式一致。
一個(gè)內(nèi)部類的例子:
- public class Outer {
- private int radius = 1 ;
- public static int count = 2 ;
- public Outer () {
- }
- class inner {
- public void visitOuter () {
- System.out.println( "visit outer private member variable:" + radius);
- System.out.println( "visit outer static variable:" + count);
- }
- }
- }
二、內(nèi)部類的種類
內(nèi)部類可以分為四種:成員內(nèi)部類、局部?jī)?nèi)部類、匿名內(nèi)部類和靜態(tài)內(nèi)部類。
靜態(tài)內(nèi)部類
定義在類內(nèi)部的靜態(tài)類,就是靜態(tài)內(nèi)部類。
- public class Outer {
- private static int radius = 1;
- static class StaticInner {
- public void visit() {
- System.out.println("visit outer static variable:" + radius);
- }
- }
- }
靜態(tài)內(nèi)部類可以訪問外部類所有的靜態(tài)變量,而不可訪問外部類的非靜態(tài)變量;靜態(tài)內(nèi)部類的創(chuàng)建方式, new外部類.靜態(tài)內(nèi)部類(),如下:
- Outer.StaticInner inner = new Outer.StaticInner();
- inner.visit();
成員內(nèi)部類
定義在類內(nèi)部,成員位置上的非靜態(tài)類,就是成員內(nèi)部類。
- public class Outer {
- private static int radius = 1;
- private int count =2;
- class Inner {
- public void visit() {
- System.out.println("visit outer static variable:" + radius);
- System.out.println("visit outer variable:" + count);
- }
- }
- }
成員內(nèi)部類可以訪問外部類所有的變量和方法,包括靜態(tài)和非靜態(tài),私有和公有。成員內(nèi)部類依賴于外部類的實(shí)例,它的創(chuàng)建方式 外部類實(shí)例.new內(nèi)部類(),如下:
- Outer outer = new Outer();
- Outer.Inner inner = outer.new Inner();
- inner.visit();
局部?jī)?nèi)部類
定義在方法中的內(nèi)部類,就是局部?jī)?nèi)部類。
- public class Outer {
- private int out_a = 1;
- private static int STATIC_b = 2;
- public void testFunctionClass(){
- int inner_c =3;
- class Inner {
- private void fun(){
- System.out.println(out_a);
- System.out.println(STATIC_b);
- System.out.println(inner_c);
- }
- }
- Inner inner = new Inner();
- inner.fun();
- }
- public static void testStaticFunctionClass(){
- int d =3;
- class Inner {
- private void fun(){
- // System.out.println(out_a); 編譯錯(cuò)誤,定義在靜態(tài)方法中的局部類不可以訪問外部類的實(shí)例變量
- System.out.println(STATIC_b);
- System.out.println(d);
- }
- }
- Inner inner = new Inner();
- inner.fun();
- }
- }
定義在實(shí)例方法中的局部類可以訪問外部類的所有變量和方法,定義在靜態(tài)方法中的局部類只能訪問外部類的靜態(tài)變量和方法。局部?jī)?nèi)部類的創(chuàng)建方式,在對(duì)應(yīng)方法內(nèi), new內(nèi)部類(),如下:
- public static void testStaticFunctionClass(){
- class Inner {
- }
- Inner inner = new Inner();
- }
匿名內(nèi)部類
匿名內(nèi)部類就是沒有名字的內(nèi)部類,日常開發(fā)中使用的比較多。
- public class Outer {
- private void test(final int i) {
- new Service() {
- public void method() {
- for (int j = 0; j < i; j++) {
- System.out.println("匿名內(nèi)部類" );
- }
- }
- }.method();
- }
- }
- //匿名內(nèi)部類必須繼承或?qū)崿F(xiàn)一個(gè)已有的接口
- interface Service{
- void method();
- }
除了沒有名字,匿名內(nèi)部類還有以下特點(diǎn):
- 匿名內(nèi)部類必須繼承一個(gè)抽象類或者實(shí)現(xiàn)一個(gè)接口。
- 匿名內(nèi)部類不能定義任何靜態(tài)成員和靜態(tài)方法。
- 當(dāng)所在的方法的形參需要被匿名內(nèi)部類使用時(shí),必須聲明為 final。
- 匿名內(nèi)部類不能是抽象的,它必須要實(shí)現(xiàn)繼承的類或者實(shí)現(xiàn)的接口的所有抽象方法。
匿名內(nèi)部類創(chuàng)建方式:
- new 類/接口{
- //匿名內(nèi)部類實(shí)現(xiàn)部分
- }
三、內(nèi)部類的優(yōu)點(diǎn)
我們?yōu)槭裁匆褂脙?nèi)部類呢?因?yàn)樗幸韵聝?yōu)點(diǎn):
- 一個(gè)內(nèi)部類對(duì)象可以訪問創(chuàng)建它的外部類對(duì)象的內(nèi)容,包括私有數(shù)據(jù)!
- 內(nèi)部類不為同一包的其他類所見,具有很好的封裝性;
- 內(nèi)部類有效實(shí)現(xiàn)了“多重繼承”,優(yōu)化 java 單繼承的缺陷。
- 匿名內(nèi)部類可以很方便的定義回調(diào)。
一個(gè)內(nèi)部類對(duì)象可以訪問創(chuàng)建它的外部類對(duì)象的內(nèi)容,包括私有數(shù)據(jù)!
- public class Outer {
- private int radius = 1;
- protected void test(){
- System.out.println("我是外部類方法");
- }
- class Inner {
- public void visit() {
- System.out.println("訪問外部類變量" + radius);
- test();
- }
- }
- }
我們可以看到,內(nèi)部類Inner是可以訪問外部類Outer的私有變量radius或者方法test的。
內(nèi)部類不為同一包的其他類所見,具有很好的封裝性
當(dāng)內(nèi)部類使用 private修飾時(shí),這個(gè)類就對(duì)外隱藏了。當(dāng)內(nèi)部類實(shí)現(xiàn)某個(gè)接口,并且進(jìn)行向上轉(zhuǎn)型,對(duì)外部來說,接口的實(shí)現(xiàn)已經(jīng)隱藏起來了,很好體現(xiàn)了封裝性。
- //提供的接口
- interface IContent{
- String getContents();
- }
- public class Outer {
- //私有內(nèi)部類屏蔽實(shí)現(xiàn)細(xì)節(jié)
- private class PContents implements IContent{
- @Override
- public String getContents() {
- System.out.println("獲取內(nèi)部類內(nèi)容");
- return "內(nèi)部類內(nèi)容";
- }
- }
- //對(duì)外提供方法
- public IContent getIContent() {
- return new PContents();
- }
- public static void main(String[] args) {
- Outer outer=new Outer();
- IContent a1=outer.getIContent();
- a1.getContents();
- }
- }
我們可以發(fā)現(xiàn),Outer外部類對(duì)外提供方法getIContent,用內(nèi)部類實(shí)現(xiàn)細(xì)節(jié),再用private修飾內(nèi)部類,屏蔽起來,把Java的封裝性表現(xiàn)的淋漓盡致。
內(nèi)部類有效實(shí)現(xiàn)了“多重繼承”,優(yōu)化 java 單繼承的缺陷。
我們知道Java世界中,一個(gè)類只能有一個(gè)直接父類,即以單繼承方式存在。但是內(nèi)部類讓“多繼承”成為可能:
- 一般來說,內(nèi)部類繼承某個(gè)類或者實(shí)現(xiàn)某個(gè)接口,內(nèi)部類的代碼操作創(chuàng)建它的外圍類的對(duì)象。內(nèi)部類提供了某種進(jìn)入其外圍類的窗口。
- 每個(gè)內(nèi)部類都可以隊(duì)里的繼承自一個(gè)(接口的)實(shí)現(xiàn),所以無論外圍類是否已經(jīng)繼承了某個(gè)(接口的)實(shí)現(xiàn),對(duì)于內(nèi)部類沒有影響
- 接口解決了部分問題,一個(gè)類可以實(shí)現(xiàn)多個(gè)接口,內(nèi)部類允許繼承多個(gè)非接口類型(類或抽象類)。
一份來自Java編程思想,內(nèi)部類實(shí)現(xiàn)“多繼承”的溫暖如下:
- 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());
- }
- }
代碼中出現(xiàn)了一個(gè)類D,一個(gè)抽象類E。然后,用類Z繼承D,內(nèi)部類構(gòu)造返回E。因此,當(dāng)你不管要的是D還是E,Z都可以應(yīng)付,“多繼承”的特點(diǎn)完美表現(xiàn)出來。
匿名內(nèi)部類可以很方便的定義回調(diào)。
什么是回調(diào)?假設(shè)有兩個(gè)類A和B,在A中調(diào)用B的一個(gè)方法b,而b在執(zhí)行又調(diào)用了A的方法c,則c就稱為回調(diào)函數(shù)。
當(dāng)然,回調(diào)函數(shù)也可以是a函數(shù),這就是同步回調(diào),最簡(jiǎn)單的回調(diào)方式?;卣{(diào)應(yīng)用場(chǎng)景挺多的,如android中的事件監(jiān)聽器。匿名內(nèi)部類可以很方便的定義回調(diào),看個(gè)例子
- //定義一個(gè)CallBack接口
- public interface CallBack {
- void execute();
- }
- public class TimeTools {
- /**
- * 測(cè)試函數(shù)調(diào)用時(shí)長(zhǎng),通過定義CallBack接口的execute方法
- * @param callBack
- */
- public void testTime(CallBack callBack) {
- long beginTime = System.currentTimeMillis(); //記錄起始時(shí)間
- callBack.execute(); ///進(jìn)行回調(diào)操作
- long endTime = System.currentTimeMillis(); //記錄結(jié)束時(shí)間
- System.out.println("[use time]:" + (endTime - beginTime)); //打印使用時(shí)間
- }
- public static void main(String[] args) {
- TimeTools tool = new TimeTools();
- tool.testTime(new CallBack(){
- //匿名內(nèi)部類,定義execute方法
- public void execute(){
- TestTimeObject testTimeObject = new TestTimeObject();
- testTimeObject.testMethod();
- }
- });
- }
- }
在調(diào)用testTime()測(cè)時(shí)間的時(shí)候,用匿名內(nèi)部類實(shí)現(xiàn)一個(gè)方法execute(),在該方法內(nèi)搞事情(執(zhí)行目標(biāo)函數(shù)),執(zhí)行完后,又回到testTime方法,很好了實(shí)現(xiàn)測(cè)試函數(shù)調(diào)用時(shí)長(zhǎng)的功能。顯然,匿名內(nèi)部類讓回調(diào)實(shí)現(xiàn)變得簡(jiǎn)單。
四、內(nèi)部類的底層
內(nèi)部類標(biāo)志符
每個(gè)內(nèi)部類都會(huì)產(chǎn)生一個(gè).class文件,其中包含了如何創(chuàng)建該類型的對(duì)象的全部信息。內(nèi)部類也必須生成一個(gè).class文件以包含它們的Class對(duì)象信息。內(nèi)部類文件的命名有嚴(yán)格規(guī)則:外圍類的名字+$+內(nèi)部類的名字。
一個(gè)簡(jiǎn)單例子:
- public class Outer {
- class Inner{
- }
- }
javac Outer.java編譯完成后, 生成的class文件如下:
如果內(nèi)部類是匿名的,編譯器會(huì)簡(jiǎn)單地產(chǎn)生一個(gè)數(shù)字作為其標(biāo)識(shí)符。如果內(nèi)部類是嵌套在別的內(nèi)部類之中(靜態(tài)內(nèi)部類),只需直接將它們的名字加在其外圍類標(biāo)志符與“$”的后面。
為什么內(nèi)部類可以訪問外部類的成員,包括私有數(shù)據(jù)?
由上一小節(jié),我們知道內(nèi)部類可以訪問外部類的成員,包括私有數(shù)據(jù)。那么它是怎么做到的呢?接下來揭曉答案。
先看這個(gè)簡(jiǎn)單地例子:
- public class Outer {
- private int i = 0;
- class Inner{
- void method(){
- System.out.println(i);
- }
- }
- }
一個(gè)外部類Outer,一個(gè)外部類私有屬性i,一個(gè)內(nèi)部類Inner,一個(gè)內(nèi)部類方法method。內(nèi)部類方法訪問了外部類屬性i。
先編譯,javac Outer.java,生成.class文件,如下:
用命令 javap-classpath.-vOuter$Inner,反編譯Outter$Inner.class文件得到以下信息:
我們可以看到這一行,它是一個(gè)指向外部類對(duì)象的指針:
- final innerclass.Outer this$0;
雖然編譯器在創(chuàng)建內(nèi)部類時(shí)為它加上了一個(gè)指向外部類的引用, 但是這個(gè)引用是怎樣賦值的呢?編譯器會(huì)為內(nèi)部類的構(gòu)造方法添加一個(gè)參數(shù),進(jìn)行初始化, 參數(shù)的類型就是外部類的類型,如下:
- innerclass.Outer$Inner(innerclass.Outer);
成員內(nèi)部類中的Outter this&0 指針便指向了外部類對(duì)象,因此可以在成員內(nèi)部類中隨意訪問外部類的成員。
局部?jī)?nèi)部類和匿名內(nèi)部類訪問局部變量的時(shí)候,為什么變量必須要加上final?
局部?jī)?nèi)部類和匿名內(nèi)部類訪問局部變量的時(shí)候,為什么變量必須要加上final呢?它內(nèi)部原理是什么呢?
先看這段代碼:
- public class Outer {
- void outMethod(){
- final int a =10;
- class Inner {
- void innerMethod(){
- System.out.println(a);
- }
- }
- }
- }
反編譯(Outer$1Inner)得到以下信息:
我們?cè)趦?nèi)部類innerMethod方法中,可以看到以下這條指令:
- 3:bipush 10
- 它表示將常量10壓入棧中,表示使用的是一個(gè)本地局部變量。
- 其實(shí),如果一個(gè)變量的值在編譯期間可以確定(demo中確定是10了),則編譯器會(huì)默認(rèn)在匿名內(nèi)部類(局部?jī)?nèi)部類)的常量池中添加一個(gè)內(nèi)容相等的字面量或直接將相應(yīng)的字節(jié)碼嵌入到執(zhí)行字節(jié)碼中。
- 醬紫可以確保局部?jī)?nèi)部類使用的變量與外層的局部變量區(qū)分開,它們只是值相等而已。
以上例子,為什么要加final呢?是因?yàn)樯芷诓灰恢拢? 局部變量直接存儲(chǔ)在棧中,當(dāng)方法執(zhí)行結(jié)束后,非final的局部變量就被銷毀。而局部?jī)?nèi)部類對(duì)局部變量的引用依然存在,如果局部?jī)?nèi)部類要調(diào)用局部變量時(shí),就會(huì)出錯(cuò)。加了final,可以確保局部?jī)?nèi)部類使用的變量與外層的局部變量區(qū)分開,解決了這個(gè)問題。
我們?cè)賮砜匆欢未a,其實(shí)就是把變量a挪到傳參方式進(jìn)來。
- public class Outer {
- void outMethod(final int a){
- class Inner {
- void innerMethod(){
- System.out.println(a);
- }
- }
- }
- }
反編譯可得:
我們看到匿名內(nèi)部類Outer$1Inner的構(gòu)造器含有兩個(gè)參數(shù),一個(gè)是指向外部類對(duì)象的引用,一個(gè)是int型變量,很顯然,這里是將變量innerMethod方法中的形參a以參數(shù)的形式傳進(jìn)來對(duì)匿名內(nèi)部類中的拷貝(變量a的拷貝)進(jìn)行賦值初始化。
那么,新的問題又來了,既然在innerMethod方法中訪問的變量a和outMethod方法中的變量a不是同一個(gè)變量,當(dāng)在innerMethod方法中修改a會(huì)怎樣?那就會(huì)造成數(shù)據(jù)不一致的問題了。
怎么解決呢?使用final修飾符,final修飾的引用類型變量,不允許指向新的對(duì)象,這就解決數(shù)據(jù)不一致問題。注意: 在Java8 中,被局部?jī)?nèi)部類引用的局部變量,默認(rèn)添加final,所以不需要添加final關(guān)鍵詞。
五、內(nèi)部類的應(yīng)用場(chǎng)景。
一般我們?cè)谀男﹫?chǎng)景下使用內(nèi)部類呢?
場(chǎng)景之一:一些多算法場(chǎng)合
一些算法多的場(chǎng)合,也可以借助內(nèi)部類,如:
- Arrays.sort(emps,new Comparator(){
- Public int compare(Object o1,Object o2)
- {
- return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
- }
- });
場(chǎng)景二:解決一些非面向?qū)ο蟮恼Z句塊。
如果一些語句塊,包括if…else語句,case語句等等比較多,不好維護(hù)擴(kuò)展,那么就可以借助內(nèi)部類+設(shè)計(jì)模式解決。
場(chǎng)景之三:適當(dāng)使用內(nèi)部類,使得代碼更加靈活和富有擴(kuò)展性。
適當(dāng)?shù)氖褂脙?nèi)部類,可以使得你的代碼更加靈活和富有擴(kuò)展性。如JDK的lamda表達(dá)式,用內(nèi)部類非常多,代碼優(yōu)雅很多。如下:
- // JDK8 Lambda表達(dá)式寫法
- new Thread(() -> System.out.println("Thread run()")).start();
場(chǎng)景四:當(dāng)某個(gè)類除了它的外部類,不再被其他的類使用時(shí)。
如果一個(gè)類,不能為其他的類使用;或者出于某種原因,不能被其他類引用。那我們就可以考慮把它實(shí)現(xiàn)為內(nèi)部類。數(shù)據(jù)庫(kù)連接池就是這樣一個(gè)典型例子。
六、內(nèi)部類常見面試題
最后,我們來看一道經(jīng)典內(nèi)部類面試題吧。
- public class Outer {
- private int age = 12;
- class Inner {
- private int age = 13;
- public void print() {
- int age = 14;
- System.out.println("局部變量:" + age);
- System.out.println("內(nèi)部類變量:" + this.age);
- System.out.println("外部類變量:" + Outer.this.age);
- }
- }
- public static void main(String[] args) {
- Outer.Inner in = new Outer().new Inner();
- in.print();
- }
- }
運(yùn)行結(jié)果: