Java基礎(番外) 爲什麼匿名內部類只能訪問final類型局部變量

問題再現

首先我們將該問題演示一下。

Java8之前,在匿名內部類中訪問外部方法的局部變量,該局部變量必須顯式聲明爲final類型。

// JDK 1.7
public class TestInnerClass {
	
	@SuppressWarnings("unused")
	private void function() {
		final int localvar = 1;
		new Runnable() {
			@Override
			public void run() {
				System.out.println(localvar);
			}
		};
	}
	
}

到了Java8,在匿名內部類中訪問的外部方法的局部變量不需要顯式聲明爲final類型。這其實是Java8的一個語法糖——如果在匿名內部類中訪問外部方法的局部變量,那麼該局部變量會被隱式聲明爲final類型。一旦你嘗試在外部方法中改變局部變量的值,就會出現報錯信息:Local variable localvar defined in an enclosing scope must be final or effectively final。

//JDK 1.8
public class TestInnerClass {
	
	@SuppressWarnings("unused")
	private void function() {
		int localvar = 1;
		new Runnable() {
			@Override
			public void run() {
//				localvar++;     // Error
				System.out.println(localvar);
			}
		};
//		localvar++;    // Error
	}
	
}

問題闡述完畢,下面進入揭祕環節。

字節碼文件與局部變量

首先我們要明確一點:匿名內部類也是一個類。作爲一個類,它就會有對應的字節碼文件(.class文件)。字節碼文件是怎麼產生的呢?我們知道Java文件通過編譯之後,就會生成對應的字節碼文件。也就是說,字節碼文件是編譯階段的產物。

下圖是TestInnerClass類的Java文件和編譯之後生成的字節碼文件。可以看到TestInnerClass.java文件編譯之後生成了兩個字節碼文件:TestInnerClass.class和TestInnerClass$1.class,TestInnerClass$1.class就是匿名內部類的字節碼文件。

那麼局部變量是怎麼產生的呢?局部變量是在程序在運行期動態生成的。如此一來問題就出現了,編譯期生成的匿名內部類的字節碼文件如何訪問運行期動態生成的局部變量呢

下面我們就一探究竟匿名內部類的字節碼文件是如何訪問到局部變量的。

查看反編譯後的TestInnerClass$1.class文件:

class TestInnerClass$1 implements Runnable {
    TestInnerClass$1(TestInnerClass var1) {
        this.this$0 = var1;
    }

    public void run() {
        System.out.println(1);
    }
}

可以看到編譯器直接將該局部變量的值對應的字節碼嵌入到了匿名內部類的字節碼文件中。那麼這就是匿名內部類字節碼文件訪問局部變量的方式嗎?

這樣下結論未免太早,例子中匿名內部類訪問的局部變量是一個編譯期就可以確定下來的值,如果該局部變量的值在編譯期無法確定下來呢?譬如這樣:

//JDK 1.7
public class TestInnerClass {
	
	@SuppressWarnings("unused")
	private void function() {
		final double localvar = Math.random();  // 運行期纔會生成的數據
		new Runnable() {
			@Override
			public void run() {
				System.out.println(localvar);
			}
		};
	}
	
}

我們再次查看反編譯後的TestInnerClass$1.class文件:

class TestInnerClass$1 implements Runnable {
	TestInnerClass$1(TestInnerClass var1, double var2) {
		this.this$0 = var1;
		this.val$localvar = var2;
	}

	public void run() {
		System.out.println(this.val$localvar);
	}
}

我們可以看到編譯器將匿名內部類訪問的局部變量作爲該類構造方法的參數(var2)。

這就是說,如果匿名內部類訪問的外部方法的局部變量的值在編譯期無法確定下來,那麼編譯器會將該局部變量作爲匿名內部類構造方法的參數傳入。等到匿名內部類創建實例對象時,再將該局部變量的值作爲匿名內部類構造方法的參數傳入。

爲什麼是final

在瞭解上面的知識之後我們就能發現,匿名內部類訪問的局部變量並不是真正的局部變量,而是局部變量的值拷貝。也就是說在匿名內部類中修改局部變量的值,無法影響到外部方法中的局部變量。

因此爲了保證匿名內部類和外部方法中訪問的局部變量的一致性,局部變量必須使用final修飾。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章