17-各种内部类
- https://www.cnblogs.com/kxxiaomutou/p/15646878.html
- 2022-11-23 21:51:14
一、介绍
可以将一个类的定义放在另一个类的定义内部,这就是内部类
内部类可以分为几种具体的类型:
- 成员内部类(常说的内部类)
- 匿名内部类
- 局部内部类
- 静态内部类
二、具体四种内部类
1、成员内部类
成员内部类访问外部类的变量
成员内部类又称为普通内部类,它的地位就相当于外部类的一个成员,举个例子:
class animal{private String name;int size = 20;
//成员内部类 class gsize{public void getsize(){System.out.println(size);}public void getname(){System.out.println(name);} }
}
在这个例子中,gsize
类为 animal
的内部类,animal
为外部类,可以看到 gsize 类可以访问其外部类 animal 的全部成员,包括 private 类型的变量。
#重要 ⭐:#因为内部类会先创建外部类,然后再创建自己,所以可以直接访问外部类,而外部类不能直接访问内部类(因为有外部类,但是不一定就有内部类),但是外部类可以通过内部类的实例来访问内部类的字段和方法.
由于成员内部类定义在外部类的成员位置上,所以其称为成员内部类,这里有几点要注意:
- 成员内部类是可以被修饰的,默认是包的访问权限(
default
),同样可以添加protect
、private
、public
修饰,但成员内部类不能用 static 修饰,用 static 修饰就不是成员内部类了,就是后面要介绍的静态内部类了 - 成员内部类实质上是一个类的成员,所以它能访问外部类全部成员(包括 static 和 private)
外部类访问成员内部类的变量
那外部类如何访问成员内部类的成员呢?看下面的例子:
class animal {private String name;int size = 20;// 声明一个成员内部类的实例gsize x = new gsize();
public void getxx() {x.getname();x.getsize(); } // 成员内部类 class gsize {public void getsize() {System.out.println(size);}public void getname() {System.out.println(name);} }
}
外部类想要访问成员内部类的成员必须先创建一个成员内部类的对象(如例子第 5 行),并通过这个得到的对象的引用来访问内部类的元素。
成员内部类访问与外部类重名的变量
既然内部类可以随意访问外部类的成员,但如果内部类的成员与外部类的成员发生重名,那么内部类该如何访问这个两种重名的变量?看下面的的例子:
class animal{private String name;int size = 20;gsize x = new gsize();public void getxx(){x.getname();x.getsize();}class gsize{int size = 1;public void getsize(){System.out.println(animal.this.size);System.out.println(size);}public void getname(){System.out.println(name);}}
}
还是和上面相似的例子,看例子的第 10 行到 13 行,会发现内部类和外部类都有一个 size 的成员变量,而此时如果直接输出 size 就是输出的 gsize 自己内部的 size 成员,如果要访问外部类的成员则需要用
外部类.this.成员变量
外部类.this.成员方法
在外部类外****创建成员内部类对象
那在外部类外如何创建一个成员内部类的对象?看下面的例子:
class Test{public static void main(String[] args) {animal a1 = new animal();animal.gsize a2 = a1.new gsize();//法一 直接创建
animal.gsize a3 = a1.getgsize();//法二 利用方法创建 }
}
class animal{
private String name;
int size = 20;public gsize getgsize(){return new gsize(); } class gsize{public void getsize(){}public void getname(){} }
}
可以看到创建成员内部类的对象依靠外部类的对象来创建,如上述例子中的两种创建方法,前提都是拥有一个 animal 的对象,才创建了成员内部类的对象。
2、局部内部类
局部内部类就是定义在外部类的方法的局部区域的类(通常是定义在外部类方法里),作用域则是在方法体或者代码块中(代码块中也可以定义局部内部类),地位与局部变量相当,看如下例子:
class animal{private String name;private int size = 20;public void getsize() {int size = 30;// 定义局部内部类class get {public void getsize() {System.out.println(size);System.out.println(animal.this.size);//可以访问外部类的所有成员(包括 private)}}get var = new get();// 使用局部内部类}
}
从上例可以看到外部类 animal
的 getsize
方法中有一个 get
类,这个 get
类就是局部内部类。这里有几点要注意:
1、局部内部类相当于一个局部变量,是不能用访问修饰符修饰的(包括 public
、protected
、private
以及 static
),但可以用 final
修饰,这与一个局部变量十分类似
2、可以看上例第 10 行,局部内部类同样可以访问外部类的所有成员(包括 private),但如果出现重名现象,如第 5 行和第 3 行,单独用变量名字访问的是局部内部类自己的 size(就近原则),而访问外部类的 size 就要用 this,在后文会解释为什么可以用 this
3、同样的,外部类想要访问局部内部类的成员,则需要先创建对象,用对象的引用去访问
3、匿名内部类
匿名内部类是内部类中使用最多的一种内部类,之所以称之为匿名内部类,是因为它没有类名,继承自接口或者抽象类,匿名内部类也就是一个接口或者抽象类的子类,只含有基本的方法,匿名内部类同样是定义在外部类的局部位置,我举个内部类非常典型的使用场景:
- 成员内部类:需要显示使用
class
关键字声明一个内部类,而且反编译后内部类的名称是OutterClass$InnerClass
- 匿名内部类:不需要使用
class
关键字来声明一个内部类,而且反编译后内部类的名称是OutterClass$1
interface Animal{public void cry(); }
class Test{
public static void main(String[] args) {
// 定义并使用了一个继承Animal的匿名类
Animal tiger = new Animal() {
@Override
public void cry() {
System.out.println("老虎叫:");
}
};
}
}
这里发生了有意思的事情,可以发现定义的 Animal
是一个接口,但我们想创建一个实现接口的对象的时候通常是用一个类去实现,比如:
class tiger implements Animal{@Overridepublic void cry() {System.out.println("老虎叫:");}
}
但有时候这种方法会很繁琐,如果有很多“动物”,那我们使用额外创建类的实现方法就需要创建很多类,而我们用匿名内部类就解决了这个问题,我们直接在用匿名内部实现了一个接口的对象,并且重写了其 cry 方法。
我们来看看这样创建对象的底层实现,反编译后的代码:
这是上述使用匿名内部类的 Test 方法的 class 文件反编译后的代码,我截取了主要的部分。可以看到对于 Test 类来说,字节码中其实成为了两个类,一个是普通的 Test 类,第二个 Test$1
就是我们的匿名内部类,我们惊奇的发现,其实底层主动为我们创建了这样一个类Test$1
去实现了 Animal 接口并且重写了 cry(),其实与我们自己的方法差不多,但使用内部类,就把这个操作交给了编译器去实现,从而省去了许多繁琐的操作。不过需要注意的是 JVM 帮我们创建的这个类 Test$1
只有使用这个接口对象才有效,其余地方是不能使用的
我们继续来看一下这个表达式:
Animal tiger = new Animal() {@Overridepublic void cry() {System.out.println("老虎叫:");}};
tiger 是一个引用变量,它的编译类型是 Animal,那它的运行类型就是编译器帮我们创建的这个类,也就是匿名内部类
4、静态内部类
静态内部类就是定义在外部类的成员位置,并且有 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() {} }
}
其实和成员内部类表示方式差不多,只不过多了一个 static
修饰,主要的区别就是静态内部类只能访问外部类的静态成员
三、深入理解
1、我们可以观察到,成员内部类可以无条件访问外部类的成员,这是为什么呢?
先列一个例子:
class Animal{String name;int age;class Get{public void getname(){System.out.println(name);}public void getage(){System.out.println(age);}}
}
对于这样一个内部类来说,它可以访问外部类的所有成员,而这样的程序编译后通常会形成两个.class 文件,如图:
我们现在通过反编译来看看内部类的具体实现:
class Animal {java.lang.String name;int age;Animal(); }//Animal.class
ztc@mp ~ % javap /Users/javacode/neibulei/out/production/neibulei/Animal$Get.class
Compiled from "neibul.java"
class Animal$Get {
final Animal this$0;
Animal$Get(Animal);
public void getname();
public void getage();
}//Animal$Get.class
我截取了主要的代码,反编译代码中的 Animal
为外部类,Animal$Get
为其内部类,可以仔细观察一下这两行:
final Animal this$0;
Animal$Get(Animal);
这下我们可以明白,为什么内部类能调用外部类的全部成员,原来编译器会自动帮我们创建一个叫 this$0
的 Animal
类的引用变量,这个引用变量就是内部类能够随意使用外部类成员的原因
继续看第二行,发现这是一个内部类的构造函数,我们并没有去定义内部类的构造器,这本应该是一个无参构造,但编译器默认帮我们加入了一个 Animal
的参数,这个参数就是用来传入 this
引用变量要指向的对象,至此 this
变量的定义和初始化完成。
2、为什么局部内部类和匿名内部类只能访问局部 final 变量
注意只有这两个内部类:局部内部类和匿名内部类,这是因为二者都是定义在方法内部的类,静态内部类和成员内部类不用使用
final
,
先放一个例子:
class Test {public static void main(String[] args) {
}public void test(final int y) {final int x = 10;new Thread(){public void run() {System.out.println(x);System.out.println(y);};}.start(); }
}
可以看到从第 8 行到第 12 行,这里是一个匿名内部类,将 6、7 两行的 final 去掉都会报错。(实际上并不会,这是因为编译器在编译的时候自动为我们添加了final)
test 的生命周期是和 Thread 生命周期是不一致的,当 test方法 执行完毕后,test 所具有的局部变量的生命周期都结束了,也就是 int x 就消失了,而如果此时的内部类的生命周期没有结束(#问题/待解决 :#为什么生命周期没有结束?可能是生成的匿名对象处于堆中,没有立马被gc回收,逃逸分析也是到后期才有的东西),它需要输出 x 的值,这时问题就出现了,那编译器如何解决这个问题的?我们同样是反编译一下子节码
编译出来的字节码文件有两个,一个是 Test 普通类的,名为 Test.class
,另一个则是其内部类的字节码文件,名为 Test$1.class
,我们反编译一下:
//外部类文件反编译 class Test {Test();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: return
public static void main(java.lang.String[]);
Code:
0: returnpublic void test(int);
Code:
0: bipush 10
2: istore_2
3: new #2 // class Test$1
6: dup
7: aload_0
8: iload_1
9: invokespecial #3 // Method Test$1."<init>":(LTest;I)V
12: invokevirtual #4 // Method Test$1.start:()V
15: return
}//内部类文件反编译
class Test$1 extends java.lang.Thread {
final int val$b;final Test this$0;
Test$1(Test, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LTest;
5: aload_0
6: iload_2
7: putfield #2 // Field val$b:I
10: aload_0
11: invokespecial #3 // Method java/lang/Thread."<init>"😦)V
14: return
public void run();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 10
5: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_0
12: getfield #2 // Field val$b:I
15: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
18: return
}
观察内部类反编译文件中,第 49 行:这条指令表示将操作数 10
压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开
那 test 方法传入进来的 y 值呢?
这个我们再单独看一个例子:
class Test {public static void main(String[] args) {
} public void test(final int y) {new Thread(){public void run() {System.out.println(y);};}.start(); }
}
其匿名内部类的反编译文件为:
class Test$1 extends java.lang.Thread {final int val$y;final Test this$0;Test$1(Test, int);public void run();
}
看到反编译文件都第 2 行,发现编译器已经为 y 值创建了一个内部类的局部变量val$y
,同样把 y 值拷贝成了内部类本身的属性:
而这个值则通过默认的构造器来进行初始化:
这个构造器将 Test 类型的 this 引用变量和 int 型的传入参数 y 都进行了初始化,变成了匿名内部类的局部变量
那我们现在可以来总结一下:
我们首先分析的是方法里定义好的变量的传入方式,这个变量在编译期间值就确定了,所以编译器直接将确定好的值做一个拷贝,加入到匿名内部类中,作为匿名内部类自己的属性,而对于在编译期间确定不了的值,如上文中 Test 类传入值,需要等待输入,则在匿名内部类里创建一个变量,然后利用构造器对这个变量的值进行初始化,这个变量则是用来接受 Test 类传入的值。
那现在还是没有解释为什么只能是 final?
这里就要思考一个问题,如果不是 final 呢?不是 final 的话就代表这个值可以修改(引用变量的话就代表可以修改指向),但由于匿名内部类这种传值性质,如果你在匿名内部类接收好值后又去再其他地方修改这些值,会造成一个变量在不同地方有不一样的值,也就是数据不统一,会造成错误,所以匿名内部类之所以只能接受 final 变量的原因就在此,为了防止数据的不一致
3.静态内部类有特殊的地方吗?
从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的。
四、内部类的作用
为什么在 Java 中需要内部类?总结一下主要有以下四点:
- 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
- 方便编写事件驱动程序
- 方便编写线程代码
第一点是最重要的原因之一,内部类的存在使得 Java 的多继承机制变得更加完善。
参考资料:
《Java 编程思想》
https://www.cnblogs.com/dolphin0520/p/3811445.html