内部类的特点
- 内部类对同一个包中的其它类隐藏。
- 内部类可以直接访问同一个类的数据,包括私有数据。
内部类对实现回调非常重要。
使用内部类访问对象状态
class Test0 {
private int a;
class Inner {
private int b;
public int getA() {
return a;
}
public int getB() {
return b;
}
Inner(int b) {
this.b = b;
}
}
Test0(int a) {
this.a = a;
}
}
public class Test {
public static void main(String[] args) {
Test0 t = new Test0(5);
Test0.Inner i = t.new Inner(6);
System.out.println(i.getA());
}
}
可以发现,内部类不仅能访问其本身的字段,也能访问创建它的外围类对象的字段。这是因为,内部类有一个隐式引用,指向创建它的对象。
这个引用在内部类的定义是不可见的,在内部类访问外围类的字段可以想象为 outer.a
(当然,outer 不是 java 的关键字),在调用子类的构造器时隐式传入了外围类作为参数。
可以把内部类定义为私有,这样,只有外围类可以调用内部类的构造器。
内部类的特殊语法规则
我们可以用 OuterClass.this
在内部类中调用外围类,比如,
class Test0 {
int a;
class Inner {
private int b;
public int getA() {
return a;
}
public int getB() {
return b;
}
Inner(int b) {
this.b = b;
}
Test0 outer(){
return Test0.this;
}
}
Test0(int a) {
this.a = a;
}
}
public class Test {
public static void main(String[] args) {
Test0 t = new Test0(5);
Test0.Inner i = t.new Inner(6);
System.out.println(i.outer()==t);
}
}
以上程序会输出 true。
如上述代码,在外围类的作用范围之外,内部类的类名为 outerClass.innerClass
;在外围类的作用范围之内,内部类的变量类型可以为 innerClass
。可以用 outerObject.new innerClass(args)
构造某个外部对象的内部类。
内部类中的静态字段只能为 final 的,否则可能在不同内部类中字段值不唯一。
内部类中不能有静态方法 / 也可以有静态方法,但只能调用内部类和外围类的静态方法和静态字段,不能访问外围类的非静态成员变量或方法。
事实上,根据我的尝试,以上内部类的静态字段和静态方法在 IDEA 和终端中都是可以定义和编译运行的,同时,似乎内部类静态字段都是共享的,可以直接用 outerClass.innerClass.字段名
访问,不会出现不唯一的问题。
class Test0 {
private int a=0;
class Inner {
static int b=0;
public void setB(int b0) {
b = b0;
}
public void printA(){
System.out.println(a);
}
static void test(){
System.out.println(b);
}
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Test0.Inner.b);
}
}
可以输出 0,
关于这个问题,推荐看以下三篇文章,
[Java] (为什么我可以) 在非静态内部类中声明静态方法 / 静态字段?Saniter 的博客 - CSDN 博客
Why are static methods allowed inside a non-static inner class in Java 16? - Stack Overflow
JEP 395: Records (openjdk.org)
内部类的本质
内部类实质上是一种编译器行为,与 java 虚拟机无关,比如,在编译后,上文中的 Test0.inner 类在编译后会变为 Test0$Inner.class
,虚拟机并不知道内部类和一般类的区别。
对该类使用 javap -c Test0$Inner
进行反编译后,得到
class Test0$Inner {
static int b;
final Test0 this$0;
Test0$Inner(Test0);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LTest0;
5: aload_0
6: invokespecial #7 // Method java/lang/Object."<init>":()V
9: return
public void setB(int);
Code:
0: iload_1
1: putstatic #13 // Field b:I
4: return
public void printA();
Code:
0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #1 // Field this$0:LTest0;
7: getfield #23 // Field Test0.a:I
10: invokevirtual #28 // Method java/io/PrintStream.println:(I)V
13: return
static void test();
Code:
0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #13 // Field b:I
6: invokevirtual #28 // Method java/io/PrintStream.println:(I)V
9: return
static {};
Code:
0: iconst_0
1: putstatic #13 // Field b:I
4: return
其中 static {};
是一个静态代码块,推测是用于静态变量初始化,因为内部类的静态变量不会和一般类一样在程序开始时直接被赋值。
其中 this$0 是一个隐式的外部类对象字段,你不可以在自己的代码中直接调用。
内部类的机制决定了它不能由普通的类得到,即使你可以把一个类作为外围类传给内部类的构造器,内部类也不能访问外围类的私有变量和私有方法。
既然内部类是一个单独的类,为什么它可以访问父类字段呢?
网上的答案是存在一个形如 access$0 的静态默认方法,Test0$Inner 实例则通过这静态方法访问私有字段 a,但根据上述反编译结果并不存在这样一个方法。
经过查询,这是 java11 加入的新特性,具体可见,
java11 新特性 —Nest-Based Access Control (嵌套访问控制)_weixin_34013044 的博客 - CSDN 博客
JEP 181: Nest-Based Access Control (openjdk.org)
Test0$Inner (Test0) 是一个构造器,它是包可见的。如果这是一个私有的内部类,构造器就是私有的,在初始化时会为 this$0 赋值。和内部类可以访问外围类的字段一样,在 java11 之后,外围类也可以自由调用内部类的 private 构造器、方法和字段。
局部内部类
如果内部类只在某段代码中使用,可以使用局部内部类,
class Test {
int a;
public static void main(String[] args) {
class Inner {
int b;
Inner (int b) {
this.b=b;
}
}
Inner i = new Inner(10);
}
}
以上代码中的 Inner 就是局部内部类,声明局部类不能由访问控制修饰符,局部类的作用域被限定在声明局部类的代码块中。
局部类最大的优点是对除 main 方法以外的所有类和方法隐藏。
局部类可以访问外围类的字段,也可以访问局部变量。(这些局部变量必须是事实最终变量(可以用 final 修饰的变量),和 lambda 表达式的限制一样)
这是因为,局部内部类对象可能会在其外部方法已经执行完毕之后被创建和使用。如果局部变量没有被声明为 final
,则在外部方法执行完毕后,这些局部变量的值可能会被改变,这会导致内部类中引用的变量值与预期不符。
和 lambda 表达式一样,局部内部类会对局部变量进行捕获,因为它们的生命周期可能不同。
class Test {
int a = 1;
void test() {
int b = 2;
class Inner {
int c = 3;
void Test() {
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
Inner i = new Inner();
i.Test();
}
public static void main(String[] args) {
Test t = new Test();
t.test();
}
}
使用 getClass().getName() 可以得到局部内部类的类名, 外部类名$编号|内部类名
,需要编号是因为一个外部类可能有多个同名局部内部类。事实上,在编译时,局部类也会被编译出 class 文件。
匿名内部类
使用内部类时,可以更近一步,只创建该类的一个对象,而不指定类名,比如
public class Test {
public static void main(String[] args) {
var temp=new Test(){
int a=1;
int b=2;
};
System.out.println(temp.getClass().getName());
}
}
创建方法为 new SuperClass(){方法体}
,这创建了一个 SuperClass 的匿名子类,SuperClass 也可以是一个接口,那就需要实现这个接口。
参数传给父类构造器,这个匿名类不能有自己的构造器,但可以提供一个初始化块。
匿名类的类名为 外部类名$编号
,在编译时也会编译出一个对应的 class 文件。
匿名类可以实现事件监听和回调,但 lambda 表达式更加简单。
对于多个同样的声明生成的匿名类不同,不能用 this.getClass ()!=obj.getClass () 实现 equals 类。
静态内部类
有时候,使用内部类是为了把一个类隐藏,不需要内部类有外部类的引用,为此,可以将内部类声明为 static,这样就不会生成那个引用。
构造静态内部类时也不需要先构造一个外围类的实例,可以用 外部类.构造器(参数)
构造静态内部类。