以前我室友问我他写的一个基础程序中出现了什么语法错误,我看了半天发现他把一个类定义在了另一个类的内部,导致出错。在发现了这个问题之后,我想了想,Java中真的允许一个类定义在另一类的内部吗?因为在印象中我们从来没这么使用过。可能是内部类的应用面太窄,也可能是有许多优于内部类的代码处理,也可能是内部类从语法角度上说根本不存在。因此,我查阅了相关书籍,发现:啊,天真了,内部类果然是存在的,只不过它的应用范畴比较偏,并且有能够替代它的方法。但我还是决定了解一下这个特殊机制,并将我粗浅学到的内容整理一下,分享出来。
1. 内部类
内部类,顾名思义:是定义在一个类内部的类
它的作用为:
- 内部类方法可以访问该类定义所在的类的作用域中的数据(包括private私有域)而无需使用访问器
- 内部类可以对同一个包中的其他类隐藏起来,也就是说,内部类的作用域只在其被定义的那个外围类中
- 匿名内部类在某些使用情境下比较方便快捷
我们通过一个例子来说明内部类的语法:
public class TalkingClock
{
private int interval; //闹铃的时间间隔
private boolean beep; //闹铃响起的标志
//构造器
public TalkingClock(int interval, boolean beep){…}
//闹铃启动方法
public void start(){…}
//内部类
public class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
}
这是一个闹钟类,它的功能是通过start方法的使用,实现在固定为interval的时间间隔时,闹钟周期性响铃,并显示具体响铃时间。而响铃的功能是依赖于内部类中的ActionListener完成的
内部类的构造和普通的类的构造大体相似,不同的地方在于:
- 内部类在构造时可以不存在实例域(当然也可以定义自己的实例域),它能够调用所在的外围类的实例域
- 虽然内部类被定义在一个类的内部,但这并不意味着每一个外围类都存在一个内部类的实例域
- 内部类在没有定义构造器时,系统会添加一个参数为外围类引用对象的构造器,具体形式为:
public TimePrinter(TalkingClock tc)
{
outer = tc;
}
注:outer并不存在,只是用来说明内部类的机制
内部类的对象中存在一个隐式引用,它引用了所在外围类的对象。就如同上面所述的构造器机制一样。这个引用在内部类的定义中是不体现的。所以在机理上,内部类在调用外围类的实例域时,是通过外围类的一个对象来调用的。相当于:
public class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(outer.beep) Toolkit.getDefaultToolkit().beep(); //引用了外围类的对象,通过对象调用数据域
}
}
2. 内部类特殊语法机制
我们也可以在代码中体现出内部类在调用外部类的实例域时是通过对象调用的
具体语法为:
外围类类名.this.实例域,如:
public class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
}
从这个语法规则中,我们又一次地看到了this关键字的作用:用于表示类自身的一个对象
同时,我们也可以更加明确地编写内部类对象的构建:
外围类对象.new 内部类(构造器参数),如:
ActionListener listener = this.new TimePrinter();
并且想要在外围类的作用于之外引用内部类,可以使用 :
外围类类名.内部类类名 的形式
3. 局部内部类
在某些情况下,内部类只会在外围类的某一方法中起作用,其他方法不会用到内部类,这时我们可以使用局部内部类
局部内部类即在外围类的某一方法中定义整个内部类的内容,比如我们只在start方法中使用内部类,就可以:
public void start()
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener l = new TimePrinter();
Timer t = new Timer(interval, l);
t.start();
}
注:局部类不能用public或private修饰,它的作用域仅限于这个方法,其他任何地方都无法调用此内部类
此外,局部类还可以访问局部变量,不过要求这个局部变量为final,即不可更改,如:
public void start(int interval, boolean beep)
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener l = new TimePrinter();
Timer t = new Timer(interval, l);
t.start();
}
我们将interval和beep作为形式参数,从外部传入,而不是作为外围类的实例域调用。局部内部类允许访问这类数据。
此时,外围类无需存储beep,只要调用的beep为final,即可实现访问。
但只能访问不可变的数据这一点终究不太方便,比如我们想要通过局部内部类来记录闹钟响铃的次数,而这一变量存储的数据一定是会变化的。此时我们可以通过将这一数据存放在长度为1的数组中,内部类通过访问数组来改变相应局部变量的值
4. 匿名内部类
匿名内部类为局部内部类的更高一级的应用形式,当内部类只在一个方法内中使用,并只构建一个内部类对象时,即可使用匿名内部类,形式更加简洁:
public void start(int interval, boolean beep)
{
ActionListener l = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.out.println("the time is :" + new Date();
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
Timer t = new Timer(interval, l);
t.start();
}
可以看到,内部类的定义是紧跟在对象构建之后的
相当于该内部类没有名字,而是直接定义类的方法和数据
由于一个类的构造器的名字与类名相同,而匿名内部类没有名字,所以匿名内部类没有构造器。它只能通过定义它的外围类的构造器来传递参数。如ActionListener()中构造器无参,因此不传递参数
在某些情境下,匿名内部类可以用lambda表达式进行替换,如:
public void start(int interval, boolean beep)
{
Timer t = new Timer(interval, event -> //lambda表达式
{
System.out.println("the time is :" + new Date();
if(beep) Toolkit.getDefaultToolkit().beep();
});
t.start();
}
5. individual opinion
可以看出,内部类这一语法机制用途较小,可能会用于那些需要极度封装的类中。由于内部类的作用域仅为定义它的外围类这一特性,内部类的数据无法被其它类影响。一定程度上保证了代码的安全性和封装性。
此外不得不承认的是,内部类的机制可以被其它更常见的机制所替代。比如如果我们想看一下将内部类提取出来,成为一个常规类的效果。即在原本的内部类中定义一个原外围类的对象变量,并构造一个参数为原外围类对象变量的构造器:
public class TimePrinter implements ActionListener
{
//原外围类的对象变量
TalkClock tc;
//构造器
public TimePrinter(TalkClock talkclock)
{
tc = takclock;
}
//类中的方法
...
但这样做我们就会发现,我们无法访问原外围类的数据域beep。
可是个人认为这可以通过编写原外围类的实例域访问器来解决:tc.beep无法访问,那就调用public方法 tc.getBeep() 来访问呗。
由此可见,内部类的优点就是可以省略访问器的构建,但这真的可以算作一个实用的好处吗?个人认为有待商榷。
不仅如此,从匿名内部类的机制中我们可以看到,在调用函数式接口的方法中,lambda表达式是完全可以替代内部类的,且前者比后者的形式更加简洁凝练。
所以,在实际操作时,我个人可能不会使用内部类的机制,不过学一学,见识一下java的这种嵌套式的机制也是不错的