爲什麼匿名內部類和局部內部類只能訪問final變量

是變量的作用域的問題,因爲匿名內部類是出現在一個方法的內部的,如果它要訪問這個方法的參數或者方法中定義的變量,則這些參數和變量必須被修飾爲 final。因爲雖然匿名內部類在方法的內部,但實際編譯的時候,內部類編譯成Outer.Inner,這說明內部類所處的位置和外部類中的方法處在同一 個等級上,外部類中的方法中的變量或參數只是方法的局部變量,這些變量或參數的作用域只在這個方法內部有效。因爲編譯的時候內部類和方法在同一級別上,所 以方法中的變量或參數只有爲final,內部類纔可以引用。

這幾天,在網上找了一些關於final的知識,當然並不全面,有的一時也沒有很好的理解,先收集起來,理理思路,把不懂的畫出來,以便更好地學習……

java中的final關鍵字通常的指的是“這是無法改變的”。它可能被做爲三種的修飾詞.------數據(基本類型,對象或者數組),方法(類方法、實例方法),類。

<1>final應用於類

如果類被聲明爲final,則表示類不能被繼承,也就是說不能有子類。因爲不能有子類,所以final類不能被聲明爲abstract抽象類。所以final關鍵字和abstract關鍵字不能同時使用。

一個final類中的所有方法都隱式地指定爲final

<2>final應用於類方法

使用final使用的原因有兩個:
一. 把方法鎖定,使得在子類(導出類)中不能修改它的含義。
二.效率,一個final方法被調用時會轉爲內嵌調用,不會使用常規的壓棧方式,使得運行效率較高,尤其是在方法體較簡單的 情 況下,但也並不絕對。(與C++中的inline關鍵字類似)


類的方法分爲“類方法”(類中static方法)和“實例方法”(類中非static方法)。不管是類方法還是實例方法,只要被聲明爲final,就表示不能被子類“覆蓋”或“隱藏”,也就是不能在子類中聲明爲“一模一樣”的方法。

特別的:類中的所有private方法都隱式地指定爲是final,所以在繼承關係中不存在覆蓋問題。

<3>final應用於類屬性

類中的屬性,也分爲“類屬性”(靜態屬性)和“實例屬性”(非靜態屬性)。不管是靜態屬性還是非靜態屬性,只要被聲明爲final,則屬性的值只能被指定一次(也就是說初始化後就不能再給屬性賦值),而且必須進行“顯示初始化”。

對於類的靜態屬性,可以完成顯示初始化的地方有兩個:一個是在聲明靜態屬性的時候就進行初始化;另一個,也可以在“靜態初始化塊”中進行初始化。

對於類的非靜態屬性,可以完成顯示初始化的地方有三個:一個是在聲明屬性的時候就進行初始化;另一個,也可以在“初始化塊”中進行初始化;還可以在構造方法中進行初始化。

需要注意的是,在“靜態初始化塊”中不能訪問“實例屬性”,那當然就不能完成“實例屬性”的“顯示初始化”任務了;在“初始化塊”中能訪問“類屬性”,但是不能完成“類屬性”的“顯示初始化”任務,類屬性的顯示初始化應該在“靜態初始化塊”中。

如果final屬性在聲明的時候沒有進行初始化,我們管這種final叫做“空白final”。但必須確保空白final在使用前被初始化,一般在構造方法完成。

注:在類中,如果類的屬性(包括類屬性和實例屬性)沒有被final所修飾,那麼即使我們沒有進行“顯示初始化”,那麼編譯器也會給他們進行默認初始化。而對於final屬性我們必須進行“顯示初始化”。

<4>final應用於方法參數或變量

final也可以修飾方法參數或變量。

final變量可以在聲明的時候不進行“顯示初始化”,(而且只要不對它進行使用,就不會報編譯錯誤,但是既然不使用,這個變量也就沒什麼用,顯然是多餘的),只要在使用之前進行初始化就行了,這個特性和普通的變量是一樣的。

方法參數被聲明爲final,表示它是隻讀的。

注意:方法中的變量可以聲明爲final,但是不能聲明爲static,不管這個方法是靜態的還是非靜態的


在談final之前我們現看一個最簡單的程序:

Class Main{

public String a=Test.aa;


public String b=Test.bb;


public static void main(String args[]){      ....

}

}

Class Test{

public final static String aa="HelloA";


public final static String bb=new String("HelloB");


}

大家肯定要問了,Main中的a 和 b 到底有什麼區別?


我們先什麼都不說,看一下反編譯的結果。


