final關鍵字的用法總結
目錄
final關鍵字相信大家一定不會很陌生,如果你使用過定義在方法中的內部類,你一定知道,內部類如果要訪問方法中的引數,則引數需要final宣告。今天主要系統的介紹一下final關鍵字的用法。
final關鍵字的基本用法
在java中,final關鍵字可以用來修飾類、方法、變數(包括成員變數和區域性變數)。下面我們從這三個方面瞭解一下final的用法。
1、修飾類
final修飾一個類時,表示該類不能繼承。final類中的成員變數可以根據需要設為final,但是final類中的所有成員方法都會被隱式地指定為final方法。
注意:在使用final修飾類的時候,一定要謹慎選擇,除非這個類以後不會用來繼承或者出於安全考慮,儘量不要將類設計為final類。
2、修飾方法
”使用final修飾方法的主要原因是把方法鎖定,防止任何繼承類修改它的含義”。因此,只有在想明確禁止該方法在子類中被覆蓋的情況下才將方法設定為final的。
注:“類的private方法會隱式的被指定為final方法”。
3、修飾變數
修飾變數是final用的最廣的地方,也是要重點理解的地方。先了解一下final變數的基本語法:
對於一個final變數,如果是基本資料型別的變數,則其數值一但被初始化之後便不可以修改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。
class Man{
private final int i = 0;
public Man(){
i = 1;//錯誤
final Object obj = new Object();
obj = new Object();//錯誤
}
}
上面程式碼中,對i和obj的重新賦值都會報錯。
深入探討final關鍵字
在瞭解了final的基本用法之後,下面我們詳細看一下final使用過程中容易出錯的地方。
1、類的final變數和普通變數的區別?
當用final作用於類的成員變數時,成員變數(注意是成員變數,區域性變數只需要在使用前被初始化賦值即可)必須定義時或者構造器中進行賦值,並且final變數一但被初始化賦值之後,就不能再賦值了。
那麼final變數和普通變數到底有什麼區別呢?看下面例子:
public class Test{
public static void main(String[] args){
String a = "hello2";
final String b = "hello";
String d = "hello";
String c = b + 2;
String e = d + 2;
System.out.println((a == c));
System.out.println((a == e));
}
}
輸出:true false
程式碼分析:
大家都知道String是一個final類,final類的特點就是共享資料,==是地址的比較,而final類的String相同的字串,指向的是同一個地址!String類中用“”中的字面值建立物件時,都會先在串池空間中查詢,如果有就返回字串地址,沒有在串池裡建立一個物件。如果是new在堆空間穿件String類物件,則不會有這個過程(String做字串連線也是一樣,每連線一次相當於建立一個物件)。那麼我在看一下輸出結果,為什麼第一個是true,第二個是false。這裡面就是final變數和普通變數的區別了,當final變數是基本資料型別以及String型別時,如果在編譯期間能知道它的確切值,則編譯器會把它當做編譯期常量使用。也就是說在用到該final變數的地方,相當於直接訪問的這個常量,不需要在執行時確定。由於變數b被final修飾,因此,會被當做編譯器常量,所以在使用到b的地方會直接將變數b替換為它的值。而對於變數d的訪問卻需要在執行時通過連結來進行。不過要注意,只有在編譯期間能確切知道final變數值的情況下,編譯器才會進行這樣的優化,比如下面的這段程式碼就不會進行優化:
java程式碼:
public class Test {
public static void main(String[] args) {
String a = "hello2";
final String b = getHello();
String c = b + 2;
System.out.println((a == c));
}
public static String getHello() {
return "hello";
}
}
這段程式碼的輸出結果為false。
2、被final修飾的引用變數指向的物件內容是否可變?
上面提到了被final修飾的引用變數一旦初始化賦值之後就不能再指向其他物件了,那麼該引用變數指向的物件內容可變嗎?看下面例子:
public class Test{
public static void main(String[] args){
final MyClass myclass = new MyClass();
System.out.println(++myclass.i);
}
}
class MyClass{
public int i = 0;
}
輸出結果為1。這說明引用變數被final修飾之後,雖然不能在指向其他物件,但是它指向的物件內容是可變的。
3、final與static區別?
static作用於成員變數(即靜態變數)在記憶體中只有一個拷貝(為了節約記憶體),jVM只會為靜態變數分配一次記憶體,在載入過程中完成靜態變數的記憶體分配,可用類名直接訪問。而final只是保證變數不可改變,不可改變的是引用的指向,具體的引用指向的值是可以改變的。
public class test00 {
public static void main(String[] args) {
MyClass myClass1 = new MyClass();
MyClass myClass2 = new MyClass();
System.out.println(myClass1.i);
System.out.println(myClass1.j);
System.out.println(myClass2.i);
System.out.println(myClass2.j);
}
}
class MyClass {
public final double i = Math.random();
public static double j = Math.random();
}
輸出結果表明:每次列印的j值都是一樣的,而i的值卻是不同的。
4、解釋一下為什麼區域性內部類和匿名內部類只能訪問區域性final變數?
相信這個問題大家一定都會有疑惑,下面我們來慢慢分析。舉個例子:
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
public class Test {
public static void main(String[] args) {
}
public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
}
}.start();
}
}
我們都知道經過編譯後會得到兩個.class檔案,一般情況下,會為Test.class和Test$1.class。然後,我們分析過程,當test方法執行完畢後,變數a的生命週期就結束了,而此時Thread物件的生命週期有可能還沒有結束,那麼在Thread的run方法中繼續訪問a是不可能的了,但是為了實現這種效果,java採用了“複製”的手段來解決。在編譯期間編譯器會預設複製一個本地區域性變數。如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或者直接將相應的位元組碼嵌入到執行位元組碼中。所以,匿名內部類中使用的變數是另一個區域性變數,只不過值和方法中的區域性變數相等。如果區域性變數的值在編譯期間無法確定,則通過構造器傳參的方式對拷貝進行初始化賦值。所以為了避免資料值的不一致性,必須限定變數為final型別。