您的位置 首页 java

Java——内部类详解

说起内部类,大家肯定感觉熟悉又陌生,因为一定在很多框架源码中有看到别人使用过,但又感觉自己使用的比较少,今天我就带你具体来看看内部类。

内部类基础

java 中,广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。

成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,具体使用如下:

class Circle {
 double radius = 0;

 public Circle(double radius) {
 this.radius = radius;
 }

 /**
 * 内部类
 */ class Draw {
 public void drawSahpe() {
 System.out.println("drawshape");
 }
 }
} 

这样看起来,类 Draw 像是类 Circle 的一个成员, Circle 称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括 private 成员和静态成员),例如:

class Circle {
 private double radius = 0;
 public static int count =1;
 public Circle(double radius) {
 this.radius = radius;
 }

 /**
 * 内部类
 */ class Draw {
 public void drawSahpe() {
 // 外部类的private成员
 System.out.println(radius);
 // 外部类的静态成员
 System.out.println(count);
 }
 }
} 

不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要采取以下形式进行访问:

外部类.this.成员变量
外部类.this.成员方法 

虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问,其具体形式为:

class Circle {
 private double radius = 0;

 public Circle(double radius) {
 this.radius = radius;
 // 必须先创建成员内部类的对象,再进行访问
 getDrawInstance().drawSahpe();
 }

 private Draw getDrawInstance() {
 return new Draw();
 }

 /**
 * 内部类
 */ class Draw {
 public void drawSahpe() {
 // 外部类的private成员
 System.out.println(radius);
 }
 }
} 

成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
 public static void main(String[] args) {
 // 第一种方式
 Outter outter = new Outter();
 // 必须通过Outter对象来创建
 Outter.Inner inner = outter.new Inner();

 // 第二种方式
 Outter.Inner inner1 = outter.getInnerInstance();
 }
}

class Outter {
 private Inner inner = null;
 public Outter() {
 }

 public Inner getInnerInstance() {
 if(inner == null)
 inner = new Inner();
 return inner;
 }

 class Inner {
 public Inner() {
 }
 }
} 

内部类可以拥有 private 访问权限、 protected 访问权限、 public 访问权限及包访问权限。

比如上面的例子,如果成员内部类 Inner 用 private 修饰,则只能在外部类的内部访问;如果用 public 修饰,则任何地方都能访问;如果用 protected 修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。

这一点和外部类有一点不一样,外部类只能被 public 和包访问两种权限修饰。

我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

class People{
 public People() {
 }
}

class Man{
 public Man(){
 }

 public People getWoman(){
 /**
 * 局部内部类
 */ class Woman extends People{
 int age =0;
 }
 return new Woman();
 }
} 

注意,局部内部类就像是方法里面的一个 局部变量 一样,是不能用 public 、 protected 、 private 以及 static 修饰的。

匿名内部类

匿名内部类应该是平时我们编写代码时用得最多的,比如创建一个线程的时候:

class Test {

 public static void main(String[] args) {
 Thread thread = new Thread(
 // 匿名内部类
 new Runnable() {
 @Override
 public void run() {
 System.out.println("Thread run");
 }
 }
 );
 }
} 

同样的,匿名内部类也是不能有访问修饰符和 static 修饰符的。

匿名内部类是唯一一种没有 构造器 的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。

匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字 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() {
 }
 }
} 

深入理解内部类

通过上面的介绍,相比你已经大致了解的内部类的使用,那么你的心里想必会有一个疑惑:

为什么成员内部类可以无条件访问外部类的成员?

首先我们先定义一个内部类:

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() {
 }
 }
} 

先用 javac 进行编译,你可以发现会生成两个文件: Outter$Inner.class 和 Outter.class 。接下来利用javap -p 反编译 Outter$Inner.class ,其结果如下:

Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class
 Last modified 2019-11-25; size 408 bytes
 MD5 checksum b936e37bc77059b83951429e28f3f225
 Compiled from "Outter.java"
