Java内部类

Posted Ezioooooo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内部类相关的知识,希望对你有一定的参考价值。

本文是《Java核心技术 卷1》中第六章接口与内部类中关于内部类的阅读总结。

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

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

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<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)。比如这样:

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

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

以上是关于Java内部类的主要内容,如果未能解决你的问题,请参考以下文章

# Java 常用代码片段

# Java 常用代码片段

elasticsearch代码片段,及工具类SearchEsUtil.java

片段 - 全局视图变量与本地和内部类侦听器和内存泄漏

为啥片段类应该是公开的?

ForegroundService没有从片段开始?