天天看點

【java】淺談java内部類

最近在閱讀《java核心技術 卷1》,對java内部類的基礎知識梳理總結了一下,寫下這篇文章和大家交流

前言

說到java内部類,想必大家首先會想到比較常用的“匿名内部類”,但實際上,這隻是内部類的其中一種使用方式而已。内部類的使用方式實際上總共包括:成員内部類, 方法局部類,匿名内部類,下面,我就給大家來一一介紹:

為什麼要使用内部類

有的時候你可能有這樣一種需求:對一個類(假設它為MyClass.java)建立一個和它相關的類(假設它是Part.java),但因為Part.java和MyClass之間的聯系“緊密”且“單一”,導緻我們在這種情況下,不希望像下面這樣增加一個額外的兄弟類

├─MyClass           └─Part      

而希望能将Part.java的資料隐藏在MyClass.java内部,于是這個時候内部類就堂而皇之地出現了

那麼,這個不請自來的内部類到底給我們上述的局面造成了怎樣的改變呢? 讓我們來看看:

增加一個額外的兄弟類Part:

1. 對一些沒有關聯的類可見(如果protected則對同一包内類可見,如果public則對所有類可見)

2. 不能完全自由的通路MyClass中的私有資料(必須經過通路器方法)

3. 新增了一個java檔案

使用内部類,将Part類的定義寫入MyClass内部

1. 可以減少多餘的可見性,例如可把Part在MyClass内部定義為私有,這樣對同一包内其他類也不可見了

2. 内部類(Part)可以自由通路外圍類的所有資料(MyClass),包括私有資料

3. 減少了一個java檔案,使得類結構更簡潔

成員内部類

故名思議,成員内部類嘛~  使用當然和成員變量很相似咯

你可以像

private String data      

這樣定義一個“平行的”成員内部類:

private class Inner      

 具體看下面的例子:

Outter.java:

public class Outter {       // 成員變量data       private String data = "外部資料";       //定義一個内部類       private class Inner {         public void innerPrint () {           System.out.println(data);         }        }        // 外部類的方法, new一個内部類的執行個體并調用其innerPrint方法       public void outterPrint () {         Inner i = new Inner();         i.innerPrint();       }     }      

Test.java:

public class Test {       public static void main (String [] args) {         Outter o = new Outter();         o.outterPrint();       }     }      

結果輸出:

外部資料      

看來這還是能達到我們預期的效果的:由于将Inner内部類設為private,它變得隻對我們目前的外部類Outter類可見,我們成功地把它"隐藏"在了Outter類内部,與此同時,它還自由地通路到了Outter類的私有成員變量data

兩個this

雖然上面的例子看起來挺簡單的,但實際上内部類的作用機制還是比較複雜的。

首先要考慮的是“this”的問題,外部類和内部類各有一個this,關鍵在于内部類中我們如何對這兩個this作出區分:

我們假設上面的例子中的Inner類内部有一個方法fn:

private class Inner {       public  void fn () {         Outter.this // 指向Outter執行個體對象的this引用         this  // 指向Inner執行個體對象的this引用       }     }      

在這個方法fn裡,Outter.this是指向Outter執行個體對象的this的引用, 而this是指向Inner執行個體對象的this的引用

我們通路類中成員變量有兩種方式: 隐式通路(不加this)和顯式通路(加this)

隐式通路類中成員變量

讓我們對上面的Outter.java做一些改動,增加一行代碼:

public class Outter {       // 成員變量data       private String data = "外部資料";        //定義一個内部類       private class Inner {         // 增加Inner類對data成員變量的聲明         private String data = "内部資料"          public void innerPrint () {           System.out.println(data);         }       }        // 外部類的方法, new一個内部類的執行個體并調用其innerPrint方法       public void outterPrint () {         Inner i = new Inner();         i.innerPrint();       }     }      
内部資料      

如此可見,内部類内聲明的資料會覆寫外部類的同名資料。或者說, 在上述例子中,對于data成員變量,它會首先在Inner的this中查找有無這個成員變量,然後沒有,那麼就再在Outter.this中查找

顯式通路類中成員變量

但有的時候我們希望既能通路外部類的成員變量,同時也能通路内部類的成員變量,這個時候我們就要使用到this了,但是如何區分内部類和外部類的this呢?你可以這樣:

以上述例子為例:

通路外部類定義的成員變量:Outter.this.data

通路内部類定義的成員變量:this.data

如下圖所示

public class Outter {       // 外部類的成員變量data       private String data = "外部資料";        //定義一個内部類       private class Inner {         // 内部類的成員變量data         private String data = "内部資料";         public void innerPrint () {           System.out.println(Outter.this.data);           System.out.println(this.data);         }       }        // 外部類的方法, new一個内部類的執行個體并調用其innerPrint方法       public void outterPrint () {         Inner i = new Inner();         i.innerPrint();       }     }      

局部内部類

局部内部類是内部類的第二種形式,它讓内部類的“隐藏”得更深一層——寫在外部類的方法内部,而不是處于和外部類方法平行的位置。

讓我們對上面成員内部類處理的場景做些思考:我們的Inner内部類僅僅隻在outterPrint方法中使用了一次:

public void outterPrint () {       Inner i = new Inner();       i.innerPrint();     }      

那麼我們能不能把Inner内部類直接定義在outterPrint的内部呢?這樣的話,它就能更好地隐藏起來,即使是類Outter中除outterPrint外的方法,也不能通路到它:

現在的Outter的類看起來像這樣:

public class Outter {       public void outterPrint () {// 外部類方法         class LocalInner { // 局部内部類           public void innerPrint () {   }         }          LocalInner i = new LocalInner(); // 執行個體化局部内部類         i.innerPrint();       }     }      

相比于成員内部類,局部内部類多了一項能通路的資料,那就是局部變量(由外部類方法提供)

成員内部類:外部類資料,内部類資料

局部内部類: 外部類資料,内部類資料, 局部資料

具體示例如下:

Outter.java

public class Outter {       private String data = "外部資料";  // 外部類資料       public void outterPrint (final String localData) { // 局部資料         class LocalInner {           private String data = "内部資料";  // 内部類資料           public void innerPrint () {             System.out.println(Outter.this.data);  // 列印外部類資料             System.out.println(this.data);   //  列印内部類資料             System.out.println(localData);  // 列印局部資料           }         }          LocalInner i = new LocalInner();         i.innerPrint();       }     }      
public class Test {       public static void main (String [] args) {         Outter o = new Outter();         o.outterPrint("局部資料");       }     }      
外部資料     内部資料     局部資料      

