前言
說到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();
}
}
復制
Test.java:
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并不會妨礙我們:
Outter.java
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();
}
}
復制
Test.java:
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 { }
復制
Outter.java:
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