一、前言

  在研究公司某个项目的源码,看到前人使用了挺多内部类,内部类平时我用的比较多的是匿名内部类,平时用的多的是匿名内部类,其他形式的用的比较少,然后我就有个疑惑:到底内部类是基于什么样的考虑,才让java设计者搞这么一套实现?还有,平时在什么情境下会考虑使用内部类呢?这个我将在另外一篇博文进行介绍,详情参见:xxxxxx,本篇博文是在查阅内部类相关资料时,看到很多博文在解释为什么匿名内部类在引用局部变量时需要将局部变量指定为final(不可变)说的不清楚,所以这边我将我的理解写出来,以供参考~

二、正文

  本篇博文基于的Java环境是:jdk1.6,在jdk1.8中,匿名内部类引用局部变量时,已经废弃使用final修饰局部变量,但这并不意味着jdk1.8在实现方面有什么很大的变动,这个博文最后也会给予说明。

  在讨论本篇博文之前,需要与看官达成一些共识:

  1)Java中数据类型分为两大类:基本数据类型和引用数据类型。相应的,变量也分为基本类型和引用类型。基本类型的变量保存原始值,即它代表的值就是数值本身,而引用类型的变量保存引用值,"引用值"指向内存空间的地址(也就是地址值),代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置

  2)final修饰的变量,赋予过初值之后,不允许变更。对基本类型而言,其保存的原始值不能变更,对引用类型而言,其引用的地址值不允许变更,也就是说不能引用其他对象

    final int a = 1;
a = 2;//编译报错 final Integer b = new Integer(1);
b = new Integer(2);//编译报错

  先来个匿名内部类使用局部变量的例子:

public class OutClass {
private String msg = "OutClass's msg"; public void check(final String m){
(new Thread(){
public void run(){
System.out.println(m);
System.out.println(msg);
}
}).start();
}
public static void main(String[]args){
OutClass oc = new OutClass();
oc.check("Need to pass to inner Class");
} }
//输出
Need to pass to inner Class
OutClass's msg

  编译器编译的时候,匿名内部类会生成一个:外部类$x.class,其中x是正整数,这个实例中匿名内部类为:OutClass$1.class,我们可以使用jdk自带的反编译工具,将生成的这个class反编译出来看下,在dos窗口执行:javap -c OutClass$1.class,

  我相信,只要熟悉内部类的童鞋,都知道,如果要实例化内部类,那么必须先实例化外部类,这就是原因所在,内部类需要持有外部类的一个引用,这也是内部类能无条件访问外部类所有成员方法和成员变量的根本原因。

  通过上面反编译出来的内容可以看出,如果匿名内部类使用了局部变量,那么编译器会将使用的值拷贝一份,作为构造函数的一个参数传递进来(构造函数是编译器自动添加)。因为局部变量在方法或者代码块执行完毕,就会被销毁,所以编译器在编译的时候,就拷贝了一份局部变量存储的字面值或者地址值,这样局部变量被销毁时,匿名内部类依然拥有之前传递进来的值。现在我们从语义上来理解下Java设计者的考虑:假如传递到匿名内部类的局部变量,不加final修饰,那么意味着局部变量可以改变,这就意味着匿名内部类里面值的变更和外部的变量的变更不能同步,虽然内部类持有的是局部变量值的拷贝,但是语义应该保持一致,语义保持一致的前提是值要能同步,因为java编译器的设计无法提供两边同步变更的机制,所以直接锁死,不允许内外变更~

 package com.finalparams.main;

 public class Main {
public static void main(String[]args){
Main m = new Main();
m.show();
} public void show(){
int a = 1;
(new innerClassInterface(){
public void doChange(){
a = 3;//编译报错:Cannot refer to the non-final local variable a defined in an enclosing scope
}
}).doChange();
System.out.println(a);
}
} interface innerClassInterface{
public void doChange();
}

  以上代码第13行编译报错,假设编译器允许编译过去,按照编译器现有的实现,内部类里面的变更是没有办法反应到外面的, 也就是里面变更了,但是外部还是没变,这样是不能接受的,所以直接锁死,不让变更~

  特别说明:在jdk1.8之后,匿名内部类使用局部变量的时候,局部变量已经不需要使用final修饰了,可以编译通过,那是不是说编译器的实现机制变了呢?非也,只是编译器在编译的时候不允许对这个变量进行变更,也就是说,如果你内外只是使用这个变量,而不进行重新赋值,那么就编译通过,如果内外有重新赋值,那么还是会报编译错误:

......................................................................................
public void show(){
int a = 1;
(new innerClassInterface(){
public void doChange(){
System.out.println(a);//jdk1.8之前编译报错,1.8之后,不报错,因为没有改变变量的值
}
}).doChange();
System.out.println(a);
}
......................................................................................
public void show(){
int a = 1;
(new innerClassInterface(){
public void doChange(){
System.out.println(a);//jdk1.8之前编译报错,1.8之后,不报错,因为没有改变变量的值
a = 4;//jdk1.8编译报错
}
}).doChange();
System.out.println(a);
}

三、参考链接

https://www.zhihu.com/question/21395848

  https://zhidao.baidu.com/question/266984801594415645.html

  

四、联系本人

  为方便没有博客园账号的读者交流,特意建立一个企鹅群(纯公益,非利益相关),读者如果有对博文不明之处,欢迎加群交流:261746360,小杜比亚-博客园

