程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java內部類

Java內部類

編輯:JAVA綜合教程

Java內部類


本文是《Java核心技術 卷1》中第六章接口與內部類中關於內部類的閱讀總結。

Java中的內部類(inner class)是定義在另一個類內部的類。那麼內部類有什麼用呢?這裡主要由三個內部類存在的原因:

內部類方法可以訪問該類定義所在的作用域中的數據,包括私有的數據。即,如果類A中定義了類B,那麼類B可以訪問類A中的數據,甚至是私有數據,但類A不能訪問類B中的私有數據;內部類可以對同一個包中的其他類隱藏起來。在一個包中,定義一個類時,即使不加上訪問權限關鍵詞,這個類也是包內其他類可訪問的,不過如果定義成內部類,就相當於對包內其他類隱藏起來了;當想要定義一個回調函數且不想編寫大量代碼時,使用匿名(anonymous)內部類比較便捷;

在C++中和Java內部類概念相似的是嵌套類。一個被嵌套的類包含在外圍類的作用域內。

Java的內部類又一個功能,使得內部類比C++的嵌套類更加有用。內部類的對象有一個隱式引用,它引用了實例化該內部對象的外圍類對象。通過這個指針,可以訪問外圍類對象的全部狀態。

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

內部類的語法比較復雜,這裡使用一個簡單的例子來說明內部類的使用方式。下面的代碼構造一個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);

注意,上面的代碼都是編譯器自動添加的。下面是TalkingClock類的完整定義:

 

 

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();
		}
	}
}
運行代碼,結果如下:

 

\

2 內部類的特殊語法規則

在上面,已經介紹了內部類有一個外部類的隱式引用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

3 編譯器如何處理內部類

內部類是一個編譯器現象,與虛擬機無關。編譯器會把內部類翻譯成用$分隔外部類名和內部類名的常規類文件,虛擬機並不會知道。

在上面那個例子中,我們可以看到在編譯後的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這個非法的方法名。

4 局部內部類

在上面的示例中,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類中的其它方法也不能訪問。

這個例子和上面那個例子的運行結果相同。

5 由外部方法訪問final變量

與其它內部類相比,局部類還有一個優點,就是它們不僅能夠訪問包含它們的外部類,還能訪問局部變量。不過,這些變量必須被聲明為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();
}

在這裡,interval和beep作為start的參數,這樣TalkingClock類中就不需要定義這兩個成員變量了。

 

不過,既然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;>

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved