当前位置:网站首页 > 技术博客 > 正文

java的内部类和外部类



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

1、成员内部类:

成员内部类看起来像是外部类的一个成员,所以内部类可以拥有private、public等访问权限修饰;当然,也可以用static来修饰。成员内部类分为:

  • 静态成员内部类:使用static修饰类;
  • 非静态成员内部类:未用static修饰类,在没有说明是静态成员内部类时,默认成员内部类指的就是非静态成员内部类;

注:只有成员内部类才能加static变成静态内部类。

1.1)静态内部类:

使用static修饰的内部类我们称之为静态内部类,我们要知道只要是static修饰的类那它一定是内部类,不可能是外部类。静态内部类与非静态内部类之间存在一个最大的区别,非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类的对象,但是静态内部类却没有。没有这个引用就意味着:

  • 它的创建是不需要依赖于外围类的对象
  • 它不能使用任何外围类的非static成员变量和方法(因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象)
  • 静态内部类内允许有static属性、方法;
 
  

1.2)非静态成员内部类:

 
  

1)成员内部类访问外部类的信息:

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

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

 
  

2)创建内部类对象:

成员内部类是依附外部类而存在的,所以要创建成员内部类的对象,前提是必须存在一个外部类的对象。通常有如下两种方法:

 
  

3)外部类访问成员内部类信息:

同样,外部类也可以访问内部类的所有成员变量和方法(包括private),但外部类想访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

 
  

4)成员内部类中不能存在任何static的变量和方法:

对于成员内部内并不是完全不能出现static字段的,如果你是使用final和static同时修饰一个属性字段,并且这个字段是基本类型或者String类型的,那么是可以编译通过的。原因:

  1. 非静态成员内部类要依赖外部类,所以不能有static变量;
  2. 在类加载那一章我们了解到,对于final static的变量是存放在常量池中的,不涉及到类的加载;

说明:这一条对于局部内部类也是适用的。

1.3)应用:

1)api接口响应数据中,我们可以使用成员内部类这种方式来定义复杂的结构,实现json序列化:

 
  

2)常量:

 
  

3) 静态内部类实现单例:

 
  

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

4)成员内部类实现多继承:

 
  

2、局部内部类:

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

 
  

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

3、匿名内部类:

 
  

匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

1、内部类(包括匿名内部类、局部内部类)会持有外部类的引用:

以普通的内部类为例:

 
  

切到Demo.java所在文件夹,命令行执行 javac Demo.java,在Demo类同目录下可以看到生成了二个class文件

普通内部类生成class.png

Demo.class很好理解,另一个 类

 
  

就是我们的内部类编译出来的,它的命名也是有规律的,外部类名Demo+$+内部类名DemoRunnable。 查看反编译后的代码(IntelliJ IDEA本身就支持,直接查看class文件即可)

 
  

生成的类只有一个构造器,参数就是Demo类型,而且保存到内部类本身的this$0字段中。到这里我们其实已经可以想到,内部类持有的外部类引用就是通过这个构造器传递进来的,它是一个强引用。

验证我们的想法

怎么验证呢?我们需要在Demo.class类中加一个方法,来实例化这个DemoRunnable内部类对象

 
  

再次执行 javac Demo.java,再执行javap -verbose Demo.class,查看Demo类的字节码,前方高能,需要一些字节码知识,这里我们重点关注run方法(插一句题外话,字节码简单的要能看懂,-。-)

 
  
  • 先通过new指令,新建了一个Demo$DemoRunnable对象
  • aload_0指令将外部类Demo对象自身加载到栈帧中
  • 调用Demo$DemoRunnable类的init方法,注意这里将Demo对象作为了参数传递进来了

到这一步其实已经很清楚了,就是将外部类对象自身作为参数传递给了内部类构造器,与我们上面的猜想一致。

匿名内部类的实现

 
  

同样执行javac Demo.java,这次多生成了一个Demo$1.class,反编译查看代码

 
  

可以看到匿名内部类和普通内部类实现基本一致,只是编译器自动给它拼了个名字,所以匿名内部类不能自定义构造器,因为名字编译完成后才能确定。 方法局部内部类,我这里就不赘述了,原理都是一样的,大家可以自行试验。

2、为什么匿名内部类使用到外部类方法中的局部变量时需要是final类型的?

这里先申明一下,这个问题本身是有问题的,问题在哪呢?因为java8中并不一定需要声明为final。我们来看个例子

 
  

匿名内部类对象runnable,使用了外部类方法中的age局部变量。编译运行完全没问题,而age并没有final修饰啊! 那我们再在run方法中,尝试修改age试试

 
  

编译器报错了,提示信息是”age is access from inner class, need to be final or effectively final“。很显然编译器很智能,由于我们第一个例子并没有修改age的值,所以编译器认为这是effectively final,是安全的,可以编译通过,而第二个例子尝试修改age的值,编译器立马就报错了。

外部类变量是怎么传递给内部类的?

这里对于变量的类型分三种情况分别来说明

非final局部变量

我们去掉尝试修改age的代码,然后执行javac Demo.java,查看Demo$1.class的实现代码

 
  

