Java內部類
java內部類是指一個類定義在另一個類的內部,其中的方法可以訪問包含他們外部類的域。這是一種比較複雜的技術,內部類的主要用於設計那些有協作性關係的類之間。特別是在java處理GUI事件中得到了廣泛的應用。除此之外,內部類最常用的原因有以下幾點:
- 內部類方法可以訪問該類定義所在的作用域中的資料,包括私有的資料
- 內部類可以對同一個包中的其他類隱藏起來
- 用匿名內部類來定義回撥函式會方便很多
- 內部類能獨立地繼承一個(介面的)實現,無論外圍類是否已經繼承了某個(介面的)實現,對於內部類沒有影響。
前兩點很好理解,第三點涉及到一個回撥函式的概念,回撥函式有反饋的作用,這裡推薦一篇很好理解的文章:
回撥函式傳送門:
http://blog.csdn.net/xiaanming/article/details/8703708/
(一) 簡單的內部類例子
內部類是一個非常複雜的功能。千里之行,始於足下,在徹底研究透徹之前,我們不妨從一個最簡單的例子開始看起。
package Inner;
/**
*
* @author QuinnNorris
* 外圍類ExampleInner類,其中包含一個內部類InnerClass
*/
public class ExampleInner {
private int interval;
private boolean beep;//一個在ExampleInner內的變數,在內部類InnerClass中,我們會用到它
public ExampleInner(int interval, boolean beep) {
this.interval=interval;
this.beep=beep;
}//構造器
public void start() {
InnerClass ic = new InnerClass();
}//start方法建立內部類物件
//內部類
public class InnerClass {
public void action() {
if (beep)
System.out.println("beep is true!");
}
}
}
需要注意,這裡的InnerClass位於ExampleInner內部,但這並不意味著每個ExampleInner物件都有一個InnerClass例項域。
(二) 內部類與外圍類
在這裡,我們把包含內部類的類暫且先叫做外圍類。
在上面的程式碼中,內部類InnerClass中的方法中有一個beep變數,但令人驚訝的是,在InnerClass中並沒有這個beep變數,之所以這段程式碼沒有問題,是因為內部類自動的引用了外圍類中的這個beep變數。內部類是怎樣做到這一點的呢?
1.內部類維護隱式引用和外圍類相聯絡
事實上,內部類的物件會自動的維護一個隱式引用,這個引用指向了建立它的外圍類物件。這個引用的語法表示式為:
OuterClass.this //內部類的隱式引用,指向外圍類,在上述例子中為:ExampleInner.this
也就是說,我們內部類中的action方法可以這樣來編寫:
public void action() {
if (ExampleInner.this.beep)//完全寫法,隱式引用OuterClass.this可以省略
System.out.println("beep is true!");
}
通過這個隱式指標,內部類獲得了比普通類更大的訪問許可權——它可以訪問外圍類中的私有域。
2.編譯器自動生成內部類構造器,存放隱式引用
這個隱式引用是在構造器中設定的,編譯器修改了所有內部類的構造器,添加了一個外圍類的引用引數。比如,我們上面的InnerClass類沒有構造器,編譯器為其生成了一個預設構造器:
public InnerClass(ExampleInner ei){
ExampleClass.this = ei; //將外圍類的一個物件賦給隱式引用
}
當我們要例項化一個內部類時我們預設的省略了外圍類的引用,這個引用通常是this引用。
public void start() {
InnerClass ic = this.new InnerClass();//this引用指示了建立的內部類的外圍類,通常省略
}//start方法建立內部類物件
3.外圍類作用域外,通過外圍類物件構造內部類例項
通常情況下,this引用是多餘的。正如我們上面說過,InnerClass位於ExampleInner內部,但這並不意味著每個ExampleClass物件都有一個InnerClass例項域。在內部類是公有的情況下,我們可以通過顯示的命名將外圍類引用設定為某個ExampleInner物件,這樣一來我們可以在外圍類的外部隨意的例項化這兩個類物件來使用。
在外圍類作用域之外,引用內部類語法:
OuterClass.InnerClass object = object.new InnerClass();
- 1
我們在外圍類之外用OuterClass.InnerClass的方法來表示內部類的類名。與此同時,我們需要用一個已經例項化好的外圍類物件object來進行 .new InnerClass( )的操作,此時就不能用this來表示外圍類了,而且這個外圍類物件也不能省略,否則報錯。
下面我們來看一下在測試類中我們建立這兩個類例項的情況。
package Inner;
/**
*
* @author QuinnNorris
* 在ExampleInner類的外部,我們通過如下方式將外圍類和內部類例項化。
*/
public class Test {
public static void main(String[] args) {
ExampleInner ei = new ExampleInner(10,true);//例項化外圍類
//OuterClass.InnerClass object = object.new InnerClass
ExampleInner.InnerClass eiic = ei.new InnerClass();
//ok 通過上述的特殊語法,我們在外圍類的外部,成功的例項化了內部類
ExampleInner.InnerClass eiic = new InnerClass();
//error 此時如果不用外圍類物件去.new InnerClass()會報錯,即使直接引內部類的包也不行
eiic.action();//ok 呼叫內部類的方法,沒問題
}
}
(三) 虛擬機器中的內部類(語法糖)
當在java1.1的java語言中增加內部類時,很多程式設計師都認為這將是一項很主要的新特性,但內部類的語法如此麻煩,以至於違背了java要比c++更加簡單的設計理念。儘管如此,雖然內部類很複雜,但它其實只是一種語法糖,虛擬機器對內部類一無所知,那麼虛擬機器中是如何處理內部類的呢?
糖衣語法(語法糖)傳送門:http://blog.csdn.net/quinnnorris/article/details/54849155
1.內部類反編譯
為了能夠得到虛擬機器中內部類實際的情況,我們使用了javap將內部類的原始碼進行反編譯,結果如下:
public ExampleInner$InnerClass{
public ExampleInner$InnerClass(ExampleInner);//構造器
public void action();
final ExampleInner this$0;
}
可見,虛擬機器中將內部類解釋為:外圍類名+$+內部類名。(這裡有個基本知識,java是可以使用$來作為變數名的一部分的,甚至是變數名的開始,但是並不推薦)前兩個函式還是很好理解的,第一個構造器正好印證了我們上面說的自動生成的內容,第二個是內部類中的函式。第三個this$0是一種合成方法,編譯器為了引用外圍類,生成了一個附加的例項域this$0,(這個名字是編譯器合成的,在自己的編寫程式碼中不能引用它,而且這個名字是隨機的)。
2.建立獲取外圍類的私有域的靜態方法
不僅僅將內部類反編譯,我們將外圍類也反編譯之後會發現,除了其他一樣的程式碼外,外圍類會多出一個靜態方法:
static boolean access$0(ExampleInner);
這個靜態方法的功能是,將傳入的外圍類物件中的beep變數值返回給內部類。我們上面知道,內部類中需要用到的私有的beep變數,這個方法正是編譯器生成的一個合成方法。如果內部類中需要用到其他的私有域變數,編譯器會繼續的生成這樣的合成方法來返回那些私有域的值。這也正是,為什麼內部類可以訪問外圍類的私有域的原因。
(四) 區域性內部類
如果內部類只在一個方法中被使用,其他的地方無需這個內部類,我們可以把它定義在這個方法中,我們把這種定義在一個方法中(或定義在一個作用域中)的內部類叫做區域性內部類。
1.區域性內部類作用域
區域性內部類不能用public或private來宣告,它的作用域被限定在宣告這個區域性類的塊中,也正因此,區域性類對於除了這個方法之外的外部世界可以完全的隱藏起來,除了這個方法,沒人知道有這樣一個類的存在。
package Inner;
/**
*
* @author QuinnNorris
* 區域性內部類,只對飽含著他的方法或作用域可見
*/
public class LocalInner {
private int interval;
private boolean beep;
public LocalInner(int interval, boolean beep) {
this.interval=interval;
this.beep=beep;
}//構造器
public void start() {
//區域性內部類,只對start方法可見
class InnerClass {
public void action() {
if (beep)
System.out.println("beep is true!");
}
}
InnerClass ic = new InnerClass();
}
}
2.區域性內部類訪問final變數
相比較其他型別的內部類,區域性內部類還有一個顯著的優點。它不僅可以訪問包含著它的外圍類,還可以訪問在方法、作用域中的區域性變數,但是前提是這些變數要用final修飾。這個特點看似天經地義,畢竟內部類在方法中,為什麼不能訪問方法的區域性變數呢?不妨看一下下面的例子:
在有些情況下,在方法執行過程中,方法中的一條語句將區域性內部類作為一個引數傳遞給其他的方法,而後方法結束。在其他的方法呼叫內部類時,內部類中原方法的區域性變數已經隨著方法的結束被釋放,這個時候無法找到區域性變數。
正是這個原因,才讓我們明白一個區域性內部類訪問作用域中的區域性變數是需要處理的,不是天經地義的。
編譯器會為那些在區域性內部類中要用到的區域性變數做備份,也正是做備份的原因,導致區域性變數需要用final修飾,如果不是這樣,很有可能區域性變數後來被修改,導致和備份的內容不一樣從而出錯。
(五) 匿名內部類
在區域性內部類的基礎上再深入一步,如果我們只需要用到這個類一次,那麼我們只需要建立一個物件就好,不用命名了。這種不命名的內部類被稱作匿名內部類。
1.表示匿名內部類
那麼問題來了,如果我們不去命名一個內部類,我們怎麼才能知道我們建立的是什麼東西呢?解決的方法是,我們用實現一個介面或者擴充套件一個超類來表示我們正在編寫的內部類。
package Inner;
/**
*
* @author QuinnNorris
* 匿名內部類,實現了CallBack介面
*/
public class AnonymousInner {
private int interval;
private boolean beep;
public AnonymousInner(int interval, boolean beep) {
this.interval=interval;
this.beep=beep;
}//構造器
public void start() {
//匿名內部類,我們只知道它是一個實現了CallBack的子類,因為匿名,他沒有自己的名字。
//CallBack介面要存在,我們不可空穴來風的建立匿名類。
CallBack cb = new CallBack(){
public void solve(String result){
result="result";
}
};
}
}
2.匿名內部類的構造器
需要注意的是,匿名內部類沒有構造器。這是理所當然的,因為它本身連名字都沒有,它只能使用父類的構造方法,如果匿名內部類實現的是介面那麼更簡單,只需要在後面跟上一對空的圓括號即可,就像我們剛才例子中做的那樣。如果是繼承父類,則需要使用父類構造器。
3.匿名內部類與區域性內部類
因為匿名內部類也存在於方法之中,所以,區域性內部類的final理論也適用於匿名內部類。
匿名類在處理一些程式碼較短、事件較為簡單的內容時具有很大的優勢,更切實際,也更易於理解。現在很多很火的技術中,這種匿名機制也已經屢見不鮮了。
(六) 靜態內部類(巢狀內部類)
有的時候,其實我們用內部類只是為了把一個類隱藏在另外一個類內部,甚至不需要這兩個類之間的聯絡。那麼這個時候可以把內部類宣告為static,取消兩個類產生的引用,這就是靜態內部類(也叫巢狀內部類)。當我們把什麼東西和靜態的聯絡到一起時,還是那套理論,這個東西和物件無關,純粹變成了類的產物。那麼靜態內部類的特點也很好推斷:
- 靜態內部類不再有對外圍類的引用特權
- 靜態內部類能夠使用外圍類的靜態變數,能使用同時也能被外圍類的靜態方法使用
- 只有內部類可以被宣告為靜態類
- 如果需要在外圍類之外使用靜態內部類,可以用OuterClass.InnerClass的方法
- 靜態內部類一般設定為public而不是private,便於呼叫
(七) 總結
內部類一共可以被分為四種:成員內部類(我們一開始舉例的最普通的內部類),區域性內部類,匿名內部類,靜態內部類。這四種各有各的特點,在運用的時候要綜合考慮。除此之外,我們還一起研究了在虛擬機器中內部類的實現情況,這是很有必要的,懂得它的原理才能在使用時判斷的更準,在出錯時直擊要害。