一 final關鍵字
1) 關於final的重要知識點
- final關鍵字可以用於成員變量、本地變量、方法以及類。
- final成員變量必須在聲明的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。
- 你不能夠對final變量再次賦值。
- 本地變量必須在聲明時賦值。
- 在匿名類中所有變量都必須是final變量。
- final方法不能被重寫。
- final類不能被繼承。
- final關鍵字不同於finally關鍵字,后者用於異常處理。
- final關鍵字容易與finalize()方法搞混,后者是在Object類中定義的方法,是在垃圾回收之前被JVM調用的方法。
- 接口中聲明的所有變量本身是final的。
- final和abstract這兩個關鍵字是反相關的,final類就不可能是abstract的。
- final方法在編譯階段綁定,稱為靜態綁定(static binding)。
- 沒有在聲明時初始化final變量的稱為空白final變量(blank final variable),它們必須在構造器中初始化,或者調用this()初始化。不這么做的話,編譯器會報錯“final變量(變量名)需要進行初始化”。
- 將類、方法、變量聲明為final能夠提高性能,這樣JVM就有機會進行估計,然后優化。
- 按照Java代碼慣例,final變量就是常量,而且通常常量名要大寫:
1 |
|
- 對於集合對象聲明為final指的是引用不能被更改,但是你可以向其中增加,刪除或者改變內容。譬如:
1 2 3 4 |
|
2) final的主要用法
在java中,final的含義在不同的場景下有細微的差別,但總體上來說,它指的是“這是不可變的”。下面,我們來講final的四種主要用法。
1.修飾數據
在編寫程序時,我們經常需要說明一個數據是不可變的,我們成為常量。在java中,用final關鍵字修飾的變量,只能進行一次賦值操作,並且在生存期內不可以改變它的值。更重要的是,final會告訴編譯器,這個數據是不會修改的,那么編譯器就可能會在編譯時期就對該數據進行替換甚至執行計算,這樣可以對我們的程序起到一點優化。不過在針對基本類型和引用類型時,final關鍵字的效果存在細微差別。我們來看下面的例子:
1 class Value {
2 int v;
3 public Value(int v) {
4 this.v = v;
5 }
6 }
7
8 public class FinalTest {
9
10 final int f1 = 1;
11 final int f2;
12 public FinalTest() {
13 f2 = 2;
14 }
15
16 public static void main(String[] args) {
17 final int value1 = 1;
18 // value1 = 4;
19 final double value2;
20 value2 = 2.0;
21 final Value value3 = new Value(1);
22 value3.v = 4;
23 }
24 }
上面的例子中,我們先來看一下main方法中的幾個final修飾的數據,在給value1賦初始值之后,我們無法再對value1的值進行修改,final關鍵字起到了常量的作用。從value2我們可以看到,final修飾的變量可以不在聲明時賦值,即可以先聲明,后賦值。value3時一個引用變量,這里我們可以看到final修飾引用變量時,只是限定了引用變量的引用不可改變,即不能將value3再次引用另一個Value對象,但是引用的對象的值是可以改變的,從內存模型中我們看的更加清晰:
上圖中,final修飾的值用粗線條的邊框表示它的值是不可改變的,我們知道引用變量的值實際上是它所引用的對象的地址,也就是說該地址的值是不可改變的,從而說明了為什么引用變量不可以改變引用對象。而實際引用的對象實際上是不受final關鍵字的影響的,所以它的值是可以改變的。
另一方面,我們看到了用final修飾成員變量時的細微差別,因為final修飾的數據的值是不可改變的,所以我們必須確保在使用前就已經對成員變量賦值了。因此對於final修飾的成員變量,我們有且只有兩個地方可以給它賦值,一個是聲明該成員時賦值,另一個是在構造方法中賦值,在這兩個地方我們必須給它們賦初始值。
最后我們需要注意的一點是,同時使用static和final修飾的成員在內存中只占據一段不能改變的存儲空間。
2.修飾方法參數
前面我們可以看到,如果變量是我們自己創建的,那么使用final修飾表示我們只會給它賦值一次且不會改變變量的值。那么如果變量是作為參數傳入的,我們怎么保證它的值不會改變呢?這就用到了final的第二種用法,即在我們編寫方法時,可以在參數前面添加final關鍵字,它表示在整個方法中,我們不會(實際上是不能)改變參數的值:
public class FinalTest {
/* ... */
public void finalFunc(final int i, final Value value) {
// i = 5; 不能改變i的值
// v = new Value(); 不能改變v的值
value.v = 5; // 可以改變引用對象的值
}
}
3.修飾方法
第三種方式,即用final關鍵字修飾方法,它表示該方法不能被覆蓋。這種使用方式主要是從設計的角度考慮,即明確告訴其他可能會繼承該類的程序員,不希望他們去覆蓋這個方法。這種方式我們很容易理解,然而,關於private和final關鍵字還有一點聯系,這就是類中所有的private方法都隱式地指定為是final的,由於無法在類外使用private方法,所以也就無法覆蓋它。
4.修飾類
了解了final關鍵字的其他用法,我們很容易可以想到使用final關鍵字修飾類的作用,那就是用final修飾的類是無法被繼承的。
上面我們講解了final的四種用法,然而,對於第三種和第四種用法,我們卻甚少使用。這不是沒有道理的,從final的設計來講,這兩種用法甚至可以說是雞肋,因為對於開發人員來講,如果我們寫的類被繼承的越多,就說明我們寫的類越有價值,越成功。即使是從設計的角度來講,也沒有必要將一個類設計為不可繼承的。Java標准庫就是一個很好的反例,特別是Java 1.0/1.1中Vector類被如此廣泛的運用,如果所有的方法均未被指定為final的話,它可能會更加有用。如此有用的類,我們很容易想到去繼承和重寫他們,然而,由於final的作用,導致我們對Vector類的擴展受到了一些阻礙,導致了Vector並沒有完全發揮它應有的全部價值。
總結
final關鍵字是我們經常使用的關鍵字之一,它的用法有很多,但是並不是每一種用法都值得我們去廣泛使用。它的主要用法有以下四種:
- 用來修飾數據,包括成員變量和局部變量,該變量只能被賦值一次且它的值無法被改變。對於成員變量來講,我們必須在聲明時或者構造方法中對它賦值;
- 用來修飾方法參數,表示在變量的生存期中它的值不能被改變;
- 修飾方法,表示該方法無法被重寫;
- 修飾類,表示該類無法被繼承。
上面的四種方法中,第三種和第四種方法需要謹慎使用,因為在大多數情況下,如果是僅僅為了一點設計上的考慮,我們並不需要使用final來修飾方法和類。
二 static關鍵字
1.修飾成員變量
在我們平時的使用當中,static最常用的功能就是修飾類的屬性和方法,讓他們成為類的成員屬性和方法,我們通常將用static修飾的成員稱為類成員或者靜態成員,這句話挺起來都點奇怪,其實這是相對於對象的屬性和方法來說的。請看下面的例子:(未避免程序太過臃腫,暫時不管訪問控制)
public class Person {
String name;
int age;
public String toString() {
return "Name:" + name + ", Age:" + age;
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "zhangsan";
p1.age = 10;
Person p2 = new Person();
p2.name = "lisi";
p2.age = 12;
System.out.println(p1);
System.out.println(p2);
}
/**Output
* Name:zhangsan, Age:10
* Name:lisi, Age:12
*///~
}
上面的代碼我們很熟悉,根據Person構造出的每一個對象都是獨立存在的,保存有自己獨立的成員變量,相互不會影響,他們在內存中的示意如下:
從上圖中可以看出,p1和p2兩個變量引用的對象分別存儲在內存中堆區域的不同地址中,所以他們之間相互不會干擾。但其實,在這當中,我們省略了一些重要信息,相信大家也都會想到,對象的成員屬性都在這了,由每個對象自己保存,那么他們的方法呢?實際上,不論一個類創建了幾個對象,他們的方法都是一樣的:
從上面的圖中我們可以看到,兩個Person對象的方法實際上只是指向了同一個方法定義。這個方法定義是位於內存中的一塊不變區域(由jvm划分),我們暫稱它為靜態存儲區。這一塊存儲區不僅存放了方法的定義,實際上從更大的角度而言,它存放的是各種類的定義,當我們通過new來生成對象時,會根據這里定義的類的定義去創建對象。多個對象僅會對應同一個方法,這里有一個讓我們充分信服的理由,那就是不管多少的對象,他們的方法總是相同的,盡管最后的輸出會有所不同,但是方法總是會按照我們預想的結果去操作,即不同的對象去調用同一個方法,結果會不盡相同。
我們知道,static關鍵字可以修飾成員變量和方法,來讓它們變成類的所屬,而不是對象的所屬,比如我們將Person的age屬性用static進行修飾,結果會是什么樣呢?請看下面的例子:
public class Person {
String name;
static int age;
/* 其余代碼不變... */
/**Output
* Name:zhangsan, Age:12
* Name:lisi, Age:12
*///~
}
我們發現,結果發生了一點變化,在給p2的age屬性賦值時,干擾了p1的age屬性,這是為什么呢?我們還是來看他們在內存中的示意:
我們發現,給age屬性加了static關鍵字之后,Person對象就不再擁有age屬性了,age屬性會統一交給Person類去管理,即多個Person對象只會對應一個age屬性,一個對象如果對age屬性做了改變,其他的對象都會受到影響。我們看到此時的age和toString()方法一樣,都是交由類去管理。
雖然我們看到static可以讓對象共享屬性,但是實際中我們很少這么用,也不推薦這么使用。因為這樣會讓該屬性變得難以控制,因為它在任何地方都有可能被改變。如果我們想共享屬性,一般我們會采用其他的辦法:
public class Person {
private static int count = 0;
int id;
String name;
int age;
public Person() {
id = ++count;
}
public String toString() {
return "Id:" + id + ", Name:" + name + ", Age:" + age;
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "zhangsan";
p1.age = 10;
Person p2 = new Person();
p2.name = "lisi";
p2.age = 12;
System.out.println(p1);
System.out.println(p2);
}
/**Output
* Id:1, Name:zhangsan, Age:10
* Id:2, Name:lisi, Age:12
*///~
}
上面的代碼起到了給Person的對象創建一個唯一id以及記錄總數的作用,其中count由static修飾,是Person類的成員屬性,每次創建一個Person對象,就會使該屬性自加1然后賦給對象的id屬性,這樣,count屬性記錄了創建Person對象的總數,由於count使用了private修飾,所以從類外面無法隨意改變。
2.修飾成員方法
static的另一個作用,就是修飾成員方法。相比於修飾成員屬性,修飾成員方法對於數據的存儲上面並沒有多大的變化,因為我們從上面可以看出,方法本來就是存放在類的定義當中的。static修飾成員方法最大的作用,就是可以使用"類名.方法名"的方式操作方法,避免了先要new出對象的繁瑣和資源消耗,我們可能會經常在幫助類中看到它的使用:
public class PrintHelper {
public static void print(Object o){
System.out.println(o);
}
public static void main(String[] args) {
PrintHelper.print("Hello world");
}
}
上面便是一個例子(現在還不太實用),但是我們可以看到它的作用,使得static修飾的方法成為類的方法,使用時通過“類名.方法名”的方式就可以方便的使用了,相當於定義了一個全局的函數(只要導入該類所在的包即可)。不過它也有使用的局限,一個static修飾的類中,不能使用非static修飾的成員變量和方法,這很好理解,因為static修飾的方法是屬於類的,如果去直接使用對象的成員變量,它會不知所措(不知該使用哪一個對象的屬性)。
3.靜態塊
在說明static關鍵字的第三個用法時,我們有必要重新梳理一下一個對象的初始化過程。以下面的代碼為例:
package com.dotgua.study;
class Book{
public Book(String msg) {
System.out.println(msg);
}
}
public class Person {
Book book1 = new Book("book1成員變量初始化");
static Book book2 = new Book("static成員book2成員變量初始化");
public Person(String msg) {
System.out.println(msg);
}
Book book3 = new Book("book3成員變量初始化");
static Book book4 = new Book("static成員book4成員變量初始化");
public static void main(String[] args) {
Person p1 = new Person("p1初始化");
}
/**Output
* static成員book2成員變量初始化
* static成員book4成員變量初始化
* book1成員變量初始化
* book3成員變量初始化
* p1初始化
*///~
}
上面的例子中,Person類中組合了四個Book成員變量,兩個是普通成員,兩個是static修飾的類成員。我們可以看到,當我們new一個Person對象時,static修飾的成員變量首先被初始化,隨后是普通成員,最后調用Person類的構造方法完成初始化。也就是說,在創建對象時,static修飾的成員會首先被初始化,而且我們還可以看到,如果有多個static修飾的成員,那么會按照他們的先后位置進行初始化。
實際上,static修飾的成員的初始化可以更早的進行,請看下面的例子:
class Book{
public Book(String msg) {
System.out.println(msg);
}
}
public class Person {
Book book1 = new Book("book1成員變量初始化");
static Book book2 = new Book("static成員book2成員變量初始化");
public Person(String msg) {
System.out.println(msg);
}
Book book3 = new Book("book3成員變量初始化");
static Book book4 = new Book("static成員book4成員變量初始化");
public static void funStatic() {
System.out.println("static修飾的funStatic方法");
}
public static void main(String[] args) {
Person.funStatic();
System.out.println("****************");
Person p1 = new Person("p1初始化");
}
/**Output
* static成員book2成員變量初始化
* static成員book4成員變量初始化
* static修飾的funStatic方法
* ***************
* book1成員變量初始化
* book3成員變量初始化
* p1初始化
*///~
}
在上面的例子中我們可以發現兩個有意思的地方,第一個是當我們沒有創建對象,而是通過類去調用類方法時,盡管該方法沒有使用到任何的類成員,類成員還是在方法調用之前就初始化了,這說明,當我們第一次去使用一個類時,就會觸發該類的成員初始化。第二個是當我們使用了類方法,完成類的成員的初始化后,再new該類的對象時,static修飾的類成員沒有再次初始化,這說明,static修飾的類成員,在程序運行過程中,只需要初始化一次即可,不會進行多次的初始化。
回顧了對象的初始化以后,我們再來看static的第三個作用就非常簡單了,那就是當我們初始化static修飾的成員時,可以將他們統一放在一個以static開始,用花括號包裹起來的塊狀語句中:
class Book{
public Book(String msg) {
System.out.println(msg);
}
}
public class Person {
Book book1 = new Book("book1成員變量初始化");
static Book book2;
static {
book2 = new Book("static成員book2成員變量初始化");
book4 = new Book("static成員book4成員變量初始化");
}
public Person(String msg) {
System.out.println(msg);
}
Book book3 = new Book("book3成員變量初始化");
static Book book4;
public static void funStatic() {
System.out.println("static修飾的funStatic方法");
}
public static void main(String[] args) {
Person.funStatic();
System.out.println("****************");
Person p1 = new Person("p1初始化");
}
/**Output
* static成員book2成員變量初始化
* static成員book4成員變量初始化
* static修飾的funStatic方法
* ***************
* book1成員變量初始化
* book3成員變量初始化
* p1初始化
*///~
}
我們將上一個例子稍微做了一下修改,可以看到,結果沒有二致。
4.靜態導包
相比於上面的三種用途,第四種用途可能了解的人就比較少了,但是實際上它很簡單,而且在調用類方法時會更方便。以上面的“PrintHelper”的例子為例,做一下稍微的變化,即可使用靜態導包帶給我們的方便:
/* PrintHelper.java文件 */
package com.dotgua.study;
public class PrintHelper {
public static void print(Object o){
System.out.println(o);
}
}
/* App.java文件 */
import static com.dotgua.study.PrintHelper.*;
public class App
{
public static void main( String[] args )
{
print("Hello World!");
}
/**Output
* Hello World!
*///~
}
上面的代碼來自於兩個java文件,其中的PrintHelper很簡單,包含了一個用於打印的static方法。而在App.java文件中,我們首先將PrintHelper類導入,這里在導入時,我們使用了static關鍵字,而且在引入類的最后還加上了“.*”,它的作用就是將PrintHelper類中的所有類方法直接導入。不同於非static導入,采用static導入包后,在不與當前類的方法名沖突的情況下,無需使用“類名.方法名”的方法去調用類方法了,直接可以采用"方法名"去調用類方法,就好像是該類自己的方法一樣使用即可。
5 總結
static是java中非常重要的一個關鍵字,而且它的用法也很豐富,主要有四種用法:
- 用來修飾成員變量,將其變為類的成員,從而實現所有對象對於該成員的共享;
- 用來修飾成員方法,將其變為類方法,可以直接使用“類名.方法名”的方式調用,常用於工具類;
- 靜態塊用法,將多個類成員放在一起初始化,使得程序更加規整,其中理解對象的初始化過程非常關鍵;
- 靜態導包用法,將類的方法直接導入到當前類中,從而直接使用“方法名”即可調用類方法,更加方便。
三 final和static結合使用
static final用來修飾成員變量和成員方法,可簡單理解為“全局常量”!
對於變量,表示一旦給值就不可修改,並且通過類名可以訪問。
對於方法,表示不可覆蓋,並且可以通過類名直接訪問。
特別要注意一個問題:對於被static和final修飾過的實例常量,實例本身不能再改變了,但對於一些容器類型(比如,ArrayList、HashMap)的實例變量,不可以改變容器變量本身,但可以修改容器中存放的對象,這一點在編程中用到很多。看個例子吧:
public class TestStaticFinal {
private static final String strStaticFinalVar = "aaa";
private static String strStaticVar = null;
private final String strFinalVar = null;
private static final int intStaticFinalVar = 0;
private static final Integer integerStaticFinalVar = new Integer(8);
private static final ArrayList<String> alStaticFinalVar = new ArrayList<String>();
private void test() {
System.out.println("-------------值處理前----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
//strStaticFinalVar="哈哈哈哈"; //錯誤,final表示終態,不可以改變變量本身.
strStaticVar = "哈哈哈哈"; //正確,static表示類變量,值可以改變.
//strFinalVar="呵呵呵呵"; //錯誤, final表示終態,在定義的時候就要初值(哪怕給個null),一旦給定后就不可再更改。
//intStaticFinalVar=2; //錯誤, final表示終態,在定義的時候就要初值(哪怕給個null),一旦給定后就不可再更改。
//integerStaticFinalVar=new Integer(8); //錯誤, final表示終態,在定義的時候就要初值(哪怕給個null),一旦給定后就不可再更改。
alStaticFinalVar.add("aaa"); //正確,容器變量本身沒有變化,但存放內容發生了變化。這個規則是非常常用的,有很多用途。
alStaticFinalVar.add("bbb"); //正確,容器變量本身沒有變化,但存放內容發生了變化。這個規則是非常常用的,有很多用途。
System.out.println("-------------值處理后----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
}
public static void main(String args[]) {
new TestStaticFinal().test();
}
}
運行結果如下:
-------------值處理前----------
strStaticFinalVar=aaa
strStaticVar=null
strFinalVar=null
intStaticFinalVar=0
integerStaticFinalVar=8
alStaticFinalVar=[]
-------------值處理后----------
strStaticFinalVar=aaa
strStaticVar=哈哈哈哈
strFinalVar=null
intStaticFinalVar=0
integerStaticFinalVar=8
alStaticFinalVar=[aaa, bbb]
Process finished with exit code 0
看了上面這個例子,就清楚很多了,但必須明白:通過static final修飾的容器類型變量中所“裝”的對象是可改變的。這是和一般基本類型和類類型變量差別很大的地方。