内部类

Java中的内部类(inner class)是定义在另一个类内部的类。那么内部类有什么用呢?这里主要由三个内部类存在的原因:

  1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。即,如果类A中定义了类B,那么类B可以访问类A中的数据,甚至是私有数据,但类A不能访问类B中的私有数据;
  2. 内部类可以对同一个包中的其他类隐藏起来。在一个包中,定义一个类时,即使不加上访问权限关键词,这个类也是包内其他类可访问的,不过如果定义成内部类,就相当于对包内其他类隐藏起来了;
  3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷;

在C++中和Java内部类概念相似的是嵌套类。一个被嵌套的类包含在外围类的作用域内。

Java的内部类又一个功能,使得内部类比C++的嵌套类更加有用。内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。

1 使用内部类访问对象状态

内部类的语法比较复杂,这里使用一个简单的例子来说明内部类的使用方式。下面的代码构造一个TalkingClock类,里面定义了一个TimePrinter类。构造一个TalkingClock时需要两个参数:时间间隔interval和开关铃声标志beep:


  1. public class TalkingClock {  
  2.     private int interval;  
  3.     private boolean beep;  
  4.     public TalkingClock(int interval,boolean beep){...}  
  5.     public void start(){...}  
  6.       
  7.     public class TimePrinter implements ActionListener{  
  8.                 //an inner class  
  9.         public void actionPerformed(ActionEvent event)  
  10.         {  
  11.                         ...  
  12.         }  
  13.     }  
  14. }  

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类的定义如下:


  1. public class TimePrinter implements ActionListener{  
  2.         public void actionPerformed(ActionEvent event)  
  3.         {  
  4.             Date now=new Date();  
  5.             System.out.println("At the tone,the time is "+now);  
  6.             if(beep)Toolkit.getDefaultToolkit().beep();  
  7.         }  
  8.     }  

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方法将等价于下列形式:


  1. public class TimePrinter implements ActionListener{  
  2.         public void actionPerformed(ActionEvent event)  
  3.         {  
  4.             Date now=new Date();  
  5.             System.out.println("At the tone,the time is "+now);  
  6.             if(outer.beep)Toolkit.getDefaultToolkit().beep();  
  7.         }  
  8.     }  

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没有定义构造器,所以编译器为这个类生成了一个默认的构造器,代码如下:


  1. public TimePrinter(TalkingClock clock)  
  2. {  
  3.         outer=clock;  
  4. }  

public TimePrinter(TalkingClock clock)
{
        outer=clock;
}
不过要注意,outer并不是Java的关键字。

在start方法中创建了TimePrinter对象后,编译器就会将this引用传递给当前的TalkingClock的构造器:


  1. ActionListener listener=new TimePrinter(this);  

ActionListener listener=new TimePrinter(this);

注意,上面的代码都是编译器自动添加的。下面是TalkingClock类的完整定义:


  1. import java.awt.*;  
  2. import java.awt.event.*;  
  3. import java.util.Date;  
  4.   
  5. import javax.swing.Timer;  
  6.   
  7. public class TalkingClock {  
  8.     private int interval;  
  9.     private boolean beep;  
  10.     public TalkingClock(int interval,boolean beep){  
  11.         this.interval=interval;  
  12.         this.beep=beep;  
  13.     }  
  14.     public void start(){  
  15.         ActionListener listener=new TimePrinter();  
  16.         Timer t=new Timer(interval,listener);  
  17.         t.start();  
  18.     }  
  19.       
  20.     public class TimePrinter implements ActionListener{  
  21.         public void actionPerformed(ActionEvent event)  
  22.         {  
  23.             Date now=new Date();  
  24.             System.out.println("At the tone,the time is "+now);  
  25.             if(beep)Toolkit.getDefaultToolkit().beep();  
  26.         }  
  27.     }  
  28. }  

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方法:


  1. public void actionPerformed(ActionEvent event)  
  2. {  
  3.         ...  
  4.         if(TalkingClock.this.beep)Toolkit.getDefaultToolkit().beep();  
  5. }  

public void actionPerformed(ActionEvent event)
{
        ...
        if(TalkingClock.this.beep)Toolkit.getDefaultToolkit().beep();
}
反过来,可以使用下列语法格式更加明确地编写内部类对象的构造器:


  1. outerObject.new InnerClass(construction parameters);  

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方法中使用一次。这时,就可以将内部类定义为局部内部类。


  1. public void start(){  
  2. <span style="white-space:pre">    </span>class TimePrinter implements ActionListener{  
  3.         public void actionPerformed(ActionEvent event){  
  4.             Date now=new Date();  
  5.             System.out.println("At the tone,the time is "+now);  
  6.             if(beep)Toolkit.getDefaultToolkit().beep();  
  7.         }  
  8.     }  
  9.     ActionListener listener=new TimePrinter();  
  10.     Timer t=new Timer(interval,listener);  
  11.     t.start();  
  12. }  

