Java内部类,相信大家都用过,但是多数同学可能对它了解的并不深入,只是靠记忆来完成日常工作,却不能融会贯通,遇到奇葩问题更是难以有思路去解决。这篇文章带大家一起死磕Java内部类的方方面面。 友情提示:这篇文章的讨论基于JDK版本 1.8.0_191

开篇问题

我一直觉得技术是工具,是一定要落地的,要切实解决某些问题的,所以我们通过先抛出问题,然后解决这些问题,在这个过程中来加深理解,最容易有收获。 so,先抛出几个问题。(如果这些问题你早已思考过,答案也了然于胸,那恭喜你,这篇文章可以关掉了)。

  • 为什么需要内部类?
  • 为什么内部类(包括匿名内部类、局部内部类),会持有外部类的引用?
  • 为什么匿名内部类使用到外部类方法中的局部变量时需要是final类型的?
  • 如何创建内部类实例,如何继承内部类?
  • Lambda表达式是如何实现的?

为什么需要内部类?

要回答这个问题,先要弄明白什么是内部类?我们知道Java有三种类型的内部类

普通的内部类

public class Demo {

    // 普通内部类
public class DemoRunnable implements Runnable {
@Override
public void run() {
}
}
}
复制代码

匿名内部类

public class Demo {

    // 匿名内部类
private Runnable runnable = new Runnable() {
@Override
public void run() { }
};
}
复制代码

方法内局部内部类

public class Demo {

    // 局部内部类
public void work() {
class InnerRunnable implements Runnable {
@Override
public void run() { }
}
InnerRunnable runnable = new InnerRunnable();
} }
复制代码

这三种形式的内部类,大家肯定都用过,但是技术在设计之初肯定也是要用来解决某个问题或者某个痛点,那可以想想内部类相对比外部定义类有什么优势呢? 我们通过一个小例子来做说明

public class Worker {
private List<Job> mJobList = new ArrayList<>(); public void addJob(Runnable task) {
mJobList.add(new Job(task));
} private class Job implements Runnable {
Runnable task;
public Job(Runnable task) {
this.task = task;
} @Override
public void run() {
runnable.run();
System.out.println("left job size : " + mJobList.size());
}
}
}
复制代码

定义了一个Worker类,暴露了一个addJob方法,一个参数task,类型是Runnable,然后定义 了一个内部类Job类对task进行了一层封装,这里Job是私有的,所以外界是感知不到Job的存在的,所以有了内部类第一个优势。

  • 内部类能够更好的封装,内聚,屏蔽细节

我们在Job的run方法中,打印了外部Worker的mJobList列表中剩余Job数量,代码这样写没问题,但是细想,内部类是如何拿到外部类的成员变量的呢?这里先卖个关子,但是已经可以先得出内部类的第二个优势了。

  • 内部类天然有访问外部类成员变量的能力

内部类主要就是上面的二个优势。当然还有一些其他的小优点,比如可以用来实现多重继承,可以将逻辑内聚在一个类方便维护等,这些见仁见智,先不去说它们。

我们接着看第二个问题!!!

为什么内部类(包括匿名内部类、局部内部类),会持有外部类的引用?

问这个问题,显得我是个杠精,您先别着急,其实我想问的是,内部类Java是怎么实现的。 我们还是举例说明,先以普通的内部类为例

普通内部类的实现

public class Demo {
// 普通内部类
public class DemoRunnable implements Runnable {
@Override
public void run() {
}
}
}
复制代码

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

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

Demo$DemoRunnable.class
复制代码

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

package inner;

public class Demo$DemoRunnable implements Runnable {
public Demo$DemoRunnable(Demo var1) {
this.this$0 = var1;
} public void run() {
}
}
复制代码

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

验证我们的想法

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

   // Demo.java
public void run() {
DemoRunnable demoRunnable = new DemoRunnable();
demoRunnable.run();
}
复制代码

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

  public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class inner/Demo$DemoRunnable