public class Outter$Inner
 minor version: 0
 major version: 52
  flags : ACC_PUBLIC, ACC_SUPER
Constant pool:
 #1 = Fieldref #3.#13 // test/Outter$Inner.this$0:Ltest/Outter;
 #2 = Methodref #4.#14 // java/lang/Object."<init>":()V
 #3 = Class #16 // test/Outter$Inner
 #4 = Class #19 // java/lang/Object
 #5 = Utf8 this$0
 #6 = Utf8 Ltest/Outter;
 #7 = Utf8 <init>
 #8 = Utf8 (Ltest/Outter;)V
 #9 = Utf8 Code
 #10 = Utf8 LineNumberTable
 #11 = Utf8 SourceFile
 #12 = Utf8 Outter.java
 #13 = NameAndType #5:#6 // this$0:Ltest/Outter;
 #14 = NameAndType #7:#20 // "<init>":()V
 #15 = Class #21 // test/Outter
 #16 = Utf8 test/Outter$Inner
 #17 = Utf8 Inner
 #18 = Utf8 InnerClasses
 #19 = Utf8 java/lang/Object
 #20 = Utf8 ()V
 #21 = Utf8 test/Outter
{
 final Outter this$0;
 descriptor: Ltest/Outter;
 flags: ACC_FINAL, ACC_SYNTHETIC

 public Outter$Inner(Outter);
 descriptor: (Ltest/Outter;)V
 flags: ACC_PUBLIC
 Code:
 stack=2, locals=2, args_size=2
 0: aload_0
 1: aload_1
 2: putfield #1 // Field this$0:Ltest/Outter;
 5: aload_0
 6: invokespecial #2 // Method java/lang/Object."<init>":()V
 9: return
 LineNumberTable:
 line 16: 0
 line 17: 9
}
SourceFile: "Outter.java"
InnerClasses:
 protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter 

32行的内容为:final Outter this$0;

学过 C 的朋友应该能知道,这是一个指向外部类 Outter 对象的指针,也就是说编译器会默认为成员内部类添加一个指向外部类对象的引用,这样也就解释了为什么成员内部类能够无条件访问外部类了。

那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:public Outter$Inner(Outter);

从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,但编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的 Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。

从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对 Outter this&0 引用进行初始化赋值,也就无法创建成员内部类的对象了。

为什么局部内部类和匿名内部类只能访问局部final变量?

我们还是采用和之前一样的解答方式,先定义一个类:

public class Outter {

 public static void main(String[] args) {
 Outter outter = new Outter();
 int b = 10;
 outter.test(b);
 }

 public void test(final int b) {
 final int a = 10;
 new Thread(){
 public void run() {
 System.out.println(a);
 System.out.println(b);
 };
 }.start();
 }
} 

通过 javac 编译 Outter,也会生成两个文件: Outter.class 和 Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为 Outter$x.class( x 为正整数)。

根据我提供的类,可以思考一个问题:

当 test 方法执行完毕之后,变量 a 的生命周期就结束了,而此时 Thread 对象的生命周期很可能还没有结束,那么在 Thread 的 run 方法中继续访问变量 a 就变成不可能了,但是又要实现这样的效果,怎么办呢?

Java 采用了复制的手段来解决这个问题。将 Outter$1.class 反编译可以得到下面的内容:

Classfile /D:/project/Test/src/test/java/test/Outter$1.class
 Last modified 2019-11-25; size 653 bytes
 MD5 checksum 2e238dafbd73356eba22d473c6469082
 Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Thread
 minor vers io n: 0
 major version: 52
 flags: ACC_SUPER
