20.內部類【Java溫故系列】

參考自–《Java核心技術卷1》

內部類(inner class)是定義在另一個類中的類。使用內部類的原因如下:

  • 內部類方法可以訪問該類定義所在的作用域中的數據,包括私有的數據
  • 內部類可以對同一個包中的其他類隱藏起來
  • 當想要定義一個回調函數且不想編寫大量代碼時,使用匿名內部類比較敏捷

1 使用內部類訪問對象狀態

通過一個例子認識內部類:每隔一段時間輸出當前時間

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{
        @Override
        public void actionPerformed(ActionEvent e) {
            if(beep){
                System.out.println(new Date());
            }
        }
    }
}

TimePrinter 類位於 TalkingClock 類內部。但是這並不意味着每個 TalkingClock 都有一個 TimePrinter 實例域,只有 start 方法才能訪問到TimePrinter 實例。

需要注意,TimePrinter 類沒有實例域(即沒有名爲 beep 的變量),它引用的 beep 是創建TimePrinterTalkingClock 對象的域

內部類既可以訪問自身的數據域,也可以訪問創建它的外圍類對象的數據域。

究其原理:內部類的對象總有一個隱式引用,它指向了創建它的外部類對象。
在這裏插入圖片描述
上述的 outer 即指代這個隱式引用。這個引用在內部類的定義中是不可見的。

內部類中,外圍類的引用(隱式引用)在構造器中設置。編譯器修改了所有內部類的構造器,添加了一個外圍類引用的參數。因爲上述的TimePrinter 沒有定義構造器,所有編譯器爲這個類生成了一個默認的構造器,其代碼如下所示:

public TimePrinter(TalkingClock clock){
	outer = clock;
}

//actionPerformed方法的實現中調用外部類的beep
if(outer.beep){ ... }

:outer 不是 Java 的關鍵字,它在此處只用於指代外圍類的引用。

TalkingClockstart 方法創建 TimePrinter 對象時,編譯器傳入TalkingClock 類的 this:

ActionListener listener = new TimePrinter(this);

訪問控制上,若TimePrinter 只是一個常規類,它就只能通過訪問TalkingClock 類中的公有方法訪問 beep域,而使用內部類則可以直接訪問TalkingClock 的 beep域。

TimePrinter 類可以聲明爲私有的;這樣就只有TalkingClock 的方法可以構造TimePrinter 對象。只有內部類可以是私有類,而常規類只可以具有包可見性,或公有可見性。


2 內部類的特殊語法規則

在上述內容中,使用 outer 代指外圍類的引用,事實上,使用外圍類引用的正規語法還要複雜一些。

表達式:OuterClass.this 表示外圍類引用,如:可以這樣編寫內部類TimePrinteractionPerformed 方法

public void actionPerformed(ActionEvent e) {
	if(TalkingClock.this.beep){
		System.out.println(new Date());
	}
}

同樣,可以採用如下語法更加明確地編寫內部對象的構造器:

outerObject.new InnerClass(內部類構造參數);

例如:

ActionListener listener = this.new TimePrinter();

在外圍類的作用域之外,可以這樣引用內部類(前提是此內部類是公有內部類):

OuterClass.InnerClass

例如:

TalkingClock outer = new TalkingClock(1000,true);
TalkingClock.TimePrinter listener = outer.new TimePrinter();

:內部類中聲明的所有靜態域都必須是 final(如果是可變的靜態域,則不同外部類對象的內部類實例可能會有所不同).內部類不能有static方法(也可以有,但只能訪問外圍類的靜態域和靜態方法)。


3 局部內部類

在上述的例子中,仔細看可以發現,TimePrinter 這個內部類只在 start 方法中創建這個內部類對象時使用了一次。這種情況就可以在方法中定義局部類:

public void start(){
	class TimePrinter implements ActionListener{
		public void actionPerformed(ActionEvent e) {
			if(beep){
				System.out.println(new Date());
			}
		}
	}
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval,listener);
    t.start();
}

局部類不能使用 public 或 private 訪問說明符進行聲明。它的作用域被限定在聲明這個局部類的塊中。

局部類有一個優勢:它對外部代碼可以完全地隱藏起來。即使 TalkingClock 類中的其他代碼也不能訪問它。除了 start 方法外,沒有任何其他方法知道 TimePrinter 類的存在。

相對其他內部類,局部類還有一個優點:它不僅能夠訪問包含它們的外部類,還可以訪問局部變量(不過這些局部變量必須事實上爲 final,它們一旦賦值就不能改變)。

如下:將TalkingClock 類的 interval 和 deep 域移至 start 方法參數上

public void start(int interval,boolean deep){
	class TimePrinter implements ActionListener{
		public void actionPerformed(ActionEvent e) {
			if(beep){
				System.out.println(new Date());
			}
		}
	}
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval,listener);
    t.start();
}

此處,局部類引用的 deep 變量是一個局部變量。

看起來很正常,走一遍控制流程看看:

1)調用 start 方法