3: dup
4: aload_0
5: invokespecial #3 // Method inner/Demo$DemoRunnable."<init>":(Linner/Demo;)V
8: astore_1
9: aload_1
10: invokevirtual #4 // Method inner/Demo$DemoRunnable.run:()V
13: return 复制代码
  • 先通过new指令,新建了一个Demo$DemoRunnable对象
  • aload_0指令将外部类Demo对象自身加载到栈帧中
  • 调用Demo$DemoRunnable类的init方法,注意这里将Demo对象作为了参数传递进来了

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

匿名内部类的实现

public class Demo {
// 匿名内部类
private Runnable runnable = new Runnable() {
@Override
public void run() { }
};
}
复制代码

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

package inner;

class Demo$1 implements Runnable {
Demo$1(Demo var1) {
this.this$0 = var1;
} public void run() {
}
}
复制代码

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

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

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

   // Demo.java
public void run() {
int age = 10;
Runnable runnable = new Runnable() {
@Override
public void run() {
int myAge = age + 1;
System.out.println(myAge);
}
};
}
复制代码

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

    public void run() {
int age = 10;
Runnable runnable = new Runnable() {
@Override
public void run() {
int myAge = age + 1;
System.out.println(myAge);
age = 20; // error
}
};
}
复制代码

编译器报错了,提示信息是”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的实现代码

package inner;

class Demo$1 implements Runnable {
Demo$1(Demo var1, int var2) {
this.this$0 = var1;
this.val$age = var2;
} public void run() {
int var1 = this.val$age + 1;
System.out.println(var1);
}
}
复制代码

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

final局部变量

age修改为final

    public void run() {
final int age = 10;
Runnable runnable = new Runnable() {
@Override
public void run() {
int myAge = age + 1;
System.out.println(myAge);
}
};
}
复制代码

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

class Demo$1 implements Runnable {
Demo$1(Demo var1) {
this.this$0 = var1;
} public void run() {
byte var1 = 11;
System.out.println(var1);
}
}
复制代码

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

    public void run() {
final int age = (int) System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
int myAge = age + 1;
System.out.println(myAge);
}
};
}
复制代码

再看Demo$1 字节码实现

class Demo$1 implements Runnable {
Demo$1(Demo var1, int var2) {
this.this$0 = var1;
this.val$age = var2;
} public void run() {
int var1 = this.val$age + 1;
System.out.println(var1);
}
}
复制代码

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

外部类成员变量

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

public class Demo {
int age = 10;
public void run() {
Runnable runnable = new Runnable() {
@Override
public void run() {
int myAge = age + 1;
System.out.println(myAge);
age = 20;
}
};
}
}
复制代码

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

class Demo$1 implements Runnable {
Demo$1(Demo var1) {
this.this$0 = var1;
} public void run() {
int var1 = this.this$0.age + 1;
System.out.println(var1);
this.this$0.age = 20;
}
}
复制代码

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

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

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

如何创建内部类实例,如何继承内部类?

由于内部类对象需要持有外部类对象的引用,所以必须得先有外部类对象

Demo.DemoRunnable demoRunnable = new Demo().new DemoRunnable();
复制代码

那如何继承一个内部类呢,先给出示例

    public class Demo2 extends Demo.DemoRunnable {
public Demo2(Demo demo) {
demo.super();
} @Override
public void run() {
super.run();
}
}
复制代码

必须在构造器中传入一个Demo对象,并且还需要调用demo.super(); 看个例子

public class DemoKata {
public static void main(String[] args) {
Demo2 demo2 = new DemoKata().new Demo2(new Demo());
} public class Demo2 extends Demo.DemoRunnable {
public Demo2(Demo demo) {
demo.super();
} @Override
public void run() {
super.run();
}
}
}
复制代码

由于Demo2也是一个内部类,所以需要先new一个DemoKata对象。 这一个问题描述的场景可能用的并不多,一般也不这么去用,这里提一下,大家知道有这么回事就行。

Lambda表达式是如何实现的?

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

来看一个最简单的例子

public class Animal {
public void run(Runnable runnable) {
}
}
复制代码

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

run(new Runnable() {
@Override
public void run() {
}
});
复制代码

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

    public void run(Runnable runnable) {
} public void test() {
run(() -> {});
}
复制代码

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

  public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
