Java匿名类遇上final

时间: 2018/10/19

Content

  1. final的普通语义

  2. final遇见内部类

  3. 闭包

  4. 内存泄漏

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().acreateInner.a不是同一个对象(在内存中不是同一个), 同样的两个map(值,存在于堆)在内存中也是不同的,但是两个map的都指向了堆上的同一个HashMap对象。理论上我们是可以重新设置Inner().aInner().map的值的,但是java编译器并不允许这样做, 具体原因我认为可能是如下原因:

在匿名类内部访问外面的变量看起来是一个很正常的需求,而且直观看起来应该是同一个东西。但是在方法调用结束以后局部变量会被销毁(栈里面的内容,也就是createInner.a, createInner.map。如果是同一个东西的话,那么意味着jvm在方法调用结束以后还不能销毁这些局部变量,需要将这些局部变量的生命周期保持到和Inner一样长,这样让jvm的实现起可能会更为复杂(提升这些变量的生命周期)。

所以,为了实现在Inner中可以访问createInner()中的a, map,同时他们看起来和createInner()中的一样(一致),并且避免JVM对对象生命周期的管理过于复杂,采用了一个中折中的办法:

  1. 将被用到的变量作为Inner的构造函数参数传入并在Inner内部设置对应的实例(private)。
  2. 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了)。

关于闭包的定义:Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长

此处有一个系列的参考文章关于``Javascript```中闭包的内容,图文并茂。深入理解javascript原型和闭包]

4. 内存泄漏

Java中并没有真正的实现延长生命周期, 但是间接实现了createInner.map的生命周期,因为Inner.map是一个对实际的HashMap()(位于堆中)对象的引用, 所以在createInner()方法中创建,但是却不会在该方法执行以后被GC回收, 该对象的生命周期和其创建Inner实例一样长。在本例中的代码的内存泄漏就由此而生。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容