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

内部类基础

所谓内部类就是在类的内部继续定义其他内部结构类。

在 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() {@Overridepublic 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.classLast modified 2019-11-25; size 408 bytesMD5 checksum b936e37bc77059b83951429e28f3f225Compiled from "Outter.java"
public class Outter$Innerminor version: 0major version: 52flags: 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_SYNTHETICpublic Outter$Inner(Outter);descriptor: (Ltest/Outter;)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: aload_12: putfield      #1                  // Field this$0:Ltest/Outter;5: aload_06: invokespecial #2                  // Method java/lang/Object."<init>":()V9: returnLineNumberTable:line 16: 0line 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.classLast modified 2019-11-25; size 653 bytesMD5 checksum 2e238dafbd73356eba22d473c6469082Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Threadminor version: 0major version: 52flags: 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: Iflags: ACC_FINAL, ACC_SYNTHETICfinal test.Outter this$0;descriptor: Ltest/Outter;flags: ACC_FINAL, ACC_SYNTHETICtest.Outter$1(test.Outter, int);descriptor: (Ltest/Outter;I)Vflags:Code:stack=2, locals=3, args_size=30: aload_01: aload_12: putfield      #1                  // Field this$0:Ltest/Outter;5: aload_06: iload_27: putfield      #2                  // Field val$b:I10: aload_011: invokespecial #3                  // Method java/lang/Thread."<init>":()V14: returnLineNumberTable:line 10: 0public void run();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;3: bipush        105: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;11: aload_012: getfield      #2                  // Field val$b:I15: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V18: returnLineNumberTable:line 12: 0line 13: 8line 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 引用的。

总结

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

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

https://death00.github.io/

匿名内部类 可以访问外部类_Java——内部类详解相关推荐

  1. 匿名内部类 可以访问外部类_Java 内部类与外部类的互访使用小结

    内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员. 内部类访问外部类 里面的可以自由访问外面的,规则和static一样.(访问非静态时必须先创建对象) 具体如下: 非静态内部类的非静态方法 ...

  2. java中局部内部类_Java内部类详解--成员内部类,局部内部类,匿名内部类,静态内部类...

    一.内部类基础 在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类.广泛意义上的内部类一般来说包括这四种:成员内部类.局部内部类.匿名内部类和静态内部类.下面就先来了解一 ...

  3. java内部类实现方式_Java内部类详解

    一 内部类是什么 Java类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类.根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种. Java为什 ...

  4. java内部类外部类_Java内部类:如何在内部类中返回外部类对象

    Nasted Class 的介绍,请详见参考 今天讨论的不是不是内部类的概念,而是具体使用的一个场景-如何在内部类中返回外部对象 (孤猪:我在程序里也碰到过一次这样的状况,非静态内部类可以直接调用外部 ...

  5. java的内部类和外部类_java内部类 和外部类的区别

    下面说一说内部类(Inner Class)和静态内部类(Static Nested Class)的区别: 定义在一个类内部的类叫内部类,包含内部类的类称为外部类.内部类可以声明public.prote ...

  6. java 函数内部类_java 内部类详解 转

    classOuter {classInner { } } (二) 内部类的访问规则 ​ A:可以直接访问外部类的成员,包括私有 ​ B:外部类要想访问内部类成员,必须创建对象 (三) 内部类的分类 ​ ...

  7. java 内部类_Java——内部类详解

    说起内部类,大家肯定感觉熟悉又陌生,因为一定在很多框架源码中有看到别人使用过,但又感觉自己使用的比较少,今天我就带你具体来看看内部类. 内部类基础 所谓内部类就是在类的内部继续定义其他内部结构类. 在 ...

  8. java内部类选择题_java内部类详解(附相关面试题)

    说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法.今天我们就来一探究竟. 一.内 ...

  9. java声明内部类_Java 内部类详解

    什么 定义在一个类内部的类,称为内部类(累不累),如下: public class A { private int c = 1; public class C { public void test() ...

最新文章

  1. 【经验总结】C#常用线程同“.NET研究”步方法应用场景和实现原理
  2. PPPoE***2:PADR耗竭sessionid
  3. Matlab篇(二)MATLAB中addpath的用法 (转)
  4. 802.11n 中HT20 HT40的区别和信道划分
  5. java bmp信息隐藏_BMP图像信息隐藏及检测
  6. Kubernetes pod状态出现ImagePullBackOff的原因
  7. python实现: protobuf解释器
  8. [转] 值得推荐的C/C++框架和库
  9. 【BZOJ3207】花神的嘲讽计划Ⅰ Hash+主席树
  10. 重构wangEditor(web富文本编辑器),欢迎指正!
  11. C语言除法运算符“/”和求余运算符“%”
  12. 明御:APT攻击预警平台
  13. Greasy Fork 视频网页全屏脚本
  14. Android性能优化的问题
  15. tab标签页-选项卡后边+后端所返数据的数量
  16. 数据结构习题集作业代码(第一章)
  17. OLE程序开发利用(开发EXCEL) 之 一
  18. 浅谈变压器(主要是电子高频变压器)
  19. 服务器配置文件的英文表达,服务器配置 英文
  20. 医学图像分割新网络:Boundary-aware Context Neural Network for Medical Image Segmentation

热门文章

  1. delphi 调用 c# 写的webservice
  2. 掌握jsp自定义标签:(四)
  3. 使用WebBrowser控件时在网页元素上绘制文本或其他自定义内容
  4. 二分答案——木材加工(洛谷 P2440)
  5. mysql私房菜_老男孩MySQL私房菜深入浅出精品视频第7章备份与恢复基础实践视频课程...
  6. window.location.href如何多次请求_何为幂等?如何设计?
  7. 解析markdown_利用 markdown 生成页面实践
  8. 数据库每日一题 2020.05.08
  9. 懒人看执行计划神器 for Oracle
  10. 带你了解极具弹性的Spark架构的原理