本质原因是因为局部内部类和匿名内部类对象的生命周期和一般的局部变量生命周期不一致,为了保持数据一致性。

这么讲你可能听不懂,举个例子。就拿 Android 里的点击监听来说明。

// MainActivity.javapublic class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

Button btnWelcome;

final String greeting;

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

greeting = "Hello World";

btnWelcome = findViewById(R.id.btnMainWelcome);

btnWelcome.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this, greeting, Toast.LENGTH_LONG)

.show();

} // onClick() }); // btnWelcome.setOnClickListener() } // onCreate()} // MainActivity Class

以上一段代码,初学安卓编程的也应该都能看懂,就是 MainActivity 的界面中有一个 ID 叫 btnMainWelcome 的按钮,点击它会弹出一个小贴士,显示 Hello World。

但是我们的局部字符串变量 greeting 在 onCreate() 方法执行完毕后就已经不存在了,我们点击这个按钮的时候是怎么显示出 Hello World 贴士的呢?其实,内部类用到的局部变量,在内部类实例生成的时候,就已经往内部类里拷贝了一份引用。即使外部的局部变量已不再,内部类仍然持有相应对象的引用。所以,内部类里的 greeting 和局部变量的 greeting,其实是两份引用。我们换一种写法,自然而然就明白了。

// MainActivity2.javapublic class MainActivity2 extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

Button btnWelcome;

String greeting;

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

greeting = "Hello World";

btnWelcome = findViewById(R.id.btnMainWelcome);

btnWelcome.setOnClickListener(new MyListener(greeting)); // 传递局部变量的引用 } // onCreate()

// 自定义监听器,实现 View.OnClickListener 接口 private class MyListener implements View.OnClickListener {

private String greeting;

// 向构造器中传递局部变量的引用 private MyListener(String greeting) {

this.greeting = greeting;

} // MyListener() (Class Constructor)

@Override

public void onClick(View v) {

// 使用内部类自己的引用对象 this.greeting, // 生命周期不受原局部变量 greeting 的影响 Toast.makeText(MainActivity.this, this.greeting, Toast.LENGTH_LONG)

.show();

} // onClick() } // MyListener Inner Class} // MainActivity2 Class

前者的本质就是后者。

现在来看看为什么要用 final。这主要是为了保持数据一致性。这里假设我们对上面的代码做一些修改:

// MainActivity.javapublic class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

Button btnWelcome;

String greeting; // 去掉 final

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

greeting = "Hello World";

btnWelcome = findViewById(R.id.btnMainWelcome);

btnWelcome.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this, greeting, Toast.LENGTH_LONG)

.show();

} // onClick() }); // btnWelcome.setOnClickListener()

greeting = "Hello New World"; // 对 greeting 二次赋值 } // onCreate()} // MainActivity Class

// MainActivity2.javapublic class MainActivity2 extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

Button btnWelcome;

String greeting;

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

greeting = "Hello World";

btnWelcome = findViewById(R.id.btnMainWelcome);

btnWelcome.setOnClickListener(new MyListener(greeting));

greeting = "Hello New World";

} // onCreate()

private class MyListener implements View.OnClickListener {

private String greeting;

private MyListener(String greeting) {

this.greeting = greeting;

} // MyListener() (Class Constructor)

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this, this.greeting, Toast.LENGTH_LONG)

.show();

} // onClick() } // MyListener Inner Class} // MainActivity2 Class

以上代码中,MainActivity.java 的代码无法编译通过,MainActivity2.java 的代码可以编译通过。我们姑且先分析 MainActivity2.java 的代码,毕竟两者本质是一样的。

我们在 onCreate() 中,对 greeting 进行二次赋值后,只是这个局部变量的引用改变了,监听器中的引用并没有被改变。所以,我们即使对 greeting 进行二次赋值,实际运行时,点击按钮仍然会显示 Hello World 而不是 Hello New World。然后我们回过头来看 MainActivity.java,本质也是一样的,编译器其实是可以选择不报错的,只是运行时会给出非预期的结果。但编译器选择了一了百了,强制要求要在内部类中使用的局部变量和方法参数声明为 final,从语义上避免二义性问题。

最后就是,Java 8 以上版本已经改为内部类使用的局部变量只需要 effectively final(事实上的 final)就可以了,而不强制显式 final。只要相应的局部变量只赋值一次就行了,在编译阶段,编译器会自动为相应内容加上 final 关键字。再配合上 lambda 表达式实现匿名内部类,代码看起来优雅多了。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