匿名内部类访问的局部变量为什么必须为final

18 篇文章 0 订阅
129 人赞同了该回答
喜欢看生肉的同学就不用看我的回答了,直接看R大的三篇回答,尤其是第一篇后面的回复部分。
我只是试着用大白话做个简单的整理,希望能更容易理解一点。

1. 闭包(Closure)
什么是闭包,大白话不怎么严谨的说就是:
  1. 一个依赖于外部环境自由变量的函数
  2. 这个函数能够访问外部环境里的自由变量

看下面这个Javascript闭包的例子:
function Add(y) {  
    return function(x) {  
        return x + y  
    }  
} 

对内部函数function(x)来讲,y就是自由变量,而且function(x)的返回值,依赖于这个外部自由变量y。而往上推一层,外围Add(y)函数正好就是那个包含自由变量y的环境。而且Javascript的语法允许内部函数function(x)访问外部函数Add(y)的局部变量。满足这三个条件,所以这个时候,外部函数Add(y)对内部函数function(x)构成了闭包。

闭包的结构,如果用λ演算表达式来写,就是多参数的Currying技术。
> λx.λy.x+y

但在Java中我们看不到这样的结构。因为Java主流语法不允许这样的直接的函数套嵌和跨域访问变量。

2. 类和对象
但Java中真的不存在闭包吗?正好相反,Java到处都是闭包,所以反而我们感觉不出来在使用闭包。因为Java的“对象”其实就是一个闭包。其实无论是闭包也好,对象也好,都是一种数据封装的手段。看下面这个类,
class Add{
    private int x=2;
    public int add(){
    int y=3;
        return x+y;
    }
}

看上去x在函数add()的作用域外面,但是通过Add类实例化的过程,变量”x“和数值”2“之间已经绑定了,而且和函数add()也已经打包在一起。add()函数其实是透过this关键字来访问对象的成员字段的。

如果对闭包有疑问,可以看这个更详细的回答:
闭包(计算机科学)是什么? - 胖胖的回答

3. Java内部类是闭包:包含指向外部类的指针
那Java里有没有除了实例对象之外的闭包结构?Java中的内部类就是一个典型的闭包结构。例子如下,
public class Outer {
    private class Inner{
        private y=100;
        public int innerAdd(){
            return x+y;
        }
    }
    private int x=100;
}
下图画的就是上面代码的结构。内部类(Inner Class)通过包含一个指向外部类的引用,做到自由访问外部环境类的所有字段,变相把环境中的自由变量封装到函数里,形成一个闭包。
<img src="https://pic2.zhimg.com/50/a5fc0f99e8cb53266ccc783f2c26949d_hd.jpg" data-rawwidth="984" data-rawheight="550" class="origin_image zh-lightbox-thumb" width="984" data-original="https://pic2.zhimg.com/a5fc0f99e8cb53266ccc783f2c26949d_r.jpg">

4. 别扭的匿名内部类
但Java匿名内部类就做得比较尴尬。下面这个例子中,getAnnoInner负责返回一个匿名内部类的引用。
interface AnnoInner(){addXYZ();}
public class Outer {
    public AnnoInner getAnnoInner(final int x){
        final int y=100;
        return new AnnoInner(){
            int z=100;
            public int addXYZ(){return x+y+z;}
            //public void changeY(){y+=1;} //这个函数无法修改外部环境中的自由变量y。
        };
    }

    private int num=100;
}

匿名内部类因为是匿名,所以不能显式地声明构造函数,也不能往构造函数里传参数。不但返回的只是个叫AnnoInner的接口,而且还没有和它外围环境getAnnoInner()方法的局部变量x和y构成任何类的结构。但它的addXYZ()函数却直接使用了x和y这两个自由变量来计算结果。这就说明,外部方法getAnnoInner()事实上已经对内部类AnnoInner构成了一个闭包。

