
1.什么是内部类?
在一个类A中声明一个类B,那么被声明的类B就是内部类,类A称为外部类。内部类虽然定义方式和属性有点相似但实质完全不同(内部类是类,而属性是类的成员)。
此外,内部类会隐式持有外部类对象的引用,因此在内部类中可以任意调用外部类中的方法。非静态内部类对象的创建依赖外部类对象,如果没有外部类的对象,也没有内部类的对象。
2.语法
- 声明类型:
外部类名.内部类名 - 实例化:
外部类对象.new - 内部类中获取外部类的对象引用:
外部类名.this
public class syntax {
public static void main(String[] args) {
/*
语法:声明内部类的类型 创建内部类对象
*/
OutterClass outterClass = new OutterClass();
OutterClass.InnerClass innerClass = outterClass.new InnerClass();
}
}
class OutterClass {
class InnerClass {
/**
* 语法:在内部类中获得外部类的引用
*/
public OutterClass getOutterClass() {
return OutterClass.this;
}
}
}
3.特性:
-
内部类和平常类一样具有OOP三大特性(封装、继承、多态),拥有属性和方法,可以继承、被继承、向上转型等,具有类生命周期。
-
内部类对象和外部类对象的关联:
由于内部类对象和外部类对象之间有关联,非静态内部类对象的创建必须依赖外部类对象,内部类对象也会隐式持有外部类对象的引用。
而静态内部类属于静态成员,和外部类的对象没有联系,只是表示一种结构关系。 -
内部类与外部类天然的构成了闭包:
闭包: 闭包是在函数内部可以任意访问函数外部自由变量的一种现象表述。
因为非静态内部类的对象持有外部类对象,所以在内部类中可以任意访问外部类的变量和方法。因此,内部类与外部类天然的构成了闭包。 -
独立性:
内部类也是一个类,拥有自己的域。因此每个被定义的内部类单独属于它的外部类,不能被外部类的子类中同名的内部类覆盖。
4.内部类的种类:
内部类可以被全部权限修饰符修饰,因此可以分为以下几种:
- 非private成员内部类
- 被public、protected、默认权限修饰的内部类。
- 由于类加载顺序的缘故,非static内部类中不能存在static成员。
- protected关键字带来的坑:
protected内部类可以被外部类、外部类的子类、与外部类同包的类访问,但protected内部类的构造函数默认也是protected的,这样会导致一个现象,内部类构造函数在与外部类不同包的外部子类中会出现无法调用内部类构造函数的现象,此时需要把内部类的构造函数改为public才能访问。
- private成员内部类
- private内部类只对外部类可见。如继承一个公共接口,其他类(外部类除外)无法访问内部类中不属于公共接口的方法。
- 这种设计完全隐藏了实现细节,完全阻止依赖类型的编码:
因为private内部类在外部没有访问权限,只能通过接口或父类操作,无法实例化它的真实类型,所以并没有只依赖于某种类型的代码。
- 静态内部类
- static关键字带来的特性:
1.全局
2.加载顺序的变化
3.不能访问非静态成员 - 不再和外部类对象有联系:
静态内部类的创建不再需要外部类对象,可以直接通过外部类名.内部类名
来new。 - 虽然定义在类的内部,但不是真正意义上的“内部类”,只是方便管理类结构
- 在接口中定义的类默认是静态内部类
- 局部内部类
- 定义在条件语句中的内部类叫局部内部类。
- 局部内部类没有访问权限符,因为它不是外部类的一部分。
- 为什么需要?
1.在方法内实现某接口,并返回对其的引用。
2.需要创建一个类,但又不希望这个类是公共可用的。 - 在作用域外不可访问、创建局部内部类的实例。
例如:在 if 内嵌入的一个类,运行时这个类其实在编译期就被编译过了 ,所以并不是说当达到条件时,才定义一个类,而是这个类在if的定义域内才可用。
- 匿名内部类
- 匿名内部类最大的特性就是没有名字。
- 因为没有名字所以没有构造函数,只能通过实例初始化来实现构造函数的功能。
- 匿名内部类可以继承某类或者实现接口,但只能二选一,且接口只能实现一个。
5.为什么匿名内部类和局部内部类中引用外部定义的参数必须是final的?
简单版:
因为java中支持闭包,但对闭包的支持又不够彻底,如果需要在内部类中用到外部的参数,为了防止参数和内部类中调用的参数不一致,则参数必须是final的(java 8以后不需要显式写final,编译器自动添加 )。
详细版:
因为匿名内部类的外围环境是在方法中,而在内部类可以调用内部类外的参数,实际上已经构成了闭包。
但是方法的声明周期和类生命周期不同,当一个方法结束后,生命周期就完结了,而匿名内部类的生命周期和普通类一样,且匿名内部类可能还会被它所向上转型的基类或接口引用所回调。
所以为了防止生命周期带来的问题,编译器在编译匿名内部类时会把所引用到的外部参数拷贝一份到内部类中,为了防止内外参数的一致性,所以参数必须是final的。
同样地,在局部内部类中也有类似的闭包问题。
另外,在jdk1.8后,编译器自动隐式为我们添加final。
那么,为什么匿名内部类中调用外部类中的方法或参数不需要final?
因为内部类持有一个外部类的引用,回调时候一定能访问到。
6.内部类被继承:特殊的构造方法
非private的成员内部类和静态内部类可以被继承,静态内部类的继承和普通类相同,但成员内部类的继承有点特殊:
public class OutterClass {
public class InnerClass {
String name;
public InnerClass(String name) {
this.name = name;
}
}
}
class InnerClassChild extends OutterClass.InnerClass {
//特殊的构造函数
public InnerClassChild(OutterClass outterClass, String name) {
outterClass.super(name);
}
}
如果反编译的话我们可以看到构造函数实际是这样的:
public InnerClassChild(OutterClass outterClass, String name) {
super(outterClass, name);
}
因此看上去调用super()
构造方法的是外部类的对象,实际上只是把外部类对象传入而已。
所以实际上这句特殊的语法执行的是:
构造InnerClassChild
之前,先调用它的父类OutterClass.InnerClass
的构造方法,但创建内部类必须通过外部类的对象,所以需要传入外部类的对象。
7.内部类的编译
所有内部类被编译器编译后都会产生class文件,class文件命名规则按内部类种类分3种情况:
- 有名字的内部类(绝大部分情况):
外部类类名 + $ +内部类类名 .class
- 匿名内部类:内部类类名为数字
- 嵌套在其他内部类中的内部类:
外部类类名 + $ +内部类类名 + $ +子内部类类名 + ... + .class
实例:
/**
* 外部类
*/
public class ParameterTest {
/**
* 内部类
*/
public class PP{
/**
* 内部类中的内部类
*/
public class GG {
}
}
public IteratorTest getIteratorTest(){
/**
* 匿名内部类
*/
return new IteratorTest(){
};
}
}
上面的代码编译可得下面的字节码文件:

8.内部类的应用
- 在内部类中可以不受权限控制,轻松访问外部类的方法。
- 通过内部类可以实现多重继承。
- 匿名内部类简化代码编写:匿名内部类初始化。
- 优化代码结构。
网友评论