java 语法糖

Java 语法糖(五): 成员内部类

2020-07-04  本文已影响0人  jyjz2008

参考文献

  1. https://mp.weixin.qq.com/s/xl1ibkfUc00NY8HCa_fcPg (深入理解Java内部类)
  2. https://www.benf.org/other/cfr/inner-class-fake-friends.html
  3. https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.1.3

正文

内部类的种类

https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.1.3 提到了内部类(inner class)

An inner class is a nested class that is not explicitly or implicitly declared static.
An inner class may be a non-static member class (§8.5), a local class (§14.3), or an anonymous class (§15.9.5). A member class of an interface is implicitly static (§9.5) so is never considered to be an inner class.

根据这个描述,内部类(inner class)可以分为如下三种

  1. 成员(member)内部类
  2. 局部(local)内部类
  3. 匿名(anonymous)内部类

我们先来了解 成员内部类

成员内部类

我们用名为 Master.java 的文件来进行探索(其完整内容如下)

/**
 * Master 类是外围类, Slave 类是 Master 的成员内部类
 */
public class Master {
    private int cnt = 0;

    /**
     * 生成一个 Slave 的实例, 并对其 someValue 字段赋值
     */
    public Slave generateAndAssign(int someValue) {
        Slave slave = new Master.Slave();
        slave.someValue = someValue;
        return slave;
    }

    private class Slave {
        private int id = Master.this.cnt++;
        private int someValue;
    }
}

在上述程序中,Master 类是外围类, Slave 类是 Master 类的成员内部类。那么会有几个问题

  1. 成员内部类是如何实现的?
  2. 成员内部类是如何访问外围类的?
  3. 外围类是如何访问成员内部类的?

问题一: 成员内部类是如何实现的?

执行如下的命令来编译 Master.java

javac Master.java

就会看到生成了如下 class 文件

  1. Master.class
  2. Master$1.class
  3. Master$Slave.class

Master.classMaster 类对应,
Master$Slave.classSlave 类对应。
其实在虚拟机看来,成员内部类就是一个普通的类(Master$1.class 也对应一个内部类,在 generateAndAssign(...) 方法中创建 Slave 类的实例时,会用到 Master$1,这里就不展开了)。

问题二: 成员内部类是如何访问外围类的?

先说结论,具体如下。

成员内部类会持有外围类对象的引用。如果成员内部类需要访问外围类的私有字段或私有方法,则编译器会在外围类中合成一个相应的包访问级别(package access)的静态方法,供成员内部类访问。

我们再看一下 Master.java 中的代码,其中有如下几行

    private class Slave {
        private int id = Master.this.cnt++;
        private int someValue;
    }

这里有两个问题

  1. 代码里的 Master.this 是什么?
  2. Slave 类为何可以直接访问Master类的 private 字段(也就是名为 cnt 的字段)?

如果我们用 javap 来解析刚才生成的class文件的内容的话,也能了解内部类的运作机制,但是 javap 的解析结果太底层了,我们可以借助 cfr 工具(在 https://www.benf.org/other/cfr/
页面可以下载对应的 jar 包) 来查看 class 文件的内容。

这里我补充一句,cfr 虽然方便,但是它支持的选项比较杂,我感觉它的行为比较复杂,所以也不必完全以 cfr 的解析结果为准(可以适当参考 javap 的解析结果)。所谓尽信书则不如无书。

我下载了 cfr-0.150.jar。先用它查看 Master$Slave.class 的内容(完整的命令如下)

# 假如想用 javap 的话,可以参考下一行
# javap -v -p 'Master$Slave'
java -jar cfr-0.150.jar 'Master$Slave' --removeinnerclasssynthetics false

执行上述命令后得到的完整结果如下

/*
 * Decompiled with CFR 0.150.
 */
private class Master.Slave {
    private int id;
    private int someValue;
    final /* synthetic */ Master this$0;

    private Master.Slave(Master master) {
        this.this$0 = master;
        this.id = Master.access$208(this.this$0);
    }

    /* synthetic */ Master.Slave(Master master, Master.1 var2_2) {
        this(master);
    }

    static /* synthetic */ int access$102(Master.Slave slave, int n) {
        slave.someValue = n;
        return slave.someValue;
    }
}