public void start(){
<span style="white-space:pre">	</span>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方法中:


  1. public void start(int interval,final boolean beep){  
  2.     class TimePrinter implements ActionListener{  
  3.         public void actionPerformed(ActionEvent event){  
  4.             Date now=new Date();  
  5.             System.out.println("At the tone,the time is "+now);  
  6.             if(beep)Toolkit.getDefaultToolkit().beep();  
  7.         }  
  8.     }  
  9.     ActionListener listener=new TimePrinter();  
  10.     Timer t=new Timer(interval,listener);  
  11.     t.start();  
  12. }  

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变量:


  1. public static int count(){  
  2.     final int[] counter=new int[1];  
  3.     Date[] dates=new Date[100];  
  4.     for(int i=0;i<dates.length;i++)  
  5.     {  
  6.         dates[i]=new Date(){  
  7.             public int compareTo(Date other)  
  8.             {  
  9.                 counter[0]++;  
  10.                 return super.compareTo(other);  
  11.             }  
  12.         };  
  13.     }  
  14.     Arrays.sort(dates);  
  15.     return counter[0];  
  16. }  

public static int count(){
	final int[] counter=new int[1];
	Date[] dates=new Date[100];
	for(int i=0;i<dates.length;i++)
	{
		dates[i]=new Date(){
			public int compareTo(Date other)
			{
				counter[0]++;
				return super.compareTo(other);
			}
		};
	}
	Arrays.sort(dates);
	return counter[0];
}
这里定义了一个长度为1的数组,虽然不能使它引用另一个数组,不过数组中的内容可以改变。

上述代码结果如下:

99

6 匿名内部类

将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类叫做匿名内部类(anonymous inner class)。比如这样:


  1. public void start(int interval,final boolean beep){  
  2.     ActionListener listener=new ActionListener()  
  3.     {  
  4.         public void actionPerformed(ActionEvent event)  
  5.         {  
  6.             Date now=new Date();  
  7.             System.out.println("At the tone,the time is "+now);  
  8.             if(beep)Toolkit.getDefaultToolkit().beep();  
  9.         }  
  10.     };  
  11.     Timer t=new Timer(interval,listener);  
  12.     t.start();  
  13. }  

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方法在{}内部。

通常的语法格式是:


  1. new SuperType(construction parameters)  
  2. {  
  3.         inner class methods and data  
  4. }  

new SuperType(construction parameters)
{
        inner class methods and data
}

其中,SuperType可以是一个接口,于是内部类就要实现这个接口;也可以是一个类,于是内部类就要扩展它。

如果一个内部类的代码很少,就可以使用匿名内部类。

7 静态内部类

如果一个内部类并不需要引用外部类对象,那就可以将一个内部类隐藏在外部类内。为此,可以将内部类声明为static,以便取消产生的引用。

下面是一个使用静态内部类的典型例子。如果要计算一个数组的最大值和最小值,如果使用两个方法的话,需要对数组遍历两次。如果在一次遍历中获得最大值和最小值,又需要返回两个结果。为此可以定义一个包含两个值的Pair类:


  1. class Pair  
  2. {  
  3.     private double first;  
  4.     private double second;  
  5.     public Pair(double first,double second)  
  6.     {  
  7.         this.first=first;  
  8.         this.second=second;  
  9.     }  
  10.     public double getFirst(){  
  11.         return first;  
  12.     }  
  13.     public double getSecond(){  
  14.         return second;  
  15.     }  
  16. }  

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。完整的代码如下:


  1. public class ArrayAlg {  
  2.     public static class Pair  
  3.     {  
  4.         private double first;  
  5.         private double second;  
  6.         public Pair(double first,double second)  
  7.         {  
  8.             this.first=first;  
  9.             this.second=second;  
  10.         }  
  11.         public double getFirst(){  
  12.             return first;  
  13.         }  
  14.         public double getSecond(){  
  15.             return second;  
  16.         }  
  17.     }  
  18.     public static Pair minmax(double[] values){  
  19.         double min=Double.MIN_VALUE;  
  20.         double max=Double.MAX_VALUE;  
  21.         for(double x:values){  
  22.             if(min>x)min=x;  
  23.             if(max<x)max=x;  
  24.         }  
  25.         return new Pair(min,max);  
  26.     }  
  27. }  

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 Pair(min,max);
	}
}

只有内部类可以声明为static。静态内部类的对象除了没有产生它的外部类对象的引用特权外,和所有的内部类都一样。在这个例子中,必须定义为static是由于这个内部类是定义在静态方法中的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值