本文是《Java核心技術 卷1》中第六章接口與內部類中關於內部類的閱讀總結。
Java中的內部類(inner class)是定義在另一個類內部的類。那麼內部類有什麼用呢?這裡主要由三個內部類存在的原因:
內部類方法可以訪問該類定義所在的作用域中的數據,包括私有的數據。即,如果類A中定義了類B,那麼類B可以訪問類A中的數據,甚至是私有數據,但類A不能訪問類B中的私有數據;內部類可以對同一個包中的其他類隱藏起來。在一個包中,定義一個類時,即使不加上訪問權限關鍵詞,這個類也是包內其他類可訪問的,不過如果定義成內部類,就相當於對包內其他類隱藏起來了;當想要定義一個回調函數且不想編寫大量代碼時,使用匿名(anonymous)內部類比較便捷;在C++中和Java內部類概念相似的是嵌套類。一個被嵌套的類包含在外圍類的作用域內。
Java的內部類又一個功能,使得內部類比C++的嵌套類更加有用。內部類的對象有一個隱式引用,它引用了實例化該內部對象的外圍類對象。通過這個指針,可以訪問外圍類對象的全部狀態。
內部類的語法比較復雜,這裡使用一個簡單的例子來說明內部類的使用方式。下面的代碼構造一個TalkingClock類,裡面定義了一個TimePrinter類。構造一個TalkingClock時需要兩個參數:時間間隔interval和開關鈴聲標志beep:
public class TalkingClock { private int interval; private boolean beep; public TalkingClock(int interval,boolean beep){...} public void start(){...} public class TimePrinter implements ActionListener{
//an inner class public void actionPerformed(ActionEvent event) {
... } } }這裡的TimePrinter類位於TalkingClock類的內部。不過,這不是說每個TalkingClock都有一個TimePrinter實例域。還有,TimePrinter對象是由TalkingClock類的方法構造的。
TimePrinter類的定義如下:
public class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event) { Date now=new Date(); System.out.println("At the tone,the time is "+now); if(beep)Toolkit.getDefaultToolkit().beep(); } }
這裡的TimePrinter類只有一個方法actionPerformed,不過這個方法裡面使用了外圍類TalkingClock類的變量beep,而自己沒有beep這個實例域或變量。也就是說,對內部類,它即可以訪問自身的數據域,也可以訪問創建它的外圍類的數據域。
那內部類是如何使用外圍類的變量的呢?內部類的對象總有一個隱式引用,這個引用指向了創建它的外部類對象:
這個引用在內部類的定義中是不可見的。為了說明這個概念,我們可以將外部類對象的引用稱為outer。於是actionPerformed方法將等價於下列形式:
public class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event) { Date now=new Date(); System.out.println("At the tone,the time is "+now); if(outer.beep)Toolkit.getDefaultToolkit().beep(); } }外部類的引用在構造器中設置。編譯器修改了所有的內部類的構造器,添加了一個外部類引用的參數。因為TimePrinter沒有定義構造器,所以編譯器為這個類生成了一個默認的構造器,代碼如下:
public TimePrinter(TalkingClock clock) { outer=clock; }不過要注意,outer並不是Java的關鍵字。
在start方法中創建了TimePrinter對象後,編譯器就會將this引用傳遞給當前的TalkingClock的構造器:
ActionListener listener=new TimePrinter(this);
import java.awt.*; import java.awt.event.*; import java.util.Date; import javax.swing.Timer; public class TalkingClock { private int interval; private boolean beep; public TalkingClock(int interval,boolean beep){ this.interval=interval; this.beep=beep; } public void start(){ ActionListener listener=new TimePrinter(); Timer t=new Timer(interval,listener); t.start(); } public class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event) { Date now=new Date(); System.out.println("At the tone,the time is "+now); if(beep)Toolkit.getDefaultToolkit().beep(); } } }運行代碼,結果如下:
在上面,已經介紹了內部類有一個外部類的隱式引用outer。事實上,使用外部類引用的正規語法還要復雜一些。下面的表達式:
OuterClasss.this
表示外部類的引用。比如可以像下面這樣編寫TimePrinter內部類的actionPerformed方法:
public void actionPerformed(ActionEvent event) { ... if(TalkingClock.this.beep)Toolkit.getDefaultToolkit().beep(); }反過來,可以使用下列語法格式更加明確地編寫內部類對象的構造器:
outerObject.new InnerClass(construction parameters);比如:
ActionListener listener=this.new TimePrinter();
在這裡,最新構造的TimePrinter對象的外部類引用被設置為創建內部類對象的方法中的this引用。這其實是多余的。
在外部類作用域外,還可以這樣引用內部類:
OuterClass.InnerClass
內部類是一個編譯器現象,與虛擬機無關。編譯器會把內部類翻譯成用$分隔外部類名和內部類名的常規類文件,虛擬機並不會知道。
在上面那個例子中,我們可以看到在編譯後的bin文件夾下的.class文件。對於上面的項目,這裡有兩個.class文件:
TalkingClock.class和TalkingClock$TimePrinter.class
說明編譯器會把內部類作為一個常規類文件。那麼這個類有什麼特別的麼?
可以使用javap來反編譯.class文件查看這個類的具體信息,輸入命令javap -private TalkingClock$TimePrinter,結果如下:
可以看到,在編譯後的文件中,有我們自己編寫的方法actionPerformed,除此還有一個final變量this$0,也就是說外部類的隱式引用,這個名字是編譯器合成的,在自己編寫的代碼中不能使用,還有編譯器生成的一個構造器,在這個構造器中正是有一個外部類的參數。
既然編譯器能夠自動轉化,那麼能不能不用內部類自己實現呢?
首先將TimePrinter定義成一個常規類,在TalkingClock類的外部,TalkingClock中構造TimePrinter對象時,傳遞一個this指針。而在TimePrinter中,使用傳進來的TalkingClock指針訪問TalkingClock內部的beep實例。
問題出現了,在TalkingClock類中,beep是私有的,外部的類不能訪問。
也就是說,內部類有對外部類的訪問特權,那麼編譯器是如何保存這個訪問特權的呢?
使用javap反編譯TalkingClock類,看看結果:
這裡除了我們自己定義的實例域和方法外,多了一個靜態方法access$0,這個方法有一個參數,就是這個類的引用。這個方法的返回類型正好是內部類要使用的beep的類型。也就是說,內部類通過調用這個方法來得到外部類的私有成員變量。即:
if(beep)
就相當於:
if(access$0(outer))
這樣可能會有風險,畢竟每個人都可以通過access$0方法訪問外部類的私有成員。不過這個方法隱藏在編譯後的字節碼中,很難找到這個方法的具體地址。當然,自己的代碼中也不可能使用access$0這個非法的方法名。
在上面的示例中,TimePrinter類的只有在TalkingClock類中的start方法中使用一次。這時,就可以將內部類定義為局部內部類。
public void start(){ class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event){ Date now=new Date(); System.out.println("At the tone,the time is "+now); if(beep)Toolkit.getDefaultToolkit().beep(); } } ActionListener listener=new TimePrinter(); Timer t=new Timer(interval,listener); t.start(); }局部內部類不能使用public或private訪問說明符修飾,它的作用域被限定在聲明這個局部類的塊中。
局部類有個優勢,就是對外部世界可以完全隱藏起來,即使TalkingClock類中的其它方法也不能訪問。
這個例子和上面那個例子的運行結果相同。
與其它內部類相比,局部類還有一個優點,就是它們不僅能夠訪問包含它們的外部類,還能訪問局部變量。不過,這些變量必須被聲明為final。下面的代碼將interval和beep放在start方法中:
public void start(int interval,final boolean beep){ class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event){ Date now=new Date(); System.out.println("At the tone,the time is "+now); if(beep)Toolkit.getDefaultToolkit().beep(); } } ActionListener listener=new TimePrinter(); Timer t=new Timer(interval,listener); t.start(); }
不過,既然TimePrinter類在start內部,就應該能訪問這個變量。
為了能夠清楚的看到內部的問題,考慮控制流程:
(1)調用start方法;
(2)調用內部類TimePrinter的構造器,以便初始化對象變量listener;
(3)將listener引用傳遞給Timer構造器,定時器開始計時,start方法結束。此時,start方法中的beep參數變量不復存在;
(4)然後,actionPerformed方法執行if(beep);
可beep變量已經沒了啊,actionPerformed方法怎麼還知道beep的值?可能的原因是內部類TimePrinter構造listener的時候就把這個值保存起來了。使用javap來看看內部類的定義:
可以看到,除了自己定義的,多了一個final的變量val$beep,而且自動生成的構造器除了一個外部類的引用參數外還有一個boolean類型的參數,這個參數起始就是傳遞beep變量的。這就證實了我們的猜測。實際上,當創建一個對象的時候,beep就會被傳遞給構造器,並存儲在val$beep域中。編譯器必須檢查對局部變量的訪問,為每一個變量建立相應的數據域,並將局部變量拷貝到構造器中,以便將這些數據域初始化為局部變量的副本。
將beep變量聲明為final,對它進行初始化後就不能再進行修改,保證了局部變量和在局部類中的副本保持一致。
不過,如果需要修改這個final的值怎麼辦?比如需要更新在一個封閉作用域內的計數器。這裡,要統計一下排序過程中調用compareTo方法的次數。
這時,final由於不能更新所以不能成功。不過可以通過下面的技巧能夠修改final變量:
public static int count(){ final int[] counter=new int[1]; Date[] dates=new Date[100]; for(int i=0;i這裡定義了一個長度為1的數組,雖然不能使它引用另一個數組,不過數組中的內容可以改變。 上述代碼結果如下:
99
6 匿名內部類
將局部內部類的使用再深入一步。假如只創建這個類的一個對象,就不必命名了。這種類叫做匿名內部類(anonymous inner class)。比如這樣:
public void start(int interval,final boolean beep){ ActionListener listener=new ActionListener() { public void actionPerformed(ActionEvent event) { Date now=new Date(); System.out.println("At the tone,the time is "+now); if(beep)Toolkit.getDefaultToolkit().beep(); } }; Timer t=new Timer(interval,listener); t.start(); }這個語法的含義是:創建一個實現ActionListener接口的類的新對象,需要實現的actionPerformed方法在{}內部。
通常的語法格式是:
new SuperType(construction parameters) { inner class methods and data }
其中,SuperType可以是一個接口,於是內部類就要實現這個接口;也可以是一個類,於是內部類就要擴展它。
如果一個內部類的代碼很少,就可以使用匿名內部類。
7 靜態內部類
如果一個內部類並不需要引用外部類對象,那就可以將一個內部類隱藏在外部類內。為此,可以將內部類聲明為static,以便取消產生的引用。
下面是一個使用靜態內部類的典型例子。如果要計算一個數組的最大值和最小值,如果使用兩個方法的話,需要對數組遍歷兩次。如果在一次遍歷中獲得最大值和最小值,又需要返回兩個結果。為此可以定義一個包含兩個值的Pair類:
class Pair { private double first; private double second; public Pair(double first,double second) { this.first=first; this.second=second; } public double getFirst(){ return first; } public double getSecond(){ return second; } }然後定義一個可以返回Pair類型的結果的方法minmax。完整的代碼如下:
public class ArrayAlg { public static class Pair { private double first; private double second; public Pair(double first,double second) { this.first=first; this.second=second; } public double getFirst(){ return first; } public double getSecond(){ return second; } } public static Pair minmax(double[] values){ double min=Double.MIN_VALUE; double max=Double.MAX_VALUE; for(double x:values){ if(min>x)min=x; if(max<x)max=x; return="" new="" pre=""> 只有內部類可以聲明為static。靜態內部類的對象除了沒有產生它的外部類對象的引用特權外,和所有的內部類都一樣。在這個例子中,必須定義為static是由於這個內部類是定義在靜態方法中的。<p> </p> </x)max=x;>