前言
本章节使用 JDK 环境版本如下 :
C:\Users\chenjz20>java -version
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
内部类的分类
- 成员内部类
- 局部内部类
- 匿名内部类
- 静态内部类
成员内部类
public class Circle {
private double radius = 0;
public Circle(double radius) {
this.radius = radius;
getDrawInstance().drawSahpe(); //1. 外部类返回内部类
}
public Draw getDrawInstance() {
return new Draw();
}
class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //2. 外部类访问外部类的变量
}
}
public static void main(String[] args) {
Circle circle = new Circle(12.5D);
// 3. 创建对象返回内部类方法
circle.getDrawInstance().drawSahpe();
}
}
成员内部类和外部类返回调用之间的关系从上面的代码可以看到
局部内部类
class People{
public People() {
}
}
class Man{
public Man(){
}
public People getWoman(){
class Woman extends People{ //局部内部类
int age =0;
}
return new Woman();
}
}
在方法中创建一个类 ,额 ,还没见过这种~
匿名内部类
匿名内部类大家就熟了
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.test1();
}
public void test1(){
final int i = 5;
new Thread(() -> {
int a = i + 1;
System.out.println("a: "+ a);
}).start();
final MyClass myClass = new Test.MyClass();
new Thread(() -> {
myClass.myInt++;
System.out.println("myInt: "+ myClass.myInt);
}).start();
}
class MyClass {
private int myInt = 10;
}
}
上面的例子用到了上篇文章讲的 final
变量 , 假如要让 MyClass
成为一个线程安全的类这样肯定是不行的 , 即使在传参数的时候定义了 final
回到我们今天的内部类
匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
内部类是为了隐藏, 现在又通过 static 暴露出来, 那么静态内部类存在的动机是啥呢 ?
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
class Outter {
public Outter() {
}
static class Inner {
public Inner() {
}
}
}
深入内部类
为什么成员内部类可以无条件访问外部类的成员?
原因是内部类的构造函数持有了一个外部对象的引用
我们以下面这段代码为例子 , 代码来源 : https://www.cnblogs.com/dolphin0520/p/3811445.html
public class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
protected class Inner {
public Inner() {
}
}
}
编译以后我们可以看到 , 内部类也会生成一个 class 文件
然后我们查看一下内部类的字节码 , 命令是
javap -v Outter$Inner
看一下输出
PS E:\java_project\my\test\justjava\target\classes> javap -v Outter$Inner
Classfile /E:/java_project/my/test/justjava/target/classes/Outter.class
Last modified 2022-11-25; size 531 bytes
MD5 checksum e501b1ac0e245a8ccec8e300609f28f1
Compiled from "Outter.java"
public class Outter
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#24 // Outter.inner:LOutter$Inner;
#3 = Class #25 // Outter$Inner
#4 = Methodref #3.#26 // Outter$Inner."<init>":(LOutter;)V
#5 = Class #27 // Outter
#6 = Class #28 // java/lang/Object
#7 = Utf8 Inner
#8 = Utf8 InnerClasses
#9 = Utf8 inner
#10 = Utf8 LOutter$Inner;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 LOutter;
#18 = Utf8 getInnerInstance
#19 = Utf8 ()LOutter$Inner;
#20 = Utf8 StackMapTable
#21 = Utf8 SourceFile
#22 = Utf8 Outter.java
#23 = NameAndType #11:#12 // "<init>":()V
#24 = NameAndType #9:#10 // inner:LOutter$Inner;
#25 = Utf8 Outter$Inner
#26 = NameAndType #11:#29 // "<init>":(LOutter;)V
#27 = Utf8 Outter
#28 = Utf8 java/lang/Object
#29 = Utf8 (LOutter;)V
{
public Outter();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aconst_null
6: putfield #2 // Field inner:LOutter$Inner;
9: return
LineNumberTable:
line 3: 0
line 2: 4
line 5: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LOutter;
public Outter$Inner getInnerInstance();
descriptor: ()LOutter$Inner;
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field inner:LOutter$Inner;
4: ifnonnull 19
7: aload_0
8: new #3 // class Outter$Inner
11: dup
12: aload_0
13: invokespecial #4 // Method Outter$Inner."<init>":(LOutter;)V
16: putfield #2 // Field inner:LOutter$Inner;
19: aload_0
20: getfield #2 // Field inner:LOutter$Inner;
23: areturn
LineNumberTable:
line 8: 0
line 9: 7
line 10: 19
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this LOutter;
StackMapTable: number_of_entries = 1
frame_type = 19 /* same */
}
SourceFile: "Outter.java"
InnerClasses:
protected #7= #3 of #5; //Inner=class Outter$Inner of class Outter
其中最后
InnerClasses:
protected #7= #3 of #5; //Inner=class Outter$Inner of class Outter
对应 #7=
和#3
可以看到从常量池中传来 Outter
的一个引用 ,即传过来一个父类的引用 . 所以说成员内部类必须依赖与外部类的存在
为什么局部内部类和匿名内部类只能访问局部final变量?
我们先思考一下 , 当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了复制的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:
public class Test1 {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
benjious@DESKTOP-UKPC3DN MINGW64 /e/my_project/back_project/java_project/java草稿项目/benjious-justjava-master/justjava/target/classes/test
$ javap -v Test1$1.class
Classfile /E:/my_project/back_project/java_project/java▒ݸ▒▒▒Ŀ/benjious-justjava-master/justjava/target/classes/test/Test1.class
Last modified 2022-11-27; size 594 bytes
MD5 checksum 6b02b1709048c50cb67d3167e97bf097
Compiled from "Test1.java"
public class test.Test1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // test/Test1$1
#3 = Methodref #2.#28 // test/Test1$1."<init>":(Ltest/Test1;I)V
#4 = Methodref #2.#29 // test/Test1$1.start:()V
#5 = Class #30 // test/Test1
#6 = Class #31 // java/lang/Object
#7 = Utf8 InnerClasses
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Ltest/Test1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 test
#20 = Utf8 (I)V
#21 = Utf8 b
#22 = Utf8 I
#23 = Utf8 a
#24 = Utf8 SourceFile
#25 = Utf8 Test1.java
#26 = NameAndType #8:#9 // "<init>":()V
#27 = Utf8 test/Test1$1
#28 = NameAndType #8:#32 // "<init>":(Ltest/Test1;I)V
#29 = NameAndType #33:#9 // start:()V
#30 = Utf8 test/Test1
#31 = Utf8 java/lang/Object
#32 = Utf8 (Ltest/Test1;I)V
#33 = Utf8 start
{
public test.Test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/Test1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
public void test(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=4, locals=3, args_size=2
0: bipush 10
2: istore_2
3: new #2 // class test/Test1$1
6: dup
7: aload_0
8: iload_1
9: invokespecial #3 // Method test/Test1$1."<init>":(Ltest/Test1;I)V
12: invokevirtual #4 // Method test/Test1$1.start:()V
15: return
LineNumberTable:
line 9: 0
line 10: 3
line 15: 12
line 16: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ltest/Test1;
0 16 1 b I
3 13 2 a I
}
SourceFile: "Test1.java"
InnerClasses:
#2; //class test/Test1$1
线程的这个内部类名称为 Test1$1.class ,
我们先看第一处 :
从下面找到 Test1$1 构造函数相关的字节码在
#2 = Class #27 // test/Test1$1
#3 = Methodref #2.#28 // test/Test1$1."<init>":(Ltest/Test1;I)V
再看这一句
bipush 10
其中 bipush
代表的是入栈的指令 , 将 10
入栈
这两个地方需要注意一下 .我先说一下结论 :
如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。
而上面指出的字节码分别对弈上面结论的前者和后者, 先看 如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值
, 我们可以看到第一处的字节码 ,内部类的构造方法传进来了一个变量 ,然后如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝
,也即是 bipush 10
.
复制过去到内部类中, 试想一下,传给线程知道, 这个值被修改了 ,而线程又不知道, 肯定会带来数据不一致的情况 , 为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
静态内部类有特殊的地方吗?
从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。
题外话
看到海子的文章 中 为什么成员内部类可以无条件访问外部类的成员?
这个章节中代码Outter$Inner
打出的字节码和本文的不同 (应该是 JDK 版本不同导致的),这里贴出来 ,他的字节码 :
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz Inner;
{
final com.cxh.test2.Outter this$0;
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
5: aload_0
6: invokespecial #12; //Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 16: 0
line 18: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cxh/test2/Outter$Inner;
}
可以看到同样是 Inner 内部类, 但是他引入父类引用的方式是这样的
final com.cxh.test2.Outter this$0;
引入一个变量 .