时间: 2018/10/19
Content
final的普通语义
final遇见内部类
闭包
内存泄漏
1. final的普通语义
关于Java中final关键字的常规语义就是表明其修饰的对象是不可变的, 被修饰的对象通常有. 值变量,引用变量,类,函数。此处需要注意的是,如果final修饰的是引用变量,那么引用变量的值(地址)不可变,但是引用变量值对应的对象实可以变的。
分别介绍一下修饰不同对象的情况:
1). 值变量
2). 引用变量
3). 类
4). 方法
// 1)
final int a = 1;
a = 1; // 这个语句会报错,不允许修改
// 2)
final Map map = new HashMap();
map.put("key", "value"); // map值(地址)对应对象(在堆)可以被修改,一般认为对象被生成以后,其地址就是确定了
map = new HashMap(); // 会报错, map值(地址)不允许修改
// 3)
final class Cls{
// .....
}
class SubCls extends Cls{ //编译报错,Cls不能为继承
// ....
}
// 4)
class Parent{
public final void method(){
// ...
}
}
class Child extends Parent{
public void mehtod(){ // 编译报错,不允许覆盖
// ..
}
}
2. final遇见内部类
Java中要求如果方法中定义的中类如果引用方法中的局部变量,那要要求局部变量必须要用final修饰(JDK8中已经不需要,但是本质也是和final类似——只读),实例代码如下:
interface Inner{
void method();
}
class Outer{
public Inner createInner(){
final int a = 12;
final Map map = new HashMap();
Inner inner = new Inner(){
public void method(){
int b = a + 1;
System.out.println(" in Inner, b=" + b);
map.put("innerKey", "innerValue");
}
};
System.out.println("in Outer, createInner finish!");
return inner;
}
public static void main(String []args){
Inner inner = new Outer().createInner();
inner.method();
}
}
输出如下:
in Outer, createInner finish!
in Inner, b=13
Note: 上述代码仅仅是展示使用,其中createInner()方法中的map变量是存在内存泄漏的,因为外界无法访问他,但是却会被一致持有。关于内存泄漏的问题,通过查看上述代码便后的class文件的内容即可发现。
上述文件编译后,生成了三个文件
Inner.class
Outer.class
Outer$1.class
打开Outer$1.class可以看到如下内容:
class Outer$1 implements Inner {
Outer$1(Outer this$0, int var2, Map var3) {
this.this$0 = this$0;
this.val$a = var2;
this.val$map = var3;
}
public void method() {
int b = this.val$a + 1;
System.out.println(" in Inner, b=" + b);
this.val$map.put("innerKey", "innerValue");
}
}
可以看到编译后的内容,Inner匿名类拥有另一个带有三个参数的构造方法,
Outer this$0: 也就是拥有了Outer(外部类)当前对象的一个引用,所以我们Inner的子类中,可以通过Outer.this访问外部Outer类的当前实例。
var2: 此处应该为Outer createInner()方法中的局部变量a
var3: 此处应该为Outer createInner()方法中的局部变量Map
通过上述编译后的代码,我们大概可以明白为什么匿名类可以访问其外部数据的原因,接下来我们可以讨论一下为什么要对createInner()中的局部变量a, map用final进行修饰。
网络上有很多人说是生命周期的问题,但是我觉得不是这个原因,也觉得不存在生命周期的问题(欢迎讨论)。
为了简化表述,以下将Inner匿名类里面的a表述为Inner().a, 将createInner()方法中的a表示为 createInner.a.
通过编译后的代码可以看出来,Inner().a和createInner.a不是同一个对象(在内存中不是同一个), 同样的两个map(值,存在于堆)在内存中也是不同的,但是两个map的都指向了堆上的同一个HashMap对象。理论上我们是可以重新设置Inner().a和Inner().map的值的,但是java编译器并不允许这样做, 具体原因我认为可能是如下原因:
在匿名类内部访问外面的变量看起来是一个很正常的需求,而且直观看起来应该是同一个东西。但是在方法调用结束以后局部变量会被销毁(栈里面的内容,也就是createInner.a, createInner.map。如果是同一个东西的话,那么意味着jvm在方法调用结束以后还不能销毁这些局部变量,需要将这些局部变量的生命周期保持到和Inner一样长,这样让jvm的实现起可能会更为复杂(提升这些变量的生命周期)。
所以,为了实现在Inner中可以访问createInner()中的a, map,同时他们看起来和createInner()中的一样(一致),并且避免JVM对对象生命周期的管理过于复杂,采用了一个中折中的办法:
将被用到的变量作为Inner的构造函数参数传入并在Inner内部设置对应的实例(private)。
将createInner().a, createInner().map设置为final,并且匿名类类部不可以修改对应实例属性的值,保证一致性。
通过上述的 1中,可以很自然实现在Inner中很自然的访问createInner中局部变量的值;由于Inner中使用的变量实际上和外部函数中的局部变量是不一样的,通过上述2可以保证他们一致(都不允许修改了,肯定一致), 否则开发者在内部修改值,但是却不会影响到外面的局部变量,这会让人困惑(天然看起来应该是一个东西啊,但是却不能一起变化)。
3. 闭包
此处引出了Java对闭包的支持,其实Java目前是支持了闭包的,匿名类就是一个典型的例子。将自由变量(createInner.a,createInner.map)封装到Inner中,但是Java的闭包确实有条件的闭包,因为Java只实现了capture-by-value, 只是把局部变量的值copy到了匿名类中, 没有实现capture-by-reference。如果是capture-by-reference的实现方式,可能需要将局部变量提升到对象中(也就是讲局部变量的生命周期延长,变为和Inner类一样长, 那么在createInner()执行完毕以后,就不会销毁 a, map了)。
此处有一个系列的参考文章关于``Javascript```中闭包的内容,图文并茂。深入理解javascript原型和闭包]
4. 内存泄漏
Java中并没有真正的实现延长生命周期, 但是间接实现了createInner.map的生命周期,因为Inner.map是一个对实际的HashMap()(位于堆中)对象的引用, 所以在createInner()方法中创建,但是却不会在该方法执行以后被GC回收, 该对象的生命周期和其创建Inner实例一样长。在本例中的代码的内存泄漏就由此而生。