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