在一个类内部的类就是内部类(或者叫嵌套类),包含内部类的类被称为外部类(也叫宿主类)。
内部类比较常见的是匿名内部类,其他内部类其实实际开发很少使用。
掌握内部类需要对内部类进行分类,并且了解不同内部类在外部类的内部访问方式和在外界的访问方式。
可以把内部类分为以下几类:
本篇博文会依次介绍不同的内部类的用法细节。
语法如下:
[访问控制修饰符] class 内部类名 {类体}
可以看到,非静态内部类的语法格式和普通类差不多,核心区别是内部类放在外部类的内部,而普通类是单独作为一个程序单元独立存在。
非静态内部类是外部类的一个实例成员,和实例变量、实例方法、初始化块、构造器的地位差不多。
非静态内部类里面不能包含static成员,包括静态代码块、类变量、类方法。既然是非静态内部类,就表明里面的成员都是对象相关的。
想要在内部类中定义static成员,就只能使用静态内部类。
非静态内部类可以直接访问外部类的私有实例成员,比如private修饰的实例变量或者私有的实例方法。
这是因为内部类虽然是类,但它在外部类的内部,地位和其他实例成员一样的。而在外部类的内部,所有的实例成员是可以任意交互的(哪怕是最小的private权限)。
要想创建内部类的对象,必须先创建对应的外部类对象。在内部类对象创建完之后,会保存一份它所寄生的外部类对象的引用(也就是外部类名.this)。这个引用可以帮助内部类对象直接访问外部类对象的私有成员。
反过来,外部类想访问非静态内部类的实例成员,必须先创建非静态内部类的对象。
这其实很好理解,因为如果存在内部类对象,必然有与之对应的外部类对象。
但有外部类对象不代表已经创建了内部类对象,如果根本不存在内部类对象,直接访问其实例成员,当然会报错。
而且,外部类的静态成员(比如类方法)不能访问非静态内部类,比如创建非静态内部类的对象。因为非静态内部类对象也是实例成员,静态成员无法访问实例成员。
如果非静态内部类的成员和外部类的成员同名。比如有同名的变量或者同名的方法,都遵从就近原则。
比如在非静态内部类的方法里访问某个变量时,会优先查找是否存在同名的局部变量,如果不存在,则查找内部类的同名实例变量,如果不存在,才会查找外部类是否存在同名的成员变量,如果还不存在,则报错。
如果想绕开就近原则,就必须使用this关键字:
用static修饰的内部类就是静态内部类。和非静态内部类不同,静态内部类是属于外部类本身,而不是属于外部类对象。
静态内部类里面可以有静态成员和非静态成员,而非静态内部类不能有静态成员。
静态内部类不能访问外部类的实例成员,但可以直接访问外部类的类成员,因为需要满足静态成员不能访问非静态成员的规则。
其实很简单,静态内部类是与外部类本身联系的,它没有保存任何关于外部类对象的信息,自然不能访问外部类对象相关的实例成员。
外部类要想访问静态内部类的类成员,需要用类名.类成员的方式访问。
外部类要想访问静态内部类的实例成员,需要创建静态内部类的对象,用引用变量名.类成员的方式访问。
接口里也可以有内部类,接口的内部类默认是public static访问权限,所以接口的内部类只能是公开的静态内部类。
局部内部类是一个很鸡肋的概念,因为局部内部类只在方法内有效,它的地位很像局部变量。所以局部内部类没有访问修饰符,也不能用static修饰,但是可以用final修饰,表示在方法内不能创建局部内部类的子类。
局部内部类举例:
public class LocalInnerClass {
public static void main(String[] args) {
class InBase {
int a = 10;
}
class InSub extends InBase {
int b = 20;
}
var is = new InSub();
System.out.println(is.a + "," + is.b);
}
}
局部内部类的创建、使用和定义子类都必须在方法内部进行,范围实在太小了,所以复用性很差,实际开发基本不用。
局部内部类的地位和局部变量差不多,它们都在某个方法内部,而类中的方法可以访问类的其他成员(当然静态方法不能访问非静态成员),所以局部内部类由于在方法内部,也可以自由访问类中的其他成员。只是在静态方法中的局部内部类不能访问非静态成员。
如果是在外部类的内部使用内部类,其实和使用普通类大同小异,前面已经讲过了。
但在外界使用内部类时,创建类的格式和普通类不一样:
从上面可以看到,内部类的类型在外界是OuterClass.InnerClass,必须加上外部类类名的前缀,才能区分是哪一个外部类里的内部类(如果外部类有包名,还需要加上完整的包名)。
而在创建非静态内部类对象时,必须先创建外部类对象。因为非静态内部类对象是寄生在外部类对象里的。所以需要在new关键字前加上外部类对象的引用名。
2.在外部类以外使用静态内部类
首先静态内部类的访问权限必须允许外界访问。
定义的语法格式:
OuterClass.InnerClass 引用变量名 = new OuterClass.InnerClass(参数列表);
创建静态内部类的对象比较简单,因为不需要绑定外部类的对象,只需要借助外部类本身。所以只需要new 外部类类名.内部类类名(参数列表)即可。
可以看出,不管是静态内部类还是非静态内部类,它们的引用变量名格式都为:OuterClass.InnerClass。只需要外部类类名的前缀就可以区分到底是哪个外部类的内部类。
内部类在外界也可以有子类,而且内部类的子类可以是一个外部类。
子类继承父类需要调用父类的构造器,而内部父类的构造器调用需要依靠外部类。非静态内部类的构造器调用需要借助外部类的对象,静态内部类的调用需要借助外部类本身。
以创建非静态内部类的子类举例:
class Out {
class In {
}
}
public class SubClass extends Out.In {
public SubClass(Out out) {
//想要调用内部类的父类构造器,必须要提供外部类的对象
out.super();
}
}
以创建静态内部类的子类举例:
class StaticOut {
static class StaticIn {
public StaticIn(int i) {}
}
}
public class StaticSubClass extends StaticOut.StaticIn{
public StaticSubClass() {
// 调用静态内部类父类的构造器不需要借助外部类的对象,直接调用super语句即可
super(10);
}
}
注意:子类可以继承外部类的非静态内部类,但不能重写。
因为非静态内部类是外部类对象的实例成员,所以可以被子类对象继承。
但是,由于内部类的全称需要外部类的前缀,即使子类有和外部父类同名的内部类,它们的前缀也是不同的,不满足重写的规则。
所以,子类只是可以继承父类的非静态内部类,如果定义了同名的内部类,只能算新定义了一个内部类。
编译结束后,内部类也会生成.class文件,不过在格式上和普通类有区别。
格式为:
由于成员内部类(包括静态内部类和非静态内部类)在外部类的内部,所以内部类的访问权限有4种:private、default(也就是省略访问修饰符)、protected、public。
而局部内部类和匿名内部类的访问权限在方法内部,和局部变量的地位差不多,也就不需要访问控制修饰符修饰。
如果用private修饰内部类,表示需要把内部类封装在外部类内部,它只希望外部类自身访问,不希望暴露给外界访问。
比如Cow类有一个CowLeg的非静态内部类,由于奶牛的腿脱离奶牛没有任何意义,所以CowLeg类应该用private修饰。
如果用更高权限修饰内部类,则表示不仅希望外部类自身使用内部类,也希望外界可以直接创建内部类。
匿名内部类是内部类最常用的概念,它的创建是一次性的,连类名都没有。
创建匿名内部类时会立刻得到一个该类的实例,之后类的定义立即消失,无法重复使用。
匿名内部类必须继承一个父类或者实现一个接口,而且只能实现一个接口,不能使用多继承。
定义匿名内部类的语法如下:
new 实现接口() | 父类构造器(实参列表)
{
类体
}
由于匿名内部类必须在定义时立刻创建一次性的实例,所以匿名内部类不能是抽象类。这意味着匿名内部类必须实现抽象父类或者接口的所有抽象方法。
而且匿名内部类不能有构造器,因为它根本没有类名。匿名内部类初始化的工作需要依赖初始化块和定义时设置默认值。
对于实现接口的匿名内部类,在初始化对象时会调用自身隐藏的默认构造器(空的无参构造器),对于继承父类的匿名内部类,在初始化对象时会拥有一个和父类相同参数类型的构造器,这个构造器会调用父类的构造器,根据实参列表去匹配父类的某个构造器。
外部类只能在某个方法内部使用匿名内部类,但是匿名内部类可以直接访问外部类的成员,如果是在类方法中的匿名内部类,只能访问静态成员,如果在实例方法中的匿名内部类,可以访问所有成员。这其实很好理解,外部类的成员之间本来就可以互相访问,匿名内部类在方法中,自然可以随意访问外部类的其他成员。
和匿名内部类一样,局部内部类自然也可以做类似的事情。因为局部内部类也是方法中的代码,也可以直接访问外部类的其他成员(根据所在方法是否是类方法决定是否能访问实例成员)。
如果需要一次性的对象,匿名内部类就更加合适,因为它的定义相比定义一个独立的类更加简洁。
但如果类需要实现的方法过多,就不要用匿名内部类,这样会显得代码很臃肿。
很常见的场景是对于参数是接口类型的方法,需要提供一个一次性的实现类实例。
匿名(局部)内部类内部可以访问方法中的局部变量,在java8以前,被匿名(局部)内部类访问的局部变量必须用final标记。但java8以后可以省略final,系统会自动帮你标记final。
所以,如果匿名(局部)内部类内部使用了方法的局部变量,就不能对该局部变量再进行赋值。