Constant pool:
 #1 = Fieldref #6.#23 // test/Outter$1.this$0:Ltest/Outter;
 #2 = Fieldref #6.#24 // test/Outter$1.val$b:I
 #3 = Methodref #7.#25 // java/lang/Thread."<init>":()V
 #4 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
 #5 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
 #6 = Class #30 // test/Outter$1
 #7 = Class #32 // java/lang/Thread
 #8 = Utf8 val$b
 #9 = Utf8 I
 #10 = Utf8 this$0
 #11 = Utf8 Ltest/Outter;
 #12 = Utf8 <init>
 #13 = Utf8 (Ltest/Outter;I)V
 #14 = Utf8 Code
 #15 = Utf8 LineNumberTable
 #16 = Utf8 run
 #17 = Utf8 ()V
 #18 = Utf8 SourceFile
 #19 = Utf8 Outter.java
 #20 = Utf8 EnclosingMethod
 #21 = Class #33 // test/Outter
 #22 = NameAndType #34:#35 // test:(I)V
 #23 = NameAndType #10:#11 // this$0:Ltest/Outter;
 #24 = NameAndType #8:#9 // val$b:I
 #25 = NameAndType #12:#17 // "<init>":()V
 #26 = Class #36 // java/lang/System
 #27 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
 #28 = Class #39 // java/io/PrintStream
 #29 = NameAndType #40:#35 // println:(I)V
 #30 = Utf8 test/Outter$1
 #31 = Utf8 InnerClasses
 #32 = Utf8 java/lang/Thread
 #33 = Utf8 test/Outter
 #34 = Utf8 test
 #35 = Utf8 (I)V
 #36 = Utf8 java/lang/System
 #37 = Utf8 out
 #38 = Utf8 Ljava/io/PrintStream;
 #39 = Utf8 java/io/PrintStream
 #40 = Utf8 println
{
 final int val$b;
 descriptor: I
 flags: ACC_FINAL, ACC_SYNTHETIC

 final test.Outter this$0;
 descriptor: Ltest/Outter;
 flags: ACC_FINAL, ACC_SYNTHETIC

 test.Outter$1(test.Outter, int);
 descriptor: (Ltest/Outter;I)V
 flags:
 Code:
 stack=2, locals=3, args_size=3
 0: aload_0
 1: aload_1
 2: putfield #1 // Field this$0:Ltest/Outter;
 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
 LineNumberTable:
 line 10: 0

 public void run();
 descriptor: ()V
 flags: ACC_PUBLIC
 Code:
 stack=2, locals=1, args_size=1
 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
 LineNumberTable:
 line 12: 0
 line 13: 8
 line 14: 18
}
SourceFile: "Outter.java"
EnclosingMethod: #21.#22 // test.Outter.test
InnerClasses:
 #6; //class test/Outter$1 

我们看到在 run 方法中有一条指令:bipush 10

这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。

这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。

这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

接下来也来看一下 test.Outter$1 的构造方法:test.Outter$1(test.Outter, int);

我们看到匿名内部类 Outter$1 的构造器含有两个参数,一个是指向外部类对象的引用,一个是 int 型变量,很显然,这里是将变量 test 方法中的形参 b 以参数的形式传进来对匿名内部类中的拷贝(变量 b 的拷贝)进行赋值初始化。

也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

从上面可以看出,在 run 方法中访问的变量 b 根本就不是test方法中的局部变量 b 。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在 run 方法中访问的变量 b 和test方法中的变量 b 不是同一个变量,那么当在 run 方法中改变变量 b 的值的话,会出现什么情况?

会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题, Java 编译器就限定必须将变量 b 限制为 final ,不允许对变量 b 进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用 final 进行限定了。

静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。

另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译 class 文件看一下就知道了,是没有 Outter this&0 引用的。

总结

今天介绍了内部类相关的知识,包括其一般的用法以及内部类和外部类的依赖关系,通过对字节码进行反编译详细了解了其实现模式,最后留给大家一个任务自己去实际探索一下静态内部类的实现。希望通过这篇介绍可以帮大家更加深刻了解内部类。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

文章来源:智云一二三科技

文章标题:Java——内部类详解

文章地址:https://www.zhihuclub.com/184689.shtml

关于作者: 智云科技

热门文章

网站地图