但这里别扭的地方是这两个x和y都必须用final修饰,不可以修改。如果用一个changeY()函数试图修改外部getAnnoInner()函数的成员变量y,编译器通不过,
error: cannot assign a value to final variable y
这是为什么呢?因为这里Java编译器支持了闭包,但支持地不完整。说支持了闭包,是因为编译器编译的时候其实悄悄对函数做了手脚,偷偷把外部环境方法的x和y局部变量,拷贝了一份到匿名内部类里。如下面的代码所示。
interface AnnoInner(){addXYZ();}
public class Outer {
    public AnnoInner getAnnoInner(final int x){
        final int y=100;
        return new AnnoInner(){
            int copyX=x;    //编译器相当于拷贝了外部自由变量x的一个副本到匿名内部类里。
            int copyY=y;    //编译器相当于拷贝了外部自由变量y的一个副本到匿名内部类里。
            int z=100;
            public int addXYZ(){return x+y+z;}
            //public void changeY(){y+=1;} //这个函数无法修改外部环境中的自由变量y。
        };
    }

    private int num=100;
}
所以用R大回答里的原话说就是:
Java编译器实现的只是capture-by-value,并没有实现capture-by-reference。

而只有后者才能保持匿名内部类和外部环境局部变量保持同步。

但Java又不肯明说,只能粗暴地一刀切,就说既然内外不能同步,那就不许大家改外围的局部变量。

5. 其他和匿名内部类相似的结构
《Think in Java》书里,只点出了匿名内部类来自外部闭包环境的自由变量必须是final的。但实际上,其他几种不太常用的内部类形式,也都有这个特性。

比如在外部类成员方法内部的内部类。
public class Outer {
    public foo(final int x){
        final int y=100;
        public class MethodInner{
            int z=100;
            public int addXYZ(){return x+y+z;}
        }
    }
}

比如在一个代码块block里的内部类。
public class Outer {
    {
        final int x=100;
        final int y=100;
        class BlockInner{
            int z=100;
            public int addXYZ(){return x+y+z;}
        }
        BlockInner bi=new BlockInner();
        num=bi.addXYZ();
    }
    private int num;
}
129 ​收起评论
​分享
​收藏 ​感谢
收起

17 条评论

​切换为时间排序
玖麼炎
玖麼炎 1 年前
本身传递的就是引用吧。
changeInfo 仅仅是修改了 info这个引用指向的实例。info和innerInfo指向的都不是一个实例了,结果当然不同了。
这跟同步有什么关系吗?我也是菜鸟一个,就提出自己的疑惑哈
胖胖
胖胖 (作者) 1 年前
对 innerInfo是像你说的这样。 但这个例子里主要的是为了说明paraInfo的问题。 说必须是final,是指paraInfo的位置必须是final。 按理说匿名内部类的参数直接用外部类字段的引用传进去,外部类的字段引用变了之后,内部类的参数也该跟着联动。但实际并没有。而是传了一个拷贝进去。 这里innerInfo只是个龙套,可以不用看他。
玖麼炎
玖麼炎 回复胖胖 (作者) 1 年前
是不是可以这样理解:
1.形参为什么要拷贝一份:
不拷贝的话,匿名内部类中使用的引用与外部的引用是同一份引用,但是这个引用的生命周期与函数相同,若匿名内部类的生命周期超过这个函数的话(比如新开了一个线程),再去访问这个引用会有空指针等问题。
2.形参为什么是final
从我们的角度来看,是看不到这个拷贝的过程。我们看到匿名内部类直接使用了这个引用,会造成一种错觉:当在匿名内部类中修改这个引用的时候(指向别的实例),我们同时修改的是这个外面的引用,因为看起来他们是同一个嘛。然而并不是这样的。为了防止这种错觉,java索性将其设为final,即这个引用是无法修改的。

不知道说得对不对,求教。
胖胖
胖胖 (作者) 回复玖麼炎 1 年前
我在考虑怎么修改这个回答,尽量说得再清楚一点。改完之后会私信你。
胖胖
胖胖 (作者) 回复玖麼炎 1 年前
闭包还是是纯正的catch-by-reference更好吧。Java在这点上没什么好找借口的。毕竟其他大多数语言都是catch-by-reference,内外都是可以协变的。技术上也没有什么做不到的。Java以后估计还是会屈服的,哈哈。

