Java内部类详解
详细解释内部内的一些使用规则的原因
概览
定义:在一个类的内部定义的类。它的定义位于另一个类的内部,并且可以访问外部类的成员,包括私有成员。
为什么要用
我觉得一个是为了符合OOP的封装原则,因为毕竟也可以直接把内部类函数和成员放到外面写。
另外就是既然可以写一个类,为什么要把它写在内部?原因是可能这个类的功能只能或者只需要被外部类使用。
还有一点:实现回调机制。Java没有函数指针和C#那种委托,但可以用函数式接口配合匿名内部类实现类似效果。Lambda表达式底层依然是会转换成匿名内部类的。
类型
静态内部类
可以看作外部类的静态成员,但是这个成员就像static Object
一样,并没有初始化,如果他是public
的,外界调用的时候是需要new的。
可以定义静态方法,普通成员等。只能访问外部类的静态变量和方法。
OuterClass.InnerClass inner = new OuterClass.InnerClass();
成员内部类
看作外部类的实例成员,使用他需要依靠外部类实例。
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.innerMethod();
因为他不是静态的,所以内部不能定义静态成员和方法。 // 底层原因没弄明白,但是有大致猜测
Java语法:非静态代码块不能定义静态方法和变量
局部内部类
似乎没什么特色,就是在局部作用域实现的类。
public void outerMethod() {
int outerData = 10;
// 局部内部类
class LocalInnerClass {
public void innerMethod() {
System.out.println("Inner Method: " + outerData);
}
}
LocalInnerClass inner = new LocalInnerClass();
inner.innerMethod();
}
匿名内部类
通过继承父类或者实现接口的方式创建一个匿名内部类。Lambda底层也是转换成匿名内部类,只不过是只有一个方法的类。
InterfaceType obj2 = new InterfaceType() {
@Override
public void interfaceMethod() {
}
};
ParentClass obj1 = new ParentClass() {
@Override
public void someMethod() {
}
};
底层原理
结论
用javap
反汇编.class文件。
javap 参数
-c 查看JVM指令
-p 查看私有成员和函数
-v 输出行号、本地变量表信息、反编译汇编代码、当前类用到的常量池等信息。
jvm参数
-Djdk.internal.lambda.dumpProxyClasses 生成因为lambda表达式产生的内部类
注:PowerShell输入这个要用
''
引号括起来
提前说明,其实很多规定都是编译器的规定,如果硬要打破,是可以的(比如运行时用反射),因为JVM并没有反对这些规定,但由于大多数时候我们写代码然后运行,都不得不经过编译器检查,所以不得不遵循这些规则,当然这些规则并无害,只是学习的时候可能会产生很多疑惑:他为什么要这么规定?
内部类的各种访问规则很多,如果不了解底层很容易搞乱。
底层规则有以下几条,后面给出实例证明。
-
内部类也会被单独编译成class文件,命名格式为
OuterClass$InnerClass.class
-
成员内部类访问外部类的私有成员
- 成员和静态内部类:底层实现是:用外部类实例创建内部类的时候会把他的引用传递给内部类的构造函数(编译器生成),内部类就通过这个引用来访问,并且,能够访问私有成员(静态或非静态),编译器会生成对应的静态方法用于获取私有成员。这个解释适用于后面所有内部类。
此处不明白为什么获取非静态的私有成员的时候,编译器要生产静态的方法,按理说非静态的也行。可能是为了统一吧,毕竟静态成员就是用的静态方法访问的,这样写格式统一,后面看到他生成的方法就知道了。
另外,我一开始发现生成的静态方法后,想尝试直接手动调用,编译报错,看到网上解释说编译器不允许这么干。虽然这个方法编译时期已经是存在了的。Ref 隐藏的访问权限synthetic
-
局部和匿名内部类:如果访问局部的变量,那变量必须是
final
或者等价于final
的(即,局部内没有改变过)。底层实现:局部变量会直接作为值传递给这两种内部类的构造函数,赋值给内部的一个对应的final
变量。如果访问的外部类的静态成员,则这个内部类必须是在非静态函数里面。
总的来说:如果获取外部非静态变量,底层实现都是直接赋值给内部类中的final变量,而静态变量则是直接通过类来访问。
其中,非静态变量分为局部变量和外部类的变量,局部变量必须为final或等同final,外部类的变量无此限制。
-
静态内部类访问外部类成员:由于实例化的时候,静态内部类是通过外部类(而非实例)创建的,所以不会获得实例对象的引用,因此无法访问外部类的非静态成员。
-
外部类访问内部类成员:
-
静态成员:直接类名.成员,原理同上,编译器生成静态方法。
注:内部类中只有静态内部类才能有静态成员。这是编译器规定的。原因不明,可能是防止语法二义性,详见 ref。或者就是一个浮于表面的解释:非静态代码块不能定义静态函数、成员。
只有内部类才能是静态类。
-
非静态成员:要先创建对象,然后对象.成员,原理同上。
-
-
Lambda表达式:底层会在外部类编译时创建对应的函数(如果表达式在静态函数内则创建静态函数,反之则反之),然后运行时会动态创建一个类。访问
final
局部变量,则变量直接传入,访问外部类的静态或者动态变量,会写在编译时再外部类创建的方法中,该方法的逻辑就是lambda表达式中的逻辑,而生成的类只是调用这个方法,顺便把局部变量传进去。
探究过程
全部源码如下,使用javac Main.java
后使用java '-Djdk.internal.lambda.dumpProxyClasses' Main
运行(cmd不用引号),可以生成全部需要分析的.class
文件,其中运行时生成的时Lambda表达式相关的。
interface IAnoInner {
void print();
}
public class Main {
private static int privateStaticInt = 1;
private int privateInt = 2;
public static void testLambda(IAnoInner anoInner) {
System.out.println(anoInner.getClass().getName());
anoInner.print();
}
public void test() {
// Lambda -> void lambda$test$0, Main$Lambda$2
privateInt = 3;
testLambda(() -> {
System.out.println(privateInt);
});
}
public static void main(String[] args) {
// NoStatic Inner Class
Main m = new Main();
NonStaticInnerClass nsi = m.new NonStaticInnerClass();
m.test();
System.out.println(nsi.InnerNoStaticInt);
// Static Inner Class
System.out.println(StaticInnerClass.InnerStaticInt);
System.out.println((new Main.StaticInnerClass()).InnerNoStaticInt);
int localVarial = 1; // must be final or equals to final (not change)
// localVarial = 2; // error, not equals to final
// Local Inner Class -> Main$1LocalInnerClass
class LocalInnerClass {
public void print() {
System.out.println(privateStaticInt + localVarial);
}
}
LocalInnerClass lic = new LocalInnerClass();
lic.print();
// Anonymous InnerClass -> Main$1.class
new IAnoInner() {
@Override
public void print() {
System.out.println(privateStaticInt + localVarial);
}
};
// Lambda -> void lambda$main$0, Main$Lambda$1
testLambda(() -> {
System.out.println(privateStaticInt + localVarial);
});
}
public class NonStaticInnerClass {
// private static int InnerStaticInt = 12;
private int InnerNoStaticInt = 12;
public void print() {
System.out.println(privateInt + privateStaticInt);
}
}
public static class StaticInnerClass {
private static int InnerStaticInt = 12;
private int InnerNoStaticInt = 12;
public void print() {
System.out.println(privateStaticInt);
}
}
}
所有.class
文件:
Lambda : Main$$Lambda$1.class
Lambda : Main$$Lambda$2.class
匿名内部类 : Main$1.class
局部内部类 : Main$1LocalInnerClass.class
局部内部类 : Main$1NoStaticLocalInnerClass.class
非静态内部类 :Main$NonStaticInnerClass.class
静态内部类 :Main$StaticInnerClass.class
Main.class
IAnoInner.class
使用javap -p
反编译,下面分不同类型分析
Lambda
静态类中的Lambda只能访问静态成员或者局部变量,此处我访问的局部变量,他直接写入arg$1
中了。
非静Lambda可以访问实例成员或者局部变量。因此后者会有一个Main的引用,前者没有,自然不能访问非静态成员。
// 静态类中的Lambda
final class Main$$Lambda$2 implements IAnoInner {
private final int arg$1; // 局部变量
private Main$$Lambda$2(int);
private static IAnoInner get$Lambda(int);
public void print();
}
// 非静态类中的Lambda
final class Main$$Lambda$1 implements IAnoInner {
private final Main arg$1; // Main的引用
private Main$$Lambda$1(Main);
private static IAnoInner get$Lambda(Main);
public void print();
}
对应的,Main中为他们创建的方法,从static修饰符也能看出给谁的。
private static void lambda$main$1(int);
private void lambda$test$0(); // 非静
Lambda类会对应调用这俩。例如,(主要看注释)
public void print();
Code:
0: aload_0
1: getfield #15 // Field arg$1:LMain;
4: invokespecial #26 // Method Main.lambda$test$0:()V Lambda的逻辑写在了外部类的static方法中
7: return
一开始我疑惑:为什么这里可以访问Main的私有方法呢?
后来顿悟:
类的访问其实本没有界限,编译器插手的多了,也便有了界限,访问修饰符的检查和规定是编译器定的规矩,字节码层面其实是没有限制的。想怎么调怎么调,只不过我们自己写的代码会经过编译器检查,不允许这样访问。
注意:Lambda和匿名还有局部类的区别在于访问外部类的成员的方式,前者访问以及业务逻辑其实是在外部类自己的函数中,后两者是使用类生成的静态函数来访问,业务逻辑在内部类中。
static int access$300();
static int access$400(Main);
// Main生成的用于访问私有成员的函数
剩下的静态内部类和成员内部类的原理和上面说的一样。
访问私有成员核心就是编译器生成函数访问(Lambda例外)。
静态非静态成员访问核心就是类有没有实例引用。
局部变量就无脑final
。