Java内部类
依据Java语言规范8.1.3. Inner Classes and Enclosing Instances
的定义内部类是不带static
的嵌套类。
An inner class is a nested class that is not explicitly or implicitly declared static.
内部类和带有static的嵌套类有什么区别?
内部类可以调用外部类的方法,访问外部类的成员属性。类似于我的上一篇博客中提到的this
实现一样。内部类会隐式的生成一个字段指向外部对象。
之前在知乎看到一个问题,提问的人就是没有注意到这一点,导致临时对象被这个字段所引用影响了GC。
例如:
class Outer{
class Inner{
}
}
通过javap -p -c
反编译:
$ javap -p -c com.test.Outer\$Inner
Compiled from "Test.java"
class com.test.Outer$Inner {
final com.test.Outer this$0;
com.test.Outer$Inner(com.test.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/test/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
}
内部类隐式生成了一个字段this$0
,构造方法有一个外部类类型参数,这个参数通过putfield
给this$0
赋值。这个的this$0
字段,就是指向外部对象的引用,这一点在规范中有提到8.8.9. Default Constructor。
The default constructor has no formal parameters, except in a non-private
inner member class, where the default constructor implicitly declares one formal parameter representing the immediately enclosing instance of the class (§8.8.1, §15.9.2, §15.9.3).
局部类和匿名内部类
局部类 Local Class和匿名内部类Anonymous Class都是在代码块中,拎出来的原因是在代码块中可能存在参数传递,也就是在内部类中访问方法参数和局部变量。这个实现也没什么新意,和上面类似也是在内部类中隐式生成字段。
例如:
class A{
void fun() {
int i = 1;
new Runnable() {
@Override
public void run() {
int j = i;
}
};
}
}
反编译:
$ javap -p -c com.test.A\$1
Compiled from "Test.java"
class com.test.A$1 implements java.lang.Runnable {
final int val$i;
final com.test.A this$0;
com.test.A$1(com.test.A, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/test/A;
5: aload_0
6: iload_2
7: putfield #2 // Field val$i:I
10: aload_0
11: invokespecial #3 // Method java/lang/Object."<init>":()V
14: return
public void run();
Code:
0: aload_0
1: getfield #2 // Field val$i:I
4: istore_1
5: return
}
内部类多了一个val$i
字段,构造方法中也多了一个int
类型参数。
再反编译外部类:
$ javap -p -c -l com.test.A
Compiled from "Test.java"
class com.test.A {
com.test.A();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/test/A;
void fun();
Code:
0: iconst_1
1: istore_1
2: new #2 // class com/test/A$1
5: dup
6: aload_0
7: iload_1
8: invokespecial #3 // Method com/test/A$1."<init>":(Lcom/test/A;I)V
11: pop
12: return
LineNumberTable:
line 5: 0
line 6: 2
line 12: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/test/A;
2 11 1 i I
}
重点看到fun方法6-8行,先把this
和i
推到栈上,然后调用构造函数。也就是说外部对象,外部局部变量都是在匿名类构造的时候作为参数,给匿名类实例初始化。而匿名类内部访问的是实例的成员属性。
Java语言规范8.1.3. Inner Classes and Enclosing Instances中规定了内部类中访问过的参数、局部变量必须为final
或effectively final
。
Any local variable, formal parameter, or exception parameter used but not declared in an inner class must either be declared final or be effectively final (§4.12.4), or a compile-time error occurs where the use is attempted.
这里的effectively final
简单讲就是事实final
(我瞎起的名字),也就是说如果变量在初始化后未做修改不加final也可以。(在java8中放宽的限制)。
这里我揣测一下为什么要这样###
外部和内部变量名虽然一致,但是指向并不相同。局部变量对应的是局部变量表,而内部类中对应的是实例的成员属性,这个成员在构造的时候赋值。如果不做这个限制,外部变量的修改无法反映到内部。可能会造成开发人员的误解。
那么如何绕过这个限制呢
把要使用的变量定义成一个只有单个元素的数组,在内外都直接对元素进行操作。因为final只限制了引用不能被修改,而不能限制引用指向的对象。
在class字节码中类,字段和方法都有一个访问标志属性ACCESS_FLAG
来控制访问权限和提供一些其他的信息例如final
,synthetic
。但是在本地变量表和方法的参数中是没有这个东西的,所以这里的final只是一个编译期的语法检查。
Lambda表达式和匿名内部类的区别
以下讨论皆在模拟方法传递的前提之下。
Java语言中方法传递只能只能靠对象模拟,在这种情况下匿名内部类和Lambda表达式做的是一样的事情。Lambda的类型是依据上下文推导出来的,更加简洁。
插个无关的东西,Java语言规范的Preface里面有这么一句话:In JSR 335, the greatest complexity lurked in the interaction of implicitly typed lambda expressions with overload resolution.看来这个lambda表达式类型推导应该真的挺复杂的把。
刨去外观上的不同,Lambda表达式和匿名内部类依然是有一些区别的。
- Lambda里的
this
和所属代码块中的this
是同一个意思,在非static
方法中this
指向的是外围对象,在static
方法中this会导致编译错误。而匿名内部类this
始终指向内部类实例本身。Java语言规范 15.8.3. this
The value denoted by this
in a lambda body is the same as the value denoted by this
in the surrounding context.
例:
```
package com.test;
public class Main {
void fun() {
Runnable runnable=()->System.out.print(this);
runnable.run();
}
public static void main(String... hahaha) {
new Main().fun();
}
}
```
运行结果:
```
$ java com.test.Main
com.test.Main@297bc709
```
- 匿名内部类内部是一个独立作用域,如果变量名参数名冲突内部变量会覆盖(Shadowing)外部变量,而在Lambda表达式中,冲突的变量名会出现编译错误。这种限制和
for
里面定义变量比较类似。Java语言规范6.4. Shadowing and Obscuring对这一点做了一些说明,重点在最后一句。There are two design alternatives for handling name clashes created by lambda parameters and other variables declared in lambda expressions. One is to mimic class declarations: like local classes, lambda expressions introduce a new "level" for names, and all variable names outside the expression can be redeclared. Another is a "local" strategy: like catch clauses, for loops, and blocks, lambda expressions operate at the same "level" as the enclosing context, and local variables outside the expression cannot be shadowed. The above rules use the local strategy; there is no special dispensation that allows a variable declared in a lambda expression to shadow a variable declared in an enclosing method.
最后,虽然Lambda和匿名内部类有一些不同,不过我认为这并不是Lambda的问题。反而this是内部类模拟方法带来的副作用,在JavaScript
里匿名函数里的this
也是指向外围环境window
,这一点和Lambda表达式是一致的。
网友评论