可以看到对于非final局部变量,是通过构造器的方式传递进来的。

final局部变量

age修改为final

 
  

同样执行javac Demo.java,查看Demo$1.class的实现代码

 
  

可以看到编译器很聪明的做了优化,age是final的,所以在编译期间是确定的,直接将+1优化为11。 为了测试编译器的智商,我们把age的赋值修改一下,改为运行时才能确定的,看编译器如何应对

 
  

再看Demo$1 字节码实现

 
  

编译器意识到编译期age的值不能确定,所以还是采用构造器传参的形式实现。现代编译器还是很机智的。

外部类成员变量

将age改为Demo的成员变量,注意没有加任何修饰符,是包级访问级别。

 
  

javac Demo.java,查看匿名内部内的实现

 
  

这一次编译器直接通过外部类的引用操作age,没毛病,由于age是包访问级别,所以这样是最高效的。 如果将age改为private,编译器会在Demo类中生成二个方法,分别用于读取age和设置age,篇幅关系,这种情况留给大家自行测试。

解答为何局部变量传递给匿名内部类需要是final?

通过上面的例子可以看到,不是一定需要局部变量是final的,但是你不能在匿名内部类中修改外部局部变量,因为Java对于匿名内部类传递变量的实现是基于构造器传参的,也就是说如果允许你在匿名内部类中修改值,你修改的是匿名内部类中的外部局部变量副本,最终并不会对外部类产生效果,因为已经是二个变量了。 这样就会让程序员产生困扰,原以为修改会生效,事实上却并不会,所以Java就禁止在匿名内部类中修改外部局部变量。

3、外部类成员变量是怎么传递给内部类的?

 
  

这里age成员变量没有final修饰,内部类可以直接访问到。查看反编译代码:

 
  

这一次编译器直接通过外部类的引用操作age,没毛病,由于age是包访问级别,所以这样是最高效的。 如果将age改为private,编译器会在Demo类中生成二个方法,分别用于读取age和设置age。

4、Lambda表达式是如何实现的?

Java8引入了Lambda表达式,一定程度上可以简化我们的代码,使代码结构看起来更优雅。做技术的还是要有刨根问底的那股劲,问问自己有没有想过Java中Lambda到底是如何实现的呢?

来看一个最简单的例子

 
  

Animal类中定义了一个run方法,参数是一个Runnable对象,Java8以前,我们可以传入一个匿名内部类对象

 
  

Java 8 之后编译器已经很智能的提示我们可以用Lambda表达式来替换。既然可以替换,那匿名内部类和Lambda表达式是不是底层实现是一样的呢,或者说Lambda表达式只是匿名内部类的语法糖呢? 要解答这个问题,我们还是要去字节码中找线索。通过前面的知识,我们知道javac Animal.java命令将类编译成class,匿名内部类的方式会产生一个额外的类。那用Lambda表达式会不会也会编译新类呢?我们试一下便知。

 
  

javac Animal.java,发现并没有生成额外的类!!! 我们继续使用javap -verbose Animal.class来查看Animal.class的字节码实现,重点关注test方法

 
  

发现test方法字节码中多了一个invokedynamic #2 0指令,这是java7引入的新指令,其中#2 指向

 
  

而0代表BootstrapMethods方法表中的第一个,java/lang/invoke/LambdaMetafactory.metafactory方法被调用。

 
  

这里面我们看到了com/company/inner/Demo.lambda$test$0这么个东西,看起来跟我们的匿名内部类的名称有些类似,而且中间还有lambda,有可能就是我们要找的生成的类。 我们不妨验证下我们的想法,可以通过下面的代码打印出Lambda对象的真实类名。

 
  

打印出runnable的类名,结果如下

 
  

跟我们上面的猜测并不完全一致,我们继续找别的线索,既然我们有看到LambdaMetafactory.metafactory这个类被调用,不妨继续跟进看下它的实现

 
  

内部new了一个InnerClassLambdaMetafactory对象。看名字很可疑,继续跟进

 
  

省略了很多代码,我们重点看lambdaClassName这个字符串(通过名字就知道是干啥的),可以看到它的拼接结果跟我们上面打印的Lambda类名基本一致。而下面的ClassWriter也暴露了,其实Lambda运用的是Asm字节码技术,在运行时生成类文件。我感觉到这里就差不多了,再往下可能就有点太过细节了。-。-

Lambda实现总结

所以Lambda表达式并不是匿名内部类的语法糖,它是基于invokedynamic指令,在运行时使用ASM生成类文件来实现的。

版权声明


相关文章:

  • so文件反编译成c语言2025-07-14 23:01:00
  • redis集群api2025-07-14 23:01:00
  • select语句中的where说明2025-07-14 23:01:00
  • springcloud gateway 性能2025-07-14 23:01:00
  • devc++的使用方法2025-07-14 23:01:00
  • ce认证机构28342025-07-14 23:01:00
  • linux开启kdump服务命令2025-07-14 23:01:00
  • css字体font-family2025-07-14 23:01:00
  • utf8mb4与utf8的区别2025-07-14 23:01:00
  • pyqt5快速入门2025-07-14 23:01:00