zheng@zheng-laptop:~/workspace/Test/src$ javap -c Main 
Compiled from "Main.java"
public class Main extends java.lang.Object{
public java.lang.String MainA;
public java.lang.String MainB;
public Main();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2; //String HelloA
7: putfield #3; //Field MainA:Ljava/lang/String;
10: aload_0
11: getstatic #4; //Field Test.bb:Ljava/lang/String;
14: putfield #5; //Field MainB:Ljava/lang/String;
17: return
public static void main(java.lang.String[]);
Code:
0: return
}


在第5行java進行了一次值賦值,所以就直接霸HelloA給Main.a了。 而在第11行,java則是傳給b一個Object,也就是將Test.bb給了Main.b;

這是爲什麼了,C++中說 const其中一個功能就是爲了加快編譯速度,的確值賦值加快了編譯的速度(java應該也是這麼做的)

final放在什麼內存中等把JVM搞懂了,再來補上!


但是這可能會引發一個問題,如果修改aa的值,並且就編譯Test.java,Main中的a還是原來的”HelloA“,沒有改變,因爲final在這種情況下是直接賦值的。

對與java中的final變量,java編譯器是進行了優化的。每個使用了final類型變量的地方都不會通過連接而進行訪問。比如說Test類中使用了Data類中一個final的int數字fNumber=77,這時候,java編譯器會將77這個常數編譯到Test類的指令碼或者常量池中。這樣,每次Test類用到fNumber的時候,不會通過引用連接到Data類中進行讀取,而是直接使用自己保存在類文件中的副本。
用程序說話:
Test.java:

public class Test{
public static void main(String[] args){
System.out.println(Data.fNumber);
}
}


Data.java:

public class Data{
public static final int fNumber=77; 
}

執行命令和結果:

Microsoft Windows XP [版本 5.1.2600]
(C) 版權所有 1985-2001 Microsoft Corp.

C:\Documents and Settings\zangmeng>cd ..

C:\Documents and Settings>cd ..

C:\>javac Test.java

C:\>java Test
77

C:\>


這時候,我們更改Data.java的內容:
public class Data{
public static final int fNumber=777; 
}

然後執行如下命令:

C:\>javac Data.java

C:\>java Test
77

C:\>

這裏我們看到,雖然Data.java中的fNumber已經更改爲777,而且已經重新編譯了,但是因爲編譯器把fNumber的副本保存Test類中,所以在重新編譯Test類的前,Test類一直把fNumber認爲是77而不是777。下面我們變異Test.java,再執行,看看結果。

C:\>javac Test.java

C:\>java Test
777

C:\>
這時候,我們看到,重新編譯的Test類將新的777數值封裝到了自己類中。

整個過程如下:
Microsoft Windows XP [版本 5.1.2600]
(C) 版權所有 1985-2001 Microsoft Corp.

C:\Documents and Settings\zangmeng>cd ..

C:\Documents and Settings>cd ..

C:\>javac Test.java

C:\>java Test
77
//在這裏改變了Data.java的內容
C:\>javac Data.java

C:\>java Test
77

C:\>javac Test.java

C:\>java Test
777

C:\>


這個是java編譯器的優化,具體的,大家可以繼續參考http://www.blogjava.net/aoxj/archive/2009/11/10/165536.html

另外java的final還有inline的功能,這個和C++有異曲同工之妙。簡單的來說就是內聯函數就是指函數在被調用的地方直接展開,編譯器在調用時不用像一般函數那樣,參數壓棧,返回時參數出棧以及資源釋放等,這樣提高了程序執行速度。

但 是網上有人說這樣並沒有加快速度,這是爲什麼呢?還不太清楚!!



final使得被修飾的變量"不變",但是由於對象型變量的本質是“引用”,使得“不變”也有了兩種含義:引用本身的不變,和引用指向的對象不變。

引用本身的不變:
final StringBuffer a=new StringBuffer("immutable");
final StringBuffer b=new StringBuffer("not immutable");
a=b;//編譯期錯誤

引用指向的對象不變:
final StringBuffer a=new StringBuffer("immutable");
a.append(" broken!"); //編譯通過

