Java内部类为什么能访问外部类信息
访问外部类的静态属性
内部类获取外部类静态属性,通过字节码来看,是外部类提供了一个静态方法。例如:
public class Demo {
private static int version;
private class Inner {
void run() {
System.out.println(version);
}
}
}
这个知识点不用关内部类是静态的还是非静态的。他为什么能拿到外面类的静态属性呢javap -v Demo看下字节码:
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
static int access$000();
descriptor: ()I
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field version:I
3: ireturn
LineNumberTable:
line 1: 0
}
SourceFile: "Demo.java"
这里字节码没有放常量池信息。 能看到代码编译后又一个静态方法access$xxx,内部类(嵌套类)使用。这就是为什么能获取到外面的静态变量。
访问外部类的成员变量
上面例子的代码简单修改后是这样。
public class Demo {
private int version;
private class Inner {
void run() {
System.out.println(version);
}
}
}
同样看下字节码
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
static int access$000(Demo);
descriptor: (LDemo;)I
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #1 // Field version:I
4: ireturn
LineNumberTable:
line 1: 0
}
还是可以看到在外部类的字节码中,也有一个静态方法access$xxx不过这个方法需要一个入参,类型是Demo。再看下内部类的字节码
{
final Demo this$0;
descriptor: LDemo;
flags: ACC_FINAL, ACC_SYNTHETIC
void run();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #1 // Field this$0:LDemo;
7: invokestatic #4 // Method Demo.access$000:(LDemo;)I
10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
13: return
LineNumberTable:
line 6: 0
line 7: 13
}
看到内部类又一个成员变量this$0 这个变量的类型是Demo ,在看run 方法里的逻辑,有一步就是调用外部类的静态方法,然后把这个this$0 传入了。
实际上在构建内部类对象的时候,外部类会把自己的实例对象传入内部类的构造方法中。这也就是闭包
Flink ClosureCleaner
首先看创建kafka数据源的一个代码:
public class SourceCreator {
public FlinkKafkaConsumer011<JsonNode> createKafkaConsumer() {
Properties config = new Properties();
// config.xxx
return new FlinkKafkaConsumer011<JsonNode>("demo-topic", new AbstractDeserializationSchema<JsonNode>() {
@Override
public JsonNode deserialize(byte[] bytes) throws IOException {
return Demo.objectMapper.readTree(bytes);
}
}, config);
}
}
然后我的程序应该是从这个SourceCreator 实例中获取数据源。
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
FlinkKafkaConsumer011<JsonNode> kafkaConsumer = new SourceCreator().createKafkaConsumer();
env.addSource(kafkaConsumer);
// xxx
// env.execute();
}
懂得人已经知道问题在哪了。在flink(或者spark)这种分布式计算的框架中,我们写的代码其实基本上都是一个个零件(函数式编程),这些东西最后都是需要由 JobManager序列化后,分发到各个计算节点上的。所以能否序列化很关键。在env.addSource(kafkaConsumer);这里,flink 中调用了一个clean 方法。
/**
* Returns a "closure-cleaned" version of the given function. Cleans only if closure cleaning
* is not disabled in the {@link org.apache.flink.api.common.ExecutionConfig}
*/
@Internal
public <F> F clean(F f) {
if (getConfig().isClosureCleanerEnabled()) {
ClosureCleaner.clean(f, getConfig().getClosureCleanerLevel(), true);
}
ClosureCleaner.ensureSerializable(f);
return f;
}
这个clean其实主要是进行闭包清理,一般我们开发可能是直接在flink 留的api里传入一个匿名内部类的实例,这样这个实例里面可能又一个this$0成员指向外部类的实例。闭包清理主要就是将这个成员的值改为null,关键源码如下:
// 通过反射拿到成员变量,然后改为null。进行闭包清理。清除无用的东西
this0.setAccessible(true);
this0.set(func, null);
闭包清理后,还要进行检查能否序列化。