其实不光是外部环境方法的参数必须用final,外部环境方法的所有局部变量都必须是final的。而且也不仅仅是匿名内部类是这样,成员方法里面的内部类,以及大括号{}block里的内部类,都是这样。外围环境的局部变量都必须是final。

可以看我上面更新的内容,希望能更容易理解一些。
Keith
Keith 1 年前
我有个问题不是很理解,为什么仅仅针对方法中的参数限制final,而访问外部类的属性就可以随意
胖胖
胖胖 (作者) 回复Keith 1 年前
因为每个内部类的实例都隐藏了一个指向外部类实例的引用。java只是没有显式地写出来而已。内部类访问外部类成员都是透过这个引用。之所以能有这个引用,是因为两者都是实例,都有自己的内存空间。而匿名内部类的外围环境函数只是一个函数,执行完之后,也就是匿名内部类诞生(初始化)完成的那一刻,它的生存周期就结束了。函数内部的局部变量(包括函数的参数)也就跟着被销毁了。所以产生出来的内部类根本无法像保留外部类的引用那样保留外围环境函数的引用。所以只能退而求其次,只保留一份局部变量的拷贝值。
胖胖
胖胖 (作者) 回复Keith 1 年前
另外再补充说明一下,一个函数的成员变量在函数执行完之后必须销毁,是因为执行函数的内存开销是在栈上。每执行一个函数,都会在栈上压一个新的栈帧,函数的局部变量,包括参数都存在这个栈帧的局部变量表里。函数执行完之后,根据栈的LIFO顺序,当前栈帧就被从栈上弹出销毁。内存上就没有这个函数的痕迹了。
顾兮
顾兮 1 年前
本来没太看懂,幸好来翻了翻评论。你说的【匿名内部类初始化的那一刻,外围环境函数就结束了,所以函数的局部变量也销毁了。那么内部类就没法保留函数的引用,所以只能保留一份拷贝值。】那可不可以理解为【无法保留引用干脆直接写成final?】
胖胖
胖胖 (作者) 回复顾兮 1 年前
嗯嗯 就相当于挂了个牌子说“此处正在施工” 大家先不要想着改外围的变量。 等我以后弄好了,变成capture-by-value,成了真闭包了,大家就可以自由地改了。
顾兮
顾兮 回复胖胖 (作者) 1 年前
了解了!非常感谢
胖胖
胖胖 (作者) 回复顾兮 1 年前
XD 很用功的少女呢
生活半篇记
生活半篇记 7 个月前

java8没有这些限制了

Jimu杨
Jimu杨 7 个月前

作者答者好:

在匿名内部类中不可以对xy进行修改的理由我可以简单的概括为真正的xy已经随着函数执行结束“烟消云散”了,内部类中的xy只是一个复制的“替代品”,没有理由也没有必要对xy进行修改,对吗?

但是在外部函数中,为什么也不能对xy进行修改呢?换而言之,这种capture by value是在何时发生的呢? 个人一点小猜测:首先肯定要在外部环境建立之后,即调用该外部函数并传入x之后(因为此时才有x的值才可以复制),那么如果是在new

匿名对象时发生的capture,为什么不能在此之前对自由变量做出修改呢?

还是说只是一种设计上的考虑。

Jimu杨
Jimu杨 7 个月前

看了楼下博客发现自己问了愚蠢的问题,不可以在外围改的原因应该是:这个修改可能发生在new 匿名内部类之后……

cxthisisit
cxthisisit 回复生活半篇记 6 个月前

【Error: java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量】 没错,jdk1.8及1.9版本的确可以不写final了,但是还是不可变,相当于final。

李秉卓
李秉卓 2 个月前

之前翻了那本《head first JavaScript》,了解到有闭包这种东西。不够一直没有把它和java语言的匿名内部类传参需要final关联起来。今天看到这个答案实在是如梦初醒呀。我现在还有个疑问,Jvm通过栈帧和它包含的操作数栈和局部变量表来维持方法内的变量,这导致了java的匿名内部类无法实现真正的闭包,那么JS是通过什么样的机制实现的函数闭包呢?

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值