可見,final只對引用的“值”(也即它所指向的那個對象的內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。這很類似==操作符:==操作符只負責引用的“值”相等,至於這個地址所指向的對象內容是否相等,==操作符是不管的。

Java的局部內部類以及final類型的參數和變量

本文是Thinking In Java中其中一段的閱讀總結。如果定義一個匿名內部類,並且希望它使用一個在其外部定的對象,那麼編譯器會要求其參數引用是final 的。經研究,Java虛擬機的實現方式是,編譯器會探測局部內部類中是否有直接使用外部定義變量的情況,如果有訪問就會定義一個同類型的變量,然後在構造方法中用外部變量給自己定義的變量賦值。

Thinking In Java裏面的說法(唯一正確的說法): 如果定義一個匿名內部類,並且希望它使用一個在其外部定的對象,那麼編譯器會要求其參數引用是final 的。

public class Tester {

public static void main(String[] args) {

A a = new A();

C c = new C();

c.shoutc(a.shout(5));

} } ////////////////////////////////////////////////////////

class A {

public void shouta() {

System.out.println("Hello A");

}

public A shout(final int arg) {

class B extends A {

public void shouta() {

System.out.println("Hello B" + arg);

} }

return new B();

} } ////////////////////////////////////////////////////////

class C {

void shoutc(A a) {

a.shouta();

} } 

c.shoutc(a.shout(5)),在a.shout(5)得到返回值後,a的shout()方法棧被清空了,即arg不存在了,而c.shoutc()卻又調用了a.shouta()去執行System.out.println("Hello B" + arg)。

再來看Java虛擬機是怎麼實現這個詭異的訪問的:有人認爲這種訪問之所以能完成,是因爲arg是final的,由於變量的生命週期,事實是這樣的嗎?方法棧都不存在了,變量即使存在,怎麼可能還被訪問到?試想下:一個方法能訪問另一個方法的定義的final局部變量嗎(不通過返回值)?

研究一下這個詭異的訪問執行的原理,用反射探測一下局部內部類 。編譯器會探測局部內部類中是否有直接使用外部定義變量的情況,如果有訪問就會定義一個同類型的變量,然後在構造方法中用外部變量給自己定義的變量賦值,而後局部內部類所使用的變量都是自己定義的變量,所以就可以訪問了。見下:

class A$1$B {

A$1$B(A, int);

private final int var$arg;

private final A this$0;


A$1$B類型的對象會使用定義的var$arg變量,而不是shout()方法中的final int arg變量,當然就可以訪問了。

那麼爲什麼外部變量要是final的呢?即使外部變量不是final,編譯器也可以如此處理:自己定義一個同類型的變量,然後在構造方法中賦值就行了。原因就是爲了讓我們能夠挺合邏輯的直接使用外部變量,而且看起來是在始終使用 外部的arg變量(而不是賦值以後的自己的字段)。

考慮出現這種情況:在局部內部類中使用外部變量arg,如果編譯器允許arg不是final的,那麼就可以對這個變量作變值操作(例如arg++),根據前面的分析,變值操作改變的是var$arg,而外部的變量arg並沒有變,仍然是5(var$arg纔是6)。因此爲了避免這樣如此不合邏輯的事情發生:你用了外部變量,又改變了變量的值,但那個變量卻沒有變化,自然的arg就被強行規定必須是final所修飾的,以確保讓兩個值永遠一樣,或所指向的對象永遠一樣(後者可能更重要)。

還有一點需要注意的是內部類與方法不是同時執行的,比如實現ActionListener,只有當事件發生的時候纔會執行,而這時方法已經結束了。

也有人這樣說
匿名內部類要訪問局部變量,但是函數的局部變量在執行完後會立即退出,銷燬掉所有臨時變量。而產生的匿名內部類可能會保留。在java中方法不是對象,不存儲狀態,這時候匿名內部類已經沒有外部環境了。我猜想匿名內部類可能會把需要訪問的外部變量作爲一個隱藏的字段,這樣只是得到了一個變量的引用拷貝,所以是隻讀的,所以編譯器要求給要訪問的外部局部變量加final。

可以用一個包裝對象來突破這一限制。
final Result result=new Result();
sqlMaker.selectById(id).execute(getTransaction(),new Function(){
public Object call(Object... args) {
ResultSet rs=(ResultSet)args[0];
Object obj=sqlMaker.getTable().readFirstObject(rs);
result.setValue(obj);
return null;
}});
T r= (T)result.getValue();


理解final問題有很重要的含義。許多程序漏洞都基於此----final只能保證引用永遠指向固定對象,不能保證那個對象的狀態不變。在多線程的操作中,一個對象會被多個線程共享或修改,一個線程對對象無意識的修改可能會導致另一個使用此對象的線程崩潰。一個錯誤的解決方法就是在此對象新建的時候把它聲明爲final,意圖使得它“永遠不變”。其實那是徒勞的


請教大家個問題,想了好久也不明白,爲什麼在某方法內定義一個匿名內部類,並且希望它使用外部定義的對象,那麼要求此方法的參數引用要聲明爲final?(我只知道final的作用對於對象引用來説,此對象引用不能指向新的對象,對於基本類型就是不能改變它的值) 

因爲內部要copy一份自己使用,怕你在外邊改了造成一些不確定的問題。所以乾脆final

http://forums.sun.com/thread.jspa?threadID=5325241&messageID=10392871


這是一個編譯器設計的問題,如果你瞭解java的編譯原理的話很容易理解。 
首先,內部類被編譯的時候會生成一個單獨的內部類的.class文件,這個文件並不與外部類在同一class文件中。 
當外部類傳的參數被內部類調用時,從java程序的角度來看是直接的調用例如: 
public void dosome(final String a,final int b){ 
class Dosome{public void dosome(){System.out.println(a+b)}}; 
Dosome some=new Dosome(); 
some.dosome(); 

從代碼來看好像是那個內部類直接調用的a參數和b參數,但是實際上不是,在java編譯器編譯以後實際的操作代碼是
class Outer$Dosome{ 
public Dosome(final String a,final int b){ 
this.Dosome$a=a; 
this.Dosome$b=b; 

public void dosome(){ 
System.out.println(this.Dosome$a+this.Dosome$b); 

}} 
從以上代碼看來,內部類並不是直接調用方法傳進來的參數,而是內部類將傳進來的參數通過自己的構造器備份到了自己的內部,自己內部的方法調用的實際是自己的屬性而不是外部類方法的參數。 
這樣理解就很容易得出爲什麼要用final了,因爲兩者從外表看起來是同一個東西,實際上卻不是這樣,如果內部類改掉了這些參數的值也不可能影響到原參數,然而這樣卻失去了參數的一致性,因爲從編程人員的角度來看他們是同一個東西,如果編程人員在程序設計的時候在內部類中改掉參數的值,但是外部調用的時候又發現值其實沒有被改掉,這就讓人非常的難以理解和接受,爲了避免這種尷尬的問題存在,所以編譯器設計人員把內部類能夠使用的參數設定爲必須是final來規避這種莫名其妙錯誤的存在。 

實現的確是如此,不過final只是讓一個引用不能修改而已,照樣可以修改它指向的數據的內容。 

再 一次闡述 內部類,final

1)所謂“局部內部類”就是在對象的方法成員內部定義的類。而方法中的類,訪問同一個方法中的局部變量,是天經地義的。那麼爲什麼要加上一個final呢? 
2)原因是:編譯程序實現上的困難,難在何處:內部類對象的生命週期會超過局部變量的生命期。爲什麼?表現在:局部變量的生命期:當該方法被調用時,該方法中的局部變量在棧中被創建(誕生),當方法調用結束時(執行完畢),退棧,這些局部變量全部死亡。而:內部類對象生命期,與其它類一樣,當創建一個該局部類對象後,只有沒有其它人再引用它時,它才能死亡。完全可能:一個方法已調用結束(局部變量已死亡),但該局部類的對象仍然活着。即:局部類的對象生命期會超過局部變量。 
3)退一萬步:局部類的對象生命期會超過局部變量又怎樣?問題的真正核心是:如果:局部內部類的對象訪問同一個方法中的局部變量,是天經地義的,那麼:只要局部內部類對象還活着,則:棧中的那些它要訪問的局部變量就不能“死亡”(否則:它都死了,還訪問個什麼呢?),這就是說:局部變量的生命期至少等於或大於局部內部類對象的生命期。而:正是這一點是不可能做到的
4)但是從理論上:局部內部類的對象訪問同一個方法中的局部變量,是天經地義的。所以:經過努力,達到一個折中結果:即:局部內部類的對象可以訪問同一個方法中的局部變量,只要這個變量被定義爲final.那麼:爲什麼定義爲final變可以呢?定義爲final後,編譯程序就好實現了:具體實現方法是:將所有的局部內部類對象要訪問的final型局部變量,都成員該內部類對象中的一個數據成員。這樣,即使棧中局部變量(含final)已死亡,但由於它是final,其值永不變,因而局部內部類對象在變量死亡後,照樣可以訪問final型局部變量。

不管變量是不是final,他的生命週期都在於{}中。

不管對象是不是final,他的生命週期都是 new開始,垃圾回收結束。 

類對象(class對象)與其它對象不同,類對象的生命週期 開始於類被加到內存中那一刻,結束於垃圾回收。 
類變量(static)與類對象的生命週期相同。


解析就是對於編譯型常量使用直接的內存地址代替變量,如final static int a = 10;但是對於在編譯的時候不能得到具體值得變量不做變換,如final static int a = Math.random()。


final和abstract一樣,都是非訪問控制符,當然也不會改變作用域 protect,private,public纔是訪問控制符


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