这篇博文是我很久很久以前写的,当时对底层的认知还很浅,现在看来这里面的说法也有不少问题,所以各位看官当作辅助资料看看就行了,不要较真= ̄ω ̄=

  很多人对于局部内部类访问局部变量的限制有些疑惑,这里我就简单的解答以下。

什么是局部内部类

  和局部变量类似,局部内部类指的就是在方法体中声明的类,包括匿名内部类和非匿名内部类,例如以下代码中的“A”、“new Object(){}”、“lambda表达式” 都是局部内部类:(以下简称内部类)

1
2
3
4
5
6
7
8
public class Test {
public void run() {
class A { }
new Object() { };
List<?> list = new ArrayList<>();
list.forEach(System.out::println);
}
}

内部类的限制

  很多人都知道的的一个内部类的使用限制就是不能修改局部变量,比如下面这段代码就是错误的:

1
2
3
4
5
6
7
8
9
10
//代码块 A1
public void run() {
int k = 0;
new Object() {
{
System.out.println(k);
k = 1;
}
};
}

  编译器会毫不客气的抛出==Variable ‘k’ is accessed from within inner class, needs to be final or effectively final==。

  显而易见,错误的原因是k没有被final修饰,那到底时什么原因导致内部类不能修改局部变量呢?同时需要注意的是导致编译错误的是“k = 1”而非“println(k)”。

其他人的见解

初始化问题

  这个问题我曾经请教过其它人,得到的回复是:

  我认为是因为在创建内部类的时候局部变量没有得到初始化,即值是不确定的,所以导致编译错误。

  但是我很快否定了这个说法,因为这样的说法无法解释“println(k)”这样子的代码可以通过编译。

高深解法

  网上有很多关于这个问题的“高深解法”,比如这里,但是这些说辞不适用于新手或者是水平还没有达到相对应的地点的人,这里我就引用一部分内容,后文中我将围绕这一小部分内容在代码层面进行讲解:

  内部类并不是直接使用传递进来的参数,而是将传递进来的参数通过自己的构造器备份到自己内部,表面看是同一个变量,实际调用的是自己的属性而不是外部类方法的参数,如果在内部类中,修改了这些参数,并不会对外部变量产生影响,仅仅改变局部内部类中备份的参数。但是在外部调用时发现值并没有被修改,这种问题就会很尴尬,造成数据不同步。所以使用final避免数据不同步的问题。
————————————————
版权声明:本文为CSDN博主「T9的第三个三角」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dazhaoDai/article/details/83097017

个人讲解

  上面这段话对于一部分人可能仍然很难理解,不要担心,下面这些内容会对这一段话进行展开。

  上文中说内部类中引用外部局部变量是一个拷贝,这句话可以用下面的代码理解:

1
2
3
4
5
6
7
8
9
10
11
//代码块 A2 | 实际与A1相同
public void run() {
int k = 0;
new Object() {
//final int k = k;
{
System.out.println(k);
k = 1;
}
}
}

  这样子就可以比较直观的理解“拷贝”的含义,注释掉的代码可以理解成一个隐式声明。相当于编译器为你自动复制了可访问的局部变量的值。

  那这个声明为什么不能更改呢?这就涉及到了上文中提到的同步问题,如果你在内部类中修改了这个值,你修改的是复制后的值,在内部类的外部根部无法观测到这个改变,如果编译器允许你的更改,那就容易造成一个不太容易发现的BUG:我明明修改了这个值,为什么它没有变化?为了彻底避免这个问题,所以就干脆让这个声明是不可以修改的了。

深度理解“复制”

  这里说的复制是什么意思呢?在了解复制前我们要首先了解我们是怎么访问对象的:

1
2
3
4
5
Object o = new Object();
Object o2 = new Object();
System.out.println("o == o2 is " + o == o2); //false
o = o2;
System.out.println("o == o2 is " + o == o2); //true

  这段代码的输出结果代表了什么?在修改"o"的值后"o"与"o2"的地址是一样的,也就是说"o = o2"并没有触发深度复制,而是知识简单的让"o"也指向了"o2"所指向的值。

  显而易见,"o"和"o2"并没有直接存储一个对象,我们可以简单的认为存储的对象的地址(实际上没有这么简单,具体方法略微有些复杂,读者可以自行查阅资料),“o = o2"这个语句就是将"o2"指向的对象的地址复制给"o”,这时候修改两者中的任意一个,另一个也会收到影响。

  同样,编译器为我们复制变量的时候也没有进行深度复制,而是进行简单的复制,这个特性我们下文中会再说到。

修改外部值的方法

1.把这个值变成全局变量:

  这样子就可以随意更改,但是并不建议这么做因为这样子写不但性能不高,还容易让代码变得晦涩难懂。

2.将要修改的量声明为数组:

  细心的小伙伴写代码的时候可能会发现下面的情况:

1
2
new ArrayList<int[]>();     //right
new ArrayList<int>(); //error

  第一行代码可以通过编译而第二行不可以,具体原因这里不再解释,读者只要明白int和int[]的区别就可以了。在Java中int是基本类型,但是int[]并不是,可以当作对象处理,所以int[]也可以当作泛型的参数。

  知道这个有什么用呢?很简单,被final修饰的数组虽然我们不能再修改它的地址,但是我们可以修改数组中存储的数据,比如说我们可以写出来下面的代码:

1
2
3
4
5
6
public void run() {
final int[] k = { 0 };
new Object() {
{ k[0] = 1; }
};
}

3.使用类存储数据:

  第二种方法看起来很正确,但是很容易混淆代码的意图,我们同样根据复制的原理,将值存在一个类中,然后我们通过这个类访问值即可进行修改:

1
2
3
4
5
6
public void run() {
final AtomicInteger k = new AtomicInteger(0);
new Object() {
{ k.set(1); }
};
}