 局部類所使用的外部類方法的形參必須用final修飾

這裡要注意一點, 局部類所使用的外部類方法的形參必須用final修飾,否則會編譯不通過,也就是說傳入後不許改變

為什麼這個方法形參一定要用final修飾?

 (僅個人了解,如有不同的意見或者更好的了解歡迎在評論區讨論)

如果不用final修飾會怎樣? 且聽我慢慢道來:

首先要說一下:

1.内部類和外部類在編譯之後形式上是一樣的,不會有内外之分

2.局部内部類對于使用的外部方法的值會用構造函數做一個拷貝(編譯後)

例如對于下面outterPrint方法中的LocalInner

public void outterPrint (final String data) {       class LocalInner {         public void innerPrint () {         // 使用 data         }       }     }      

編譯之後大概長這樣:

public class Outter$LocalInner{        public LocalInner(String data){         this.LocalInner$data = data; // 對于使用的data做了一次拷貝       }       public void innerPrint (){ /* 使用 data */ }     }      

這裡要注意的是:

1. 編譯後,LocalInner并非直接使用data,而是用構造器拷貝一份後再使用

2. java是值傳遞的,是以包裹 LocalInner的外部方法outterPrint也會對傳入的data參數做一次拷貝(基本類型資料拷貝副本,對象等則拷貝引用)

OK,現在的情況是:

方法内的局部類對data拷貝了兩次:外部方法outterPrint值傳遞時的拷貝,和LocalInner構造函數的拷貝

方法内除了局部類外的作用域隻拷貝了data一次: 外部方法outterPrint值傳遞時的拷貝

拷貝兩次和拷貝一次,導緻在outterPrint方法内部, 局部類内部的data和局部類外部的data是不同步的! 也即你在局部類内部改了data不影響局部類外部的data,在局部類外部改了data也不影響局部類内部的data(注意一個前提,值是基本類型的,如果是對象的話因為拷貝的是引用仍然可以“同步”)

圖示一:

【java】淺談java内部類

 圖示二:

【java】淺談java内部類

于是java說: 哎呀媽呀, 這都data都不同步了, 要是讓你修改這還了得!!! 于是就強行要求我們加上final

【注意】所謂的不同步主要是針對基本類型來說的,如果是對象之類的話因為拷貝的是引用是以仍然可以“同步”

如何突破必須用final的限制

我們上面說到,局部内部類所使用的方法形參必須用final修飾的限制。

例如

public void outterPrint (String data) {// 沒加上final       class LocalInner {          public void changeData () {           data = "我想修改data的值";  // 在這一行編譯報錯         }        }      }      

提示:

Cannot refer to a non-final variable data inside an inner class defined in a different method      

那麼,如果我們有對該形參必須能修改的硬性需求怎麼辦?

你可以通過一種有趣的方式繞開它:使用一個單元素數組。因為用final修飾的基本類型的變量不允許修改值,但是卻允許修改final修飾的單元素數組裡的數組元素, 因為存放數組的變量的值隻是一個引用,我們修改數組元素的時候是不會修改引用指向的位址的,在這點上final并不會妨礙我們:

public class Outter {       public void outterPrint (final String []  data) {          class LocalInner {            public void innerPrint () {             data[0] = "堂而皇之地修改它!!";   // 修改資料             System.out.print(data[0]);  // 輸出修改後的資料           }         }          LocalInner i = new LocalInner();         i.innerPrint();       }     }      
public class Test {       public static void main (String [] args) {         Outter o = new Outter();         String [] data = new String [1];         data[0] = "我是資料";         o.outterPrint(data);  // 修改資料并且輸出       }     }      
堂而皇之地修改它!!      

【注意】局部類不能用public或private通路符進行聲明!!

匿名内部類

倘若我們再把局部内部類再深化一下, 那就是匿名内部類

匿名内部類的使用方式

new [超類/接口] {   /* 類體 */   }      

讓我們看看下面這個例子:

Other.java:

public class Other {    }      
public class Outter {       public void outterPrint (String data) {          Other o = new Other() {  }; // 匿名内部類       }     }      

何謂之匿名?

“诶,不是說好的匿名嗎? 那麼為什麼還有個Other的類名呢?”

Other o = new Other() {  /* 匿名内部類的類體 */   };      

實際上,這裡的Other并不是我們的匿名内部類,而是我們匿名内部類的超類,上面一行代碼其實相當于(用成員内部類來表示的話)

// annoymous翻譯為匿名     public class Outter {       private class annoymous extends Other{  }         public void outterPrint () {          Other a = new annoymous();       }     }      

同時要注意,我們在使用匿名内部類的方式,是在定義一個内部類的同時執行個體化該内部類:

new Other() {  /* 匿名内部類的類體 */  };  // new操作和定義類的代碼是緊緊結合在一起的      

匿名函數的作用

用匿名函數的作用在于在一些特定的場景下寫起來很簡單,例如事件監聽器:

ActionListener listener = new ActionListener() {        public void actionPerformed(ActionEvent e) {   }     };      

避免了再建立另外一個類檔案

講的有點亂, 對匿名内部類做個總結:

1. 省略被定義的類的類名

2. 必須結合超類或者接口使用,即 new [超類/接口] {   /* 類體 */   }

3. 在定義該匿名類的同時執行個體化該匿名類

4. 在一些場景下能簡化代碼

【注意】匿名類不能有構造器, 因為構造器和類同名,而匿名類沒有類名,是以匿名類不能有構造器

文章總結

我們使用内部類的原因主要有三點:

1.實作資料隐藏, 避免多餘的可見性

2.自由通路外部類的變量

3. 在使用監聽器等場景的時候使用匿名内部類,避免增加的大量代碼

關于成員内部類, 方法局部類,匿名内部類的關系

從成員内部類,方法局部類到匿名内部類是一個不斷深入的關系, 成員内部類進一步隐藏可見性就成為了方法局部類, 方法局部類省去類名,并将類的定義和執行個體化操作合并到一起,就是匿名内部類。是以,匿名内部類沿襲了成員内部類和方法局部類的基本特特性

内部類的一些特殊的要求

1.局部類不能用public或private通路符進行聲明

2.局部類所使用的外部類方法的形參必須用final修飾

3. 匿名内部類不能有構造器

參考資料:

《java核心技術 卷1》—— Cay S. Horstmann, Gary Cornell

【java】淺談java内部類

我叫彭湖灣,請叫我胖灣