内部类
1. 定义
某些情况下,会把一个类放在另一个类的内部定义,这个定义在其他类内部的类称为内部类,包含内部类的类也被称为外围类。
- 内部类提供了更深的封装;
- 内部类成员可以直接访问外围类的私有数据,因为内部类被当成相应外围类的成员;
- 外围类不能访问内部类的实现细节,例如内部类的成员变量;
- 匿名内部类适合用于创建那些仅需要一次使用的类。
外围类的上级程序单元是包,内部类的上一级程序单元是外围类。
内部类的定义与外围类的定义语法大致相同,但存在如下两点区别:
- 内部类定义在一个外围类的内部;
- 内部类比外围类可以多使用三个修饰符:
private, protected, static
; - 非静态内部类不能拥有静态成员。
接口里的内部类/内部接口
Java还允许在接口里定义内部类,接口里定义的内部类隐式使用public static
修饰,即接口内部类只能是静态内部类。
此外,接口里还能定义内部接口。
在外部定义内部类
在外围类以外的地方定义内部类变量的语法格式为:OuterClass.InnerClass varName;
。当然,如果外围类有包名还应增加包前缀,静态和非静态都是如此。
特殊性
通常而言,内部类都被作为成员内部类定义,而非作为局部内部类。
需要指出,内部类是一个编译器现象,与虚拟机无关。编译器将会把内部类转换为常规的类文件,用$
分隔外部类名与内部类名,而虚拟机对此一无所知。
虽然内部类是外围类的成员,但不可以在外围类的子类中再定义一个内部类来重写其父类中的内部类成员。因为内部类的类名不再是简单地由内部类的类名组成,它实际上还把外围类的类名作为一个命名空间,作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,也就不可能重写。
2. 非静态内部类
Java不允许在非静态内部类里定义静态成员(Java语言规范对这个限制没有做任何解释)。
非静态内部类里可以直接访问外围类的private
成员,这是因为在非静态内部类对象里,保存了一个它所寄生的外围类对象的引用:
外围类的引用在构造器中设置,编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数。