观察上述内容后,会发现

  1. Slave 类有两个构造函数
  2. Slave 类中有一个名为 access$102 的方法,这个方法是编译器自动合成的
  3. Slave 类中有一个字段类型为 Master,名称为 this$0,这个字段也是编译器自动合成的

我们用如下命令查看 Master.class 的内容

# 假如想用 javap 的话,可以参考下一行
# javap -v -p 'Master'
java -jar cfr-0.150.jar 'Master' --removeinnerclasssynthetics false

完整的结果如下

/*
 * Decompiled with CFR 0.150.
 */
public class Master {
    private int cnt = 0;

    public Slave generateAndAssign(int n) {
        Slave slave = new Slave(this, null);
        Slave.access$102(slave, n);
        return slave;
    }

    static /* synthetic */ int access$208(Master master) {
        return master.cnt++;
    }

    private class Slave {
        private int id;
        private int someValue;
        final /* synthetic */ Master this$0;

        private Slave(Master master) {
            this.this$0 = master;
            this.id = Master.access$208(this.this$0);
        }

        /* synthetic */ Slave(Master master, 1 var2_2) {
            this(master);
        }

        static /* synthetic */ int access$102(Slave slave, int n) {
            slave.someValue = n;
            return slave.someValue;
        }
    }
}

刚才提到两个小问题

  1. 代码里的 Master.this 是什么?
  2. Slave 类为何可以直接访问Master类的 private 字段(也就是名为 cnt 的字段)?

简要的解答如下

  1. 代码里的 Master.this 是成员内部类持有的外围类对象的引用,该引用是在成员内部类的构造函数中被赋值的
  2. Slave 类并不能直接访问 Master 类的 cnt 字段(因为该字段是 private 的),编译会在 Master 类中合成包访问级别的静态方法,Slave 类通过调用该静态方法来间接操作 Master 对象的 cnt 字段

这样回答可能还是有点抽象,我在 cfr 解析结果的基础上,加了一些注释来进行解释,具体如下

public class Master {
    private int cnt = 0;

    public Slave generateAndAssign(int n) {
        // 这里调用了 Slave 类的第二个构造函数(第二个构造函数是包可见级别的)
        // Slave 类的第二个构造函数会调用第一个构造函数
        Slave slave = new Slave(this, null);
        Slave.access$102(slave, n);
        return slave;
    }

    // 编译器合成了这个方法
    // 这个方法是包访问级别的,所以对 Slave 类是可见的
    // Slave 类的对象通过调用这个方法,来操作 Master 类中的 cnt 字段
    static /* synthetic */ int access$208(Master master) {
        return master.cnt++;
    }

    private class Slave {
        private int id;
        private int someValue;

        // 编译器合成了这个字段
        // 成员内部类会持有外围类对象的引用,这个引用就保存在 this$0 字段中
        final /* synthetic */ Master this$0;

        // Slave 类有两个构造函数,这是第一个
        // 成员内部类会持有外围类对象的引用,这个引用就保存在 this$0 字段中
        private Slave(Master master) {
            this.this$0 = master;
            this.id = Master.access$208(this.this$0);
        }

        // Slave 类有两个构造函数,这是第二个
        // 这个构造函数有两个入参
        // 第二个参数的类型是 Master$1,第二个参数相当于一个占位符
        // 这个构造函数是编译器合成的
        // 这个构造函数是包访问级别的,所以对 Master 类是可见的
        /* synthetic */ Slave(Master master, 1 var2_2) {
            this(master);
        }

        // 编译器合成了这个方法
        // 这个方法是包访问级别的,所以对 Master 类是可见的
        // Master 类的对象通过调用这个方法,来操作 Slave 类中的 someValue 字段
        static /* synthetic */ int access$102(Slave slave, int n) {
            slave.someValue = n;
            return slave.someValue;
        }
    }
}

第三个问题: 外围类访问成员内部类?

在解答了第二个问题的基础上,我就直接给出第三个问题的解答了。

  1. 如果外围类需要访问成员内部类的私有构造函数,则编译器会在成员内部类中合成相应的(包访问级别的)构造函数,供外围类访问
  2. 如果外围类需要访问成员内部类的私有成员,则编译器会在成员内部类中合成相应的(包访问级别的)静态方法,供外围类访问
上一篇下一篇

猜你喜欢

热点阅读