6: invokevirtual #3 // Method run:(Ljava/lang/Runnable;)V
9: return SourceFile: "Demo.java"
InnerClasses:
public static final #34= #33 of #37; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #18 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#19 ()V
#20 invokestatic com/company/inner/Demo.lambda$test$0:()V
#19 ()V 复制代码

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

#2 = InvokeDynamic      #0:#21         // #0:run:()Ljava/lang/Runnable;
复制代码

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

BootstrapMethods:
0: #18 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#19 ()V
#20 invokestatic com/company/inner/Demo.lambda$test$0:()V
#19 ()V
复制代码

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

    public void run(Runnable runnable) {
System.out.println(runnable.getClass().getCanonicalName());
} public void test() {
run(() -> {});
}
复制代码

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

com.company.inner.Demo$$Lambda$1/764977973
复制代码

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

    public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
复制代码

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

public InnerClassLambdaMetafactory(...)
throws LambdaConversionException {
//....
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//....
}
复制代码

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

Lambda实现总结

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

死磕Java内部类的更多相关文章

  1. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  2. 死磕 java并发包之LongAdder源码分析

    问题 (1)java8中为什么要新增LongAdder? (2)LongAdder的实现方式? (3)LongAdder与AtomicLong的对比? 简介 LongAdder是java8中新增的原子 ...

  3. 死磕 java同步系列之AQS起篇

    问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...

  4. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  5. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  6. 死磕 java同步系列之zookeeper分布式锁

    问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...

  7. 死磕 java线程系列之线程池深入解析——普通任务执行流程

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 前面我们一起学习了Java中 ...

  8. 死磕 java线程系列之线程池深入解析——定时任务执行流程

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:本文基于ScheduledThreadPoolExecutor定时线程池类. 简介 前面我们一起学习了普通 ...

  9. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

随机推荐

  1. 微信小程序练习笔记(更新中。。。)

    微信小程序练习笔记 微信小程序的练习笔记,用来整理思路的,文档持续更新中... 案例一:实现行的删除和增加操作  test.js // 当我们在特定方法中创建对象或者定义变量给与初始值的时候,它是局部 ...

  2. [转] vue父组件触发子组件事件

    1. 父组件中获取子组件方法 $children 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template>     < ...

  3. c# 类实例序列化反序列化json文件 (原发布 csdn 2017-10-01 20:02:12)

    前言 前段时间使用了net.json保存对象数据.添加完成后,测试发现300多实例数据保存加载json文件,速度比原方式(BinaryFormatter)慢.但是功能加上后也懒再删掉代码了,索性就采用 ...

  4. Python多任务—进程

    一.进程以及状态 1.进程 正在运行的应用程序就是一个进程.进程是资源分配的基本单元. Python多进程可以在多核CPU上运行,多进程充分利用了多核的资源. 2. 进程的状态 工作中,任务数往往大于 ...

  5. centos7设置时间

    1.查看时间时区 date 2.修改时区 timedatectl set-timezone Asia/Shanghai # 设置系统时区为上海 3.安装ntp 联网校准时间 yum install n ...

  6. javascript实现上传图片并展示

    我们也都知道上传图片的样子是这样的(选择前)是这样的(选择后). 先在HTML设置图片上传 <form action="" method=""> & ...

  7. vue单元素/组件的过渡

    (1)过渡的类名 v-enter:定义进入过渡的开始状态.在元素被插入之前生效,在元素被插入之后的下一帧移除. v-enter-active:定义进入过渡生效时的状态.在整个进入过渡的阶段中应用,在元 ...

  8. js 字符串换行 显示 使用 \ 转义

     js 字符串 有没有 像C# @ 那种 换行也可以显示的方法\ 

  9. 【MySQL】备份和恢复

    语法 mysqldump -uslave -p -h127.0.0.1 --single-transaction --set-gtid-purged=OFF database1 table1 tabl ...

  10. AjAX2 异步通信 异常处理

    <!DOCTYPE html> <html lang="en"> <head> <title>xmlhttprequest ajax ...