可以使用OuterClassName.this
来表示外围类引用,这通常用于当外围类成员、内部类成员、内部类中方法的局部变量同名时,使用OuterClassName.this
、this
作为限定来区分:
public class DiscernVariable{
private String prop = "外围类的实例变量";
private class InClass{
private String prop = "内部类的实例变量";
public void info() {
String prop = "局部变量";
// 通过`外围类类名.this.varName`访问外围类实例变量
System.out.println("外围类的实例变量值:" + DiscernVariable.this.prop);
// 通过`this.varName`访问内部类实例的变量
System.out.println("内部类的实例变量值:" + this.prop);
// 直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test() {
InClass in = new InClass();
in.info();
}
public static void main(String[] args) {
new DiscernVariable().test();
}
}
反过来,在外围类中,可以采用以下语法更加明确地编写内部类对象的构造器:outerObject.new InnerClass(construction paramerters)
,例如:
ActionListener listener = this.new TimePrinter();
根据静态成员不能访问非静态成员的规则,外围类的静态成员中不能直接使用非静态内部类。
3. 静态内部类
有时候,使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类有外围类对象的一个引用,为此,可以将内部类声明为static
,这样就不会生成那个引用。
- 使用
static
修饰一个内部类,表明这个内部类属于外围类本身,而不属于外围类的某个对象; - 静态内部类可以包含静态成员,也可以包含非静态成员;
- 当然,静态内部类不能访问外围类的实例成员,只能访问外围类的类成员;
- 静态内部类是外围类的一个静态成员,因此外围类的所有方法、初始化块中可以使用静态内部类来定义变量、创建对象等;
4. 在外围类以外使用内部类
4.1 使用非静态内部类
由于非静态内部类的对象必须寄生在外围类的对象里,创建非静态内部类对象之前必须先创建其外围类对象:
// 语法:OuterInstance.new InnerConstructor();
class Out {
// 定义一个内部类,不使用访问控制符
class In {
public In(String msg) {
System.out.println(msg);
}
}
}
public class CreateInnerInstance {
public static void main(String[] args) {
Out.In in = new Out().new In("测试信息");
/*
* 上面代码可以改成如下三行代码:
* Out.In in;
* Out out = new Out();
* in = out.new In("测试信息");
*/
}
如果需要在外围类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外围类对象来调用。故而,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外围类对象:
public class SubClass extends Out.In {
// 显式定义SubClass的构造器
public SubClass(Out out) {
// ͨ通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
非静态内部类的子类不一定是内部类,但非静态内部类的子类实例一样需要保留一个引用,该引用指向其所在外围类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应的外围类对象。
4.2 使用静态内部类
创建静态内部类对象时无须创建外围类对象,语法:New OuterClass.InnerConstructor()
创建静态内部类的子类也比较简单,如下为静态内部类StaticIn
类定义了一个空子类:
public class StaticSubClass extends StaticOut.StaticIn {
}
使用静态内部类相对简单许多,只要把外围类当成静态内部类的包空间即可。
5 局部内部类
局部内部类是一个非常“鸡肋”的语法。
显然,定义局部内部类时,不能有访问说明符,其作用域被限定在声明这个局部类的块中。局部内部类的class
文件总是以如下命名:
OuterClass$NInnerClass.class
- 多了一个数字
N
,是因为同一个类里不可能有两个同名的成员内部类,但可能有两个以上处于不同方法中的同名局部内部类
与其他内部类相比较,局部内部类有一个优点。它们不仅能访问外部类的字段,还可以访问局部变量!不过,这样局部变量必须是事实最终变量(effectively final):
public void start(int interval, boolean beep) {
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("...");
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
6. 匿名内部类
6.1 定义
使用局部内部类时,通常还可以再进一步。假如只想创建这个类的一个对象,甚至不需要为类指定名字,这样一个类被称为匿名内部类(anonymous inner class)。
创建匿名内部类时会立即创建一个该类的实例(显然,匿名内部类不能是抽象类),同时这个类定义立即消失,匿名内部类不能重复使用。其定义语法如下:
new 实现接口() | 父类构造器(实参列表)
{
// 匿名内部类的类体部分
}
由定义语法可知,匿名内部类必须继承一个父类或实现一个接口,但最多只能继承一个父类或实现一个接口。
由于匿名内部类没有类名,所以匿名内部类不能定义构造器。在定义语法中相应的构造参数是传给父类构造器的,要注意的是,只要内部类实现了一个接口,就不能再有任何构造参数。很多场景下,倒是可以通过定义初始化块来完成构造器需要完成的事情。
interface A {
void test();
}
class ATest {
public static void main(String[] args) {
int age = 8;
// age = 1; // 若存在这条语句会发生编译错误
A a = new A() {
public void test() {
System.out.println(age);
}
};
a.test();
}
}
6.2 双括号初始化
下面的技巧称为双括号初始化(double brace initialization),这里实际上利用了内部类语法。
假设你想构造一个数组列表,并将它传递到一个方法:
var friends = new ArrayList<String>(); friends.add("Harry"); friends.add("Tony"); invite(friends);
如果不再需要这个数组列表,最好让它作为一个匿名列表,如此,可通过如下方式为匿名列表添加元素:
invite(new ArrayList<String>() {{add("Harry"); add("Tony");}});
注意这里的双括号。外层括号建立了
ArrayList
的一个匿名子类,内层括号则是一个对象初始化块。
6.3 一个小小的使用场景
生成日志或调试消息时,通常希望包含当前类的类名,如:
System.err.println("Something awful happened in " + getClass());
不过,这对于静态方法不奏效。毕竟,调用getClass
时调用的是this.getClass
。一种替换的表达式如下:
new Object(){}.getClass.getEnclosingClass() // gets class of static method
上述中的new Object(){}
会建立Object
的匿名子类的一个匿名对象,getEnclosingClass
则得到其外围类,也就是包含这个静态方法的类。