最近在閱讀《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說: 哎呀媽呀, 這都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
我叫彭湖灣,請叫我胖灣