细说匿名内部类引用方法局部变量时为什么需要声明为final的更多相关文章

  1. 内部类访问局部变量时,为什么需要加final关键字

    是变量的作用域的问题,因为匿名内部类是出现在一个方法的内部的,如果它要访问这个方法的参数或者方法中定义的变量,则这些参数和变量必须被修饰为final.因为虽然匿名内部类在方法的内部,但实际编译的时候, ...

  2. 为什么java内部类访问局部变量必须声明为final?

    https://blog.csdn.net/z55887/article/details/49229491 先抛出让我疑惑了很久的一个问题 编程时,在线程中使用局部变量时候经常编译器会提示:局部变量必 ...

  3. [多问几个为什么]为什么匿名内部类中引用的局部变量和参数需要final而成员字段不用?(转)

    昨天有一个比较爱思考的同事和我提起一个问题:为什么匿名内部类使用的局部变量和参数需要final修饰,而外部类的成员变量则不用?对这个问题我一直作为默认的语法了,木有仔细想过为什么(在分析完后有点印象在 ...

  4. 局部内部类访问它所在方法的局部变量时,要求该局部变量必须声明为final的原因

    这是java的一条规则.那么为什么会有这条规则呢?要想弄懂这个问题,就需要弄懂局部内部类对象和局部变量的生命周期的谁更长的问题. 首先,看一段代码,以没有将变量声明为final的代码作为例子,代码如下 ...

  5. uni-app 父组件引用子组件时怎么调用子组件的方法

    1.写一个简单的子组件main/index.vue: <template> <view> </view> </template> <script& ...

  6. 匿名内部类不能访问外部类方法中的局部变量,除非变量被声明为final类型

    1. 这里所说的"匿名内部类"主要是指在其外部类的成员方法内定义,同时完成实例化的类,若其访问该成员方法中的局部变量,局部变量必须要被final修饰.2. 原因是编译程序实现上的困 ...

  7. 【细说Java】方法重载的简单介绍

    1. 什么是重载 方法名称相同,但它们的参数类型或个数不同,这样,方法在被调用时编译器就可以根据参数的类型与个数的不同加以区分,这就是方法的重载. 既然可以通过参数类型或参数个数来作为重载条件,那返回 ...

  8. 匿名内部类访问方法成员变量需要加final的原因及证明(转)

    https://blog.csdn.net/wjw521wjw521/article/details/77333820 在java编程中,没用的类定义太多对系统来说也是一个负担,这时候我们可以通过定义 ...

  9. C#中引用类型的变量做为参数在方法调用时加不加 ref 关键字的不同之处

    ​ 一直以为对于引用类型做为参数在方法调用时加不加 ref 关键字是没有区别的.但是今天一调试踪了一下变量内存情况才发现大有不同. 直接上代码,结论是:以下代码是使用了 ref 关键字的版本.它输出1 ...

随机推荐

  1. Docker开篇之基础概念篇

    What--什么是容器? 容器技术,是一种操作系统层的虚拟化(Operating system-level virtualization),它将应用软件系统打包成一个软件容器(Container),内 ...

  2. vuejs中的生命周期

    vue中生命周期分为初始化,跟新状态,销毁三个阶段 1.初始化阶段:beforeCreated,created,beforeMount,mounted 2.跟新状态:beforeUpdate,upda ...

  3. BZOJ3436_小K的农场_KEY

    题目传送门 差分约束基础,对于每种关系建不同的边,求是否有负环. code: /************************************************************ ...

  4. CC3200-LAUNCHXL驱动不能正常识别的问题

    1. 本次使用利尔达的CC3200底板,完全兼容官方CC3200-LAUNCHXL,如果上电之后驱动识别为2路串口,是有问题的.原因是FT2232外接的EEPROM没有烧写固件. 2. 安装FT_Pr ...

  5. 「题目代码」P1066~P1070(Java)

    P1066 谭浩强C语言(第三版)习题8.6 import java.util.*; import java.io.*; import java.math.*; import java.lang.Ch ...

  6. 「日常训练」Alternative Thinking(Codeforces Round #334 Div.2 C)

    题意与分析 (CodeForces - 603A) 这题真的做的我头疼的不得了,各种构造样例去分析性质... 题意是这样的:给出01字符串.可以在这个字符串中选择一个起点和一个终点使得这个连续区间内所 ...

  7. 测试开发的成长之路 - 自动化一站式平台(UI、接口)

    前言 在自动化测试过程中,随着对接的自动化需求不断增加,测试用例数量显著上升,参与自动化测试的人也越来越多,多人协作就会碰到很多问题,包括脚本.数据.版本.项目整合.持续集成等,而且也增加了后期维护的 ...

  8. Java学习 · 初识 面向对象基础一

    面向对象基础 1.1面向过程与面向对象的区别 面向过程和面向对象二者都是思考问题的方式,再简单的事物时,可以线性思考时使用面向过程,但当事物较为复杂时,只能使用面向对象设计.但二者并不是对立的,在解决 ...

  9. 【QT】常用类

    官方文档 doc QWidget QWidget类是所有用户界面对象的基类. 窗口部件是用户界面的一个基本单元:它从窗口系统接收鼠标.键盘和其它事件,并且在屏幕上绘制自己. 每一个窗口部件都是矩形的, ...

  10. 特殊符号 & 以太坊

    &表示取二进制的末尾 &1表示如果末尾是奇数和偶数两种情况 0 偶数 1奇数 举例子: int a=1;int p=&a; 其中,p是指针,&a就是将a在内存中的实际地 ...