2)調用內部類 TimePrinter 的構造器,初始化對象變量 listener

3)將 listener 引用傳遞給 Timer 構造器,定時器開始計時,start 方法結束。此時 start 方法的 deep 參數變量已經不存在了

4)然後,actionPerformed 方法運行 if(beep) ...

爲了讓actionPerformed 方法正常工作(即可以調用 beep 變量),TimePrinter 類在 beep 域釋放之前將 beep 域用 start 方法的局部變量進行備份。實際上也是這樣做的。編譯器在創建 TimePrinter 對象時,beep 就會被傳遞給構造器,並存儲在 TimePrinter 類的 final 域中。


4 匿名內部類

將局部內部類的使用再深入一步。假如只創建這個類的一個對象,就不必給這個類命名了。這種類被稱爲匿名內部類

public void start(int interval,boolean deep){
    //匿名類
	ActionListener listener = new ActionListener(){
		public void actionPerformed(ActionEvent e) {
			if(beep){
				System.out.println(new Date());
			}
		}
	}
    Timer t = new Timer(interval,listener);
    t.start();
}

上述語句的含義是:創建一個實現 ActionListener 接口的類的新對象,需要實現的方法 actionPerformed 定義在 { } 中。

通常的匿名內部類的語法格式爲:

new SuperType(構造器參數){
    //實現超類的內部類
	inner class methods and data
}

其中,如果SuperType 是接口,那麼內部類就要實現這個接口;如果 SuperType 是一個類,內部類就需要擴展它。

由於構造器的名字必須與類名相同,而匿名類沒有類名,所以,匿名類不能有構造器。取而代之的是,將構造器參數傳遞給超類構造器。尤其是在內部類實現接口的時候,不能有任何構造參數。

new InterfaceType(){
    //實現接口的內部類
	inner class methods and data
}

內部類一般用於實現事件監聽器和其他回調,lambda 表達式也可以實現類似功能,甚至還更加簡便:

public void start(int interval,boolean deep){
    //lambda表達式
    Timer t = new Timer(interval,event -> {
        if(beep){
			System.out.println(new Date());
		}
    });
    t.start();
}

:匿名列表,“雙括號初始化”技巧:

假設構造一個數據列表,並把它傳遞到一個方法(invite):

ArrayList<String> friends = new ArrayList<>();
friends.add("Tom");
friends.add("Mary")invite(friends);

如果此後不再需要這個數組列表,最好將它作爲一個匿名列表:

invite(new ArrayList<String>(){{ add("Tom");add("Mary"); }});

特別注意此處的雙括號。外層括號建立了 ArrayList 的一個匿名子類;內層括號則是一個對象構造塊。


5 靜態內部類

有時候使用內部類只是爲了把一個類隱藏在另外一個類的內部,並不需要內部類引用外圍類對象。因此,可以將內部類聲明爲 static ,以便取消產生的引用。

下面是一個使用靜態內部類的例子:考慮計算數組中最小值和最大值的問題。

可以編寫兩個方法,一個方法用於計算最小值,另一個方法用於計算最大值。但是,在調用這兩個方法時,數組被遍歷了兩次。如果只遍歷一次數組,便能計算出數組的最小值和最大值,就可以提高代碼效率:

定義需要的方法和類:

class ArrayAlg {
    //用於存儲最小值和最大值的靜態內部類
	public static class Pair {  
        //最小值域和最大值域
		private double first;
      	private double second;
       
      	public Pair(double f, double s){
         	first = f;
         	second = s;
      	}
       
      	public double getFirst(){
         	return first;
      	}

     	public double getSecond(){
         	return second;
      	} 
  	}
	//定義方法,遍歷一次數組計算出數組的最小值和最大值(會返回兩個數值,此時可以使用定義的靜態內部類Pair)
  	public static Pair minmax(double[] values){
      	double min = Double.MAX_VALUE;
      	double max = Double.MIN_VALUE;
      	for (double v : values){
         	if (min > v) min = v;
         	if (max < v) max = v;
      	}
      	return new Pair(min, max);
  	}
}
//主函數,調用方法計算得出數組的最小值和最大值
public class StaticInnerClassTest {
   	public static void main(String[] args){
      	double[] d = new double[20];
      	for (int i = 0; i < d.length; i++)
         	d[i] = 100 * Math.random();
        //定義靜態內部類對象
      	ArrayAlg.Pair p = ArrayAlg.minmax(d);
      	System.out.println("min = " + p.getFirst());
      	System.out.println("max = " + p.getSecond());
   	}
}

顯然,上述的 Pair 對象不需要引用任何其他的對象。

只有內部類可以聲明爲 static 。靜態內部類的對象除了沒有生成它的外圍類對象的引用特權外,與其他所有內部類完全是一樣的。

:在內部類不需要訪問外圍類對象時,有關使用靜態內部類。與常規內部類不同,靜態內部類可以有靜態域和方法。

:聲明在接口中的內部類自動稱爲 static 和 public 類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章