Java編程思想讀書筆記


第一章對象導論

>>>>封裝<<<<

被隱藏(也即封裝)的部分通常代表對象內部脆弱的部分,它們很容易被程序員所毀壞,因此將實現隱藏起來可以減少程序的bug

 

隱藏是通過訪問控制修飾符(publicprotected、包訪問、private)來實現的。

 

訪問控制的第一個存在原因就是讓調用者無法直接觸及他們不應該觸及的部分,但從另一方面來看,其實這不失為一項服務,因為他們可以很容易地看出哪些東西對他們來說很重要,而哪些東西可能不關心;訪問控制的第二個存在原因就是允許庫設計者可以改變類的內部的工作方式而不用擔心會影響到調用者。

 

>>>>繼承<<<<

代碼復用:復用是面向對象程序設計所提供最了不起的優點之一。

 

最簡單的代碼復用就是直接調用類的方法,此外,我們還可以將該類置於某個新類中,使它成為新類的屬性成員。新的類也可由任意數量、任意類型的其他對象以任意可以實現新的類中想要功能的方式所組成,這種使用現有的類合成新的類方式稱為組合復用。

 

組合復用帶來了極大的靈活性,使得它能在運行時動態的修改其實現行為,但繼承並不具備這樣的靈活性,因為繼承是在編譯時期就已經確定其行為,在運行時期是不能修改的。

 

繼承兩種實現方式,第一種方式非常直接:直接在子類中添加新的方法,即擴展父類接口。第二種方式就是子類覆寫父類方法,但不新增父類沒有接口。

 

is-a是一個”與“is-like-a像是一個”。繼承時,我們使用第一種還是第二種方式呢?這可能引起爭論:繼承應該只覆蓋基類的方法,而並不添加在基類中沒有的新方法嗎?如果這樣做,就意味着子類與基類是完全相同的類型,因為它們具有完全相同的接口,結果可以用一個子類對象來完全替代一個基類對象,這可被認為是純粹替代,通常稱之為替代原則,這也是一種理想的方式,我們經常稱這種情況下的子類與基類的關系是“is-a是一個”;有時必須在子類型中添加新的接口,這樣也就擴展了接口,這個新的類型仍可以替代基類,但是這種替代並不完美,因為基類無法訪問新添加的方法,這種情況下我們可以描述為“is-like-a像是一個”關系。

 

>>>>多態<<<<

一個非面向對象編程的編譯器產生的函數調用會引起所謂的前期綁定,而向對象程序設計語言使用了后期綁定的概念。

 

方法的調用就是編譯器將產生對一個具體函數名字的調用,前期綁定是在運行時將這個調用解析到將要被執行的代碼的絕對地址。然而在OOP中,程序直到運行時才能夠確定代碼的地址,為了執行后期綁定,Java編譯器使用了一小段特殊代碼來替代絕對地址調用,這段代碼使用對象中存儲的信息來計算方法體的地址,根據這段特殊代碼,每個對象都可以具有不同的行為表現。

 

在某些語言中,必須明確地聲明某個方法具備后期綁定屬性所帶來的靈活性,如C++使用virtual關鍵字來實現,在默認情況下,C++不是動態綁定的,而在Java中,動態綁定是默認行為,不需要添加額外的關鍵字來實現多態。

 

>>> Java語言支持四種類型<<<

接口(interface)、類(class)、數組(array)和基本類型(primitive)。前三種類型通常被稱為引用類型(reference type),類實例和數組是對象(object),而基本類型的值則不是對象。類的成員(member)由它的域(field)、方法(method)、成員類(member class)和成員接口(member interface)組成。方法簽名(signature)由它的名稱和所有參數類型組成;簽名不包括它的返回類型。

 

>>>>運行Java<<<<

javac命令編譯一個打包的類時,如果沒有加參數"-d"時,則編譯出的類不會放在包中(即相應的文件夾中),是沒有包路徑的,除非用參數"-d"指定類存放的位置,–d 指示的是編譯后的class文件放在哪個目錄下,並且會自動創建包名文件夾。

比如現有如下類:
package a.b;

class A{}

       javac A.java 時會在當前工作目錄下產生一個A.class文件不會創建包目錄結構。

       javac –d . A.java 則會在當前工作目錄產生 a/b/A.class 目錄與文件。

所以使用javac編譯時要想產生相應的類包名上當結構,則需要帶上–d .”這樣的參數。

 

java命令運行一個類時,如果該類是存放在包中的,則運行時一定要帶上包名,並且在環境變量要有該包存放的路徑。

     java -classpath . a.

     注,如果用java命令運行時,沒有配置classpath環境變量,則這里的classpath不能缺少。

 

 

>>>類與類之間的關系<<<

類和類、類和接口、接口和接口之間有如下幾種關系:泛化關系、實現關系、關聯關系(聚合、合成)、依賴關系。

l  泛化:表示類與類之間的繼承關系,使用extends關鍵字來表示。在圖形上使用虛線三角形箭頭表示。

l  實現:表示類與接口之間的實現關系,使用implements關鍵字來表示。在圖形上使用實線三角形箭頭表示。

l  關聯:類與類之間的聯接。關聯可以是雙向的,也可以是單向的,雙向的關聯可以有兩個箭頭或都沒有箭頭。單向的關聯有一個箭頭,表示關聯的方向。在Java里,關聯關系是使用實例變量實現的。在每一個關聯的端點,還可以有一個基數,表時這一端的類可以有幾個實例。常見的基數有:0..1(零個或者一個實例)、0..*或者*(沒限制,可以是零)、1(只有一個實例)、1..*(至少有一個實例)。一個關聯關系可以進一步確定為聚合與合成關系。在圖形上使用實線的箭頭表示。

l  聚合:是關聯關系的一種,是強的關聯關系,聚合是整體和個體之間的關系。關聯與聚合僅僅從Java語法是上是分辨不出的,需要考察所涉及的類之間的邏輯關系。如果不確定是聚合關系,可以將之設置為關聯關系。圖形是在實線的箭頭的尾部多一個空心菱形。

l  合成:是關聯關系的一種,是比聚合關系強的關系。它要求普通的聚合關系中代表整體的對象負責代表部分的對象的生命周期。整體消亡,則部分與消亡。圖形是在實線的箭頭的尾部多一個黑心菱形。

l  依賴:類與類之間的連接,依賴總是單向的。表示一個類依賴於另一個類的定義。一般而言,依賴關系在Java里體現為局部變量、方法的參數、以及對靜態方法的調用。但如果對方出現在實例變量中,那關系就超過了依賴關系,而成了關聯關系了。在圖形上使用虛線的箭頭表示。

第二章一切都是對象

 

>>>>對象存放位置與生命周期<<<<

C++創建的對象可以存放在棧、靜態存儲區與堆(heap)中,放在棧中的對象用完后不需手動釋放,會自動銷毀,但放在堆中的對象需手動釋放,棧中的對象所需空間與生命周期都是確定的,堆中的對象內存分配是動態的,在運行時才知道需要多少內存以及生命周期,如果說在堆上創建對象,編譯器就會對它的生命周期一無所知,C++就需要以編程的方式來確定何時銷毀對象,這可能因不正確處理而導致內存泄漏,而Java則提供了自動垃圾回收機制。

 

Java對象的創建采用了動態內存分配策略,即創建的堆都是放在堆中的。

 

 

>>>>數據內存分配<<<<

寄存器——位於處理器內部,這是最快的存儲區,大小極其有限,一般不能直接控制,但CC++允許你向編譯器建議寄存器分配方式。

堆棧——位於通用RAM(隨機訪問存儲器)中,堆棧指針向下移動,則分配新的內存;若向上移動,則釋放內存。這是一種快速有效的分配方法,速度僅次於寄存器。創建程序時,Java系統必須知道存儲在堆棧內所有項的確切生命周期,以便上下移動堆棧指針。這一約束限制了程序的靈活性,所以雖然某些Java數據存儲於堆棧中(如對象引用),但是Java對象並不存儲於其中。

堆——一種通用的內存池(也位於RAM區),用於存放所有的Java對象,堆不同於堆棧的好處是,編譯器不需要知道存儲的數據在堆里存活多長時間。因此,在堆棧分配存儲有很大的靈活性。當然,這種靈活性導致了分配需要更多的時間,時間效率上不如堆棧。

常量存儲:常量值通常直接存放到程序代碼內部,這樣做是安全的,因為它們永遠不會被改變。

RAM存儲:數據可完全存活於程序之外,在沒有運行機制時也可以存在,如持久化對象的存放。

JVM有兩類存儲區:常量緩沖池和方法區。常量緩沖池用於存儲類名稱、方法和字段名稱以及串常量。方法區則用於存儲Java方法的字節碼。

 

image003

Java字節碼的執行有兩種方式:

  1.即時編譯方式:解釋器先將字節碼編譯成機器碼,然后再執行該機器碼。

  2.解釋執行方式:解釋器通過每次解釋並執行一小段代碼來完成Java字節碼程 序的所有操作。

通常采用的是第二種方法。由於JVM規格描述具有足夠的靈活性,這使得將字節碼翻譯為機器代碼的工作具有較高的效率。對於那些對運行速度要求較高的應用程序,解釋器可將Java字節碼即時編譯為機器碼,從而很好地保證了Java代碼的可移植性和高性能。

 

 

 

轉載C/C++內存分配:

 

堆和棧的區別

一、預備知識—程序的內存分配

一個由C/C++編譯的程序占用的內存分為以下幾個部分

1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

2、堆區(heap 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。

3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束后有系統釋放

4、文字常量區—常量字符串就是放在這里的。 程序結束后由系統釋放

5、程序代碼區—存放函數體的二進制代碼。

二、例子程序

這是一個前輩寫的,非常詳細

//main.cpp

int a = 0; 全局初始化區

char *p1; 全局未初始化區

main()

{

int b;

char s[] = "abc";

char *p2;

char *p3 = "123456"; "123456"在常量區,p3在棧上。

static int c =0 全局(靜態)初始化區

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

上面分配得來得1020字節的區域就在堆區。

strcpy(p1, "123456"); "123456"放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方,即指向同一個字符串。

}

 

二、堆和棧的理論知識

2.1申請方式

stack:

由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間

heap:

需要程序員自己申請,並指明大小,在cmalloc函數

p1 = (char *)malloc(10);

C++中用new運算符

p2 = (char *)malloc(10);

但是注意p1p2本身是在棧中的。

 

2.2

申請后系統的響應

棧:只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。

堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,

會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。

 

2.3申請大小的限制

棧:在Windows,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。

堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

 

2.4申請效率的比較:

棧由系統自動分配,速度較快。但程序員是無法控制的。

堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.

另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。

 

2.5堆和棧中的存儲內容

棧: 在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。

當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。

堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。

 

2.6存取效率的比較

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在運行時刻賦值的;

bbbbbbbbbbb是在編譯時就確定的;

但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。比如:

#include

void main()

{

char a = 1;

char c[] = "1234567890";

char *p ="1234567890";

a = c[1];

a = p[1];

return;

}

對應的匯編代碼

10: a = c[1];

00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]

0040106A 88 4D FC mov byte ptr [ebp-4],cl

11: a = p[1];

0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]

00401070 8A 42 01 mov al,byte ptr [edx+1]

00401073 88 45 FC mov byte ptr [ebp-4],al

第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,在根據edx讀取字符,顯然慢了。

 

 

>>>>基本類型<<<<

void屬於基本類型,但只能用來修飾方法,不能用來修飾變量。

image004

 

只要兩個操作數中有一個是double類型的,另一個將會被轉換成double類型,並且結果也是double類型;

否則,只要兩個操作數中有一個是float類型的,另一個將會被轉換成float類型,並且結果也是float類型;

否則,只要兩個操作數中有一個是long類型的,另一個將會被轉換成long類型,並且結果也是long類型;

否則,兩個操作數(包括byteshortintchar)都將會被轉換成int類型,並且結果也是int類型。

 

Java提供了兩個用於高精度計算類:BigIntegerBigDecimal,大體屬於“包裝器類”范疇,但都沒有對應的基本類型。

BigInteger支持任意精度的整數,可表示任何大小的整數值。

BigDecimal支持任意精度的定點數,例如,可以用它進行精確的貨幣計算。

 

>>>>引用與對象生存周期<<<<

{

       String s = new String("a string");

}

引用s在作用域終點就消失了,然而,s指向的String對象繼續占據內存,最后由垃圾回收器回收。

 

>>>>方法簽名<<<<

方法名和參數列表(合起來被稱為“方法簽名”)唯一地標識出某個方法。

 

>>>>static修飾字段與方法的區別<<<<

一個static字段對每個對象來說都只有一份空間,而非static字段則是對每個對象都有一個存儲空間,但是如果static作用於某個方法時,差別卻沒有那么大,static方法的一個重要的用法就是在不創建任何對象的前提下就可以調用它,這一點對定義main()方法很重要,該方法是運行時程序的一個入口。

 

第三章操作符

char c = 0xffff;//最大字符串

byte b = -0x80;//最小字節型 byte b = (byte)0x80; 因為0x80形式為正,即第一位不是符號位而是數字位,所以超過byte范圍,但-0x80卻明確說明第一位是符號位,即為負,沒有超過byte范圍類型

byte b = 0x7f;//最大字節型

 

如果將比較小的類型傳遞給Integer.toBinaryString()方法,則該類型將自動被提升為int再執行。

 

如果對charbyteshort類型的數值進行移位處理,那么在移位進行之前,它們會被轉換為int類型,並且得到的結果也是一個int類型的值;如果為intlong,則結果類型不變。

 

“移位”可與“等號”(<<=>>=>>>=)組合使用,此時,操作符左邊的值會移動由右邊的值指定的位數,再將得到的結果賦給左邊的變量,但在進行“無符號”右移結合賦值操作時,可能會遇到一個問題:如果對byteshort值進行這樣的位移運算時,得到的可能不是正確的結果,它們會先轉換成int類型,再進行右移操作,然后被截斷,賦值給原來的類型,在這種情況下可能得到-1的結果。

 

long l = -1;

System.out.println(Long.toBinaryString(l));// 641

l >>>= 10;

System.out.println(Long.toBinaryString(l));// 541

 

int i = -1;

System.out.println(Integer.toBinaryString(i));// 321

i >>>= 10;

System.out.println(Integer.toBinaryString(i));// 221

 

byte b = -1;

// 字節型-1提升到整型-1,所以結果為321

System.out.println(Integer.toBinaryString(b));// 321

// b先提升到int 即在原來81前再補241,然后無符號右移,再截最后8位並賦值給b

b >>>= 10;

// b會先提升為int,再運算

System.out.println(Integer.toBinaryString(b));// 321

b = -1;

// 不使用 >>>= 時,結果為正確,雖然在執行前b提升為int,但計算后沒有截斷,直接輸出整型結果

System.out.println(Integer.toBinaryString(b >>> 10));// 221

 

 

floatdouble轉型為整型值時,總是對該數字執行截尾((int) 0.7結果為0),如果想要得到舍入的結果,就需要使用java.lang.Math中的round()方法(Math.round(0.7)結果為1)。

 

一般來說,如果在程序里使用了“直接常量”,編譯器可以准確地知道要生成什么樣的類型,但有時候卻是不明確的,此時可以在常量后附加上一些特定類型字符來明確。

/*

* 計算表達式中如果都整型數常量,則計算過程中還是整型,這有可能引起計算

* 溢出問題,但編譯與運行都不會報錯,對於此種情況,在表達式中的某個常量

* 后加上L即可使表達式計算過程中以long型來計算

*/

long l = 2147483647 * 2;// -2

l = 2147483647L * 2;// 4294967294

 

// 編譯運行都不會錯

int i = 2147483647 * 2;// -2

 

// !! 以下編譯時就出錯

// short s1 = 32767 * 2;

// byte b1 = 127 * 2;

 

// short s2 = 16384 * 2;

// byte b2 = 64 * 2;

 

// short s3 = 2147483647 * 3;//乘以奇數就不行,偶數就可以?

// byte b3 = 2147483647 * 3;

 

// 但下面可編譯運行

short s = 2147483647 * 2;// -2

byte b = 2147483647 * 2;// -2

 

第四章流程控制

Foreach循環可用於數組,以及實現了java.util.Iterator接口的對象。

public interface Iterable<T> {

    Iterator<T> iterator();

}

 

如果在返回void的方法中沒有return語句,那么在該方法的結尾處會有一個隱式的return,因此在方法中並非總是必須要有一個return語句。但是,如果一個方法聲明它將返回void之外的其他東西,那么必須確保每一條代碼路徑都將返回一個值。

 

int i;

for (i = 0; i <= 5; i++) {

       if (i == 2) {

              break;//退出時i不會再遞增,但continue

       }

}

System.out.println("i=" + i);//2

 

for(;;)while(ture)等效。

 

盡管goto仍是Java中的一個保留字,但在語言中並未使用它,Java沒有goto。但可使用帶標簽的 continuebreak來完成類似的跳轉操作。

 

帶標簽與不帶標簽的continuebreak用於迭代語句時規則:

一般的continue會退回到最內層循環的開頭,並繼續執行循環。

帶標簽的continue會到標簽的位置,並重新進入緊接在那個標簽后面的循環。

一般的break會中斷並跳出當前循環。

帶標簽的break會中斷並跳出標簽所指的循環。

 

Java里需要使用標簽的唯一理由就是因為有循環嵌套存在,而且想從多層嵌套中breakcontinue到外層循環外。

 

標簽只能緊跟在循環語句前(注:如果中間還有其他語句,則continuebreak語句編譯出錯,但如果標簽不應用到continuebreak中,則不會有問題)

 

int i = 0, j = 0;

outer:

// !! 注,標簽的下面不能寫任何其他非迭代語句

for (; i < 5; i++) { // 死循環

       inner:

       // !! 注,標簽的下面不能寫任何其他非迭代語句

       for (; j < 10; j++) {

              System.out.println(("i=" + i + " j = " + j));

              if (j == 2) {

                     System.out.println("continue");

                     continue;// 回到內層循環起始處繼續執行內層循環,j會自動遞增

              }

              if (j == 3) {

                     System.out.println("break");

                     // 為了下次循環不再走該分支,則要使用i遞增1,因為breakj不會自動遞增

                     j++;

                     break;// 跳出內層循環,回到外層循環起始處繼續執行外層循環

              }

              if (j == 7) {

                     System.out.println("continue outer");

                     // 由於帶標簽的continue跳到了外層循環起始處,所以j不會自動遞增,但為了

                     // 下一次不再走該分支,所以要手動遞增1

                     j++;

                     continue outer;

              }

              if (j == 8) {

                     System.out.println("break outer");

                     break outer;// j8時,退出內外層循環,實質上執行最后打印語句

              }

              for (int k = 0; k < 5; k++) {

                     if (k == 3) {

                            System.out.println("continue inner");

                            // j01456時分別會執行一遍

                            continue inner;

                     }

              }

       }

}

// 由於 break outer 跳出,所以i不會遞增,最后還是2

System.out.println(("i = " + i));

 

switch(integer-selector){

       case integer-value1: statement;break;

       case integer-value2: statement;break;

case integer-value3: statement;break;

//…

default: statement;break;

}

 

switch語句是可用的選擇數據類型有 intcharenum

 

case均以一個break結尾,這樣可使執行流程跳轉至switch主體的末尾。這是構建switch語句的一種傳統方式,但break是可選的。若省略break,會繼續執行后面的case語句,直到遇到一個break為止。注意最后的default語句沒有break,因為執行流程已到了break的跳轉目的地。當然,如果考慮到編程風格方面的原因,完全可以在default語句的末尾放置一個break,盡管它並沒有任何實際的用處。

 

第五章初始化與清理

重載:方法名相同,參數列表不同(參數類型、個數、順序)。

 

以基本類型參數重載方法時,會優先調用該類型的參數的方式,如果沒有找到相同類型時,則提升最近的參數類型。

 

this(type x)用來在構造函數中調用其他構造函數,也只能用於構造函數中,並且只能用在第一行。

 

如果你的對象(並非使用new)獲得了一塊“特殊”的內存區域中,由於垃圾回收器只知道釋放那些經由new分配的內存,所以它不知道該如何釋放該對象的這塊“特殊”內存。

 

finalize():該方法並不是用來釋放由Java程序本身分配的空間,因為它有可能不被調用,由Java程序本身分配的空間會由JVM在適當的時機釋放,有可能不調用,分配的空間直到程序運行完時一次性還給操作系統,那其作用是什么呢?由於在分配內存時可能采用了類似C語言中的做法,而非Java中的通常做法。這種情況主要發生在使用“本地方法”的情況下,在非Java代碼中,也許會調用Cmalloc()函數系列來分配存儲空間,而且除非調用了free()函數,否則存儲空間將得不到釋放,從而造成內存泄露。當然free()CC++中的函數,所以需要在finalize()中用本地方法來調用它。

 

C++中在堆棧中創建的對象相當於局部對象(Java是不可以在棧上創建對象的),會在方法調用完畢時自動銷毀對象。但如果是采用的new方式創建的對象(類似Java),那么程序員調用C++delete操作符時(Java中沒有該命令),就會調用相應的析構函數。如果沒有調用delete,那么就不會調用析構函數,這樣就會引起內存泄露。

 

>>>>可變參數<<<<

如果有多個參數,則可變參數只能放在最后,否則編譯不能通過。

可變參數接收到所有參數時,實質上是一個數組,所以可變參數的所有參數都是同一類型的。

即然是可變的,當然我們也可不輸出任何參數。

 

public static void main(String[] args) {

       print("you input:", 1, 2, 3);

       //與下面等效,即可變參數也可使用數組進行傳遞

       print("you input:", new int[] { 1, 2, 3 });

}

private static void print(String msg, int... is) {

       System.out.print(msg + " ");

       for (int i : is) {

              System.out.print(i + " ");

       }

}

 

 

可變參數重載時,有時有問題,如下:

static void f(float i, Character... args) {}

static void f(Character... args) {}

因為在只傳一個參數時可能引起模糊,所以這樣是不允許的,如果你給這兩個方法都添加一個非可變參數,就可以解決問題:

static void f(float i, Character... args) {}

static void f(char c, Character... args) {}

 

第六章訪問權限控制

 

public

protected

缺省

private

同包同類

Y

Y

Y

Y

同包子類

Y

Y

Y

N

同包不子類

Y

Y

Y

N

不同包子類

不同包不子類

 

要學會把變動的代碼與保持不變的代碼區分開來。

 

如果有必要,你盡可能將一切方法都定為private

 

public類在其它包中是訪問不到的。

 

所有默認包的類都是屬於同一個包,盡管它們在不同的文件夾下面。

 

private,只允許本類所有對象可訪問,其他任何類都不能訪問,哪怕是它的子類或同一包中的類都不行。

 

如果一類繼承自不同包中的類,則該子類不能繼承與訪問父類中的“包訪問權限或默認訪問權限”的屬性與方法,只能繼承與訪問protectedpublic的屬性與方法。

 

設計一個類時一般按照publicprotected、默認、private的順序來定義屬性與方法,這樣讓人最先注意到公開的,當然,我們一般把接口與實現分離更好,這樣用戶只關心他所需要的接口,而對實現不必理會。

 

控制對成員的訪問權限有兩個原因:第一是為了使用戶不必關心那此不必關心的private部分,而只關心類提供了哪些服務接口,即public修飾的部分。第二就是讓類庫設計者可以更改類的內部工作方式,而不必擔心這樣會客戶程序產生重大的影響。

 

>>>關於protected修飾符<<<

 

定義規則前,我這里約定有三個類,一個是Base類,一個是Base類的子類Sub類,一個是Sub類的子類SubSub類,另一個是Other類且與BaseSubSubSub沒有繼承關系,並假設Base中有protected方法與屬性,都叫YYY吧。

 

先看看protected規則:首先要搞清楚什么叫訪問?這里在講到的訪問是有二種的:

一、就是在類中通過“XXX x = new XXX(); x.YYY;”的形式來訪問(不妨叫此種形式為“外部訪問”吧,此種訪問形式除了可以應用到自己與子類中外,還可以應用在其他類中訪問,其中XXX表示定義的類型,這里可為BaseSubSubSubYYY為方法或屬性);

二、就是this.YYY的形式來訪問(不妨叫此種形式為“內部訪問”吧,不過這種訪問形式只能應用在在自己的類或是子類中)。

 

protected方法與屬性可訪問的地方有三個:

1.         在自己的類Base中:上面的“XXX x = new XXX(); x.YYY;”與“this.YYY”兩種訪問形式都可以訪問的到自己定義的portected方法或屬性;

2.         二是子類SubSubSub中,這要分三種訪問方式:

a.         SubSubSub 中的“this.YYY”內部訪問形式:在此種方式形式下,不管是否重寫或重新定義過父類Baseprotected方法與屬性,子類SubSubSub一定可以訪問的。

b.         SubSubSub 中“Base x = new XXX (); x.YYY;”外部訪問形式:此種形式就不一定的能訪問的到了,這要看父類Base與子類SubSubSub是否在同一包(注意,此時與是否重寫或重新定義過這些protedted方法與屬性沒有關系);

c.         SubSub 中“Sub x = new XXX (); x.YYY; 外部訪問形式:此種訪問形式能否訪問關鍵看Sub是否重寫或重新定義過Base的屬性與方法:

                        i.              如果重寫或重新定義過,則看SubSubSub是否在同包中

                      ii.              如果沒有,則看BaseSubSub是否在同包中

3.         在其他類Other中:此時只支持外部訪問形式,不過到底是要求OtherBase同包還是要求OtherSub同包,則要依你訪問方式而定了:

a.         如果是通過父類引用“Base x = new XXX (); x.YYY;”形式來訪問的,則要求OtherBase同包;

b.         如果是通過子類引用“Sub x = new Sub (); x.YYY;”形式來訪問的,情況又會比較復雜了,此時關鍵是看子類Sub是否重寫或重新定義過父類Base中的protected方法與屬性:

                        i.              如果重寫或重新定義過了,則要求OtherSub同包即可;

                      ii.              如果沒有重寫或重新定義過了,則要求OtherBase同包即可;

 

規則總結肯定有遺漏的地方,不過我只想到這些,希望大家一起看看。看文字比較繞,下面來應用上面的規則看看這些實例也許會好理解一些:

                                 

package pk1.a;

public class Base {

       protected int i = 1;

       protected void protect() {

              System.out.println("Base::protect");

       }

}

 

package pk1.a;

import pk1.b.Sub;

public class SubSub extends Sub {

       void g() {

              Sub s = new SubSub();

              //!! s.protect();//規則2.c.i

              System.out.println(s.i);//規則2.c.ii

       }

}

 

package pk1.b;

import pk1.a.Base;

public class Sub extends Base {

       private void prt() {}

       protected void protect() {

              System.out.println("Base::protect");

       }

       void f() {

              //規則2.a

              this.protect();

              this.i = 2;

 

              //規則2.b

              Base a2 = new Sub();

              //!! a2.protect();

              //!! System.out.println(a2.i);

 

              //規則1

              Sub b = new Sub();

              b.protect();

              b.i = 1;

              b.prt();

       }

}

 

package pk1.b;

public class SubSub extends Sub {

       void g() {

              Sub s = new SubSub();

              s.protect();//規則2.c.i

              //!! System.out.println(s.i);//規則2.c.ii

       }

}

 

package pk1.c;

import pk1.a.Base;

import pk1.b.Sub;

public class SubSub extends Sub {

       void g() {

              this.protect();//規則2.a

 

              //規則2.b

              Base b = new SubSub();

              //!! b.protect();

              //!! System.out.println(b.i);

 

              //規則2.b

              Sub s = new SubSub();

              //!! s.protect();

              //!! System.out.println(s.i);

 

       }

}

 

package pk2.a;

public class Base {

       protected int i = 1;

 

       protected void protect() {

              System.out.println("Base::protect");

       }

}

 

package pk2.a;

import pk2.b.Sub;

public class Other {

       void g() {

              //規則3.a

              Base b = new Sub();

              b.protect();

              System.out.println(b.i);

 

              //規則3.b.ii

              Sub s = new Sub();

              s.protect();

              System.out.println(s.i);

       }

}

 

package pk2.b;

import pk2.a.Base;

public class Other {

       void g() {

              //規則3.a

              Base b = new Sub();

              //!! b.protect();

              //!! System.out.println(b.i);

 

              //規則3.b.ii

              Sub s = new Sub();

              //!! s.protect();

              //!! System.out.println(s.i);

       }

}

 

package pk2.b;

import pk2.a.Base;

public class Sub extends Base {}

 

package pk3.a;

import pk3.b.Sub;

public class Base {

       protected int i = 1;

       protected void protect() {

              System.out.println("Base::protect");

       }

      

       static protected int i_s = 1;

       static protected void protect_s() {

              System.out.println("Static:Base::protect");

       }

      

       void f() {

              //!! Sub.i_s = 2; //規則3.b.i

              Sub.protect_s();//規則3.b.ii

       }

}

 

package pk3.a;

import pk3.b.Sub;

public class Other {

       void g() {

              Sub s = new Sub();

              //!! s.protect();//規則3.b.i

              System.out.println(s.i);//規則3.b.ii

       }

 

       void f() {

 

              //!! Sub.i_s = 2; //規則3.b.i

              Sub.protect_s();//規則3.b.ii

 

              Base.i_s = 2;//規則3.a

              Base.protect_s();//規則3.a

 

       }

}

 

package pk3.b;

import pk3.a.Base;

public class Other {

       void f() {

              Sub.i_s = 2;//規則3.b.i

              //!! Sub.protect1();//規則3.b.ii

             

              //!! Base.i1 = 2;//規則3.a

              //!! Base.protect1();//規則3.a

       }

}

 

package pk3.b;

import pk3.a.Base;

public class Sub extends Base {

       protected void protect() {

              System.out.println("Base::protect");

       }

       static protected int i_s = 2;

 

       void f() {

             

              /*

               * 在子類中可能通過子類類型或父類類型來來訪問父類中protected靜態

               * 成員,而不管子類與父類是否在同一包中,或是子類重新定義了這些成員

               *

               * 注,在父類或子類中訪問時后面的規則不再適用

               */

              System.out.println(Sub.i_s);//2

              Sub.protect_s();

      

              System.out.println(Base.i_s);//1

              Base.protect_s();

       }

}

 

 

 

最后,通過上面的規我們可以很好的解釋Object.clone()類似的問題:Object.clone()訪問修飾符為protected,如果某個類沒有重寫此方法,則Object中的clone()方法除被自己與子類能調用方法外,其他不管與這個類在同一包還是不同包都是不可見的,因為未重寫,還是屬於Object中的方法,又Objectjava.lang包中,與我們定義的包又不在java.lang包中,所以不能訪問到(這也與你在在程序里定義了Object o = new Object();你還是不能在當前類中調用o.clone();一樣)。所以如果要能被不同包中的非子類克隆,則需重寫Object.clone()並設置訪問權限為public(如果重寫后還是protected,則還是只能被同一包訪問)。

 

 

 

 

 

package a;

public class A {

       protected void f() {}

      

       void g() {}

      

       private void p(A a) {}

      

       private void m(A a) {

              p(a);

       }

 

       public static void main(String[] args) {

              A a = new A();

              a.m(a);

       }

}

 

package b;

import a.A;

public class B extends A {

       protected void f() {}

 

       private void p() {}

 

       public void call1(B b) {

              b.f();// 此種訪問方式屬於訪問同類中方式,是沒有問題的

              f();// 屬於訪問自已的方法        

              // !! g();// 父類中的包訪問權限方法不能被子類繼承訪問

              b.p();//在同一類中是可以訪問私有方法的,訪問控制是針對整個類來說的,而不是某個對象

       }

 

       public void call12(A a) {

              /*

               * 即使是f()是父類中的protected方法,並且子類還重寫過了 ,但

               * 是這種以父類類型實例訪問父類中的protected方式屬於不同包非

               * 類訪問方式,所以不能訪問,即使訪問的代碼在子類中!

               */

              // !! a.f();

              A a1 = new B();

              // 此種訪問與上面是一樣也不能編譯

              // !! a1.f();

             

             

       }

 

       public static void main(String[] args) {

              B b = new B();

              b.f();

 

              A a = new B();

              // 與上面 call2方法中的調用一樣,也是不能訪問的

              // !! a.f();

       }

}

 

第七章復用

代碼復用可分為組合復用與繼承復用。

 

屬性成員初始化的4個時機:定義時、構造器中、塊中、使用時。

 

main方法所在的類不一定要是public的(但方法一定要定義成public,如果不是public方法,則啟動時會報找不到main方法)為包訪問也是可以的,照樣可以用作程序入口。另外,main方法也可以由另一個main方法來調用,這也是允許的。

 

繼承並不只是復制基類的接口,當創建了一個子類的對象時,該對象包含了一個基類的子對象,這個子對象與你用基類直接創建的對象是一樣的,二者區別在於,后者來自於外部,而基類的子對象被包裝在子類對象內部。

 

當我們親自處理清理時,要加以小心,因為,一旦涉及垃圾回收,能夠信賴的事就不會很多了,垃圾回收器可能永遠也無法被調用,即使用被調用,它可能以任何它想要的順序來回收對象。最好的辦法是除了內存以外,不能依賴垃圾回收器去做任何事,如果需要進行清理,最好編寫你自己的清理方法,但不要使用finalize(),最好是在finally塊調用自己的清理方法。

 

組合和繼承都允許在新的類中放置子對象,組合是顯示地這樣做,而繼承則是隱藏地做。

 

組合技術通常用於想在新類中使用現有類的功能而非它的接口這種情形。即,在新類中嵌入某個對象,讓其實現所需要的功能,但新類的用戶看到的只是為新類所定義的接口,而非所嵌入對象的接口,一般需在新類中嵌入一個現有類的private對象。

 

向上轉型是從一個較專用類型向較通用類型轉換,所以總是很安全的。向上轉型的過程中,類接口唯一可能發生的事情是丟失方法,而不能訪問它們。

 

到底是該用組合還是用繼承,一個最清晰的判斷辦法就是問一問自己是否需要從新類向基類進行向上轉型。如果必須向上轉換,則繼承是必要的,但如果不需要,則應當好好考慮自己是否需要繼承。

 

使用staticfinal域只占據一段不能改變的存儲空間。

 

final數據在編譯時可能還不能確定其值,比如值是通過某個方法計算才能得到的,需在運行時才能確定。

 

對於編譯期常量這種情況,編譯器可以將該常量值代入任何可能用到它的計算式中,也就是說,可以在編譯時執行計算式,這可減輕一些運行時的負擔。在Java中,這類常量必須是基本數據類型。

 

Java早期實現中,如果將一個方法指明為final,就是同意編譯器將針對該方法的所有調用都轉為內嵌調用。當編譯器發現一個final方法調用命令時,它會跳過插入程序代碼這種正常方式而執行方法調用機制,並且以方法體中的實際代碼的副本來替代方法調用,這將消除方法調用的開銷。當然,如果方法本身很大,這種效率提升不是很明顯,反而可能下降。在最近的Java版本中,虛擬機可以探測到這些情況,並優化去掉這些效率反而降低的額外的內嵌調用,因此不再需要使用final方法來進行優化了,事實上,這種做法正在逐漸受到勸阻,在使用JavaSE5/6時,應該讓編譯器和虛擬機去處理效率問題,只有在想要明確禁止覆蓋時,才將方法設置為final

 

類中所有的private方法都隱式地指定為final,可以對private方法添加final修飾詞,但這並不能給該方法增加任何額外的意義。

 

final類中定義的屬性成員可以是final的,也可以不是的。但是,所有final類的中方法默認都是final的,因此在final類中的方法前添加final修飾符沒有什么意義。

 

有關finalfinallyfinalize區別請參考XXX

第八章多態

多態通過分離做什么和怎么做,從另一角度將接口和實現分離開來,多態不但能夠改善代碼的組織結構和可讀性,還能夠創建可擴展的程序。

 

前期綁定在編譯時期(如C),后期綁定是在運行時期(即多態,C++Java),多態得程序的后期擴展。

 

Java中除了static方法和final方法(private方法屬於final方法),其他所有的方法都是后期綁定。這意味着通常情況下,我們不必判定是否應該進行后期綁定——它會自動發生。

 

final方法會關閉動態綁定。

 

類的構造方法隱式的為static,它們實際上是static方法。

 

如果父類構造函數拋出異常,子類構造函數一定要拋出,不能被捕獲。

 

覆寫/重寫父類方法時,子類方法異常處理有以下幾種:

import java.io.IOException;

public class Parent {

       public void overwrite(int i) throws IOException {}

       public void overwrite() throws NullPointerException {}

}

 

import java.io.ObjectStreamException;

class SubSub extends Parent {

       /*

        * 父類拋出捕獲型異常,子類卻拋出運行時異常,這是可以,因為

        * 拋出運行時就相當於沒有拋出任何異常

        */

       public void overwrite(int i) throws RuntimeException {}

       /*

        * 如果父類拋出的是非捕獲型異常,則子類可以拋出任意非捕獲型異

        * 常,沒有擴大異常范圍這一問題

        */

       public void overwrite() throws RuntimeException {}

}

 

class Sub2 extends Parent {

       //也可以不拋出任何異常

       public void overwrite(int i) {}   

       /*

        * 如果父類拋出的是非捕獲異常,子類也可以不用拋出,這與父類為捕

        * 獲型異常是一樣的

        */  

       public void overwrite() {}

}

 

class Sub3 extends Parent {

       //如果拋出的是捕獲型異常,則只能是IOException的子類

       //!!public void overwrite(int i) throws ObjectStreamException{}   

       /*

        * 如果父類拋出的是非捕獲異常,子類就不能拋出任何捕獲型異常,因

        * 為這樣會擴大異常的范圍

        */

       //!! public void overwrite() throws IOException {}

}

 

 

組合更加靈活,因為它可以動態選擇類型,運行時可以動態的修改具體的行為,而繼承在編譯時就知道確切類型。“用繼承表達行為間的差異,並用字段(組合)表達狀態上的變化。”下面實例中,兩者都用到了:通過繼承得到了兩個不同的類,用於表達act()方法的差異;而Stage通過運用組合使用自己的狀態發生變化。

//演員

class Actor {

  public void act() {}

}

 

//繼承(is-a關系):HappyActor(喜劇演員)確實是一種Actor(演員)

class HappyActor extends Actor {

  public void act() { System.out.println("HappyActor"); }

}

 

//繼承(is-a關系):HappyActor(悲劇演員)確實是一種Actor(演員)

class SadActor extends Actor {

  public void act() { System.out.println("SadActor"); }

}

 

//舞台

class Stage {

  private Actor actor = new HappyActor();//組合(has-a關系):舞台上有演員

  public void change() { actor = new SadActor(); }

  public void performPlay() { actor.act(); }

}

 

public class Transmogrify {

  public static void main(String[] args) {

    Stage stage = new Stage();

    stage.performPlay();

    stage.change();//運行過程中改變具體性為

    stage.performPlay();

  }

} /* Output:

HappyActor

SadActor

*///:~

 

private方法不能被覆寫,即使子類覆寫也不會起作用,根據調用它的引用類型來調用相應的方法,實質上private方法就是final方法,所以在編譯期就已確定調用哪個方法。

public class PrivateOverride {

  private void f() { System.out.println("private f()"); }

  public static void main(String[] args) {

    PrivateOverride po = new Derived();

    po.f();

  }

}

 

class Derived extends PrivateOverride {

  public void f() { System.out.println("public f()"); }

} /* Output:

private f()

*///:~

 

 

只有非靜態的方法才有可能構造多態,屬性成員(即使是public)與靜態方法不會造成多態,即使用子類重寫了這些屬性成員與靜態方法,因為這此是在編譯期進行解析的,而不是在運行時間確定的。因此在調用屬性成員與靜態方法時只與調用它的引用類型相關。

class Super {

       public int field = 0;

 

       public int getField() {

              return field;

       }

}

 

class Sub extends Super {

       public int field = 1;

 

       public int getField() {

              return field;

       }

 

       public int getSuperField() {

              return super.field;

       }

}

 

public class FieldAccess {

       public static void main(String[] args) {

              Super sup = new Sub(); // Upcast

              System.out.println("sup.field = " + sup.field + ", sup.getField() = "

                            + sup.getField());

              Sub sub = new Sub();

              System.out.println("sub.field = " + sub.field + ", sub.getField() = "

                            + sub.getField() + ", sub.getSuperField() = "

                            + sub.getSuperField());

       }

} /*

* Output:

* sup.field = 0, sup.getField() = 1

* sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

*/// :~

上面實例中,為Super.fieldSub.field分配了不同的存儲空間。這樣,Sub實際上包含兩個稱為field的屬性成員:它自己的和它從Super類中得到的。因此在子類中要得到父類中的同名屬性成員時,則要使用super.field來獲取。

class StaticSuper {

  public static String staticGet() {

    return "Base staticGet()";

  }

  public String dynamicGet() {

    return "Base dynamicGet()";

  }

}

 

class StaticSub extends StaticSuper {

  public static String staticGet() {

    return "Derived staticGet()";

  }

  public String dynamicGet() {

    return "Derived dynamicGet()";

  }

}

 

public class StaticPolymorphism {

  public static void main(String[] args) {

    StaticSuper sup = new StaticSub(); // Upcast

    System.out.println(sup.staticGet());

    System.out.println(sup.dynamicGet());

  }

} /* Output:

Base staticGet()

Derived dynamicGet()

*///:~

 

 

如果自己處理清理動作,則銷毀的順序應該和初始化順序相反。對於字段,則意味着與聲明的順序相反(因為字段的初始化是按照聲明的順序進行的)。對於基類,應該首先對其子類進行清理,然后才是基類,這是因為子類的清理可能會調用基類中的某些方法,所以需要使用基類中的構件仍起作用而不應過早地銷毀它們,這與C++中的析構函數形式是一樣的。

 

 

在父類的構造函數中調用多態方法時,可能會引發問題:

class Glyph {

       void draw() {

              System.out.println("Glyph.draw()");

       }

 

       Glyph() {

              System.out.println("Glyph() before draw()");

              draw();//在父類構造函數中調用多態方法

              System.out.println("Glyph() after draw()");

       }

}

 

class RoundGlyph extends Glyph {

       private int radius = 1;

 

       RoundGlyph(int r) {

              radius = r;

              System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);

       }

 

       void draw() {

              System.out.println("RoundGlyph.draw(), radius = " + radius);

       }

}

 

public class PolyConstructors {

       public static void main(String[] args) {

              new RoundGlyph(5);

       }

} /* Output:

Glyph() before draw()

RoundGlyph.draw(), radius = 0

Glyph() after draw()

RoundGlyph.RoundGlyph(), radius = 5

*///:~

在構造函數內唯一能夠安全調用的那些方法是基類中的final方法(當然也適用於private方法,它們自動屬於final方法)。

 

 

子類覆蓋父類方法時,子類的返回類型可以是父類返回類型的某個子類型,但返過來不行。

注,沒有父子關系時返回類型要一樣,如果允許子類返回類型是父類返回類型的子類型,則要求在J2SE5或以上版本

class Grain {}

class Wheat extends Grain {}

 

class Mill {

       Grain p() { return new Grain(); }

       Wheat f() { return new Wheat(); }

       float g() { return 0;}

}

 

class WheatMill extends Mill {

       Wheat p() { return new Wheat(); }

      

       //子類覆蓋父類方法時返回類型不能比子類寬

       //!! Grain f() { return new Grain(); }

      

       //雖然int可以隱式的轉換成float,但int不是float的子類,所以編譯不能通過

       //!! int g() { return 0;}

}

 

 

向上轉型是安全的,因為基類不會具有大於子類的接口,因此,通過基類接口發送的消息保證都能被接;由於向下轉型是不安全的,所以在Java語言中,所有向下轉型都會得到檢查!所以即使我們只是進行一次普通賓加括弧形式的類型轉換,但在進入運行期時仍然會對其進行檢查,以便保證它的確是我們希望的那種類型,如果不是,運行時就會拋出一個ClassCastException的類型轉換異常,這種在運行期間對類型進行檢查的行為稱作“運行時類型識別”(RTTI),這種我們也可以通過編程的方式來保證,在后面反射章節我們會看到。

 

第九章接口

一個類同時繼承與實現一個類與接口時,extends要在implements前。

 

接口中的方法前可以加abstract關鍵字,也可以不加,這都將是一種抽象方法。

 

接口中可以什么都沒有,這時是一個標志性接口,如Serializable接口。

 

接口其實是一種很特殊的抽象類,是一種純的抽象類。

 

接口中方法前不能用static關鍵字,所以接口中沒有靜態的方法。

 

abtract關鍵詞不能與staticprivate同時使用。

 

接口與抽象類不能用final來修飾。

 

接口中的有成員都是public,即使沒有明確指出。

 

抽象類中抽象方法前默認訪問修飾符為包訪問限制,而不是public,這不象接口抽象方法前一定是public

 

接口中不能有具體的方法,接口中的數據成員默認為public static final,數據成員如果寫上訪問控制符,只能是public,接口中的方法默認為public abstract,但可以省略。

 

接口中的數據成員一定要初始化,因為他是final型,但也只能在定義時就初始化,不能在靜態區中賦值,因為在接口中是不能定義靜態與非靜態塊的。

 

>>>>完全解耦<<<<

只要一個方法操作的是類而非接口,那么你就只能使用這個類及其子類。如果你想要將這個方法應用於不在此繼承結構中的某個類,那么你就會發現在不修改當然程序應用的情況下很難做到功能動態的擴展。但接口可以在很大程度上具有靈活性,因此,它使得我們可以編寫可復用性更好的代碼。

 

下面是程序的第一個版本:

剛開始時沒有考慮到后期會有不同Processor處理器擴展,對當然應用來說,它已經是完美的了,程序基本上已做到了可擴展,我們可以隨時添加一種其它字符處理器。

//處理器類

class Processor {

   public String name() {

      return getClass().getSimpleName();

   }

 

       Object process(Object input) {

      return input;

   }

}

 

//大寫處理器

class Upcase extends Processor {

       String process(Object input) {

      return ((String) input).toUpperCase();

   }

}

 

//小寫處理器

class Downcase extends Processor {

       String process(Object input) {

      return ((String) input).toLowerCase();

   }

}

 

//應用

public class Apply {

   public static void process(Processor p, Object s) {

      System.out.println("Using Processor " + p.name());

      System.out.println(p.process(s));

   }

 

   public static String s = "Disagreement with beliefs is by definition incorrect";

 

   public static void main(String[] args) {

      process(new Upcase(), s);

      process(new Downcase(), s);

   }

}/* Output:

Using Processor Upcase

DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT

Using Processor Downcase

disagreement with beliefs is by definition incorrect

*///:~

上面例子中,Apply.process()方法可以接受任何類型的Processor,並將其應用到一個Object對象上,然后輸出結果,在本例中,創建一個能夠根據所傳遞參數對象的不同而具有不同行為的方法被稱為策略模式。這一類方法包含所要執行的算法是不變的,在這里不變的為Apply.process方法中的算法,而“策略”包含了變化的部分。策略就是傳遞進去的參數對象,它包含了動態執行的代碼,在這里Processor對象就暗一個策略類,所有Processor類型的對象都能應用到Apply.process中。

 

下面我們來看看另一種與Processor功能相似的過濾器Filter(濾波器):

//波形

public class Waveform {

  private static long counter;

  private final long id = counter++;

  public String toString() { return "Waveform " + id; }

}

 

//濾波器,與Processor接口相同

public class Filter {

  public String name() {

    return getClass().getSimpleName();

  }

  public Waveform process(Waveform input) { return input; }

}

 

//低通濾波器

public class LowPass extends Filter {

  double cutoff;

  public LowPass(double cutoff) { this.cutoff = cutoff; }

  public Waveform process(Waveform input) {

    return input; // 什么都不做,直接返回

  }

}

 

//高通濾波器

public class HighPass extends Filter {

  double cutoff;

  public HighPass(double cutoff) { this.cutoff = cutoff; }

  public Waveform process(Waveform input) { return input; }

}

上面是另一種處理器,與第一個版本“字符處理器”用途相當,都是用來過濾的,這就意味着我們能否重用Apply.process()代碼呢?第一個版本的Processor肯定是不行的,因為Filter並非繼承自Processor(假設它來自於其它廠商),因為Filter類的創建者壓根不清楚你想要將它用作Processor,因此你不能將Filter用於Apply.process()方法。這主要原因是Apply.process()Processor之間的耦合過緊,已經超出了所需要的程度,這就無法復用Apply.process()的代碼。

但是,如果Processor是一個接口,那么這些限制就會變得寬松,就可以重用Apply.process()方法算法。

 

下面是ProcessorApply的修改版本:

//處理器接口,只要是處理器,都會遵循此接口,那么就可以重用Apply.process

public interface Processor {

  String name();

  Object process(Object input);

}

 

public class Apply {

  public static void process(Processor p, Object s) {

    System.out.println("Using Processor " + p.name());

    System.out.println(p.process(s));

  }

}

 

下面是重寫“字符處理器”后的應用:

//字符處理器,實現處理接口

public abstract class StringProcessor implements Processor{

  public String name() {

    return getClass().getSimpleName();

  }

  //處理接口

  public abstract String process(Object input);

  public static String s ="If she weighs the same as a duck, she's made of wood";

  public static void main(String[] args) {

    Apply.process(new Upcase(), s);

    Apply.process(new Downcase(), s);

  }

 

//字符大寫處理器

class Upcase extends StringProcessor {

  public String process(Object input) {

    return ((String)input).toUpperCase();

  }

}

 

//字符小寫處理器

class Downcase extends StringProcessor {

  public String process(Object input) {

    return ((String)input).toLowerCase();

  }

}/* Output:

Using Processor Upcase

IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD

Using Processor Downcase

if she weighs the same as a duck, she's made of wood

*///:~

 

下面來將Filter應用到Apply.process()方法中。由於在前面Filter已經開發完畢,假如我們現在不能修改Filter的源碼,在這種情況下,我們可以使用適配器設計模式:

//適配器,適配成Processor接口

class FilterAdapter implements Processor {

  Filter filter;//對象適配器

  public FilterAdapter(Filter filter) {

    this.filter = filter;

  }

  public String name() { return filter.name(); }

  public Waveform process(Object input) {

    return filter.process((Waveform)input);

  }

 

//重用Apply.process算法

public class FilterProcessor {

  public static void main(String[] args) {

    Waveform w = new Waveform();

Apply.process(new FilterAdapter(new LowPass(1.0)), w);

w = new Waveform();

    Apply.process(new FilterAdapter(new HighPass(2.0)), w);

  }

} /* Output:

Using Processor LowPass

Waveform 0

Using Processor HighPass

Waveform 1

*///:~

在這種使用適配器的方式中,FilterAdapter的構造器接受接口Filter,然后返回你所需要的Processor接口對象,另外,在FilterAdapter類中用到了代理。

 

將接口從具體實現中解耦使得接口可以應用於多種不同的具體實現,因此代碼也就更具可復用性了。

 

>>>>Java中的多重繼承<<<<

有時我們需要“一個X是一個A和一個B以及一個C”,在C++中,組合多個類的接口的行為被稱為“多重繼承”,這可能會出現隱藏的問題,因為每個類都有一個具體實現,但在Java中,你只能繼承一個類,因些,組合多個接口,C++中的問題是不會在Java發生的。

 

如果一個類繼承了一個類,同時實現了某個接口,父類中有一個方法與接口中的方法簽名是一樣,則這個類默認就已實現了這個接口,無需再實現接口中的方法,這就是默認適配器模式。

 

我們應該如何選擇接口還是抽象類?如果要創建不帶任何方法定義和成員變量的基類,那么就應該選擇接口而不是抽象類,一般接口靈活性高。

 

接口(interface)可以繼承(extends)多個接口,但只能繼承一個類。

 

組合多個接口時名字沖突:
interface I1 { void f(); }

interface I2 { int f(int i); }

interface I3 { int f(); }

class C { public int f() { return 1; } }

 

class C2 implements I1, I2 {

  public void f() {}

  public int f(int i) { return 1; } // 重載

}

 

class C3 extends C implements I2 {

  public int f(int i) { return 1; } // 重載

}

 

class C4 extends C implements I3 {

  // 重寫C中的方法,實現I3接口中的方法,這里可以省去該方法

  public int f() { return 1; } //重寫

}

 

// 僅根據返回類型不同是不行的

//! class C5 extends C implements I1 {}

//! interface I4 extends I1, I3 {}

在打算組合的不同接口中使用相同的方法名通常會造成代碼可讀性混亂,請盡量避免這種情況。

 

 

>>>>嵌套接口<<<<

class A {

  interface B {//包訪問權限接口

    void f();

  }

  public class BImp implements B {

    public void f() {}

  }

  private class BImp2 implements B {

    public void f() {}

  }

 

  public interface C {//公有訪問權限接口

    void f();

  }

  class CImp implements C {

    public void f() {}

  }

  private class CImp2 implements C {

    public void f() {}

  }

 

  private interface D {//私有接口

    void f();

  }

  private class DImp implements D {

    public void f() {}

  }

  public class DImp2 implements D {

    public void f() {}

  }

 

  //雖然方法是公有的,但返回類型卻是私有的,所以

  //該方法只能被有權的對象調用它,如:receiveD

  public D getD() { return new DImp2(); }

  private D dRef;

  public void receiveD(D d) {

    dRef = d;

    dRef.f();

  }

 

interface E {

  // 接口中的內部接口默認就是 "public":

  interface G {

    void f();

  }

  // 多余的 "public":

  public interface H {

    void f();

  }

  void g();

  // 接口中的內部接口只能是public

  //! private interface I {}

  //! protected interface I {}

}

 

public class NestingInterfaces {

  public class BImp implements A.B {

    public void f() {}

  }

  class CImp implements A.C {

    public void f() {}

  }

  // 不能實現私有接口,即private接口不能在定義它的類外實現

  //! class DImp implements A.D {

  //!  public void f() {}

  //! }

  //實現某個接口時,不一定要實現其嵌套接口

  class EImp implements E {

    public void g() {}

  }

  class EGImp implements E.G {

    public void f() {}

  }

  class EImp2 implements E {

    public void g() {}

    class EG implements E.G {

      public void f() {}

    }

  }

  public static void main(String[] args) {

    A a = new A();

    // 不能訪問 A.D:

    //! A.D ad = a.getD();

   

    // 能這樣,雖然返回的是私有類型,但外界沒有訪問

    a.getD();

   

    //雖然a.getD()返回的是私有類型A.D,但這里

    //強轉成公有類型A.DImp2,這是沒有問題

    A.DImp2 di1 = (A.DImp2) a.getD();

   

    // 即使A.Dpublic,但不能將父類型賦值給子類型,除非強轉

    //! A.DImp2 di2 = a.getD();

   

    // 因為a.getD()返回的是私有類型A.D,所以不能訪問

    //! a.getD().f();

   

    //這樣也是可以的

    ((A.DImp2)a.getD()).f();

   

    //a.getD()返回的私有類型A.D對象交給有權使用它的對象

    A a2 = new A();   

    a2.receiveD(a.getD());

  }

}

 

第十章內部類

內部類對象可以直接訪問外圍對象的所有成員(包括私有的),而不需要任何特殊條件,就像調用自己的方法與屬性成員一樣。但外圍類不能直接訪問內部類中的方法,除非使用內部類的實例來訪問(也能訪問私有的)。

 

內部類自動擁有對其外圍類所有成員的訪問權。這是如何做到的呢?當某個外圍類的對象創建了一個內部類對象時,此內部對象會有一個指向外圍類對象的引用,然后在你訪問此外圍類的成員時,就是用那個引用來選擇外圍類的成員,編譯器會幫你處理所有的細節。注,這只限於非靜態的內部類。

 

構建內部類對象時,需要一個指向其外圍類對象的引用,如果編譯器訪問不到這個引用就會報錯。

 

靜態的內部類也叫嵌套類。

 

匿名內部類與普通的內部類繼承相比是有些受限的,雖然匿名內部類既可以繼承類,也可以實現接口,但是不能兩者兼備,而且如果實現接口,也只能實現一個接口

 

非靜態的內部類中不能定義static成員,但final staitc成員是可以的。因為一個成員類實例必然與一個外部類實例關聯,這個static定義完全可以移到其外部類中去。

 

內部(類中或接口中)接口一定static的。

 

接口中的內部類一定是publicstatic,但不一定是final(與數據成員不同),因為省略final時可以被繼承

 

非靜態的內部類里不能定義接口,因為內部接口(嵌套接口)默認為static,是無法改變的,所以內部類里的接口只能定義在靜態的內部類里面。

 

方法與塊里定義的內部類只能是非靜態的,不能加static,所以局部內部類里只能定義非靜態的成員。局部內部類也能直接訪問外部內所有成員。

 

靜態的內部類可以定義非靜態與靜態的東西。

 

不能在靜態內部類中直接(實例化外部類再訪問是可以的)訪問外部類中的非靜態成員與方法。而非靜態內部類是可以訪問外部類的靜態成員。

 

在方法與作用域內都可定義內部類。如果一個內部類在if條件語句中定義,則不管條件是否成立都會編譯成類,但這個類的使用域只限於該if語句中,if外面不能訪問。

 

設計模式總是將變化的事物與保持不變的事物分離開,比如模板方法模式中,模板方法是保持不變的事物,而可覆蓋的方法就是變化的事物。

 

內部類不能被重寫:父類中的內部類不能被子類中的同名內部類重寫。

 

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

 

一個類對外提供一個公共接口的實現(接口與實現完全分離,而且隱藏了實現)是內部類的典型應用,以JDK Collection類庫為例,每種Collection的實現類必須提供一個與其對應的Iterator實現,以便客戶端能以統一的方式(Iterator接口)遍歷任一Collection實例。每種Collection類的Iterator實現就被定義為該Collection類的私有的內部類。

 

內部類作用:

1、內部類方法可以訪問該類所在的作用域中的數據,包括私有數據。

2、內部類可以對同一個包中的其他類隱藏起來。

3、當想要定義一個回調函數且不想編寫大量代碼時,使用匿名內部類比較便捷。

4、實現多繼承。

 

 

 

>>>內部類機制<<<

如果有如下內部類:

public class Outer {

   private boolean b;

   private int i;

 

   //內部類

   private class Inner {

      private boolean b;

      Inner(boolean b) {

         this.b = b;

      }

      public void print() {

         System.out.println(Outer.this.b & this.b);

      }

   }

 

   public void f(final String str) {

      class Inner {//局部內部類

         public void print() {

            System.out.println(Outer.this.i);

         }

      }

   }

}

 

我們來使用Reflection反射外部及內部類,看看內部類是怎樣訪問外部類成員的:

D:\work\Test\bin>java Reflection Outer

class Outer

{

//構造器

public Outer();

//字段

private boolean b;

private int i;

//方法

static boolean access$0(Outer);// 內部類Outer$Inner通過該方法訪問外部類b成員,這樣就可以訪問一個私有成員了

static int access$1(Outer); // 局部類Outer$1Inner通過該方法訪問外部類i成員

public void f(java.lang.String);

}

 

class Outer$Inner

{

//構造器

Outer$Inner(Outer, boolean);// 在編譯期會自動傳入外部類實例

//字段

private boolean b;

final Outer this$0;//指向外部類實例

//方法

public void print();

}

 

class Outer$1Inner

{

//構造器

  Outer$1Inner(Outer, java.lang.String);//第二個參數是引用的final類型的局部變量,也是通過構造器傳入的

//字段

final Outer this$0;

private final java.lang.String val$str;//存儲引用的局部final類型變量

//方法

public void print();

}

 

 

 

 

 

非靜態內部類創建方式:

OuterClassName.InnerClassName inner = new OuterClassName().new InnerClassName();

 

靜態內部類創建方式:

OuterClassName.InnerClassName inner = new OuterClassName.InnerClassName();

 

繼承內部類語法規則:

class WithInner {

  class Inner {}

}

 

public class InheritInner extends WithInner.Inner {

  //! InheritInner() {} // 不能編譯

  /*

   * 這里的superInheritInner類的父類WithInner.Inner

   * 的默認構造函數,而不是WithInner的父類構造函數,這

   * 種特殊的語法只在繼承一個非靜態內部類時才用到,表示

   * 繼承非靜態內部類時,外圍對象一定要存在,並且只能在

   * 第一行調用,而且一定要調用一下。為什么不能直接使用

   * super()或不直接寫出呢?最主要原因就是每個非靜態的內部類

   * 都會與特有的一個外圍類實例對應,這個外圍類實例是運行時傳到內部類里去的,所以在內部類里可以直接使用那個對象(比如Outer.this),但這里是在外部內外,使用時還是需要存在外圍類實例對象,所以這里就顯示的通過構造器傳遞進來,並且在外圍對象上顯示的調用一下內部類的構造器,這樣就確保了在繼承至一個類部類的情況下,外圍對象一類會存在這樣一個約束。

   */

  InheritInner(WithInner wi) {

    wi.super();

  }

  public static void main(String[] args) {

    WithInner wi = new WithInner();

    InheritInner ii = new InheritInner(wi);

  }

}

 

 

 

靜態內部類里可以使用this(而不像靜態方法或塊里是不能使用this的),此時的this指向靜態內部類,而不是外部類,下面為LinkedList類里的內部類,代表一個節點的實現:

private static class Entry {

   Object element;

   Entry next;

   Entry previous;

 

       Entry(Object element, Entry next, Entry previous) {

       this.element = element;

       this.next = next;

       this.previous = previous;

   }

}

 

在非靜態的內部類里訪問外圍類相同屬性成員時,需在this前加上外圍類型(采用Outer.this.XX 來訪問,其中Outer為外圍類的類型),一般在訪問自己成員時,可以省略this前自身的類型,但默認應該是有的:

public class Outer {

   private int i = 1;

   public void f(){

      System.out.println("f Outer.this.i=" + Outer.this.i);//1

   }

  

   private /*static*/class Inner {

      private int i = 2;

      public void p() {

         System.out.println("p this.i=" + this.i);//2

         System.out.println("p Inner.this.i=" + Inner.this.i);//2

         //!!注,如果是靜態的內部類時,下面語句不能編譯通過,因為靜態的內部類沒與外部類實例關聯

         System.out.println("p Outer.this.i=" + Outer.this.i);//1

      }

   }

 

   public static void main(String[] args) {

      Outer outer = new Outer();

      outer.f();

      Outer.Inner inner = outer.new Inner();

      inner.p();

   }

}

 

 

匿名內部類(方法或塊中的內部類一樣)使用外部類作用域內的局部變量需定義成final,但這個變量需在匿名內部類中直接使用,如果是作為匿名構造器的參數時不需要

public class Wrapping {

  private int i;

  public Wrapping() {}

  public Wrapping(int x) { i = x; }

  public int value() { return i; }

}

 

public class Parcel {

    // 可以直接內部類或匿名的內部類訪問,不需要定義成final

    private int ii = 1;

   /*

    * 這里的參數 x 不需要定義成final,因為它只是

    * 作為構建匿名對象時傳遞的一個參數,而沒有直接

    * 在匿名內部類中使用

    */

   public Wrapping wrapping1(int x) {

              // 調用匿名內部類的基類的帶參構造函數

      return new Wrapping(x) { // 傳遞參數

         public int value() {

            //調用外部內的域成員時可直接調用,但加this時就需在

            //this前加外部類名,因為該內部類沒有定義ii

            return super.value() * 47 * Parcel.this.ii;

         }

      };

   }

 

   /*

    * 注,這里的參數 x 一定要定義成final,因為

    * 它被匿名內部類直接使用了

    */

   public Wrapping wrapping2( final int x ) {

      final int y = 1;

      return new Wrapping() {

         //不管是在定義時還是方法中使用都需要定義成final

         private int i=y;

         public int value() {

            return i * x * 47;

         }

      };

   }

   public static void main(String[] args) {

      Wrapping w = new Parcel().wrapping1(10);

      w = new Parcel().wrapping2(10);

   }

}

 

 

匿名類中不可能有命名的構造器,因為它根本沒有名字。但通過塊初始化,就能夠達到為匿名內部類創建一個構造器的效果,當然它受到了限制——你不能重載塊方法,這不像普通內部類的構造函數:

abstract class Base {

  public Base(int i) {

    System.out.println("Base constructor, i = " + i);

  }

  public abstract void f();

 

public class AnonymousConstructor {

  public static Base getBase(int i) {

    return new Base(i) {

      {   //初始化塊

         System.out.println("Inside instance initializer");

      }

      public void f() {

         System.out.println("In anonymous f()");

      }

    };

  }

  public static void main(String[] args) {

    Base base = getBase(47);

    base.f();

  }

} /* Output:

Base constructor, i = 47

Inside instance initializer

In anonymous f()

*///:~

 

 

實現接口的匿名內部類:

public class Outer {

   public Comparable getComp() {

      return new Comparable() {// Comparable為比較器接口

                     //實現接口

         public int compareTo(Object o) {

            return 0;

         }

      };

   }

但要注意,匿名內部類實現一個接口時,構造時不能帶參數:

   interface InnerI {}

   public InnerI getII() {

      // 匿名內部內實現一個接口時,構造器不能帶參數,

      // 因為接口根本就沒有構造器,更沒有帶參的構造器

      // !!return new InnerI(int i) {};

   }

}

 

 

 

>>>java實現多重繼承<<<

java不支持多繼承。即沒有extends Class1Class2的語句形式。這里的多繼承是指繼承類的屬性和行為,並且是編譯時就決定的靜態行為。

廣義的繼承是指除了組合之外的的第二種代碼復用方法,只要滿足“像一個”或者“像是一個”、“里面有一個”條件都可以看做繼承。

 

java的非靜態內部類可以使用外部類的所有成員方法和變量。這給繼承多個類的同名成員並共享帶來可能。同時非匿名內部類可以繼承一個父類和實現多個接口,因此外部類想要多繼承的類可以分別由內部類繼承,並進行Override或者直接復用。然后外部類通過創建內部類的對象來使用該內部對象的方法和成員,從而達到復用的目的,這樣外部內就具有多個父類的所有特征。

這里的多繼承可以說是外部類繼承自一個內部類對象,而不是類,內部類 is in a 外部類,外部內的所有行為都是通過內部類對象動態獲得的。

下面是采用組合多個內部類的方式模擬多繼承的實例(基於對象層面,即組合多個內部類對象):

//手機

abstract class Mobile {

   public abstract void call();

}

// MP3播放器

abstract class Mp3Palyer {

   public abstract void play();

}

// 智能手機

class SmartPhone {

   private Mobile mb = new SmartMobile();

   private Mp3Palyer mp3 = new PhoneMp3();

 

   public Mobile getMobile() {

      return mb;

   }

   public Mp3Palyer getMp3() {

      return mp3;

   }

   public class SmartMobile extends Mobile {

      @Override // 不同的智能機有的call方式

      public void call() {

         System.out.println("Call phone!");

      }

   }

   public class PhoneMp3 extends Mp3Palyer {

      @Override // 不同的智能機有的play方式

      public void play() {

         System.out.println("Play music!");

      }

   }

}

public class MutiImpTest1 {

   static void call(Mobile m) {

      m.call();

   }

   static void play(Mp3Palyer p) {

      p.play();

   }

   public static void main(String[] args) {

      SmartPhone sp = new SmartPhone();

      call(sp.getMobile());// 智能手機具有手機功能

      play(sp.getMp3());// 又具有Mp3的功能

   }

}

 

下面采用繼承與匿名類的方式模擬(一個是類層面的,一個是對象層面,即外部類首先繼承一個類,然后通過引用內部類對象來繼承另外一個類):

//手機

abstract class Mobile {

   public abstract void call();

}

// MP3播放器

abstract class Mp3Palyer {

   public abstract void play();

}

// 智能手機,繼承自Mobile

class SmartMobile extends Mobile {

   @Override // 不同的智能機有的call方式

   public void call() {

      System.out.println("Call phone!");

   }

   // 智能手機也有播放音樂的功能

   public Mp3Palyer getMp3() {

      return new Mp3Palyer() {

         @Override // 不同的智能機有的play方式

         public void play() {

            System.out.println("Play music!");

         }

      };

   }

}

public class MutiImpTest2 {

   static void call(Mobile m) {

      m.call();

   }

   static void play(Mp3Palyer p) {

      p.play();

   }

   public static void main(String[] args) {

      // 現在智能手機即是手機類型,又具有Mp3的功能

      SmartMobile sp = new SmartMobile();

      call(sp);// 智能手機即是手機類型

      play(sp.getMp3());// 又具有Mp3的功能

   }

}

 

上面的多繼承是站在外部類的角度來看的,即它們是通過外部類引用內部類來達到多態與復用的目的。反過來,內部類繼承了一個類,同時擁有了外部類的所有成員方法和屬性,我們是否可以認為內部類集成了兩個類呢?——一個是類層面的,一個是對象層面的(因為非靜態內部類使用前一定有外部類的對象來創建它,它持有外部類某個對象的引用)。如果外部類還繼承了其他類呢?內部類還是可以訪問其他類的方法與屬性。現在從內部類的角度來模擬多繼承:

//智能機抽象類

abstract class SmartMobile {

   public abstract void call();

 

   public void play() {

      System.out.println("Play music!");

   }

}

 

// 手機

class Mobile {

   /*

    * 該方法是私有的,與SmartMobile類中方法同名,所以

    * Mobile不能直接繼承自SmartMobile,因為重寫時不能

    * 縮小訪問權限,所以只能使用一個內部類來重寫。

    */

   private void call() {

      System.out.println("Call phone!");

   }

 

   public SmartMobile getSmartMobileImp() {

      // 智能機的實現,好比多繼承(繼承外部類與SmartMobile

      return new SmartMobile() {

         // 調用繼承自外部類的相應方法來實現call

         public void call() {

            // 回調外部類真真的實現

            Mobile.this.call();

         }

      };

   }

}

 

public class MutiImpTest3 {

   static void call(SmartMobile m) {

      m.call();

   }

   static void play(SmartMobile p) {

      p.play();

   }

   public static void main(String[] args) {

      // 智能機即是手機也是Mp3播放器

      SmartMobile sp = new Mobile().getSmartMobileImp();

      call(sp);// 智能機即是手機

      play(sp);// 也是是Mp3播放器

   }

}

 

另外,從上面程序可看出內部類的另一作用:如果你想繼承一個類或實現一個接口,但是這個接口或類中的一個方法和你構想的這個類中的一個方法的名稱,參數相同,但訪問權限縮小了,所以你不能直接繼承與實現它,你應該怎么辦?這時候,你可以建一個內部類繼承這個類或實現這個接口(當然你可以修改訪問權限是可以的)。由於內部類對外部類的所有內容都是可訪問的,內部類可以通過調用外部類的這個方法來重寫那個類或接口。上面的Mobile類中的call方法就是這種情況,所以你不能直接讓Mobile去繼承SmartMobile,固只能采用內部類來達到重寫的目的。

 

>>>>閉包與回調<<<<

動態語言的閉包是一個永恆的話題。閉包在編碼過程的方便和快捷使得動態語言的擁護者對它津津樂道,而靜態語言特別是Java語言的扇子們會拿出匿名內部類來說Java語言也有類似的功能。

 

JavaScript 中閉包的產生是由於 JavaScript 中允許內部 function,也就

是在一個 function 內部聲明的 function 。內部 function 可以訪問外部 function 中的局部變量、傳入的參數和其它內部 function 。當內部 function 可以在包含它的外部 function 之外被引用時,就形成了一個閉包。這個時候,即便外部 function 已經執行完成,該內部 function 仍然可以被執行,並且其中所用到的外部 function 的局部變量、傳入的參數等仍然保留外部 function 執行結束

時的值。下面是一個例子:

function Outer(){

var i=0;

function Inner(){

alert(++i);

}

return Inner;

}

var inner = Outer();

inner();

因為函數Outer外的變量inner引用了函數Outer內的函數Inner,就是說:當函數Outer的內部函數Inner被函數Outer外的一個變量inner引用的時候,就創建了一個閉包。

閉包有什么作用:簡而言之,閉包的作用就是在Outer執行完並返回后,閉包使得Javascript的垃圾回收機制GC不會收回Outer所占用的資源,因為Outer的內部函數Inner的執行需要依賴Outer中的變量。

 

 

閉包是一個可調用的對象,它記錄了一些信息,這些信息來自於創建它的作用域。通過這個定義,可以看出Java中內部類是面向對象的閉包,因為它不僅包含創建內部類的作用域的信息,還自動擁有一個指向此外圍類對象的引用,在此作用域內,內部類有權操作所有的成員,包括private成員。

 

C++有指針函數,可以實現回調。通過回調,對象能夠攜帶一些信息,這些信息允許它在稍后的某個時刻調用初始的對象。Java中沒有指針,回調是通過匿名類來實現的。

 

以下是TIJ中的閉包與回調實例:

//具有自增能力的接口,也是回調接口

interface Incrementable {

  void increment();

}

 

// 回調框架

class Caller {

  // 回調接口對象引用

  private Incrementable callbackReference;

  // 動態的傳進回調接口實現

  Caller(Incrementable cbh) { callbackReference = cbh; }

  void go() {

      callbackReference.increment();//回調

  }

}

 

//自增接口簡單實現:

class Callee1 implements Incrementable {

  privateinti = 0;

  publicvoid increment() {

    i++;

    System.out.println(i);

  }

}  

 

//另一個具有類似功能的自增具體類,但功能不是Incrementable所要現實的功能

class MyIncrement {

  //與類Callee1中的increment方法具有相同的簽名

  publicvoid increment() { System.out.println("Other operation"); }

  staticvoid f(MyIncrement mi) { mi.increment(); }

}

 

// 如果你想實現Incrementable接口而功能又不是MyIncrement的功能實現,

// 即我們又想要MyIncrement中的increment功能實現,同時又要想實現Incrementable接口,

// 此時我們只能使用一個類內部類來實現Incrementable接口

class Callee2 extends MyIncrement {

  privateinti = 0;

  // 重寫了父類的功能

  publicvoid increment() {

    super.increment();//同時又保留了父類的功能

    // 以下是Callee2具體類自己的實現

    i++;

    System.out.println(i);

  }

  // 內部類,與外部類形成一個閉包,因為內部類有一個指向外圍類的引用,且在內部類作

  // 用域內可以調用外圍類對象一切成員

  privateclass Closure implements Incrementable {//實現Incrementable接口

    publicvoid increment() {

      // 指定調用外部類的increment方法,否則會發生遞歸調用

      Callee2.this.increment();//閉包,可以訪問其作用域以外的外圍類對象信息

    }

  }

  // 提供回調對象引用

  Incrementable getCallbackReference() {

    returnnew Closure();

  }

}  

 

 

// 調用

publicclass Callbacks {

  publicstaticvoid main(String[] args) {

    Callee1 c1 = new Callee1();//簡單實現者

    Callee2 c2 = new Callee2();//MyIncrement子類,但同時實現了Incrementable功能

    MyIncrement.f(c2);

    Caller caller1 = new Caller(c1);//動態傳進回調對象

    Caller caller2 = new Caller(c2.getCallbackReference());//動態傳進回調對象

    caller1.go();//開始執行回調

    caller1.go();

    caller2.go();

    caller2.go();

  }

} /* Output:

Other operation

1

1

2

Other operation

2

Other operation

3

*/

 

 

以下是孫妹妹在《JAVA面向對象編程》寫的回調,基本上是模仿上面那個例子,但覺得她對回調理解的有問題,因為根本不像上面有回調框架類Caller存在,唉,國人寫的書啊,這里那能看的出是回調?我咋就沒有悟出來呢?我看到的只是一種閉包。請看她所列舉的例子:

在以下Adjustable接口和Base類中都定義了adjust()方法,這兩個方法的參數簽名相同,但是有着不同的功能。

interface Adjustable{//個人認為這是回調接口

   //功能:調節溫度

   public void adjust(int temperature);

}

class Base{

   private int speed;

   //功能:調節速度

   public void adjst(int spped){

      this.speed = speed;

   }

}

如果有一個Sub類同時具有調節溫度和調節速度的功能,那么Sub類需要繼承Base類,並且實現Adjustable接口,但是以下代碼並不能滿足這一需求:

class Sub extends Base implements Adjustable{

   private int temperature;

   public void adjust(int temperature) {

      this.temperature = temperature;

   }

}

以上Sub類實現了Adjustable接口中的adjust()方法,並且把Base類中的adjust()方法覆蓋了,這意味着Sub類僅僅有調節溫度的功能,但失去了調節速度的功能。或以使用內部類來解決這一問題:

class Sub extends Base {

   private int temperature;

 

   // 為了防止覆蓋父類的adjst,所以取了不同的名字

   private void adjustTemperature(int temperature) {

      this.temperature = temperature;

   }

 

   // 實現回調接口

   private class Closure implements Adjustable {

      public void adjust(int temperature) {

         // 這里是回調?

         adjustTemperature(temperature);

      }

   }

 

   // 提供回調對象引用

   public Adjustable getCallBackReference() {

      return new Closure();

   }

 

   public static void main(String[] args) {

      Sub sub = new Sub();

      // 具有調節速度的功能

      sub.adjst(1);

  

      //又具有調節溫度的功能,這里就是回調?

      sub.getCallBackReference().adjust(2);

   }

}

上面使Sub類既不覆蓋Base類的adujst()方法,又實現了Adjustable接口的adjust()方法。

客戶類先調用sub實例的getCallBackReference()方法,獲得內部類的Closure實例,然后再調用Closure實例的adjust()方法,該方法又調用Sub實例的adjustTemperature()方法。這種調用過程稱為回調(這就叫回調?不理解)。

回調實質上是指一個類盡管實際上實現了某種功能,但是沒有直接提供相應的接口,客戶類可以通過這個類的內部類的接口來獲得這種功能。而這個內部類本身並沒有提供真正的實現,僅僅調用外部類的實現。可見,回調充分發揮了內部類所具有的訪問外部類的實現細節的優勢。

 

 

 

以下回調來源於網絡:

回調的基本原理跟好萊塢原則一樣,Don't call me,I'll call you.

編程上來說,一般使用一個庫或類時,是你主動調用人家的API,這個叫Call,有的時候這樣不能滿足需要,需要你注冊(注入)你自己的程序(比如一個對象),然后讓人家在合適的時候來調用你,這叫Callback。設計模式中的Observer就是例子:所有的觀察者都需要向自己關心的主題Observable注冊,然后主題在適當時機(主題類對象的屬性發生變化時)通知所有訂閱它的觀察者並更新,其中觀察者都實現了一個統一的Observer接口中的Update方法。

/**下面應用中ICallBack接口與Printer類好比是別人提供的API*/

public interface ICallBack {//回調接口

public void print();

}

public class Printer {

    ICallBack ic;

 

    void setCallBack(ICallBack ic) {

        this.ic = ic;  

    }

  /*供外界調用,即自己提供一個接口ICallBack,由外界PrintHandler去實現,再在適當時機回頭調用外界所提供的實現print方法。

我沒有實現接口,但是我取得了一個實現接口的對象,而這個對象是外界類調用我的方法setCallBack()時所賦給我的,因此我可以在業務需要的地方來調用外界所提供的實現print方法

*/

void execute() {

   //固定算法do some thing…

        ic.print(); //抽取變化的部分,由外界去實現

//固定算法 do some thing…

    }

}

 

/**下面是外界應用*/

public class PrintHandler {

    public static void main(String[] args) {

        Printer printer = new Printer();

/*注意下面的這項代碼片段,它給printer對象傳遞了一個實現ICallBack接口的匿名類,這樣Printer類的對象就取得了一個實現回調接口的類,因此Printer可以在任何時候調用接口中的方法*/

        printer.setCallBack(new ICallBack() {

         /* print 方法在PrintHandler類中實現,但不在PrintHandler 類對象中調用,而是在Printer類對象中調用,這就是回調*/

            public void print() {

                System.out.println("This is a callback");

            }

        });

         //  這句話可以設置成當滿足某條件時再執行  

        printer.execute();  

    }  

}

 

 

觀察者模式也符合這一種理解:請參見XXXXXXXXXXXXX

 

 

 

 

 

內部類生成的class文件名規則:

public class A {//A.class

   class B {//A$B.class

      class C {}//A$B$C.class

   }

 

   {

      class B {}//A$1B.class

   }

 

       B f() {

      class D {}//A$1D.class

      return new B() {};//A$1.class

   }

 

       B g() {

      class E {//A$1E.class

                     B h() {

            return new B() {};//A$1E$1.class

         }

      }

      return new B() {};//A$2.class

   }

   static class F{}//A$F.class

 

   public static void main(String[] args) {

      A a = new A();

      System.out.println(a.f().getClass().getName());

      System.out.println(a.g().getClass().getName());

   }

}

 

 

>>>再論工廠模式<<<

接口與工廠模式

接口典型的應用就是多繼承,而工廠方法設計模式能生成實現同一接口的對象,這與我們直接在使用的地方new某個實現對象是不同的,我們通過工廠對象上調用是業務實現對象創建方法,而該工廠對象將生成接口的某個業務實現的對象,理念上,我們的代碼將完全與接口分離,這使得我們可以透明地將某個實現替換為另一個實現。下面的實例展示了工廠方法的結構:

// 業務接口

interface Service {

  void method1();

  void method2();

}

// 業務工廠

interface ServiceFactory {

  Service getService();

}

// 業務1的實現

class Implementation1 implements Service {

  Implementation1() {} // 包訪問

  public void method1() {System.out.println("Implementation1 method1");}

  public void method2() {System.out.println("Implementation1 method2");}

// 業務1的工廠

class Implementation1Factory implements ServiceFactory {

  public Service getService() {

    return new Implementation1();

  }

}

//業務2的實現

class Implementation2 implements Service {

  Implementation2() {} // 包訪問

  public void method1() {System.out.println("Implementation2 method1");}

  public void method2() {System.out.println("Implementation2 method2");}

}

//業務2的工廠

class Implementation2Factory implements ServiceFactory {

  public Service getService() {

    return new Implementation2();

  }

//工廠應用

public class Factories {

  public static void serviceConsumer(ServiceFactory fact) {

   // 通過不同的工廠獲得不同的業務實現

    Service s = fact.getService();

    s.method1();

    s.method2();

  }

  public static void main(String[] args) {

    serviceConsumer(new Implementation1Factory());

    // 業務實現可以透明的改變:

    serviceConsumer(new Implementation2Factory());

  }

} /* Output:

Implementation1 method1

Implementation1 method2

Implementation2 method1

Implementation2 method2

*/

如果不使用工廠方法,你的代碼就必須在使用處指定將要創建的Service的確切類型,以便調用合適的構造器。為什么需要這種額外的間接性呢?一個常見的原因就是想要創建框架,業務實例的生產過程對客戶是透明的,而且還可以在工廠方法返回實例前對業務對象進行額外處理。

 

內部類與工廠模式

看看再使用匿名內部類來修改上面的程序:

interface Service {

  void method1();

  void method2();

}

interface ServiceFactory {

  Service getService();

class Implementation1 implements Service {

  private Implementation1() {}// 私有的,只能通過工廠方法來返回

  public void method1() {System.out.println("Implementation1 method1");}

  public void method2() {System.out.println("Implementation1 method2");}

  public static ServiceFactory factory = // 靜態成員,只需一個工廠即可

    new ServiceFactory() {// 使用匿名類來實現工廠接口

      public Service getService() {

       // 但需多個業務對象,每次調用工廠方法都會獲得一個業務實現對象

        return new Implementation1();

      }

    };

class Implementation2 implements Service {

  private Implementation2() {}// 私有的,只能通過工廠方法來返回

  public void method1() {System.out.println("Implementation2 method1");}

  public void method2() {System.out.println("Implementation2 method2");}

  public static ServiceFactory factory =

    new ServiceFactory() {

      public Service getService() {

        return new Implementation2();

      }

    };

public class Factories {

  public static void serviceConsumer(ServiceFactory fact) {

    Service s = fact.getService();

    s.method1();

    s.method2();

  }

  public static void main(String[] args) {

    serviceConsumer(Implementation1.factory);

    serviceConsumer(Implementation2.factory);

  }

}

修改后Implementation1Implementation2的構造器都可以是private的,並且沒有任何必要去創建具有具體名字的工廠類,另外,你經常只需要一個工廠對象即可,因此在本例中它被創建為Service實現的一個static域,這樣更具有實際意義。修改后的程序是多么的完美!

第十一章持有對象

請參考十七章

 

第十二章通過異常處理錯誤

使用異常的好處:一是使用程序更加的健壯。二是它往往能夠降低錯誤處理的復雜度。如果不使用異常,那么就必須檢查特定的錯誤,並在程序中的許多地方去處理它,而如果使用異常,那就不必在方法調用處進行檢查,因為異常機制保證能夠捕獲這個錯誤,並且,只需在一個地方處理錯誤,即所謂的異常處理程序塊中。這種方式不僅節省代碼,而且把“描述在正常執行過程中做什么事”的代碼和“出了問題怎么辦”的代碼相分離,總之,與以前的錯誤處理方法相比,異常機制使用代碼的閱讀、編寫和調試工作更加高效。

 

當拋出異常后,有幾件事會隨之發生。首先,同Java中其他對象的創建一樣,將使用new在堆上創建異常對象。然后,當前的執行路徑(它不能繼續下去了)被終止,並且從當前環境中彈出對異常對象的引用。此時,異常處理機制接管程序,並開始尋找一個恰當的地方來繼續執行程序。這個恰當的地方就是異常處理程序,它的任務是將程序從錯誤狀態中恢復,以使用程序能要么換一種方式運行,要么繼續運行下去。

 

通常把錯誤信息輸出到e.printStackTrace(System.out)要比直接e.printStackTrace()要好,因為System.out也許會重定向,而e.printStackTrace()默認則是將信息輸出到操作系統標准錯誤流,所以一般我們使用e.printStackTrace(System.out)打印異常信息。如果把結果送到System.err,它就會把會隨System.out一起被重定向,這樣更容易被用戶注意。

 

使用程序包的客戶端程序員可能僅僅只是查看一下拋出的異常類型,其他的就不管了(大多數Java庫里的異常都是這么用的),所以對異常所添加的其他功能也許根本用不上,名稱代表發生的問題,並且異常的名稱應該可以望文知意。

 

可以聲明方法將拋出異常,實際上卻不拋出,這樣做的好處是,為異常先占個位子,以后就可以拋出這種異常而不用修改已有的代碼。在定義抽象基類和接口時這種能力很重要,這樣派生類或接口實現就能夠拋出這些預先聲明的異常。

 

 

Exception的方法

public class ExceptionMethods {

   public static void main(String[] args) {

      try {

         throw new Exception("My Exception");

      } catch (Exception e) {

         System.out.println("getMessage():" + e.getMessage());

         System.out.println("getLocalizedMessage():" + e.getLocalizedMessage());

         System.out.println("toString():" + e);

         System.out.println("printStackTrace():");

         e.printStackTrace(System.out);

      }

   }

}

/*

getMessage():My Exception

getLocalizedMessage():My Exception

toString():java.lang.Exception: My Exception

printStackTrace():

java.lang.Exception: My Exception

   at excep.ExceptionMethods.main(ExceptionMethods.java:10)

*/

 

 

 

使用JDK日志器記錄日志

import java.io.PrintWriter;

import java.io.StringWriter;

import java.util.logging.Logger;

 

class MyException extends Exception {

   String errKey;//錯誤鍵

 

   public MyException setErrKey(String errKey) {

      this.errKey = errKey;

      return this;

   }

 

   //重寫父類方法,輸出詳細信息

   public String getMessage() {

      return "errKey = " + errKey + " " + super.getMessage();

   }

}

 

public class LoggingExceptions {

   // JDK日志記錄器

   private static Logger logger = Logger.getLogger("LoggingExceptions");

 

   static void logException(Exception e) {// 記錄異常日志

      // 字符串緩存

      StringWriter trace = new StringWriter();

      // 將日志輸出到緩存

      e.printStackTrace(new PrintWriter(trace));

      // 輸出異常日志

      logger.severe(trace.toString());

   }

 

   public static void main(String[] args) {

      try {

         throw new MyException().setErrKey("100");

      } catch (MyException e) {

         logException(e);

      }

   }

}

/*

2010-2-9 11:17:38 excep.LoggingExceptions logException

嚴重: excep.MyException: errKey = 100 null

   at excep.LoggingExceptions.main(LoggingExceptions.java:37)

*/

 

 

printStackTrace()方法所提供的信息可以通過getStackTrace()方法來直接訪問,這個方法返回一個由棧軌跡中的元素所構成的數組,其中每一個元素都表示棧中的一楨。元素0是棧頂元素,並且是調用序列中的最后一個方法調用。數組中的最后一個元素即棧底是調用序列中的第一個方法調用。

public class WhoCalled {

   static void f() {

      // Generate an exception to fill in the stack trace

      try {

         throw new Exception();

      } catch (Exception e) {

         for (StackTraceElement ste : e.getStackTrace())

            System.out.println("getClassName: " + ste.getClassName()

                   + " getFileName: " + ste.getFileName()

                   + " getLineNumber: " + ste.getLineNumber()

                   + " getMethodName: " + ste.getMethodName()

                   + " isNativeMethod: " + ste.isNativeMethod());

      }

   }

 

   static void g() {

      f();

   }

 

   static void h() {

      g();

   }

 

   public static void main(String[] args) {

      f();

      System.out.println("--------------------------------");

      g();

      System.out.println("--------------------------------");

      h();

   }

}

/*

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 28 getMethodName: main isNativeMethod: false

--------------------------------

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 30 getMethodName: main isNativeMethod: false

--------------------------------

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 24 getMethodName: h isNativeMethod: false

getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 32 getMethodName: main isNativeMethod: false

*/

 

 

如果只是把當前異常對象重新拋出,那么printStackTrace()方法顯示的將是原來異常拋出點的調用棧信息,而並非重新拋出點的信息。要想更新這個信息,可以調用fillInStackTrace()方法,這將返回一個Throwable對象,它是通過把當前調用棧信息填入原來那個異常對象而建立的:

public class Rethrowing {

  public static void f() throws Exception {

    System.out.println("originating the exception in f()");

    throw new Exception("thrown from f()");

  }

  public static void g() throws Exception {

    try {

      f();

    } catch(Exception e) {

      System.out.println("Inside g(),e.printStackTrace()");

      e.printStackTrace(System.out);

      throw e;//再次拋出

    }

  }

  public static void h() throws Exception {

    try {

      f();

    } catch(Exception e) {

      System.out.println("Inside h(),e.printStackTrace()");

      e.printStackTrace(System.out);

      //重新拋出,這一行將成為異常的新發生行了,好比在這里重新包裝new后拋出:throw new Exception();

      throw (Exception)e.fillInStackTrace();

    }

  }

  public static void main(String[] args) {

    try {

      g();

    } catch(Exception e) {

      System.out.println("main: printStackTrace()");

      e.printStackTrace(System.out);

    }

    try {

      h();

    } catch(Exception e) {

      System.out.println("main: printStackTrace()");

      e.printStackTrace(System.out);

    }

  }

} /* Output:

originating the exception in f()

Inside g(),e.printStackTrace()

java.lang.Exception: thrown from f()

   at Rethrowing.f(Rethrowing.java:4)

   at Rethrowing.g(Rethrowing.java:8)

   at Rethrowing.main(Rethrowing.java:27)

main: printStackTrace()

java.lang.Exception: thrown from f()

   at Rethrowing.f(Rethrowing.java:4)

   at Rethrowing.g(Rethrowing.java:8)

   at Rethrowing.main(Rethrowing.java:27)

originating the exception in f()

Inside h(),e.printStackTrace()

java.lang.Exception: thrown from f()

   at Rethrowing.f(Rethrowing.java:4)

   at Rethrowing.h(Rethrowing.java:17)

   at Rethrowing.main(Rethrowing.java:33)

main: printStackTrace()

java.lang.Exception: thrown from f()

   at Rethrowing.h(Rethrowing.java:22)

   at Rethrowing.main(Rethrowing.java:33)

*///:~

 

 

常常會想要在捕獲一個異常后拋出另一個異常,並且希望把原始異常的信息保存下來,這個被稱為異常鏈。在JDK1.4以前,程序員必須自己編寫代碼來保存原始中異常的信息。現在所有Throwable的子類在構造器中都可能接受一個cause對象作為參數(Throwable(Throwable cause))。這個cause就用來表示原始異常,這樣通過把原始異常傳遞給新的異常,使得即使在當前位置創建並了新的異常,也能通過這個異常鏈追蹤到異常最初發生的位置。並可以使用initCause(Throwable cause)來重樣設置異常根原因,但此方法至多可以調用一次,如果拋出的異常是通過 Throwable(Throwable) Throwable(String,Throwable) 創建的,則該異常對象的此方法甚至一次也不能調用。

public class ExcpTest {

 

   public static void nullExc() {

      throw new NullPointerException();

   }

 

   public static void call1() throws Exception {

      try {

         nullExc();

      } catch (Exception e) {

         throw new Exception(e);

      }

 

   }

 

   public static void call2() throws Exception {

      try {

         call1();

      } catch (Exception e) {

         //該行運行時會拋異常,因為e已經設置過 cause

         throw (Exception) e.initCause(new ArithmeticException());

      }

   }

  

   public static void call3() throws Exception {

      try {

         nullExc();

      } catch (Exception e) {

         //該行運行沒問題,因為還沒有給 e 設置 cause

         throw (Exception) e.initCause(new ArithmeticException());

      }

 

   }

 

   public static void main(String[] args) {

      try {

         call1();

      } catch (Exception e) {

         e.printStackTrace();

      }

      try {

         call2();

      } catch (Exception e) {

         e.printStackTrace();

      }

      try {

         call3();

      } catch (Exception e) {

         e.printStackTrace();

      }

   }

}

/*

java.lang.Exception: java.lang.NullPointerException

at ExcpTest.call1(ExcpTest.java:11)

at ExcpTest.main(ExcpTest.java:37)

Caused by: java.lang.NullPointerException

at ExcpTest.nullExc(ExcpTest.java:4)

at ExcpTest.call1(ExcpTest.java:9)

... 1 more

java.lang.IllegalStateException: Can't overwrite cause

at java.lang.Throwable.initCause(Unknown Source)

at ExcpTest.call2(ExcpTest.java:21)

at ExcpTest.main(ExcpTest.java:42)

java.lang.NullPointerException

at ExcpTest.nullExc(ExcpTest.java:4)

at ExcpTest.call3(ExcpTest.java:27)

at ExcpTest.main(ExcpTest.java:47)

Caused by: java.lang.ArithmeticException

at ExcpTest.call3(ExcpTest.java:30)

... 1 more

*/

 

 

RuntimeException:屬於運行時異常(即非捕獲性異常),它包括繼承自它的所有子類會自動被Java虛擬機拋出,所以不向在方法聲明時拋出異常說明,所以下面的代碼也將是多余的:

if(t == null){

   throw new NullPointerException();

}

如果不捕獲運行時異常,則異常會穿過(拋出)所有的調用路徑直達main()方法,而不會被捕獲,並在程序退出前將自動調用異常的printStackTrace()方法。

 

受檢查異常是程序可以處理的異常。如果拋出異常的方法本身不能處理它,那么方法調用者應該去處理它,從而使程序運行,不至於終止程序

 

運行時異常表示無法讓程序恢復運行的異常,導致這種異常的原因通常是由於執行了錯誤操作,一旦出現了錯誤操作,建議終止程序,因此Java編譯器不檢查這種異常。如果出現運行時異常,則表示你的程序代碼本身的問題,而不是由程序外界引起的,比如讀文件時文件不存在,這不是由程序本身引起的,所以文件不存在的拋出的是檢查行異常。

 

RuntimeException代表的是編程錯誤:

1、無法預料的錯誤,比如從你控制范圍之外傳遞進來的null引用。

2、作為程序員,應該在代碼中進行檢查錯誤。(比如對於ArrayIndexOutOfBoundsException,就得注意一下數組的大小了。)在一個地方發生的異常,常常會在另一個地方導致錯誤。

 

運行時異常是應該盡量避免的(也是完全可能避免的,既然是可以避免的,所以運行時異常不需捕獲),在程序調試階段,遇到這種異常時,正確的做法是程序的設計和實現方式,修改程序中的錯誤,從而避免這種異常。捕獲運行時異常並且使程序恢復運行並不是明智的辦法,這主要有兩方面的原因:

1、這種異常一旦發生,損失嚴重。

2、即使程序恢復運行,也可能會導致程序的業務邏輯錯亂,甚至導致更嚴重的異常,或都得到錯誤的運行結果。

 

Error類及其子類表示程序本身無法修復的錯誤,它和運行時異常的相同之處是:Java編譯器都會檢查它們,當程序運行時出現它們時都會終止程序。

 

如果有必要,一般將try放循環里,這樣就避免了當Java中的異常出現時我們回到異常拋出地點再次執行的問題,這樣可以建立了一個“程序繼續執行之前必須要達到”的條件。

 

避免過於龐大的try代碼塊,因為try代碼塊越龐大,出現異常的地方就越多,要發生異常的原因就越困難。

 

不要使用catch(Exception ex)子句來捕獲所有異常,理由如下:

1、對不同的異常通常有不現的處理方式,不同的錯誤使用同樣的處理方式是不現實的。

2、會捕獲本應該拋出的運行異常,掩蓋程序中的錯誤。

 

 

什么情況下才用到finally?當要把除內存之外的資源恢復到它們的初始狀態時,就要用到finally子名,這種需要清理的資源包括:已經打開的文件或網絡連接,在屏幕上畫的圖形,甚至可以是外部對象某個狀態的恢復。

 

 

Finllay塊的異常丟失

public class ExceptionSilencer {

   public static void f() {

      try {

         throw new RuntimeException();

      } finally {

         // 從這里返回時,異常將丟失,不會再向外拋出了

         return;

      }

   }

   public static void main(String[] args) {

      f();//得不到打印的信息

   }

}

 

 

如果一個類繼承了某個類同時又實現了某個接口,他們有同樣的接口方法,但都拋出了不同的捕獲性異常,則該子類實現與重寫該方法時,則方法聲明處不能拋出任何捕獲性異常了。

 

如果調用的父類構造器拋出捕獲性異常,則子類相應的構造器也只能拋出,不能在構造器里進行捕獲。

 

 

構造器拋出異常時正確的清理方式:

比如在構造器中打開了一個文件,清理動作只有在對象使用完畢並且用戶調用了特殊的清理方法之后才能得以清理,而不能直接在構造器里的finally塊上關閉,因為finally塊是不管是否有異常都會關閉,而構造器執行成功能外界需要這個文件流。但如果在文件成功打開后才拋出異常,則需要關閉文件,並向外界拋出異常信息:

import java.io.BufferedReader;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.IOException;

 

public class InputFile {

   private BufferedReader in;

 

   // 拋出異常的構造器

   public InputFile(String fname) throws Exception {

      try {

         // 在構造器中打開一個文件流

         in = new BufferedReader(new FileReader(fname));

      } catch (FileNotFoundException e) {

         System.out.println("Could not open " + fname);

         // 如果是文件沒有找到,則不需要關閉流,只需重新拋出

         throw e;

      } catch (Exception e) {

         // 如果是其他異常,則需要關閉流,因為文件已打開

         try {

            in.close();

         } catch (IOException e2) {

            System.out.println("in.close() unsuccessful");

         }

         throw e; // 再重新拋出

      } finally {

         // 這里不能關閉流!!!

      }

   }

 

   public String getLine() {

      String s;

      try {

         s = in.readLine();

      } catch (IOException e) {

         throw new RuntimeException("readLine() failed");

      }

      return s;

   }

 

   // 在文件成功打開后,j由外界使用完后調用

   public void dispose() {

      try {

         in.close();

         System.out.println("dispose() successful");

      } catch (IOException e2) {

         throw new RuntimeException("in.close() failed");

      }

   }

}

 

class Cleanup {

   public static void main(String[] args) {

      // try是對構造器異常的捕獲,如果出現了異常則不需關閉,

      // 因為構造器內部已處理

      try {

         InputFile in = new InputFile("InputFile.java");

         // 如果運行到這里說明文件已正常打開,所以后面需關閉

         try {

            String s;

            int i = 1;

            while ((s = in.getLine()) != null)

                ; // 讀文件...

         } catch (Exception e) {

            System.out.println("Caught Exception in main");

            e.printStackTrace(System.out);

         } finally {

            // 不管讀取是否正常,用完后一定要關閉

            in.dispose();

         }

      } catch (Exception e) {

         System.out.println("InputFile construction failed");

      }

   }

}

 

 

異常處理的一個重要的原則是“只有在你知道如何處理的情況下才捕獲異常”。實際上,異常處理的一個重要目標就是把錯誤處理的代碼同錯誤發生的地點相分離。這使你能在一段代碼中專注於要完成的事情,至於如何處理錯誤,則放在另一段代碼中。這樣以來,主干代碼就不會與錯誤處理邏輯混在一起,也更容易理解和維護。

 

“被檢查的異常”可能使問題變得復雜,因為它們強制你在可能還沒有准備好處理錯誤的時候被迫加上cacth子名,即使我們不知道如何處理的情況下,這就導致了異常的隱藏:

try{

   //… throw …

}catch(Exception e){//什么都不做}

 

 

把“被檢查的異常”轉換為“不檢查的異常”:當在一個普通方法里調用別的方法時,要考慮到“我不知道該怎樣處理這個異常,但是也不能把它‘吞’了,或者只打印一些無用的消息”。JDK1.4的異常鏈提供了一種新的思路來解決這個問題,可以直接把“被檢查的異常”包裝進RuntimeException里面:

try{

   //… throw 檢查異常

}catch(IDontKnowWhatToDoWithThisCheckedException e){

   Throw new RuntimeException(e);

}

如果想把“被檢查的異常”這種功能“屏蔽”掉的話,上面是一個好的辦法。不用“吞”掉異常,也不必把它放到方法的異常聲明里面,而異常鏈還能保證你不會丟失任何原始異常的信息。你還可以在“知道如何處理的地方”來處理它,也可以其他上層catch里通過 throw e.getCause(); 再次拋出原始的“被檢查的異常”:

public class Test {

   static void f() {

      try {

         throw new Exception();

      } catch (Exception e) {

         //將檢測性異常轉換成非檢測性異常后繼續拋出

         throw new RuntimeException(e);

      }

   }

 

   static void g() {

      //不用捕獲,因為檢測異常轉換了運行異常

      f();

   }

 

   static void h() throws Exception {

      try {

         g();

      } catch (Exception e) {

         e.printStackTrace();

         System.out.println("-----");

         //可以將檢測異常繼續做為檢測異常拋出

         throw new Exception(e.getCause());

      }

   }

 

   public static void main(String[] args) {

      try {

         h();

      } catch (Exception e) {

         e.printStackTrace();

      }

   }

}

/*

java.lang.RuntimeException: java.lang.Exception

   at Test.f(Test.java:7)

   at Test.g(Test.java:13)

   at Test.h(Test.java:18)

   at Test.main(Test.java:29)

Caused by: java.lang.Exception

   at Test.f(Test.java:4)

   ... 3 more

-----

java.lang.Exception: java.lang.Exception

   at Test.h(Test.java:23)

   at Test.main(Test.java:29)

Caused by: java.lang.Exception

   at Test.f(Test.java:4)

   at Test.g(Test.java:13)

   at Test.h(Test.java:18)

   ... 1 more

*/

 

第十三章字符串

字符串是不可變的:是final類固不能繼承它;也不能通過指向它的引用來修改它的內容。

 

StringBuilderJava SE5引用的,在這之前用的是StringBuffer。后者是線程安全的,因此開銷也會大些,所以在Java SE5/6中,字符串操作應該還會更快一點。

 

JDK1.5中:String s = "a" + "b" + "c"; 在編譯時,編譯器會自動引入java.lang.StringBuilder類,並使用StringBuilder.append方法來連接。雖然我們在源碼中並沒有使用StringBuilder類,但是編譯器自動地使用了它,因為它更高效。

 

雖然在JDK1.5或以上版本中使用“+”連接字符串時為避免產生過多的字符串對象,編譯器會自加使用StringBuilder類來優化,但是如果連接操作在循環里,編譯器會為每次循環都創建一個StringBuilder對象,所以在循環里一般我們不要直接使用“+”連接字符串,而是自己在循環外顯示的創建一個StringBuilder對象,用它來構造最終的結果。但是在使用StringBuilder類時也要注意,不要這樣使用:StringBuilder.append(a + ":" + c); ,如果這樣,那編譯器就會掉入陷井,從而為你另外創建一個StringBuilder對象處理括號內的字符串連接操作。

 

如果重寫了父類的toString方法(一般是ObjecttoString),當需要打印對象的內存地址時,應該調用super.toString()方法,而不是直接打印this,否則會發生StackOverflowError異常。

 

Java SE5PrintStreamPrintWriter對象都引入了format()方法,那我們就要可以使用System.out.format()格式化輸出了。format()方法模仿自C語言的printf(),如果你比較懷舊的話,也可以使用printf(),它還是調用format()來實現的,只不過換了個名而已:

public class SimpleFormat {

  public static void main(String[] args) {

    int x = 5;

    double y = 5.332542;

    // 以前我們是這樣打印的:

    System.out.println("Row 1: [" + x + " " + y + "]");

    // 現在我們是這樣打印的:

    System.out.format("Row 1: [%d %f]\n", x, y);

    // 或者是

    System.out.printf("Row 1: [%d %f]\n", x, y);

  }

} /* Output:

Row 1: [5 5.332542]

Row 1: [5 5.332542]

Row 1: [5 5.332542]

*///:~

 

 

Java中,所有新的格式化功能(PrintStreamPrintWriterformat方法以及String的靜態方法format)都是由java.util.Formatter類來處理的,創建時需要指定輸出到哪。

Formatter的對齊格式化輸出抽象語法:

%[argument_index$][flags][width][.precision]conversion

可選的 argument_index是一個十進制整數,用於表明參數在參數列表中的位置。第一個參數由 "1$" 引用,第二個參數由 "2$" 引用,依此類推。

Width用來控制一個域的最小尺寸,如果輸出的參數值不夠寬,則添加空格來確保一個域至少達到某個寬度,在默認的情況下,數據是右對齊的,但我們可以使用“- flags標志來改變對齊方向。

width相對的是precision,它用來指明輸出的最大尺寸。Width可以應用於各種數據類型時其行為方式都是一樣,但precision不一樣,並不是所有類型都能應用precision,而且,應用於不同類型的數據轉換時,precision的意義也不同:將precision應用String時,它表示打印String時輸出字符的最大個數;而在將precision應用於浮點數時,它表示小數部分要顯示出來的位數(默認是6位小數位),如果小數位過多則舍入,太少則在尾部補零。由於整數沒有小數部分,固不能應用於整形數據類型,否則拋異常:

import java.util.Formatter;

 

public class Receipt {

   private double total = 0;

 

   // 這里輸出到控制台,我們也可以格式化后輸出到文件、StringBuilderCharBuffer

   private Formatter f = new Formatter(System.out);

 

   public void printTitle() {

      // %-15s表示輸出一個最小寬度為15的且左對齊的字符,2$表示左起輸出參數位置

      f.format("%2$-15s %1$5s %3$10s\n", "Qty", "Item", "Price");

      f.format("%-15s %5s %10s\n", "----", "---", "-----");

   }

 

   public void print(String name, int qty, double price) {

      // %-15.15s%-15s基礎上最多能輸出15個字符

      f.format("%-15.15s %5d %10.2f\n", name, qty, price);

      total += price;

   }

 

   public void printTotal() {

      // %10.2f表示輸出一個最小寬度為10,右對齊小數點后兩位的浮點數

      f.format("%-15s %5s %10.2f = %s\n", "Tax", "", total * 0.06, total + " * 0.06");

      f.format("%-15s %5s %10s\n", "", "", "-----");

      f.format("%-15s %5s %10.2f = %s\n", "Total", "", total * 1.06, total + " * 1.06");

   }

 

   public static void main(String[] args) {

      Receipt receipt = new Receipt();

      receipt.printTitle();

      receipt.print("Jack's Magic Beans", 4, 4.254);

      receipt.print("Princess Peas", 3, 5.1);

      receipt.print("Three Bears Porridge", 1, 14.285);

      receipt.printTotal();

   }

}

/*

* Output:

Item              Qty      Price

----              ---      -----

Jack's Magic Be     4       4.25

Princess Peas       3       5.10

Three Bears Por     1      14.29

Tax                         1.42 = 23.639 * 0.06

                           -----

Total                      25.06 = 23.639 * 1.06

*/

 

 

Fomatter常用類型轉換字符

%d :整數型(十進制)

%c Unicode字符

%b Boolean

%s String

%f :浮點數(十進制)

%e :浮點數(科學計數)

%x :整數(十六進制)

%h :散列碼(十六進制)

%% :字符“%

 

%b對於boolean基本類型及Boolean,其轉換結果為對應的truefalse,但是,對其他類型的參數,只要該參數不為null,那轉換的結果就永遠都是true,即使是數字0,轉換結果依然為true,而這不像其他語言(如C)為false。上面列舉的是常用的格式字符,其他可以在JDK文檔中的Formatter類部分找到。

 

 

String.format()String.format()是一個Static方法,當我們只需要使用format()方法一次時很方便,其實在String.format()內部,它也是創建一個Formatter對象,格式化后傳進的字符串后返回新的字符串。下面是一個十六進制工具:

import java.io.BufferedInputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

 

public class Hex {

   /**

    * 以十六進制格式輸出數據

    * @param counts 每行多少個

    * @param data 要格式化的數據

    * @return 格式化后的數據

    */

   public static String format(int counts, byte[] data) {

      StringBuilder result = new StringBuilder();

      int n = 0;

      int rows = 0;

      for (byte b : data) {

         if (n % counts == 0) {

            rows++;

            result.append(String.format("%05d: ", rows));

         }

         result.append(String.format("%02X ", b));

         n++;

         if (n % counts == 0) {

            result.deleteCharAt(result.length() - 1);

            result.append("\n");

         }

      }

      result.append("\n");

      return result.toString();

   }

 

   /**

    * 讀取二進制文件

    * @param bFile

    * @return

    * @throws IOException

    */

   public static byte[] read(File bFile) throws IOException {

      BufferedInputStream bf = new BufferedInputStream(new FileInputStream(

            bFile));

      try {

         byte[] data = new byte[bf.available()];

         bf.read(data);

         return data;

      } finally {

         bf.close();

      }

   }

 

   public static void main(String[] args) throws IOException {

      System.out.println(format(16, read(new File("src/Hex.class")

            .getAbsoluteFile())));

   }

}

/*

* Output:

00001: CA FE BA BE 00 00 00 31 00 7F 07 00 02 01 00 03

00002: 48 65 78 07 00 04 01 00 10 6A 61 76 61 2F 6C 61

00003: 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E 69

...

*/

 

 

判斷一個字符串是否匹配指定的模式,最簡單的是使用String對象的matches方法,需傳遞正則式參數,實質上是調用Pattern.matches(regex, string)來實現的。

 

Stringsplit()方法也使用到了正則式,該方法的重載版本允許你限制字符串分割次數split(String regex,int limit) limit 參數控制模式應用的次數,因此影響結果數組的長度,默認就是0。如果該限制 n大於 0,則模式將被最多應用 n - 1 次,數組的長度將不會大於 n,而且數組的最后項將包含超出最后匹配的定界符的所有輸入。如果 n為非正,則模式將被應用盡可能多的次數,而且數組可以是任意長度。如果 n為零,則模式將被應用盡可能多的次數,數組可有任何長度,並且結尾空字符串將被丟棄。例如,字符串 "boo:and:foo" 使用這些參數可生成下列結果:

Regex Limit 結果

: 2 { "boo", "and:foo" }

: 5 { "boo", "and", "foo" }

: -2 { "boo", "and", "foo" }

o 5 { "b", "", ":and:f", "", "" }

o -2 { "b", "", ":and:f", "", "" }

o 0 { "b", "", ":and:f" }

 

 

CharBufferStringStringBufferStringBuilder都實現了CharSequence接口,大多數的正則表達式操作都接受CharSequence類型的參數。

 

如果使用功能強大的正則表達式對象,我們使用靜態的Patter.compile()方法來編譯正則表達式即可,它會生成一個Patter對象。接下來將想要檢測的字符串傳Patter對象的matcher()方法,會生成一個Matcher對象,該對象有很多的功能可用。

 

另外,Patter類還提供了靜態的方法:static boolean matches(String regex, CharSequence input)

 

Matcher對象的groupCount方法返回該匹配器的模式中的分組數目,但第0組不包括在內。

 

Stringsplit()方法實質上是調用Pattern對象的split方法:Pattern.compile(regex).split(string, limit)來實現的。Stringreplace方法則是通過調用Matcher對象的replace方法來實現的。

 

 

Matcher對象的appendReplacement(StringBuffer sbuf,String replacement)執行漸進式的替換,而不是像replaceFirst()replaceAll()那樣只替換第一個匹配或全部匹配。這是一個非常重要的方法,它允許你調用其他方法來生成或處理replacemanetreplaceFirst()replaceAll()則只能使用一個固定的字符串),使你能夠以編程的方式將目標分割成組,從而具備列強大的替換功能,appendTail(StringBuffer sbuf),在執行了一次或多次appendReplacement之后,調用此方法可以將輸入字符串余下的部分復制到sbuf中。

 

Matcher對象的reset()reset(CharSequence input)Matcher對象重新設置到當前字符序列的起始位置,可以重用PatternMatcher對象。

 

Java SE5新增了Scanner類,它可以大大減輕掃描輸入的工作。可以通過useDelimiter(String pattern)設置next操作的定界符。還可通過hasNext(String pattern)判斷是否還存在指定正則式的串,如果有,則可通過String next(String pattern)來讀取。除此之后,它還有很多的讀取各種不同基本類型的數據的nextXX方法。

 

第十四章類型信息

在運行時識別對象和類的信息有兩種方式:一種是“傳統的”RTTI(如“(Circle)”),它假設我們在編譯時已經知道了所有的類型,但易引起ClassCastException異常,不過我們可以通過 instanceof 先檢測具體類型;另一種是“反射”機制(使用類型的Class對象),它允許我們在運行時查詢Class對象的信息。

 

Class對象相關方法:

getName():回此 Class對象所表示的實體(類、接口、數組類、基本類型或 void)名稱。如果此類對象表示的是非數組類型的引用類型,則返回該類的二進制名稱,包括包名;如果此類對象表示一個基本類型或 void,則返回的名字是一個與該基本類型或void 所對應的 Java 語言關鍵字相同的串;如果此類對象表示一個數組類,該數組嵌套深度的一個或多個 '[' 字符加元素類型名。元素類型名的編碼如下:

元素類型編碼

boolean  Z

byte  B

char  C

類或接口  Lclassname;

double  D

float  F

int  I

long  J

short  S

如:

String.class.getName()java.lang.String

byte.class.getName()byte

(new Object[3]).getClass().getName()[Ljava.lang.Object;

(new int[3][4][5][6][7][8][9]).getClass().getName()[[[[[[[I

getSimpleName():產生不包括包名的類名。

getInterfaces():返回所有實現的接口的Class對象數組。

getSuperclass():返回直接基類。如果此 Class 表示 Object 類、接口、基本類型或 void,則返回 null。如果此對象表示一個數組類,則返回表示該 Object 類的 Class 對象。

newInstance()創建此Class對象所表示的類的一個新實例。如同用一個帶有一個空參數列表的 new表達式實例化該類。使用該方法創建對象實例時,必須要有默認構造函數。

 

 

獲取某個類的Class對象:

l  Class.forName(String className)

l  通過對象的getClass()方法。

l  直接通過類的靜態屬性class,如Test.class,這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且不需要調用forName()方法,所以更高效。

另外,對於基本數據類型的包裝器類,還有一個標准字段TYPE,該字段是一個引用,指向對象的基本數據類型的Class對象。但還是建議使用“.class”的形式,以保持與普通類型的一致性。

 

>>>Class.forNameObject.classclassLoader.loadClass異同<<<

Class.forName與“.class”區別在於:前者會初始化Class對象(如靜態數據成員的初始化與靜態塊的執行),而后者不會。“.class”只是去加載類,不會鏈接(即不會給靜態域分配空間);ClassLoader類的loadClass()方法只是加載一個類,並不會分配內存空間,更不會導致類的初始化:

public class ClassLoadTest {

   // 測試時請調整虛擬機的堆大小:-Xms32M -Xmx1024M

   public static void main(String[] args) {

      // 編譯時直接將2替換,不加導致類的加載與初始化

      System.out.println("Bean.y=" + Bean.y);

      sleep("->調用類的靜態字面常量不會導致類的加載");

      /*

       * 不會初始化靜態塊與靜態成員,只是加載到內存,也沒進行

       * 鏈接操作(靜態成員分配空間),更沒有初始化類(靜態成

       * 員初始化與靜態塊的執行)

       */

      Class cl = Bean.class;

      sleep("->Bean.class只會加載類,不會分配內存空間與初始化類");

      ClassLoader cld = ClassLoader.getSystemClassLoader();

      try {

         // 也只是加載到內存,沒有進行空間的分配與初始化類

         cld.loadClass("Bean");

      } catch (ClassNotFoundException e) {

         e.printStackTrace();

      }

      sleep("->ClassLoaderloadClass()方法也只會加載類,不會" +

            "分配內存空間與初始化類");

      try {

         // 加載與初始化類,並可看到內存猛增

         cl = Class.forName("Bean");

      } catch (ClassNotFoundException e) {

         e.printStackTrace();

      }

      sleep("->Class.forName()會加載類,且分配內存空間與初始化類");

   }

 

   private static void sleep(String msg) {

      System.out.println(msg);

      try {

         Thread.sleep(5000);

      } catch (InterruptedException e) {

         e.printStackTrace();

      }

   }

}

 

class Bean {

   static {

      System.out.println("static block");

   }

   public static final int y = 2;

   public static final int i = f(1);

   public static int j = f(2);

   // 測試 A.class是否進行了鏈接操作

   public static long[] l = h(21474836);

   static {

      System.out.println("i=" + i);

      System.out.println("j=" + j);

   }

 

   static int f(int i) {

      System.out.println("static f(" + i + ")");

      return i;

   }

 

   static long[] h(int len) {

      long[] l = new long[len];

      for (int i = 0; i < len; i++) {

         l[i] = i;

      }

      return l;

   }

}

 

 

 

使用一個類前需做的三個准備步驟:

1、加載:這是由類加載器執行的。該步驟將查找字節碼文件並讀取到內存,並從這些字節碼中創建一個Class對象。

2、鏈接:驗證被加載類的正確性(驗證)、為類的靜態變量分配內存空間,並將其初始化為默認值(准備)、把類中的符號引用轉換為直接引用(解析),如將方法的調用解析成直接方法在方法區的內存地址調用。

3、類的初始化(初始化Class對象):初始化靜態成員和執行靜態初始化塊。

 

調用類一個 static final(編譯期常量,注一定要是在定義時就初始化了,否則還是會初始化靜態塊與成員的)成員時,類的Calss對象不會執行初始化操作,也就是說“編譯期常量”在類Class對象還沒有初始化就可以引用了。

 

如果一個static域不是final的,那么在對它訪問時,總是要求在它被讀取前,要先對類進行鏈接(為這個域分配存儲空間)和初始化(初始化該存儲空間)操作。

 

類的初始化時機發生在類首次主動使用時,類主動使用發生在以下時機:

1、  創建類的實例。創建實例途經:使用new(構造器隱式也是靜態的)、反射、克隆、反序列化

2、  調用類的靜態方法。

3、  訪問某個類或接口的靜態變量(注意,不能是靜態的字面常量域,靜態的字面常量在編譯時就確定,使用之前不會先加載類,更不會初始化類)。

4、  調用Class.forName()加載類。

5、  初始化一個類的子類,會導致父類初始化。

6、  Java虛擬機啟動時被標明為啟動類的類,例如: java Test”命令,Test類就是啟動類,Java虛擬機會先初始化它。

 

Java程序對類的使用方式可分為兩種:主動使用與被動使用。

所有的Java虛擬機實現必須在每個類或接口被Java程序“首次主動使用”時才初始化他們

 

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。

 

一個奇怪的初始化問題:

publicclass Singleton {

    /*

     * 這里如果為非靜態時,會因遞歸構造而堆棧溢出:

     * private Singleton s = new Singleton();

     * 因為構造器在調用前需要先初始化所有的非靜態成員,

     * 而靜態成員則不會在此時再被初始化,因為靜態成員

     * 是在類加載時就已初始了。

     *

     * 成員field1未初始化,field2需初始化。

     *

     * 初始化的動作是依照編寫的順序執行的,由於s先於

     * field2的初始化,所以在構造函數調用完后,雖然

     * field21了,但又會被緊拉着的field2初始化給覆

     * 蓋。如果將s的構造放在field2之后又會正常

     */

    privatestatic Singleton s = new Singleton();

    publicstaticintfield1;// 未初始化

    publicstaticintfield2 = eval();// 初始化

 

    private Singleton() {

       field1++;

       field2++;

       System.out.println("Sigleton field2=" + field2);

    }

 

    privatestaticint eval() {

       System.out.println("eval()");

       return 0;

    }

 

    publicstatic Singleton getInStance() {

       returns;

    }

 

    publicstaticvoid main(String[] args) {

       Singleton.getInStance();

       System.out.println("Main field1=" + Singleton.field1);// 1

       System.out.println("Main field2=" + Singleton.field2);// 0

    }

}

 

 

Java虛擬機初始化一個類時,要求它的所有父類都像自己那樣被初始,但是這條規則並不適用接口:

1、  在初始化一個類時,並不會先初始化它所實現的接口。

2、  在初始化一個接口時,並不會先初始化它的父接口。

因此,一個父接口並不會因為它的子接口或都實現類的初始化而初始化。只有當程序首次使用真真屬於他們的特定接口的靜態變量時,才會導致該接口的初始化,或者換句話來說就是只有當程序訪問的靜態變量或靜態方法的確在當前類或接口中定義時,才會引起類的加載與初始化:

class Rd {

       public static int getNumber(String msg) {

              System.out.println(msg);

              return new Random().nextInt(100);

       }

}

interface I1 {

       //注,getNumber不能拋出檢測異常,因為不能捕獲

       public final int j = Rd.getNumber("init j");

      

}

interface I2 extends I1 {

       public final int i = Rd.getNumber("init i");

       public final int y = Rd.getNumber("init y");

       public final int x = 1*2;//編譯時就已計算出結果

}

class Imp implements I2 {

       public static void main(String[] args) {

              //創建子類實現類時不會去初始化父接口

              new Imp();

              System.out.println("------");

              //由於x為靜態的字面常量,所以不會引起類加載

              System.out.println(I2.x);

              System.out.println("------");

              //子接口的初始化不會引起父接口的初始化

              System.out.println(I2.i);

              System.out.println("------");

              //只有在使用到接口中的靜態域時再初始化接口

              System.out.println(I2.j);

       }

}

 

只有當程序訪問的靜態變量或靜態方法的確在當前類或接口中定義時,才可看做是對類或接口的主動使用:

public class P {

       static int i = prt();

       static int prt(){

              System.out.println("init i");

              return 1;

       }

}

class S extends P {

       static {

              System.out.println("init S");

       }

}

class T {

       public static void main(String[] args) {

              //不會初始化S

              System.out.println(S.i);

       }

}

 

 

 

對於final類型的靜態變量,如果在編譯時就能計算取變量的取值,那么這種變量被看做編譯地時常量,即不需要等到運行時確定(如:private static final int i = 2,注:private static final int i = 2*2 也屬於編譯時常量,因為表達式是由常量組成,編譯后會使用常量 4 替換表達式);但是,對於那些在編譯時無法計算出的final類型的靜態變量,則調用他們時需要先加載類並初始化類才能使用(如:private static final int i = new Random().nextInt())。另外,如果類A中引用了B類的static final int i = 2;成員,則在編譯A類時,就會把2直接編譯到A中,因此在運行時可以不需要B.class文件都可運行。所以引用一個類的靜態字面常量的成員時,該類不會被加載。

 

>>>類加載器<<<

 

類表示被執行的代碼,而數據則表示與代碼相關聯的狀態信息。狀態信息可以改變,而代碼則一般不會變更。一個類的代碼通常都保存在一個.class為后綴的文件中。

 

Java中,一個類的固定標識為其完整的具限類名稱。該具限類名由該類的包名加上類名組成。但是在JVM中,唯一標識一個類的方式為:其具限類名與裝載該類的裝載器ClassLoader實例的組合。因此,如果一個包名為Pg,類名為C1的類,被類裝載器KlassLoader的實例kl1裝載,該類的實例C1(即C1.class)在JVM中的索引值將為(C1, Pg, kl1)。這意味着如果兩個類裝載器實例,裝載了兩個完全相同的類,則這兩個類在虛擬機中的表示(C1, Pg, kl1)和(C1, Pg, kl2)將完全不一樣,並且他們的對象實例也將完全不同,相互之間再也不能類型兼容了。

 

除了引導類裝載器以外,所有的類裝載器均有一個父類裝載器。此外,所有的類裝載器均為類型java.lang.ClassLoader的子類。

 

ClassLoaderloadClass(String name)實現如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {

    return loadClass(name, false);

}

protectedsynchronized Class<?> loadClass(String name, boolean resolve)

       throws ClassNotFoundException {

    // First, check if the class has already been loaded

    Class c = findLoadedClass(name);

    if (c == null) {

       try {

           if (parent != null) {

              //如果還有父加載,則遞歸由父加載器去加載

              c = parent.loadClass(name, false);

           } else {

              /*

               * 通過上面遞歸的找父加載器,最終會因為父加器為null

               * 即直到父加載器為根(Bootstrap)類加載器時,才結束

               * 遞歸,並開始從根類加載器開始加載指定的類。又由於根

               * 類加載只能加載核心庫,所以肯定會失敗,失敗后會調用

               * 異常塊中的 findClass(name)方法,這個方法默認是拋出

               * ClassNotFoundException異常,這會導致返回上層調用

               * c null,即加載類失敗,這樣會再次由上層調用者,

               * 即子加載器去加載,如果子加載器還是失敗,則再讓子子

               * 加載去加載,直接自己實現的類加載器去加載,此時就需

               * 要我們去實現 ClassLoader findClass

               */

              c = findBootstrapClass0(name);

           }

       } catch (ClassNotFoundException e) {

           // If still not found, then invoke findClass in order

           // to find the class.

           c = findClass(name);

       }

    }

    //外界調用loadClass(String name)方法時,實際上是執

    //loadClass(name, false),所以這里不會執行

    if (resolve) {

       resolveClass(c);

    }

    return c;

}

 

 

當要加載一個類時,調用的是ClassLoaderloadClass方法,loadClass方法先查找這個類是否己被加載,如果沒有加載則委托其父級類裝載器去加載這個類,如果父級的類裝載器無法裝載這個類,子級類裝載器才調用自己內部的findClass方法去進行真正的加載。父級類裝載器調用loadClass方法去裝載一個類時,它也是先查找其父級類裝載器,這徉一直追溯到沒有父級的類裝載器時(例如ExtClassLoader),則使用Java虛擬機內嵌的Bootstrap類裝載器進行裝載,當Bootstrap無法加載當前所要加載的類時,然后才一級級回退到子孫類裝載器去進行真正的加載。當回退到最初的類裝載器時,如果它自己也不能完成類的裝載,那就應報告ClassNotFoundException異常。

 

一個類裝載器只能創建某個類的一份字節碼數據,即只能為某個類創建一個與之對應的Class實例對象,而不能為同樣的一個類創建多個Class實例對象。在一個Java虛擬機中可以存在多個類裝載器,每個類裝載器都擁有自己的名稱空間,對於同一個類每個類裝載器都可以創建出它的一個Class實例對象,即每個類裝載器都可以分別創建出某個類的一份字節碼數據。兩個類裝載器分別創建的同一個類的字節碼數據屬於兩個完全不同的對象,相互之間沒有任何關聯,例如,在某個類中定義了一個靜態成員變量,它在不同的類裝載器之間是不可以實現教據共享的。采用委托模式給類的加載管理帶來了明顯的好處,當父級的類裝載器加載了某個類,那么子級的類裝載器就不要再去加載這個類,這樣就可以避免一個Java虛擬機中的多個類裝載器為同一個類創建多份字節碼數據的情況。

 

如果在類A中使用出new關鍵字創建類B, Java虛擬機將使用加載類A的類裝載器來加載類B。如果在一個類中調用Class.forName方法來動態加載另外一個類,可以通過傳遞給Class.forName(String name, boolean initialize, ClassLoader loader)方法的一個參數來指定另外那個類的類裝載器,如果沒有指定該參數,則使用加載當前類的類裝載器來加載。

 

每個運行中的線程都有一個關聯的上下文裝載器,可以使用Thread.setContextCIassLoader()方法設置線程的上下文類裝載器。每個線程默認的上下文類裝載器是其父線程的上下文類裝載器,而主線程的類裝載器初始被設置為 ClassLoader.getSystemC1assLoader()方法返回的系統類裝載器。當線程中運行的代碼需要使用某個類時,它使用上下文類裝載器來裝載這個類,上下文類裝載器首先會委托它的父級類裝載器來裝載這個類,如果父級的類裝載器無法裝載時,上下文類裝載器才自己進行裝載。

 

Java虛擬機自帶了以下幾種加載器:

1.         根(Bootstrap)類加載器:該加載器沒有父加載器。它負責加載虛擬機核心類庫,如java.lang.*等,這些核心的運行期Java類位於<JAVA_HOME>\jre\lib\rt.jar文件中。根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。根類加載器的實現依賴於底層操作系統,屬於虛擬機的實現的一部分,它並沒有繼承java.lang.ClassLoader類。

2.         擴展(Extension)類加載器(實現為:sun.misc.Launcher$ExtClassLoader):它的父加載器為根類加載器。它從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的<JAVA_HOME>\jre\lib\ext子目(擴展目錄)下加載類庫,如果把用戶創建的JAR文件放在這個目錄下,也會自動由擴展類加載器加載。擴展類加載器是純Java類,是java.lang.ClassLoader類的子類。

3.         系統(System)類加載器(實現為:sun.misc.Launcher$AppClassLoader,由ClassLoader.getSystemClassLoader()來獲取,並且內存中只有一個):也稱為應用類加載器,它的父加載器為擴展類加載器。它從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器。系統類加載器是純Java類,是java.lang.ClassLoader類的子類。

4.         自定義類加載器:Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器應該繼承ClassLoader類。

 

public class ClassLoaderTest {

       public static void main(String[] args) {

              ClassLoader cl, cl1;

              // 獲取系統類加載器

              cl = ClassLoader.getSystemClassLoader();

              System.out.println("系統類加載器:" + getClassName(cl));

 

              // 打印父加載器

              while (cl != null) {

                     cl1 = cl;

                     cl = cl.getParent();

                     System.out.println(getClassName(cl1) + " 的父加載器:" + getClassName(cl) + ",且由 "

                                   + cl1.getClass().getClassLoader()+ " 加載。");

              }

              // Object類的加載器

              System.out.println("Object類的加載器:" + Object.class.getClassLoader());

 

              // 用戶定義應用類的加載器

              System.out.println("應用程序的類加載器:"

                            + getClassName(ClassLoaderTest.class.getClassLoader()));

       }

 

       private static String getClassName(Object o) {

              return o == null ? "null" : o.getClass().getSimpleName();

       }

       /*

       系統類加載器:AppClassLoader

       AppClassLoader 的父加載器:ExtClassLoader,且由 null 加載。

       ExtClassLoader 的父加載器:null,且由 null 加載。

       Object類的加載器:null

       應用程序的類加載器:AppClassLoader

        */

}

從上面的打印可看出:

l  系統類加載器為AppClassLoader類的實例。

l  系統類加載的父加載器為擴展類加載器,即ExtClassLoader類的實例。

l  擴展類加載器的父加載器為根類加載。但是,VM並不會向Java程序提供根類加載器的引用,而是返回Null,這是為了VM的安全。

l  Object類是由根類加載器加載的。

l  用戶自定義類是由系統類加載器加載的。

l  系統類加載器、擴展類加載器由根類加載器來加載。

 

當通過某類加載器加載類時,首先從自己的命名空間查找是否已經加載,如果已加載,則直接返回已加載的Class對象引用。如果沒有加載,則請求父類去加載,父類再去請求父類的父類去加載,加載請求就這樣一層層向上傳,直到根加載器,如果根加載器加載不成功,則將請求往回傳,直到有一個加載器能加載為止,再將加載的Class對象返回給最開始發起加載動作的類加載器。

 

加載器之間的父子關系實際上指的是加載器對象之間的關系,而不是類之間的繼承關系。

 

可能通過ClassLoader的構造函數指定父加載器(ClassLoader(ClassLoader parent)),如果構造時沒有指定,則使用系統類加載器作為父加載器,如果設置成null,則父加載器為根加載器。

 

父親委托機制的優點是能夠提高軟件系統的安全性。因數在此機制下,用戶自定的類加載器不可能加載應該由父加載器加載的可靠類,從而防止不可靠甚至惡意的代碼代替由父加載器加載的可靠代碼。如java.lang.String類總是由根類加載器加載,其他任何用戶自定義加載器都不可能加載含有惡意代碼的自定義的java.lang.String類,因為加載時先會去看上層父加載器是否加載,由於java.lang.String已被根類加載器加載過了,所以不會再加載我們自已定義的java.lang.String類。

 

命名空間:每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。但在不同的命名空間是可以的。

 

運行時包:由同一類加載器加載的屬於相同包的類組成了運行時包。決定兩個類是不是屬於同一個運行時包,不僅要看它們的包名是否相同,還要看定義類加載器是否相同。只有屬於同一運行包的類才能互相訪問包可見(即默認訪問級別)的類和類成員。這樣的限制能避免用戶自定義的類冒充類庫的類,去訪問核心類庫的包可見成員。假設用戶自定義了一個類java.lang.XXX,並由用戶自定義的類加載器加載,由於java.lang.XX和核心類庫java.lang.*由不同的加載器加載,它們屬於不同的運行時包,所以java.lang.XX不能訪問核心類庫java.lang包中的包可見成員,所以即使我們把類所在包定義成與核心類庫的包一樣,如將自定義的類放在java.lang包中,但由於加載是我們自定義的類是由系統類加載器或是自己定義類加載加載的,所以運行時所在包是不一樣的,所以即使我們冒充在同一包中,但還是不能訪問那些具有包訪問權限的類及成員。

 

若有一個類加載器能成功加載Sample類,那么這個類加載器被稱為定義類加載器,所有能成功返回Class對象的引用的類加載(包括定義類加載器)都被稱為初始類加載器。

 

>>>創建用戶自定義的類加載器<<<

要創建用戶自己的類加載器,只需要擴展java.lang.ClassLoader類,然后覆蓋它的findClass(String name)方法即可,該方法根據參數指定的類的名字,返回對應的Class對象的引用。

下面是自定義的MyClassLoader加載器:

public class MyClassLoader extends ClassLoader {

       private String classLoaderName;// 自定義類加載器的名字

       private String loadPath;// 類加載的加載路徑

 

       public MyClassLoader(String classLoaderName) {

              super();// 使用系統類加載器作為父加載器

              this.classLoaderName = classLoaderName;

       }

 

       public MyClassLoader(ClassLoader parentLoader, String classLoaderName) {

              super(parentLoader);// 指定parentLoader為父類加載器

              this.classLoaderName = classLoaderName;

       }

 

       public String toString() {

              return classLoaderName;

       }

 

       public void setPath(String path) {

              this.loadPath = path;

       }

 

       @Override

       protected Class findClass(String className) throws ClassNotFoundException {

              FileInputStream fis = null;

              byte[] data = null;

              ByteArrayOutputStream baos = null;

 

              try {

                     fis = new FileInputStream(new File(loadPath

                                   + className.replaceAll("\\.", "\\\\") + ".class"));

                     baos = new ByteArrayOutputStream();

                     int tmpByte = 0;

                     while ((tmpByte = fis.read()) != -1) {

                            baos.write(tmpByte);

                     }

                     data = baos.toByteArray();

              } catch (IOException e) {

                     throw new ClassNotFoundException("class is not found:" + className,

                                   e);

              } finally {

                     try {

                            if(fis != null){

                                   fis.close();

                            }

                            if(fis != null){

                                   baos.close();

                            }

                           

                     } catch (Exception e) {

                            e.printStackTrace();

                     }

              }

              return defineClass(className, data, 0, data.length);

       }

 

       public static void main(String[] args) throws Exception {

              // loader1的父加載器為系統類加載器

              MyClassLoader loader1 = new MyClassLoader("loader1");

              loader1.setPath("d:/myapp/serverlib/");

              // loader2的父加載器為loader1

              MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");

              loader2.setPath("d:/myapp/clientlib/");

              // loader3的父加載器根加載器

              MyClassLoader loader3 = new MyClassLoader(null, "loader3");

              loader3.setPath("d:/myapp/otherlib/");

              test(loader2);// 使用loader2測試

              System.out.println("-------------------");

              test(loader3);// 使用loader3測試

       }

 

       public static void test(ClassLoader loader) throws Exception {

              // 注,loadClass的參數為類的完整名,即包括包名

              Class objClass = loader.loadClass("pkg.Sample");

              Object obj = objClass.newInstance();

              System.out.println("obj=" + obj);

       }

}

 

package pkg;

public class Sample {

       public int v1 = 1;

       public Sample() {

              // 我們可以通過class對象的getClassLoader方法獲取當

              // class對象類加載器

              System.out.println("Sample loaded by "

                            + this.getClass().getClassLoader());

              new Dog();// 引用Dog,會導致Dog類的加載動作

       }

}

class Dog {

       public Dog() {

              System.out.println("Dog loaded by " +

                            this.getClass().getClassLoader());

       }

}

 

開始編譯:

D:\myapp\syslib>javac -d . MyClassLoader.java

D:\myapp\syslib>javac -d . pkg/Sample.java

編譯后整個系統的目錄結構如下:

D:\MYAPP

├─clientlib

├─otherlib

├─serverlib

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Sample.class

            Dog.class

 

接下來通過改變Sample類和Dog類的存放路徑,或者修改源程序,來演示類加載器的各種特性:

1)        構造以下目錄結構:

D:\MYAPP

├─clientlib

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

  └─pkg

          Sample.class

          Dog.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Sample.class

            Dog.class

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41

Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41

obj=pkg.Sample@1a46e30

-------------------

Sample loaded by loader3

Dog loaded by loader3

obj=pkg.Sample@addbf1

 

由於父類加載的委托機制,加載一個類時,會先從根加載器開始加載,如果根加載器加載不成功或拒絕加載,則由擴展器來加載,同樣不成功或拒絕加載則由系統加載器來加載,如果還不成功則由自定義的類加載器來加載。loder2的父加載器結構為 loder2àloder1à AppClassLoaderà ExtClassLoaderà Bootstrap,由於我們自己定義的類默認是由系統類加載器AppClassLoader來加載的,系統類加載器會加載環境變量classpath的類,而運行時設置的classpath ./syslib,且在該路徑下有Sample.classDog.class,所以loader2.loadClass("pkg.Sample")加載時使用系統類加載器從classpath路徑中來加載Sample.class,又由於Sample.class的構造函數中引用Dog.class,所以先默認采用同樣的類加載器來加載Dog.class,如果系統類加載器加載Dog.class失敗時,將會怎么樣,請繼承往下看。

loader3.loadClass("pkg.Sample")就更簡單了,因為loader3的父加載器結構為loder3à Bootstrap,又根加載器不能加載用戶自定義類,所以只能由loder3D:\myapp\otherlib路徑中加載了。

從這個例子還可以看出,在loader1loader3各自的命名空間中,都在Sample類和Dog類,也就是說,在VM中有兩個SampleClass對象和兩個Dog類的Class對象。

 

2)        構造以下目錄結構:

D:\MYAPP

├─clientlib

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

  └─pkg

          Sample.class

          Dog.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader1

Dog loaded by loader1

obj=pkg.Sample@3e25a5

-------------------

Sample loaded by loader3

Dog loaded by loader3

obj=pkg.Sample@42e816

 

由於系統類加載器不能加載Sample.class,所以由loader1來嘗試,且加載成功。

 

3)        構造以下目錄結構:

D:\MYAPP

├─clientlib

  └─pkg

          Sample.class

          Dog.class

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader2

Dog loaded by loader2

obj=pkg.Sample@3e25a5

-------------------

Sample loaded by loader3

Dog loaded by loader3

obj=pkg.Sample@42e816

 

4)        構造以下目錄結構:

D:\MYAPP

├─clientlib

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

  └─pkg

          Sample.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Dog.class

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader1

Exception in thread "main" java.lang.IllegalAccessError: tried to access class pkg.Dog from class pkg.Sample

。。。

雖然Sample.classloader1來加載,而Dog.class由系統類加載器來加載的,而系統類加載器又是loader1的父加載器,根據后面的規則:“子加載器的命名空間包含所有父加載器的命名空間。因此由子加載器加載的類能看見父加載器加載的類。”,似乎可以正常運行,但這只是說能看到這個類,並不代表你能夠訪問到這個類(如果是包訪問權限的話,而這里的Dog類恰好就是包訪問權限的,所以你不能訪問這個類)及這個類里的包訪問權限的成員及方法;再根據規則“由同一類加載器加載的屬於相同包的類組成了運行時包,只有屬於同一運行包的類才能互相訪問包可見(即默認訪問級別)的類和類成員”,由於SampleDog由不同的類加載器來加載的,他們不屬於同一個運行時包,所以就出現了上面運行時錯誤。但如果將Dog類訪問權限修改成public時,則可以訪問,請繼續往下看。

 

從上面我們要注意,不是只要兩個類在同一個包中就可以相互訪問包訪問權限的類及類的成員,還要看他們是否是由同一加載器來加載的,即屬於同一運行時包的類才真正屬於同一包。

 

5)        構造以下目錄結構(將Dog類單獨寫成一個類,並將class定義成public):

D:\MYAPP

├─clientlib

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

  └─pkg

          Sample.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Dog.java

            Dog.class

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader1

Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41

obj=pkg.Sample@3e25a5

-------------------

Sample loaded by loader3

Dog loaded by loader3

obj=pkg.Sample@42e816

由於子加載器加載的類(Sample)能看見父加載器加載的類(Dog),所以以上運行正常。

 

6)        構造以下目錄結構:

D:\MYAPP

├─clientlib

├─otherlib

  └─pkg

          Sample.class

          Dog.class

├─serverlib

  └─pkg

          Dog.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Dog.java

            Sample.class

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41

Exception in thread "main" java.lang.NoClassDefFoundError: pkg/Dog

。。。

由於父加載器加載的類(Sample)不能看見子加載器加載的類(Dog),所以運行錯誤。

 

7)        不同類加載器的命名空間存在以下關系:

l  同一個命名空間內的類是相互可見的。

l  子加載器的命名空間包含所有父加載器的命名空間。因此由子加載器加載的類能看見父加載器加載的類。例如系統類加載器加載的類能看見根類加載器加載的類。

l  由父加載器加載的類不能看見子加載器加載的類。

l  如果兩個加載器之間沒有直接或間接的父子關系,那么它們各自加載的類相互不可見。

所謂類A能看見類B,就是指在類A的程序代碼中可以引用類B的名字,例如:

Class A{ B b = new B();}

下面把Sample.classDog.class僅僅拷貝到D:\myapp\serverlib目錄下,然后把MyClassLoader類的main()方法修改為下面代碼:

MyClassLoader loader1 = new MyClassLoader("loader1");

loader1.setPath("d:/myapp/serverlib/");

Class objClass = loader1.loadClass("pkg.Sample");

Object obj = objClass.newInstance();

Sample sample = (Sample)obj;//拋出NoClassDefFoundError錯誤

System.out.println(sample.v1);

 

此時的目錄結構如下:

D:\MYAPP

├─clientlib

├─otherlib

├─serverlib

  └─pkg

          Dog.class

          Sample.class

└─syslib

      MyClassLoader.java

      MyClassLoader.class

    └─pkg

            Sample.java

            Dog.java

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader1

Dog loaded by loader1

Exception in thread "main" java.lang.NoClassDefFoundError: pkg/Sample

        at MyClassLoader.main(MyClassLoader.java:70)

 

由於MyClassLoader類由系統類加載器加載,而Sample類由loader1類加載,因此MyClassLoader類看不見Sample類(根據規則“由父加載器加載的類不能看見子加載器加載的類”)。在MyClassLoader類的main()方法中使用Sample類,會導致NoClassDefFoundError錯誤。但把Sample類與Dog類的類文件拷貝到D:\myapp\syslib下時,卻又能正常,因為此時他們都是由系統類加載器加載,MyClassLoaderSampleDog屬於同一命名空間中的類,所以MyClassLoader能正常訪問Sample類。

 

當兩個不同命名空間內的類相互不可見時,可采用反射機制來訪問對方實例的屬性和方法,如果把MyClassLoader類的main()方法替換為如下代碼:

MyClassLoader loader1 = new MyClassLoader("loader1");

loader1.setPath("d:/myapp/serverlib/");

Class objClass = loader1.loadClass("pkg.Sample");

Object obj = objClass.newInstance();

Field f = objClass.getField("v1");

int v1 = f.getInt(obj);

System.out.println(v1);

 

D:\myapp>java -classpath ./syslib MyClassLoader

Sample loaded by loader1

Dog loaded by loader1

v1=1

 

 

>>>使用URLClassLoader<<<

URLClassLoader為在,它擴展了ClassLoader類,它不僅能從本地文件系統中加載類,還可以從網上下載類。下面程序演示了從jar文件中加載Sample類:

URLClassLoader urlLoader = new URLClassLoader(new URL[] { new URL(

              "file:d:/sample.jar") });

Class c = urlLoader.loadClass("pkg.Sample");

System.out.println(c.newInstance());

 

輸出:

Sample loaded by java.net.URLClassLoader@757aef

Dog loaded by java.net.URLClassLoader@757aef

pkg.Sample@19821f

 

>>>類的卸載<<<

Sample類被加載、連接和初始化后,它的生命周期就開始了。當代表Sample類的Class對象不再被引用,即不可達時,Class對象生命就會結束,Sample類在方法區內的數據也會被卸載,從而結束Sample類的生命周期。由此可見,一個類何時結束生命周期,取決於代表它的Class對象何時結束生命周期。

VM自帶的類加載器所加載的類,在VM的生命周期中,始終不會被卸載(如Object類)。VM自帶的類加載器包括根類加載器、擴展類加載器和系統類加載器。VM本身會始終引用這此類加載器,而這些類加載器則會始終引用它們所加的類的Class對象,因此這此Class對象始終是可達的,所以說由VM自帶的類加載器所加載的類始終不會被卸載。

由用戶自定義的類加載器所加載的類是可以被卸載的。

 

下面以MyClassLoader類為例,介紹Sample類被卸載時機。把Sample.classDog.class拷貝到D:\myapp\serverlib目錄下,然后把MyClassLoader類的main()方法替換為:

MyClassLoader loader1 = new MyClassLoader("loader1");// 1

loader1.setPath("d:/myapp/serverlib/");// 2

Class objClass = loader1.loadClass("pkg.Sample");// 3

System.out.println("objClass hashCode" + objClass.hashCode());// 4

Object obj = objClass.newInstance();// 5

loader1 = null;// 6

objClass = null;// 7

obj = null; // 8

loader1 = new MyClassLoader("loader1");// 9

loader1.setPath("d:/myapp/serverlib/");//10

objClass = loader1.loadClass("pkg.Sample");// 11

System.out.println("objClass hashCode" + objClass.hashCode());

 

運行以上程序時,Sample類由loader1加載。在類加載器的內部實現中,用一個Java集合來存放所加載類的引用。另一方面,一個Class對象總是會引用它的類加載器,調用Class對象的getClassLoader()方法,就能獲得它的類加載器。由此可見,代表Sample類的Class實例與loader1之間為雙向關系。

一個類實例總是引用代表這個類的Class對象。在Object類中定義了getClass()方法,這個方法返回代表對象所屬類的Class對象的引用。此外,所有的Java類都有一個靜態屬性Class,它引用代表這個類的Class對象。

當程序執行完第5步時,引用變量與對象之間的引用關系如圖:

image005

從圖可以看出,loader1變量和obj變量間接引用代表Sample類的Class對象,而objClass變量則直接引用它。

當程序執行完第8步,所有的引用變量都置為null,此時Sample對象結束生命周期,MyClassLoader對象結束生命周期,代表Sample類的Class對象也結束生命周期,Sample類在方法區內的二進制數據被卸載(注,但這里並不代表立即被回收了)。

當程序執行完第11步時,Sample類又重新被加載,在堆區會生成一個新的代表Sample類的Class實例:

以上程序輸出結果如下:

objClass hashCode10267414

Sample loaded by loader1

Dog loaded by loader1

objClass hashCode11394033

 

注,運行之前一定要先刪除syslib下面的Sample.classDog.class類文件,否則上面的程序會由系統類加載器去加載Sample類,此時Sample類在執行第8行后SampleClass對象也不會卸載,因為該Class對象是由系統類加載器加的。如果不刪除syslib下的Sample.classDog.class類文件,則輸出結果為:

objClass hashCode14285251

Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41

Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41

objClass hashCode14285251

 

Class中的getResourceAsStream

===============Class中的getResourceAsStream===================

public InputStream getResourceAsStream(String name) {

    name = resolveName(name);

    ClassLoader cl = getClassLoader0();//獲取類加載

    if (cl==null) {//如果類加載器為null,則為根類加載器

        // A system class. 而不是應用類時,使用根類加載器來加載資源

        return ClassLoader.getSystemResourceAsStream(name);

    }

    //通過調用ClassLoadergetResourceAsStream在類加載器搜索路徑中加載指定的路徑資源

    return cl.getResourceAsStream(name);

}

 

/*

* 從實現可以看出,傳進來的資源路徑 name 有兩種形式: 一是以 / 開頭的路徑,

* 它是相對於 <CLASSPATH> 路徑的文件路徑 二是不以 / 開頭的路徑,它是相對

* 於當前類所在包的路徑。

*/

private String resolveName(String name) {

       if (name == null) {

              return name;

       }

       /*

        * 如果傳進來的路徑不是以 / 開頭,則name表示只能是相對於 包路徑的文件

        * 名路徑,如某個類的完整類名為 pak1.pak2.ClassXXX 傳遞進來的 name

        * dir/filename.txt,則最后返回的路徑為 pak1/pak2/dir/filename.txt

        * 它表示在<CLASSPATH>/pak1/pak2/dir 上當下有 filename.txt 文件

        *

        * 如果name是以 / 開頭,則會去掉 開頭的 / 后返回。要注意的是, 此時的路

        * 徑開頭的 / 表示的是 <CLASSPATH>,如果此時某個文件是在 某個包目錄下,

        * name 一定要帶上包路徑

        */

       if (!name.startsWith("/")) {

              Class c = this.getClass();

              while (c.isArray()) {

                     c = c.getComponentType();

              }

              String baseName = c.getName();

              int index = baseName.lastIndexOf('.');

              if (index != -1) {

                     name = baseName.substring(0, index).replace('.', '/') + "/"

                                   + name;

              }

       } else {

              name = name.substring(1);

       }

       return name;

}

 

===============ClassLoadergetResourceAsStream===================

public InputStream getResourceAsStream(String name) {

       // 獲取資源的URL

       URL url = getResource(name);

       try {

              // 打開資源流

              return url != null ? url.openStream() : null;

       } catch (IOException e) {

              return null;

       }

}

 

//獲取資源的URL對象

public URL getResource(String name) {

       URL url;

       //如果類加載器還有父加載器,則由父加載器加載

       if (parent != null) {

              url = parent.getResource(name);

       } else {//一直遞歸到根加載器。按理說根加載器的搜索路徑為<JAVA_HOME>/jre/lib/rt.jar,所以url會返回null?????

              url = getBootstrapResource(name);

       }

       if (url == null) {

              url = findResource(name);// ClassLoader中此方法的實現返回 null

       }

       return url;

}

上面是ClassLoader類的實現,在不同的類加載器中資源的加載方式實現是不同的,比如ExtClassLoaderAppClassLoader實現就可能不一樣。

 

比如在pak1.pak2包下有xx.txt文件,下面的三個語句是等效的:

// 相對於當前類所在的包路徑 pak1/pak2

pak1.pak2.Resource.class.getResourceAsStream("xx.txt");

// /pak1/pak2/xx.txt 相對於<JAVA_HOME>

pak1.pak2.Resource.class.getResourceAsStream("/pak1/pak2/xx.txt");

/*

* ClassgetResourceAsStream就是調用ClassLoader.getResourceAsStream來實現的,

* ClassgetResourceAsStream再調用ClassLoader.getResourceAsStream之前,

* 就已經將路徑開頭的 / 去掉了,所以如果是直接調用ClassLoader.getResourceAsStream

* 時,一定不能以/開頭

*/

pak1.pak2.Resource.class.getClassLoader().getResourceAsStream("pak1/pak2/xx.txt");

通過Class類的getResourceAsStream獲取 jar 包里的資源

可以通過Class類的getResourceAsStream來獲取 jar 包里的資源,比如jar包里含有一個 /resource/res.txt 文件,並與Resource 類的class打一個 jar,結構如下:

    1src/

              src/pak1/pak2/Resource.java

    2bin/

              bin/resource/res.txt

              bin/pak1/pak2/Resource.class

可以通過下面的方法來獲取jar包里的資源

public class Resource {

       public void getResource() throws IOException{

        //<CLASSPATH>路徑中搜索資源,這里的classpathjar包所在的路徑

              InputStream is=this.getClass().getResourceAsStream("/resource/res.txt");

              BufferedReader br=new BufferedReader(new InputStreamReader(is));

              String s="";

              while((s=br.readLine())!=null)

                     System.out.println(s);

       }

}

上面是資源文件與訪問的它的類在同一jar包中,如果它們不在同一jar包中,可以這樣訪問:

public class Resource {

       public void getResource() throws IOException, Exception {

              URLClassLoader urlLoader = new URLClassLoader(new URL[] { new URL(

                            "file:d:/sample.jar") });

              // Class c = urlLoader.loadClass("pkg.Sample");

              InputStream is = urlLoader.getResourceAsStream("/resource/res.txt");

              BufferedReader br = new BufferedReader(new InputStreamReader(is));

              String s = "";

              while ((s = br.readLine()) != null)

                     System.out.println(s);

       }

}

 

通過JarURLConnection獲取 jar 包里的資源

JAR URL 的語法為: jar:<url>!/{entry}   如,jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

JarURLConnection 實例只能用於從 JAR 文件讀取內容。

 

 

 

 

 

新的轉型語法:

class Building {}

class House extends Building {}

public class ClassCasts {

  public static void main(String[] args) {

    Building b = new House();

    Class<House> houseType = House.class;

    House h = houseType.cast(b);// b向下轉型,這樣在Java SE5不會發生警告

h = (House)b; // 以前的強制轉法,但這樣在Java SE5會發生警告

// 所以為了在Java SE5強轉時不發生警告,則請使用新的轉型方式

  }

}

 

動態的instanceofClassisInstance(Object obj)方法與instanceof完全等價,判定指定的Object是否與此 Class所表示的對象賦值兼容。此方法是 Java 語言 instanceof運算符的動態等效方法。如果指定的 Object參數非空,且能夠在不引發 ClassCastException的情況下被強制轉換成該 Class對象所表示的引用類型,則返回 true,否則返回 false

 

class.isAssignableFrom(Class<?> cls)判定此class對象所表示的類或接口與指定的cls參數所表示的類或接口是否相同,或是class否是cls超類或超接口。如果是則返回true,否則返回 false

 

反射:運行時的類信息

Class類與java.lang.reflect類庫一起對反射的進行了支持,該類庫包含了FieldMethod以及Constructor類(都實現了Member接口)。這些類型的對象是由JVM在運行時創建的,用來表示未知類里對應的成員。這樣你就可以使用Constructor創建新的對象,用get()set()方法讀取和修改與Field對象關聯的字段,用invoke()方法調用與Method對象關聯的方法。另外,還可以調用Class對象的getFields()getMethods()getConstructors()等很便利的方法,以返回表示字段、方法以及構造器的對象的數組。這樣,匿名對象的類信息就能在運行時被完全確定下來,而在編譯時不需要知道任何事情,但是,這個類的.class文件對於JVM來說必須是可獲取的,要么在本機上,要么可以通過網絡取得。

 

RTTI與反射之間真正的區別只在於:對於RTTI來說,編譯器在編譯時打開與檢查.class文件,換句話說,我們可以用“普通”方式調用對象的所有方法;而對於反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時打開與檢查.class文件。

 

通過反射可以訪問類中的所有東西,包括private修飾的。

 

動態代理:

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

//實現調用處理器接口

class MethodSelectorHandler implements InvocationHandler {

       private Object proxied;// 引用被代理的真真對象

       public MethodSelectorHandler(Object proxied) {

              this.proxied = proxied;

       }

 

       // 實現接口,動態代理可以將所有調用重定向到該方法

       public Object invoke(Object proxy, Method method, Object[] args)

                     throws Throwable {

              // 在這里做額外的工作

              if (method.getName().equals("interesting")) {

                     System.out.println("Proxy detected the interesting method");

              }

              return method.invoke(proxied, args);

       }

}

 

// 代理接口,Java中的動態代理對象一類要實現某個接口

interface SomeMethods {

       void boring1();

       void boring2();

       void interesting(String arg);

       void boring3();

}

 

// 真真被代理的類

class Implementation implements SomeMethods {

       public void boring1() {

              System.out.println("boring1");

       }

 

       public void boring2() {

              System.out.println("boring2");

       }

 

       public void interesting(String arg) {

              System.out.println("interesting " + arg);

       }

 

       public void boring3() {

              System.out.println("boring3");

       }

}

 

class SelectingMethods {

       public static void main(String[] args) {

              // 創建代理對象 第一個參數為類加載器;第二個為所實現的接口,可有多個;第

              // 三個為處理器,構建處理器時需指定真真被代理的對象。返回的是代理對象

              SomeMethods proxy = (SomeMethods) Proxy.newProxyInstance(

                            SomeMethods.class.getClassLoader(),

                            new Class[] { SomeMethods.class }, new MethodSelectorHandler(

                                          new Implementation()));

 

              // 通過代理對象調用

              proxy.boring1();

              proxy.boring2();

              proxy.interesting("bonobo");

              proxy.boring3();

       }

}

 

如果使用反射創建一個對象時,而需要調用帶參的構造函數,則可以使用ConstructornewInstance(Object[] initargs)方法來代替ClassnewInstance()方法。

 

 

>>>反射工具<<<

//反射類信息

public class Reflection {

   //打印構造器

   public static void printConstructors(Class cl) {

      /*

       * 返回一個包含某些 Constructor 對象的數組,這些對象反映此 Class 對象所表

       * 示的類的所有公共構造方法。

       */

      Constructor[] constructors = cl.getDeclaredConstructors();

      for (Constructor c : constructors) {

         String name = c.getName();//構造方法的名稱

         // 輸出構造器前所有修飾符

         System.out.print(" " + Modifier.toString(c.getModifiers()));

         System.out.print(" " + name + "(");

 

         // 輸出參數類型與名稱

         Class[] paramTypes = c.getParameterTypes();

         for (int j = 0; j < paramTypes.length; j++) {

            if (j > 0) {

                System.out.print(", ");

            }

            System.out.print(paramTypes[j].getName());

         }

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

      }

   }

 

   //打印方法

   public static void printMethods(Class cl) {

      /*

       * getDeclaredMethods():

       * 返回 Method 對象的一個數組,這些對象反映此 Class 對象表示的類或接口聲明

       * 的所有方法,包括公共、保護、包訪問和私有方法,但不包括繼承的方法。

       */

      Method[] methods = cl.getDeclaredMethods();

      //准備輸出方法

      for (Method m : methods) {

         Class retType = m.getReturnType();//方法返回類型

         String name = m.getName();//方法的名字

 

         //輸出方法的修飾符、返回類型以及方法名

         System.out.print(" " + Modifier.toString(m.getModifiers()));

         System.out.print(" " + retType.getName() + " " + name + "(");

 

         //輸出參數類型

         Class[] paramTypes = m.getParameterTypes();

         for (int j = 0; j < paramTypes.length; j++) {

            if (j > 0) {

                System.out.print(", ");

            }

            System.out.print(paramTypes[j].getName());

         }

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

      }

   }

 

   //打印字段

   public static void printFields(Class cl) {

      Field[] fields = cl.getDeclaredFields();

      for (Field f : fields) {

         Class type = f.getType();//字段類型

         String name = f.getName();//字段名

         System.out.print(" " + Modifier.toString(f.getModifiers()));

         System.out.println(" " + type.getName() + " " + name + ";");

      }

   }

 

   public static void main(String[] args) {

      String name;

      if (args.length > 0) {

         name = args[0];

      } else {

         Scanner in = new Scanner(System.in);

         System.out.println("Enter class name(e.g. java.util.Date)");

         name = in.next();

      }

      try {

         Class cl = Class.forName(name);

         Class supercl = cl.getSuperclass();

         //打印類的定義

         System.out.print("class " + name);

         if (supercl != null && supercl != Object.class) {

            System.out.println(" extends " + supercl.getName());

         }

 

         System.out.println("\n{\n //構造器");

         printConstructors(cl);//打印構造器

         System.out.println(" //字段");

         printFields(cl);//打印字段

         System.out.println(" //方法");

         printMethods(cl);//打印方法

         System.out.println("}");

 

      } catch (Exception e) {

         e.printStackTrace();

      }

   }

}

應用反射機制打印對象成員值信息請看XXXXAbstractDTO

第十五章泛型

參見《XXXXXX

 

類型推斷:

import java.util.HashMap;

import java.util.List;

import java.util.Map;

 

public class LimitsOfInference {

   /*

    * 外界創建一個Map對象時只需執行

    * Map<String, List<String>> map = newMap();

    *

    * 類似的語句,而不必麻煩地使用

    * Map<String, List<String>> map = new Map<String, List<String>>();

    *

    * 所以可以試着把這些創建集合的代碼集中封裝到一個公共類中,省去創建時指

    * 定類型,它可根據賦值語句前部分聲明來推導出類型參數

    */

   static <K, V> Map<K, V> newMap() {

      /*

       * 編譯時會根據賦值語句來推斷 K,V 的參數類型。

       *

       * 所謂的方法類型推斷是指:

       * 方法是泛型的,但在執行過程中方法體中不知道確切的參數類型,即泛型

       * 方法不帶泛型參數,就像該方法是泛型方法但沒有傳遞參數類型,但如果

       * 該方法帶類型參數時(如 newMap(K k,V v),調用時就不存在類型推

       * 斷問題了,因為在調用時參數類型已經傳進了,執行時就已確定了,則最

       * 后泛型方法返回的結果類型就可以確定了。

       */

      return new HashMap<K, V>();

   }

 

   static void f(Map<String, List<String>> map) {}

  

   static Map<String, List<String>> h() {

      //將泛型方法的結果作為某方法的返回值,此時也會發生類型推斷

      return newMap();

   }

  

 

   public static void main(String[] args) {

      /*

       * 類型推斷發生在兩個時機,第一個就是直接賦值語句中。

       * 第二就是將泛型方法的結果作為某方法的返回值

       */

      // 賦值語句能推斷出newMap方法中的類型參數類型

      Map<String, List<String>> map = newMap();

     

      // 編譯沒問題,因為h()返回的類型是確定的

      f(h());

     

      //!! Does not compile,因為類型推斷只發生在賦值語句與返回時

      // f(newMap());

     

      // 但可以顯示的指定返回參數類型

      f(LimitsOfInference.<String, List<String>>newMap());

   }

}

 

 

泛型類型參數將擦除到它的第一個邊界(因為可能會有多個邊界),而普通的類型變量在未指定邊界的情況下被擦除為Object。使用與不使用泛型生成的字節碼是一樣的。

 

 

推薦使用Array.newInstance()方式創建泛型數組:

T[] arrayMaker(Class<T> kind,int size) {

  return (T[])Array.newInstance(kind, size);

}

但也可這樣:

T[] arrayMaker(int size) {

  return (T[])new Object[size];

}

這與上面相同的是最后創建出的數組類型表面上(返回給別人的)都是Object類型(因為擦除關系),但前者的真真數組類型還是由傳遞進來的Class類類型來決定。

 

 

邊界:即對象進入和離開方法的地點,這些也是編譯器在編譯期執行類型檢查並插入轉型代碼的地點。泛型中的所有動作都發生在邊界處——對象傳遞進來的值進行額外的編譯檢查,並插入對傳遞出去的值的轉型。

 

 

extends邊界:因為擦除了類型信息,所以,能通過無界(Colored<T>)泛型參數調用的方法只是那些Object中的方法(因為未使用extends定界時上界默認就是Object)。但是,如果將這個參數T限制為某個類型的子類型,那么我們就可以用這些類型子類的相關方法,所以extends的作用在於定界,而定界又是為了調用某個泛型類的方法:

//邊界接口

interface HasColor { java.awt.Color getColor(); }

class Colored<T extends HasColor> {

  T item;

  Colored(T item) { this.item = item; }

  T getItem() { return item; }

  // 允許調用邊界接口的方法:

  java.awt.Color color() { return item.getColor(); }

}

 

//邊界類

class Dimension { public int x, y, z; }

 

// 當同時有邊界類與接口時,接口放在類的后面,與繼承一樣:

//!! class ColoredDimension<T extends HasColor & Dimension> {}

 

// 多個邊界時,邊界接口要放在邊界類后面:

class ColoredDimension<T extends Dimension & HasColor> {

  T item;

  ColoredDimension(T item) { this.item = item; }

  T getItem() { return item; }

  //訪問邊界接口方法

  java.awt.Color color() { return item.getColor(); }

  //訪問邊界類中的成員

  int getX() { return item.x; }

  int getY() { return item.y; }

  int getZ() { return item.z; }

}

 

//另一邊界接口

interface Weight { int weight(); }

 

// 與繼承一樣,多個邊界時,只允許一個邊界類,但允許多個邊界接口:

class Solid<T extends Dimension & HasColor & Weight> {

  T item;

  Solid(T item) { this.item = item; }

  T getItem() { return item; }

  //訪問邊界接口

  java.awt.Color color() { return item.getColor(); }

  //訪問邊界類中的成員

  int getX() { return item.x; }

  int getY() { return item.y; }

  int getZ() { return item.z; }

  //訪問邊界接口

  int weight() { return item.weight(); }

}

 

//Solid類中的類型參數實現類

class Bounded

extends Dimension implements HasColor, Weight {

  public java.awt.Color getColor() { return null; }

  public int weight() { return 0; }

 

public class BasicBounds {

  public static void main(String[] args) {

    Solid<Bounded> solid =

      new Solid<Bounded>(new Bounded());

    solid.color();

    solid.getY();

    solid.weight();

  }

}

 

 

邊界與繼承:可以在繼承的每個層次上逐漸添加邊界限制。使用繼承的方式修改上面泛型類,這樣不必在每個類中重復定義與實現某些方法:

class HoldItem<T> {

  T item;

  HoldItem(T item) { this.item = item; }

  T getItem() { return item; }

}

 

//添加HasColor邊界接口,此時的類型參數T要是實現了HasColor的類

class Colored2<T extends HasColor> extends HoldItem<T> {

  Colored2(T item) { super(item); }

  //訪問邊界接口中的方法

  java.awt.Color color() { return item.getColor(); }

}

 

//添加Dimension邊界類,此時的類型參數T要是繼承了Dimension類並是

//實現HasColor接口的類

class ColoredDimension2<T extends Dimension & HasColor>

extends Colored2<T> {

  ColoredDimension2(T item) {  super(item); }

  //訪問新添加的邊界類中的成員

  int getX() { return item.x; }

  int getY() { return item.y; }

  int getZ() { return item.z; }

}

 

//添加Weight邊界類,此時的類型參數T要是繼承了Dimension類並

//實現HasColor接口與Weight接口的類

class Solid2<T extends Dimension & HasColor & Weight>

extends ColoredDimension2<T> {

  Solid2(T item) {  super(item); }

  //訪問新添加的邊界接口中的方法weight()

  int weight() { return item.weight(); }

}

 

//繼承邊界類測試

public class InheritBounds {

  public static void main(String[] args) {

   //Bounded符合Solid2中類型參數T

    Solid2<Bounded> solid2 =

      new Solid2<Bounded>(new Bounded());

    solid2.color();//訪問父類Colored2中的方法

    solid2.getY();//訪問父類ColoredDimension2中的方法

    solid2.weight();//訪問自身的方法

  }

}

 

 

通配符?的疑問:

class Fruit {}

class Apple extends Fruit {}

class Jonathan extends Apple {}

class Orange extends Fruit {}

 

public class CompilerIntelligence {

  public static void main(String[] args) {

   //? 通配符,flist指向存放Fruit及任何子類List容器

    List<? extends Fruit> flist =Arrays.asList(new Apple());

    //使用通配符定義的引用,不能通過該引用調用任何帶有泛型參數的方法

    //!! flist.add(new Apple());

//!! flist.add(new Fruit());

 

    //但可調用返回參數類型是泛型的方法,盡管返回類型為泛型

    //但不管返回的是什么,至少是Fruit類型,所以是合理的

    Apple a = (Apple)flist.get(0); // No warning

   

    //但可調用參數不是泛型參數的方法,通過查看源碼,我們發現

    //containsindexOf的參數類型都是Object

    flist.contains(new Jonathan());

    flist.indexOf(new Fruit());

  }

}

從上面程序可以知道,在使用通配符定義的引用后,為什么add方法不能使用,而containsindexOf卻可以。該限制不是用編譯器去檢查特定的方法是否修改了它的對象。其實編譯器並沒有這么聰明。add()接受的是一個具有泛型參數類型的參數,但是contains()indexOf()接受的是一個Object類型的參數。因此當你定義一個ArrayList<? extends Fruit>時,add(E o)的參數 E 就變成了“? extends Fruit”,因此編譯器並不知道這里需要Fruit的哪個具體子類型,所以它不能接受任何類型的Fruit,編譯器直接拒絕對參數列表中涉及通配符的方法(例如add(E o))的調用。在使用contains(Object elem)indexOf(Object elem)時,參數類型是Object,因此不涉及任何通配符,所以編譯器將允許這樣調用。因此,為了在類型中使用了通配符的情況下禁止這個類的調用,我們需要在參數列表中使用類型參數。

從下面程序也可看出這一點:

public class Holder<T> {

  private T value;

  public Holder() {}

  public Holder(T val) { value = val; }

  public void set(T val) { value = val; }

  public T get() { return value; }

  //參數不是泛型類型參數

  public boolean equals(Object obj) {

    return value.equals(obj);

  }

  public static void main(String[] args) {

    Holder<Apple> apple = new Holder<Apple>(new Apple());

    Apple d = apple.get();

    apple.set(d);

   

    //Holder<Apple>類型不是Holder<Fruit>的子類

    //!! Holder<Fruit> Fruit = apple; // Cannot upcast

   

    //但使用通配符是可以的

    Holder<? extends Fruit> fruit = apple; // OK

   

    //可以調用它的get方法,因為該方法不帶參數,盡管返回類型為泛型

    //因為不管返回的是什么,但至少是Fruit類型,所以是合理的

    Fruit p = fruit.get();

   

    //因為本身就是Apple類型,所以能安全強制向下轉型

    d = (Apple)fruit.get();

    try {

      //編譯時不會警告,但運行時發生ClassCastException

      Orange c = (Orange)fruit.get();

    } catch(Exception e) { System.out.println(e); }

   

    //因為fruit是通過通配符方式定義的,所以不能調用帶類型參數的方法

    //!! fruit.set(new Apple());

    //!! fruit.set(new Fruit());

   

    //但是equals方法參數是Object類型,所以可以調用

    System.out.println(fruit.equals(d)); // OK

  }

}

 

 

 

 

<T extends MyClass>是用來解決不能調用泛型類型參T及實例的方法的問題,即解決了類型邊界問題。

<? extends MyClass>是用來解決 ArrayList<Fruit> list = new ArrayList< Apple>();的問題或者是方法參數的傳遞問題。

 

 

超類型通配符:通配符是由某個特定類的任何基類來界定,方法是指定<? super MyClass>,甚至使用類型參數:<? super T>,但你不能對泛型參數指定一個超類型邊界(如<T super MyClass>,但<T extends MyClass>卻是可以的)。只能用於方法的參數類型說明與變量的定義,不能用於類,也不可用來定義某個類型參數T,因為沒有<T super MyClass>形式。

定義變量:List<? super Apple> l = new ArrayList<Fruit>();

方法的參數類型說明:Collections.static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

 

ArrayList<? extends Fruit>ArrayList<? super Jonathan>的區別:

class Fruit {}

class Apple extends Fruit {}

class Jonathan extends Apple {}

class OtherJonathan extends Jonathan {}

public class SuperTypeWildcards {

   static void writeTo(List<? extends Apple> apples) {

      /*

       * ArrayList<? extends Fruit>表示它定義的list1引用可指向能存放

       * Fruit及其子類的容器實例,但不能真真的向容器里放入任何東西除了

       * null

       *

       * 這里使用的是 extends ,所以new ArrayList<XXX>()中的XXX只能

       * Fruit 或其子類。

       */

      ArrayList<? extends Fruit> list1 = new ArrayList<Apple>();

      // !! list1.add(new Apple());

      // !! list1.add(new Fruit());

 

      /*

       * ArrayList<? super Jonathan>表示它定義的list2引用可指向能存

       * Jonathan及子類實例的容器,與上面不同的是可以向其中放入對象。

       *

       * 這里使用的是 super ,所以new ArrayList<XXX>()中的XXX只能

       * Jonathan 或其父類,因為只有這樣才能確保創建出來的容器能存

       * Jonathan及子類實例

       */

      ArrayList<? super Jonathan> list2 = new ArrayList<Fruit>();

      // !! list2.add(new Fruit());//不能存入Fruit

      // !! list2.add(new Apple());//不能存入Apple

      list2.add(new Jonathan());

      list2.add(new OtherJonathan());

 

      // 類型參數中含有super關鍵字所定義的引用只能指向類型參數及父類的實例

      // !! ArrayList<? super Apple> list13 = new ArrayList<Jonathan>();

   }

}

 

再看另一個實例:

class GenericWriteReading {

 

   //----------寫入

   static <T> void writeExact(List<T> list, T item) {

      list.add(item);

   }

 

   /*

    * 返回的參數類型為 t1t2的公共父類,因為Object為任何類的父類,所

    * t1t2可以是任何類型參數都可以

    */

   static <T> T writeExact1(T t1, T t2) {

      return t1 == null ? t2 : t1;

   }

 

   static void write1() {

      writeExact(new ArrayList<Apple>(), new Apple());

      writeExact(new ArrayList<Fruit>(), new Fruit());

 

      //書說這行不能編譯通過,源碼已被注釋掉了,但運行了一下可以,why?

      writeExact(new ArrayList<Fruit>(), new Apple());

 

      /*

       * 編譯不能通過,上面行可以,編譯器是如何做得到的?

       *

       * 經過自己推敲,上下兩行不同的是,list的參數類型如果是后面item參數類

       * 型的父類就可以,把List換成自己創建的類也是這樣的,編譯器也許就是根

       * List<XXX> 中的類型XXX 是否與item的類型相同或是父類來判斷的。

       *

       * 另外,從writeExact1方法可知,如果類型參數不是作為其他類型的類型參數

       * 使用(如writeExact1方法中的類型參數)時,這此參數的類型之間可以說沒

       * 有任何限制,可以傳遞任何類型的參數,因為Object為他們的公共父類。

       *

       * 但如果把類型參數作為某個類的類型參數使用時(如writeExactT被應用到

       * List<T>中),則參數間就會有直接關系了:兩個T要么相同,要么List<T>

       * 中的T類型是第二個T的父類(注,不能反過來),這其實與

       * writeWithWildcard(List<? super T> list, T item)作用是一樣的,在下

       * 面我們將會看到。

       * 在此種情況下,為什么參數類型要有直接的父子關系呢?其實也是有道理的,

       * 因為第一個List<T>類型的參變量list對象的某些方法還有可能要使用到第二

       * 個參變量item,只有在第二個參數的類型T是第一個參數類型T的子類或本身時

       * ,才能將item傳到需要它的相應方法中去。

       *

       */

      //!! writeExact(new ArrayList<Apple>(), new Fruit());

      Apple app = writeExact1(new Apple(), new Apple());

 

      /*

       * 下面兩行都可以,方法聲明的是t1t2的類型一樣,但下面為父子關系也可

       * 以,原因就是他們的父類為Object,所以可以兩者任意交換

       */

      Fruit fru = writeExact1(new Fruit(), new Apple());

      fru = writeExact1(new Apple(), new Fruit());

      //!! 返回類型只能是參數類型的公共父類,即Fruit

      //!! app = writeExact1( new Apple(),new Fruit());

 

      // ArrayListString的公共類型有SerializableObject,所以返回類型有兩種

      Serializable s = writeExact1(new ArrayList(), new String());

      Object o = writeExact1(new ArrayList(), new String());

   }

 

   static <T> void writeWithWildcard(List<? super T> list, T item) {

      list.add(item);

   }

 

   static void write2() {

      writeWithWildcard(new ArrayList<Apple>(), new Apple());

      writeWithWildcard(new ArrayList<Fruit>(), new Fruit());

      // 父類類型的容器可以存儲子類對象,並且取出時的類型至少為父類類型

      writeWithWildcard(new ArrayList<Fruit>(), new Apple());

      //!! writeWithWildcard(new ArrayList<Apple>(), new Fruit());

   }

 

   //----------讀取

 

   static <T> T readExact(List<T> list) {

      return list.get(0);

   }

 

   static List<Apple> apples = Arrays.asList(new Apple());

   static List<Fruit> fruit = Arrays.asList(new Fruit());

 

   // 通過方法直接讀取:

   static void f1() {

      Apple a = readExact(apples);

      Fruit f = readExact(fruit);

      f = readExact(apples);

   }

 

   /*

    * 然而,如果你定義的是一個泛型類,而不是方法時,當你實例化這個類

    * 時,參數類型就已經確定,調用它的方法時就不能改變了,這與泛型方

    * 法是不一樣的:

    */

   static class Reader<T> {

      T readExact(List<T> list) {

         return list.get(0);

      }

   }

 

   // 通過類來讀取

   static void f2() {

      //泛型類創建時需指定類型,即創建時類型就已確定

      Reader<Fruit> fruitReader = new Reader<Fruit>();

      Fruit f = fruitReader.readExact(fruit);

      //因為創建時類型就已定為Fruit,所以不能傳遞Apple

      //!! Fruit a = fruitReader.readExact(apples);

   }

 

   static class CovariantReader<T> {

      /*

       * 可以通過通配符邊界<? extends T>在運行時傳遞子類類型。下面方法與

       * readCovariant(List<T> list, T t) 是不一樣的,該方法中隱含着兩類

       * 參數有直接的父子關系或相同,因為只有這樣list對象才能使用t對象。

       * 而下面的方法第一個參數list的類型參數聲明成List<? extends T>,這

       * 就已經明顯的表明了第一個類型參數與第二個類型參數的關系,即第二個

       * 類型參數要是第一個類型參數的父類或相同,這與readCovariant(List<T>

       *  list, T t)形式的方法恰好相反。同樣

       *  readCovariant2(List<? super T> list, T t)也明確說明了兩個參數的

       *  的父子關系,第一個T為第二個T的父類或相同。

       */

      T readCovariant(List<? extends T> list, T t) {

 

         /*

          * 假如定義如下:List<? extends Apple> list = new ArrayList<Jonathan>();

          * 則不可以通過引用list調用任何泛型方法,為什么?

          * 原因就是這些泛型方法的真真參數類型比定義時類型要窄,如這里的Jonathan就要

          * Apple類型就要窄,所你不能通過一個Jonathan類型的變量來接收一個Apple類型

          * 的實例吧,所以不能通過被<? extends T>的引用來調用其任何泛型方法。

          *

          * 經過上面我們會很清楚的知道下面語句為什么不行了

          */

        

         //!! list.add(t);

        

         /*

          * 假如定義如下:List<? extends Apple> list = new ArrayList<Jonathan>();

          * 則可以通過引用list調用返回類型為泛型的方法呢(當然方法參數不能是泛型的),

          * 並且返回類型為Apple,為什么?因為即使你將list引用指向成ArrayList<Jonathan>

          * 類型的實例,還是將它指向ArrayList<OtherJonathan>類型的實例,但他們的都

          * 不會超過上界類型Apple,所以返回的類型至少為上界類型Apple

          *

          * 經過上面我們會很清楚的知道下面語句為什么返回的類型自然就是T了。

          */

         return list.get(0);// 但返回類型至少是T

      }

 

      //可以通過通配符邊界<? super T>在運行時傳遞父類類型,但不能作為返回類型,即可以通過泛型方法傳進,但不能通過泛型方法返回

      T readCovariant2(List<? super T> list, T t) {

         /*

          * 假如定義如下:List<? super Jonathan> list = new ArrayList<Apple>();

          * 則可以通過引用list調用其泛型方法,並且傳遞的參數只能是Jonathan或其子類

          * 為什么我們可以通過<? super Jonathan>類型的引用list來調用<Apple>類型

          * 實例的泛型方法?原因就是這些泛型方法的真真參數類型比定義時類型要寬,

          * 如這里的Apple就要比Jonathan類型就要寬,所以我們傳遞給泛型方法真實現類

          * 的子類或本身是可以的,平時我們也是這樣做的,即使用父類的引用指向子類對象

          *

          * 經過上面我們會很清楚的知道下面語句為什么可行了

          */

         list.add(t);

 

         /*

          * 假如定義如下:List<? super Jonathan> list = new ArrayList<Apple>();

          * 為什么調用返回類型為泛型類型時,返回的類型卻只能是Object呢?因為你可

          * 以將list引用指向成ArrayList<Apple>類型的實例,你也可能將它指向ArrayList

          * <Object>類型的實例,這將導致通過list引用調用返回結果為泛型類型的方法時,

          * 有可能是JonathanApple、還有 Object,所以最終只能為Object類型。最主

          * 要因為類型參數<? super T> 修飾時,該類型參數的最上界就是Object類了

          * ,所以通過被<? super T>修飾的引用調用返回類型為泛型方法時,返回的類型

          * 只能是最上界Object,而不能是下界T,也不能為它們之間的某個類型。

          *

          * 從上述就可以很清楚的知道了為什么list.get(0)返回的是Object類型了。

          */

         // !! return list.get(0);

         return (T) list.get(0);//但可以強轉

      }

   }

 

   static void f3() {

      CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();

      Fruit f = fruitReader.readCovariant(fruit, new Fruit());

      f = fruitReader.readCovariant(fruit, new Apple());

      Fruit a = fruitReader.readCovariant(apples, new Apple());

   }

 

   public static void main(String[] args) {

      write1();

      write2();

      f1();

      f2();

      f3();

   }

}

 

 

 

無界通配符:

List<?> list1:沒有明確的邊界,可以引用存放任何類型的ListList<?> list1 = new ArrayList();不會發生警告,與有邊界的通配符一樣,也不能通過list1向容器中添加除null的任何類型的對象。此種情況的邊界實質上為Object,但又與List<? extends Object> list1不一樣。

List<? extends Object> list1: 可以引用存放任何類型的ListList<? extends Object > list1 = new ArrayList();會發生警告,因為它指定了明確的邊界,所以賦值時要指定明確的邊界。

 

編譯器處理List<?>List<? extends Object>是不一樣的,前者沒有明確的邊界,后者有,但兩者的邊界都是Object

 

List list1List<?> list1的區別:

由於擦除操作,List<?>看起來等價於List<Object>(但又不完全相同,因為至少List<Object> l = new ArrayList<String>();是有問題的,而List<?> l = new ArrayList<String>();卻是可以的)。而List實際上也是List<Object>

List實際上表示“可以存放任何Object類型的原生List”,而List<?>表示“只能存放某種類型的非原生List,只是我們不知道那種類型是什么”。

 

原生類型List和參數化類型List<Object>是不一樣的。如果使用了原生類型,編譯器不會知道在list允許接受的元素類型上是否有任何限制,它會允許你添加任何類型的元素到list中。這不是類型安全的,但如果使用了參數化類型List<Object>,編譯器便會明白這個list可以包含任何類型的元素,所以你添加任何對象都是安全的。

 

 

 

 

>>>泛型問題<<<

不能將基本類型用作類型參數,如果是基本類型時只能使用其包裝類型或自動包裝機制。

 

實現參數化接口:一個類不能實現同一個泛型接口多次,下面的Hourly編譯不能通過:

interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Hourly> {}

但去掉泛型參數編譯就可以通過了:

class Employee implements Payable{}

class Hourly extends Employee implements Payable{}

或者是將泛型參數置為相同也可:

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Employee> {}

在使用某些更基本的Java接口,例如Comparable<T>時,這個問題可能會變得十分頭痛。

 

Java泛型中,有一個好像是經常使用的語法,但它有點令人費解:

class A<T extends A<T>>{},這是允許的,這個說明了extends關鍵字用於邊界與用來創建子類明顯是不同的。A類接受泛型參數T,而T是由一個邊界類來限定,這個邊界就是接受T作為類型參數的A類。這種自限定所做的,就是要求在繼承關系中,像下面這樣使用這個類:

class B extends A<B>{}

這會強制要求將正在定義的類當作參數傳遞給基類,它可以保證類型參數必須與正在被定義的類相同。

 

class A<T>{}

class B extends A<B>{}

以上也是允許的,它表示“我在創建一個新類,它繼承自一個泛型類型,這個泛型類型接受我的類的名字作為其參數”,即父類使用子類替代其類型參數。

 

 

參數協變:

方法參數類型會隨子類而變化。盡管自限定類型可以產生子類類型相同的返回類型,但在JavaSE 5中已引入參數協變:

interface Base {

  Base get();

}

 

interface Derived extends Base {

  /*

   * Java SE5開始子類方法可以返回比它重寫的基類方法更

   * 具體的類型,但是這在早先的Java版本是不允許——重寫

   * 時子類的返回類型一定要與基類相同。

   *

   * 但要注意的是:子類方法返回類型要是父類方法返回類型

   * 的子類,而不能反過來,即父類 Derived get(); 而重

   * 寫時子類為Base get();是不行的。

   */

   Derived get();

}

 

public class CovariantReturnTypes {

  void test(Derived d) {

     Derived d1 = d.get();

     Base d2 = d.get();

  }

}

使用自限定泛型修改上面程序:

interface Base<T extends Base<T>> {

  T get();

}

interface Derived extends Base<Derived> {}

 

public class CovariantReturnTypes {

  void test(Derived d) {

         Derived d1 = d.get();

         Base d2 = d.get();//也可返回基類類型

  }

}

 

 

上面程序是方法返回類型協變,返回類型協變在Java SE5中得到了支持——方法可以返回比它重寫的基類方法更具體的類型。但如果子類的方法參數是更具體類型時,這時是重載而不是重寫了(注,以前版本就是這樣):

class Base {

   void get(HashMap l) {

      System.out.println("Base get()");

   }

}

 

class Derived extends Base {

   // 這是重載,而不是重寫,重載

   void get(LinkedHashMap l) {

      System.out.println("Derived get()");

   }

 

   public static void main(String[] args) {

      Derived d = new Derived();

      d.get(new HashMap());//Base get()

      d.get(new LinkedHashMap());//Derived get()

   }

}

 

第十六章數組

數組與容器之間的區別在三個方面:效率、類型(保持存放元素的類型)、保存基本類型的能力。但從Java SE5后泛型與自動裝箱的出現,數組的優點就只是效率了。

 

無論使用哪種類型的數組,數組標識符其實只是一個引用,指向在堆中創建的一個真實對象,這個(數組)對象又可能保存指向其他對象的引用。

 

新創建的數組未初始化時,如果存儲的是對象,則所有元素自動初始化為null,基本類型初始化為0,字符型自動初始為(char)0,布爾型自動初始化為false

 

不能創建泛型數組以及帶類型參數的數組:

//T[] array = new T[SIZE]; // 不能創建泛型數組

//ArrayList<String> [] list = new ArrayList<String>[1]; // 也不能創建帶類型參數的數組

但可以定義一個泛型數組的引用:

T[] array;

ArrayList<String> [] list;

 

雖然你不能創建泛型數組,但是可以創建非泛型數組然后將其轉型:

public class ArrayOfGenerics {

  public static void main(String[] args) {

    List<String>[] ls;

    List[] la = new List[10];

    ls = (List<String>[])la; //會發生 "Unchecked" 警告

    ls[0] = new ArrayList<String>();

   

    //上面其實相當於下面一條語句

    //List<String>[] ls1 = (List<String>[])new List[10];

   

    // 編譯時會產生錯誤:

    //! ls[1] = new ArrayList<Integer>();

 

    // 問題是: List<String> Object子類

    Object[] objects = ls; //所以可以賦值給Object數組引用

    // 編譯與運行都沒有錯誤,因為創建的List數組本身就是原生數組:

    objects[1] = new ArrayList<Integer>();

  }

}

 

 

Random實例對象可隨機返回各種基本類型的數,可設置種子。但Math中的random方法只能返回[0.0, 1)之間的double型小數,實質上該方法就是調用Random實例的nextDouble()來實現的。

 

Arrays類提供了重載的equals()方法。數組相等的條件是元素個數必須相等,並且對應位置的元素也相等。

 

>>>動態創建數組<<<

現有如下應用,Employee[]數組滿后擴容,該如何做?

Employee[] a = new Employee[100]

//arry is full

a = (Employee[])arrayGrow(a);

 

錯誤作法:

static Object[] arrayGrow (Object[] a) {

       int newLength = a.length * 11 / 10 + 10;

       Object[] newArray = new Object[newLength];

       System.arraycopy(a, 0, newArray, 0, a.length);

       return newArray;

}

上面在實際應用中會遇到一個問題,這段代碼返回的數組類型是Object[],這是由於使用下面這行代碼創建的數組:new Object[newLength]。如果現在我們要對Employee[]數組進行擴展時,則在擴充后我們不能將返回的Object[]類型的對象數組轉換成Employee[]數組了(當然如果從Object[]類型對象數組中取出一個個元素后再強轉為Employee是沒問題)。將一個Employee[]臨時地轉換成Object[]數組,然后再把它轉換回來是可以的,但一個從開始就是Object[]的數組卻永遠不能轉換成Employee[]數組。為了編寫這類通用的數組代碼,需要能夠創建與原數組類型相同的新數組。因此需用到反射包中的Array類的靜態方法newInstance.

static Object arrayGrow (Object a) {//參數是Object而不是Object[],因為整型數組類型int[]可以被轉換成Object,但不能轉換成對象數組

       Class cl = a.getClass();

       if (!cl.isArray()) {

              return null;

       }

       Class componentType = cl.getComponentType();

       int length = Array.getLength(a);

       int newLength = length * 11 / 10 + 10;

       Object newArray = Array.newInstance(componentType, newLength);

       System.arraycopy(a, 0, newArray, 0, newLength);

       return newArray;

}

另外,以下轉換也是可以的:

String[] strArr =  new String[10];

Object o = strArr;

strArr= (String[]) o;

 

第十七章容器的深入研究

可以使用Arrays.asList將數組或可變參數轉換成AbstractList列表,其底層數據實現就是我們傳進的參數數組,因此不能像其他List列表那樣調整尺寸,如果你試圖用add()delete()方法在這種列表中添加或刪除元素,就有可能會引發去修改數組尺寸的嘗試,因此你將在運行時獲得“Unsupported Operation(不支持的操作)”錯誤,該列表只支持讀取與修改操作(另外,我們還可以通Collections.unmodifiableList(List)來對一個列表包裝之后,只能進行讀取操作,即使寫是不行了)。

 

class Snow {}

class Powder extends Snow {}

class Light extends Powder {}

class Heavy extends Powder {}

class Crusty extends Snow {}

class Slush extends Snow {}

 

public class AsListInference {

  public static void main(String[] args) {

    List<Snow> snow1 = Arrays.asList(

      new Crusty(), new Slush(), new Powder());

 

    // Won't compile:

    // List<Snow> snow2 = Arrays.asList(

    //   new Light(), new Heavy());

    // Compiler says:

    // found   : java.util.List<Powder>

    // required: java.util.List<Snow>

 

    // Collections.addAll() doesn't get confused:

    List<Snow> snow3 = new ArrayList<Snow>();

    Collections.addAll(snow3, new Light(), new Heavy());

 

    // Give a hint using an

    // explicit type argument specification:

    List<Snow> snow4 = Arrays.<Snow>asList(

       new Light(), new Heavy());

  }

}

當試圖創建snow2時,Arrays.asList()中只有Powder類型,因此它會創建List<Powder>而不是List<Snow>,盡管Collections.addAll()工作的很好,因為它從第一個參數中了解到了目標類型是什么。

正如你從創建snow4的操作中所看到的,可以在Arrays.asList()中間插入具體的類型,以告訴編譯器對於由Arrays.asList()產生的List類型,實際的類型應該是什么。這稱為顯示類型參數說明。

 

 

Queue接口與ListSet同一級別,都是繼承了Collection接口。LinkedList實現了Queue接口。Queue接口窄化了對LinkedList的方法的訪問權限(即在方法中的參數類型如果是Queue時,就完全只能訪問Queue接口所定義的方法了,而不能直接訪問LinkedList的非Queue的方法),以使得只有恰當的方法才可以使用。

offer(),將一個元素插入到隊尾,或者返回false

peek()element()都將在不移除的情況下返回隊頭,但是peek()方法在隊列為空時返回null,而element()會拋出NoSuchElementExcetption異常。

poll()remove()方法將移除並返回隊頭,但是poll()在隊列為空時返回null,而remove()會拋出NoSuchElementExcetption異常。

 

PriorityQueue:優先級隊列。先進先出描述了最典型的隊列規則,但優先級隊列聲明下一個彈出元素是最需要的元素(具有最高的優先級)。如果構建一個消息系統,某些消息比其他消息更重要,因而應該更快地得到外理,那么它們何時得到處理就與它們何時到達無關。PriorityQueue添加到JSE5中,是為了提供這種行為的一種自動實現。當你在PriorityQueue上調用offer()方法來插入一個對象時,這個對象會在隊列中被排序。默認的排序將使用對象在隊列中的自然順序,但是你可能通過提供自己的Comparator來修改這個順序。PriorityQueue可以確保當你調用peek()poll()remove()方法時,獲取元素將是隊列中優先級最高的元素。

 

Collection接口繼承了Iterable接口,該接口包含一個能夠產生java.util.Iteratoriterator()方法,可用於foreach語句中。

 

foreach語句可以用於數組或其他任何Iterable,但是這並不意味着數組是一個Iterable,數組與Iterable接口沒有直接的關系。

 

你必須為散列存儲和樹型存儲都創建一個equals()方法,但是hashCode()只有在這個類將會存放到HashMapHashSet或者LinkedHashMapLinkedHashSet中時才是必需的,因為這類Hash最終都是能過HashMapcontainsKey方法使用if (e.hash == hash && eq(k, e.key))來實現對比的。

 

 

Set —— 存入Set的每個元素都必須是唯一的,因為Set不允許保存重復的元素。放入到Set的元素必須定義equals()方法以確保對象的唯一性。SetCollection有完全一樣的接口。Set接口不保證維護元素的次序。

HashSet(優先選擇) —— 底層以HashMap來實現。為快速查找而設計的Set。存入HashSet的元素必須定義hashCode()

TreeSet —— 保持次序的Set,底層為樹結構。使用它可以從Set中讀取有序的序列。放入的元素必須實現Comparable接口。

LinkedHashSet —— 底層以LinkedHashMap來實現。繼承自HashSet,具有HashSet的查詢速度,且內部使用鏈表維護元素順序(插入的次序)。於是在使用迭代器遍歷Set時,結果會按元素的插入次序顯示。存入的元素也必須定義hashCode()方法。

 

HashMap(優先選擇) —— Map基於散列表的實現(它取代了Hashtable)。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設置容量和負載因子,以調容器的性能。

LinkedHashMap —— 繼承自HashMap,但是迭代遍歷它時,取得“鍵值對”的順序是其插入次序,或者是最近最小使用(LRU)的次序。只比HashMap慢一點,而在迭代訪問時反而更快,因為它使用鏈表維護內部次序。

TreeMap —— 基於紅黑樹的實現。查看“健”或“鍵值對”時,它們會被排序(次序由ComparableComparator決定)。TreeMap的特點在於,所得到的結果是經過排序的。TreeMap是唯一帶有subMap()方法的Map,它可以返回一個子樹。

WeakHashMap —— 弱鍵映射,允許垃圾回收器回收無外界引用指向象Map中鍵,這是為解決某類特殊問題而設計的。如果映射之外沒有引用指向某個“鍵”,則此“鍵”可以被垃圾收集器回收。

ConcurrentHashMap —— 一種線程安全的Map,它不synchronized同步加鎖,而是使用新的鎖機制。盡管所有操作都是線程安全的,但檢索操作不必鎖定,並且不支持以某種防止所有訪問的方式鎖定整個表。

IdentityHashMap —— 使用 == 代替equals()對“鍵”進行比較的散列映射。

 

看一個弱引用例子:

class WeakObject {

 

       String name;

 

       public WeakObject(String mwname) {

              this.name = mwname;

       }

 

       public void finalize() {

              System.out.println(name + "對象滿足垃圾收集條件,被收集!");

       }

 

       public void show() {

              System.out.println(name + "對象還可以使用!");

       }

}

 

public class WeakReferenceTest {

 

       public static void main(String[] args) {

 

              System.out.println("---對象弱引用---");

 

              WeakObject wo = new WeakObject("weakObject");

 

              //包裝成弱引用對象

              WeakReference wr = new WeakReference(wo);

 

              wo = null;

 

              ((WeakObject) wr.get()).show();

 

              System.out.println("第一次垃圾收集!");

 

              System.gc();

 

              try {

                     Thread.sleep(1000);

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

 

              if (wr.get() != null) {

                     ((WeakObject) wr.get()).show();

              }

 

              System.out.println("---弱引用map---");

              WeakHashMap whm = new WeakHashMap();

              WeakObject wo2 = new WeakObject("weakObjectKey");

              //這里的值不會被回收

              whm.put(wo2, new WeakObject("weakObjectValue"));

              wo2 = null;

              ((WeakObject) whm.keySet().iterator().next()).show();

              System.out.println("第二次垃圾回收!");

              System.gc();

              try {

                     Thread.sleep(1000);

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

              ((WeakObject) whm.keySet().iterator().next()).show();

       }

}

 

 

對於底層采用數組的ArrayList,無論列表的大小如何,這些訪問都很快和一致。而對於LinkedList,訪問時間對於較大的列表將明顯增加。很顯然,如果你需要執行大量的隨機訪問,鏈接鏈表不會是一種好的選擇;

插入時,對於ArrayList,當列表變大時,其開銷將變得很高昂,但是對於LinkedList,相對來說比較低廉,並且不隨列表尺寸而發生變化,這是因為ArrayList在插入時,插入點后面所有元素將后移,如果在插入時超過了數組的最大容量,則會重新創建一個新的數組,並將所有元素復制到新的數組中(不過插入與擴容時使用的都是System.arraycopy方法,效率上比使用循環一個個移動要高),這會隨ArrayList的尺寸增加而產生高昂的代價。LinkedList只需鏈接新的元素,而不必修改列表中剩余的元素,因此可以認為無論列表尺寸如何變化,其代價大致相同。

LinkedList中的插入和移除代價相當低廉,並且不隨列表尺寸發生變化,但是對於ArrayList插入操作代價特別高昂,並且其代價將隨列表尺寸增加而增加。

 

HashSet的性能基本上總是比TreeSet好,特別是在添加和查詢元素時,而這兩個操作也是最重要的操作。TreeSet存在的唯一原因是它可以維持元素的排序狀態;所以,只有當需要一個排好序的Set時,才應該使用TreeSet。因為其內部結構支持排序,並且因為迭代是我們更有可能執行的操作,所以,用TreeSet迭代通常比用HashSet要快。

 

除了IdentityHashMap,所有的Map實現的插入操作都會隨着Map尺寸變大而明顯變慢,但是,查找的代價通常比插入要小得多。

Hashtable的性能大體上與HashMap相當,因為HashMap是用來替代Hashtable的,因為它們使用了相同的底層存儲和查找機制,這並沒有什么令人奇怪的。

TreeMap通常比HashMap要慢。

LinkedHashMap在插入 時比HashMap慢一點,因為它維護散列數據結構的同時還要維護鏈表(以保持插入順序),正是由於這個列表,使得其迭代速度更快一點。

IdentityHashMap則具有完全不同的性能,因為它使用==而不是equals()來比較元素。

 

負載因子小的Hash表產生沖突的可能性小,因此對於插入和查找都是最理想(但是會減慢使用迭代器進行遍歷的過程,因為還有很多的空位置,這些空的位置也會在循環中遍歷到)。當容量擴大時,現有的對象將重新分布到親的桶位中(這被稱為再散列)。HashMap使用的默認負載因子為0.75(只有當表的飽和度達到四分之三時,才進行再散列),這個因子在時間和空間代價之間達到平衡,更大的負載因子可以提高空間的利用率,但是會增加查找代價。

 

如果你知道將要在HashMap中存儲多少項,那么創建一個具有恰當大小的初始容量將可以避免自動再散列的開銷。

 

形如Collections.unmodifiableCollection(Collection<? extends T> c)一類方能產生只讀容器。

 

形如Collections.synchronizedCollection(Collection<T> c, Object mutex)一類方能產生同步容器。

 

快速報錯:Java容器有一種保護機制,能夠防止多個進程(或直接通過容器本身而不是迭代器)同時修改同一個容器的內容。如果在你迭代遍歷某個容器的過程中,另一個進程介入其中,並且插入、刪除或修改此容器內的某個對象,那就會出現問題:也迭代過程已經處理過容器中的該元素,也許還沒處理,也許在調用size()之后容器的尺寸縮小了等等,Java容器類類庫采用快速報錯機制。它會先檢測容器上的任何除了你的進程所進行的操作或使用迭代器外的操作所引起的變化,一進發現改變,就會立刻拋ConcurrentModificationExcetion異常。這就是“快速報錯”的意思——即,不是使用復雜的算法在事后來檢測問題。所以應該在添加、刪除、修改完所有元素之后,再獲取迭代器。ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet都使用了可以避免ConcurrentModificationExcetion的技術。

第十八章I/O

第十九章枚舉

enumvalues()方法返回enum實例的數組,而且該數組中的元素嚴格保持其在enum中聲聲明時的順序。

 

創建enum時,編譯器會為你生成一個相關的類,這個類繼承自java.lang.Enum

 

Enum類實例的ordinal()方法返回一個int值,這是每個enum實例在聲明時的次序,從0開始。可以使用==來比較enum實例,編譯器會自動為你提供equals()hashCode()方法。

 

Enum類實現了Comparable接口,所以它具有compareTo()方法,同時,它還實現了Serializable接口。

 

name()方法返回enum實例聲明的名字,與使用toString()方法效果一樣。

 

valueOf(Class<T> enumType, String name)是在Enum中定義的static方法,它根據所給定的名字返回相應的enum實例,如果不存在給定名字的實例,將會拋出異常。

 

除了不能繼承自一個enum之外,我們基本上可以將enum看作一個常規的類,也就是說,我們可以向enum中添加方法屬性。

 

public enum OzWitch {

       // enum實例必須定義在最前,並在方法之前:

       WEST("west."), NORTH("north."),

       EAST("east."), SOUTH("south.");//如果還有其他方法,則這個分號一定要寫上

      

       private String description;//可以定義屬性

 

       // 注,枚舉的構造函數只能是包訪問或private訪問權限:

       private OzWitch(String description) {

              this.description = description;

       }

 

       //也可以有方法

       public String getDescription() {

              return description;

       }

 

       //還可以有main方法

       public static void main(String[] args) {

              for (OzWitch witch : OzWitch.values())

                     System.out.println(witch + ": " + witch.getDescription());

       }

}

 

>>>values()的神秘之處<<<

public enum Explore {

       HERE, THERE

}

使用javap反編譯后:

public final class Explore extends java.lang.Enum{

    public static final Explore HERE;

    public static final Explore THERE;

    public static final Explore[] values();// 編譯器自動添加的方法

    public static Explore valueOf(java.lang.String);// 編譯器自動添加的方法

    static {};

}

Enum類沒有values()方法,values()是由編譯器為enum添加的static方法,在創建Explore的過程中,編譯器還為它創建了valueOf(String name)方法,這可能有點怪了,Enum類不是已經有valueOf(Class<T> enumType, String name)了嗎?不過Enum中的valueOf()方法需要兩個參數,而這個新增的方法只需一個參數。

由於values()方法是由編譯器插入到enum定義中的static方法,所以,如果你將enum實例向上轉型為Enum,那么values()方法就不可訪問了。不過,在Class中有一個getEnumConstants()方法,所以即使Enum接口中沒有values()方法,我們仍然可以通過Class對象取得所有enum實例(注:只有是Enum類型的Class才能調用getEnumConstants方法,否則拋空指針異常):

enum Search { HITHER, YON }

public class UpcastEnum {

  public static void main(String[] args) {

    Search[] vals = Search.values();

    Enum e = Search.HITHER; // 向上轉型

    // e.values(); // Enum中沒有 values()方法

    // 但我們可以通過Class對象的getEnumConstants()反射出enum實例

    for(Enum en : e.getClass().getEnumConstants())

      System.out.println(en);

  }

}

 

>>>enum還可實現其它接口<<<

由於enum可以看是一個普通的類,所以他還可以實現其他接口。

enum CartoonCharacter implements Generator<CartoonCharacter> {

  SLAPPY, SPANKY, BOB;

  private Random rand = new Random(47);

  public CartoonCharacter next() {

    return values()[rand.nextInt(values().length)];

  }

}

 

 

>>>枚舉實例的“多態”表現<<<

enum LikeClasses {

  WINKEN { void behavior() { System.out.println("Behavior1"); } },

  BLINKEN { void behavior() { System.out.println("Behavior2"); } },

  NOD { void behavior() { System.out.println("Behavior3"); } };

  abstract void behavior();//抽象方法,由每個enum實例實現,當然也可是一個實體方法

  public static void main(String[] args) {

       for(LikeClasses en:values()){

              en.behavior();//好比多態

       }

  }

// 但不能把枚舉實例看作是類,因為它們只是一個實例

  // void f1(LikeClasses.WINKEN instance) {}

}

經過反編譯后,我們發現生成了四個類LikeClasses.classLikeClasses$2.class

LikeClasses$3.classLikeClasses$1.class,而且LikeClasses$xx.class繼承自LikeClasses.class類,並將各自的behavior方法代碼放入到自己相應的類文件中。

 

 

第二十章注解

JavaSE5內置了三種標准注解,定義在java.lang中的注解:

@Override,表示當前的方法定義將覆蓋超類中的方法。如果沒有重寫,編譯器會發出錯誤提示。

@Deprecated,如果程序員使用了該注解注解過的元素,那么編譯器會發出警告信息。

@SuppressWarnings,關閉不當的編譯器警告信息。

 

元注解是負責注解其他的注解,有四種元注解:

@Target 表示該注解可以用於什么地方。可能的ElementType(枚舉)參數包括:

CONSTRUCTOR:構造方法聲明

FIELD:域聲明(包括enum實例)

LOCAL_VARIABLE:局部變量聲明

METHOD:方法聲明

PACKAGE:包聲明

PARAMETER:參數聲明

TYPE:類,接口(包括注解類型)或enum聲明

@Retention 表示需要在什么級別保存注解信息。可用的RetentionPolicy(枚舉)參數包括:

SOURCE:注解將被編譯地器丟棄。

CLASS:注解在class文件中可用,但會被VM丟棄。

RUNTIMEVM將在運行期也保留注解,因此可以通過反射機制讀取注解的信息。

@Documented 將此注解包含在JavaDoc中。

@Inherited 允許子類繼承父類中的注解。

 

ClassMethodFieldConstructor類都實現了AnnotatedElement接口,他們的getAnnotation方法都返回指定類型的注解對象,如果沒有該類型的注解,則返回null值,以下是這些方法的原型:

class. getAnnotation(Class<T> annotationClass)

method. getAnnotation(Class<T> annotationClass)

field. getAnnotation(Class<T> annotationClass)

constructor. getAnnotation(Class<T> annotationClass)

 

注解的定義看起來很像接口的定義,事實上,與其他任何Java接口一樣,注解也會編譯成class文件。

 

 

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

import java.lang.reflect.Method;

import java.util.ArrayList;

import java.util.Collections;

import java.util.List;

/*

* 這是一個簡單的注解,我們可以用它來跟蹤一個項目中的用例。如果一個方法

* 或一組方法實現了某個用例的需求,那么程序員可以為此方法加上該注解。

*/

 

//注解的定義

@Target(ElementType.METHOD)//該注解用於方法

@Retention(RetentionPolicy.RUNTIME)//注解信息保留到運行期

public @interface UseCase {

  public int id();//int型的元素

  public String description() default "no description";//String型的元素

}

 

//注解的使用

class PasswordUtils {

  @UseCase(id = 47, description = "密碼必須至少包含一個數字")

  public boolean validatePassword(String password) {

    return (password.matches("\\w*\\d\\w*"));

  }

  @UseCase(id = 48)//沒有描述,使用默認的描述信息

  public String encryptPassword(String password) {

   return new StringBuilder(password).reverse().toString();

  }

  @UseCase(id = 49, description = "新密碼不能使用以前使用過的密碼")

  public boolean checkForNewPassword(

    List<String> prevPasswords, String password) {

    return !prevPasswords.contains(password);

  }

}

 

//注解處理器

class UseCaseTracker {

  public static void

  trackUseCases(List<Integer> useCases, Class<?> cl) {

    for(Method m : cl.getDeclaredMethods()) {

      // 通過反射獲取某個方法特定的注解信息

      UseCase uc = m.getAnnotation(UseCase.class);

      if(uc != null) {

        System.out.println("找到用例:" + uc.id() +

          " " + uc.description());

        useCases.remove(new Integer(uc.id()));

      }

    }

    for(int i : useCases) {

      System.out.println("警告: 所缺用例-" + i);

    }

  }

  public static void main(String[] args) {

    List<Integer> useCases = new ArrayList<Integer>();

    Collections.addAll(useCases, 47, 48, 49, 50);

    trackUseCases(useCases, PasswordUtils.class);

  }

}

/*

找到用例:47 密碼必須至少包含一個數字

找到用例:48 no description

找到用例:49 新密碼不能使用以前使用過的密碼

警告: 所缺用例-50

*/

 

 

注解里的組成元素類型:

1、  所有基本類型

2、  String

3、  Class

4、  enum

5、  Annotation

6、  以上類型的數組

如果你使用了其他類型,那編譯器就會報錯。

 

注解組成元素的默認值限制:首先,元素不能有不確定的值,也就是說,元素必須要么具有默認值,要么在使用注解時提供元素的值,不允許即沒給定默認值,在使用進也沒指定值的情況出現。其次,對於非基本類型的元素,無論是在聲明還是在使用時,都不能以null作為其值。

 

注解不支持繼承。

第二十一章並發

Thread.yield():對線程調度器(Java線程機制的一部分,可以將CPU從一個線程轉移給另一個線程)的一種建議,它是在說“我已經執行完生命期中最重要的部分了,此刻正是切換給其他任務執行一段時間的大好時機”,即對線程的一種讓步,暫停當前正在執行的線程,並執行其他線程。

 

>>>使用Executor<<<

Java.util.concurrent包中的執行器(Executor)將為你管理Thread對象,從而簡化了並發編程。

 

ExecutorsExecutorExecutorServiceScheduledExecutorServiceThreadFactory Callable 類的工廠和實用方法。

 

Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期異步任務的程序而言,這些線程池通常可提高程序性能。調用 execute將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鍾未被使用的線程。因此,長時間保持空閑的線程池不會使用任何資源。注意,可以使用 ThreadPoolExecutor構造方法創建具有類似屬性但細節不同(例如超時參數)的線程池。

 

public class LiftOff implements Runnable {

  protected int countDown = 10; // Default

  private static int taskCount = 0;

  private final int id = taskCount++;

  public LiftOff() {}

  public LiftOff(int countDown) {

    this.countDown = countDown;

  }

  public String status() {

    return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";

  }

  public void run() {

    while(countDown-- > 0) {

      System.out.print(status());

      Thread.yield();

    }

  }

}

 

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class CachedThreadPool {

  public static void main(String[] args) {

    ExecutorService exec = Executors.newCachedThreadPool();

    for(int i = 0; i < 5; i++)

      exec.execute(new LiftOff());

    exec.shutdown();

  }

}

/*

#2(9), #0(9), #4(9), #1(9), #3(9), #2(8), #0(8), #4(8), #1(8), #3(8), #2(7), #0(7), #4(7), #1(7), #3(7), #2(6), #0(6), #4(6), #1(6), #3(6), #2(5), #0(5), #4(5), #1(5), #3(5), #2(4), #0(4), #4(4), #1(4), #3(4), #2(3), #0(3), #4(3), #1(3), #3(3), #2(2), #4(2), #1(2), #3(2), #2(1), #4(1), #1(1), #3(1), #2(Liftoff!), #4(Liftoff!), #1(Liftoff!), #3(Liftoff!), #0(2), #0(1), #0(Liftoff!),

*/

 

 

Executors. newFixedThreadPool (int nThreads):可一次性預先執行代價高昂的線程分配,因而也就可以限制線程數量。不用為每個任務都固定地付出創建線程的開銷。

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class FixedThreadPool {

  public static void main(String[] args) {

    // Constructor argument is number of threads:

    ExecutorService exec = Executors.newFixedThreadPool(5);

    for(int i = 0; i < 5; i++)

      exec.execute(new LiftOff());

    exec.shutdown();

  }

}

/*

#3(9), #4(9), #0(9), #2(9), #1(9), #3(8), #4(8), #0(8), #2(8), #1(8), #3(7), #4(7), #0(7), #2(7), #3(6), #4(6), #0(6), #2(6), #3(5), #4(5), #0(5), #2(5), #3(4), #4(4), #0(4), #2(4), #3(3), #4(3), #0(3), #2(3), #3(2), #4(2), #0(2), #2(2), #3(1), #4(1), #0(1), #2(1), #3(Liftoff!), #4(Liftoff!), #0(Liftoff!), #2(Liftoff!), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!),

*/

 

Executors. newSingleThreadExecutor (int nThreads):確保任意時刻在任何都只有唯一的任務在運行,你不需要在共享資源上處理同步,可以讓你省去只是為了維持某些事物的原型而進行的各種協調努力。

public class SingleThreadExecutor {

  public static void main(String[] args) {

    ExecutorService exec =

      Executors.newSingleThreadExecutor();

    for(int i = 0; i < 5; i++)

      exec.execute(new LiftOff());

    exec.shutdown();

  }

}

/*

#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),

*/

 

 

 

 

Thread.sleep(100);等同於TimeUnit.MILLISECONDS.sleep(100);

 

Daemom線程:后面線程不屬於程序中不可缺少的部分,因此,當所有非后台線程結束時,程序也就終止了,同時會殺死進程中的所有后台線程。返過來說,只要有任何非后台線程還在運行,程序就不會終止。

 

必須在線程啟動之前調用setDaemon()方法,才能把它設置為后台線程。

 

如果一個線程是后台線程,那么它創建的任何線程將被自動設置成后台線程。

 

后台線程在不執行finally子句的情況下就會終止其run()方法,即后台線程的finally子句不一定執行。

 

在構造器中啟動線程可能會有問題,因為線程可能會在構造器結束之前開始執行,這意味着該線程能夠訪問處於不穩定狀態的對象。

 

異常不能跨線程傳播給main(),所以你必須在了本地處理所有在線程內部產生的異常。

public class ExceptionThread implements Runnable {

  public void run() {

    throw new RuntimeException();

  }

  public static void main(String[] args) {

        try {

          ExecutorService exec =

            Executors.newCachedThreadPool();

          exec.execute(new ExceptionThread());

        } catch(RuntimeException ue) {

          // 這句將不會被執行,因為線程的異常是不會傳遞到調用它的線程的

          System.out.println("Exception has been handled!");

        }

      }

}

 

Thread.UncaughtExceptionHandlerJava SE5中的新接口,它允許你在每個Thread對象上都附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException()會在因未捕獲的異常而臨近死亡時被調用。為了使用它,我們創建了一個新類型的ThreadFactory,它將在每個新創建的Thread對象上附着一個Thread.UncaughtExceptionHandler,並將這個工廠傳遞給Exceutors創建新的ExcecutorService方法:

// 線程

class ExceptionThread2 implements Runnable {

  public void run() {

    Thread t = Thread.currentThread();

    System.out.println("run() by " + t);

    System.out.println("1.eh = " + t.getUncaughtExceptionHandler());

    throw new RuntimeException();//線程運行時一定會拋出運行異常

  }

}

 

// 線程異常處理器

class MyUncaughtExceptionHandler implements

Thread.UncaughtExceptionHandler {

  // 異常處理方法

  public void uncaughtException(Thread t, Throwable e) {

    System.out.println("caught " + e);

  }

}

 

// 線程工廠,創建線程時會調用該工廠

class HandlerThreadFactory implements ThreadFactory {

  public Thread newThread(Runnable r) {//線程創建工廠方法

    System.out.println(this + " creating new Thread");

    Thread t = new Thread(r);

    System.out.println("created " + t);

    //設置異常處理器

    t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

    System.out.println("2.eh = " + t.getUncaughtExceptionHandler());

    return t;

  }

}

 

public class CaptureUncaughtException {

  public static void main(String[] args) {

    ExecutorService exec = Executors.newCachedThreadPool(

      new HandlerThreadFactory());

    exec.execute(new ExceptionThread2());

  }

} /*

HandlerThreadFactory@1a758cb creating new Thread

created Thread[Thread-0,5,main]

2.eh = MyUncaughtExceptionHandler@69b332

run() by Thread[Thread-0,5,main]

1.eh = MyUncaughtExceptionHandler@69b332

caught java.lang.RuntimeException

*/

 

 

如果你知道將要在代碼中處處使用相同的異常處理器,那么更簡單的方式是在Thread類中設置一個靜態域,並將這個處理器設置為默認的未捕獲異常處理器:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

 

>>>使用Lock對象<<<

Lock對象必須被顯式地創建、鎖定和釋放。因此,它與內建的鎖形式相比,代碼缺乏優雅性,但對於解決某些類型的問題來說,它更加靈活。

  private Lock lock = new ReentrantLock();

  public int next() {

    lock.lock();

    try {

      //…

    } finally {

      lock.unlock();

    }

  }

使用lock()unlock()方法在next()內部創建了臨界資源。還可以嘗試獲取鎖:

  private ReentrantLock lock = new ReentrantLock();

  public void untimed() {

    boolean captured = lock.tryLock();

    try {

      //…

    } finally {

      if(captured)

        lock.unlock();

    }

  }

 

>>>使用volatile對象<<<

原子操作是不能被線程調試機制中斷的操作,一旦開始操作,那么它一定會在切換到其他線程前執行完畢。

 

原子操作可以應用於除longdouble之外的所有基本類型之上的“簡單操作”,對於讀取和寫入除longdouble之外的基本類型變量這種的操作,可以保證它們會被當作原子操作來操作內存。但是JVM可以將64位(longdouble變量)的讀取和寫入當作兩個分離的32位操作來執行,這就可能會產生了在一個讀取和寫入操作中間切換線程,從而導致不同的線程看到不正確結果的可能性。但是,當你定義longdouble變量時,如果使用volatile關鍵字,就會獲得(簡單的賦值與返回操作)原子性,注:在Java SE5之前,volatile一直未能正確的工作。

 

volatile關鍵字還確保了應用中的可視性,如果你將一個域聲明為volatile的,那么只要對這個域產生了寫操作,那么所有的讀操作就都可以看到這個修改,即便使用了本地緩存,情況確實如此,volatile域會立即被寫入到主存中。

 

在非volatile域上的操作沒有刷新到主存中去,因此其他讀取該域的線程將不能必看到這個新值。因此,如果多個線程同時訪問了某個域,那么這個域就應該是volatile的,否則,這個域應該只能由同步來訪問,同步也會導致向主存中刷新,因此如果一個域完全由synchronized方法或語句塊來保護,那就不必將其設置為volatile了。

 

什么才屬於原子操作呢?對域中的值做賦值和返回操作都是原子性的。但i++; i+=2; 這樣的操作肯定不是原子性的,即線程有可能從語句的中間切換。下面來證明i++java里不是原子性操作的:

class SerialNumberGenerator {

       private static volatile int serialNumber = 0;

 

       public static /* synchronized */int nextSerialNumber() {

              // 不是線程安全,因為i++Java里不是原子操作,

              // 即使將serialNumber設置成了volatile

              return serialNumber++;

       }

}

 

class CircularSet {

       private int[] array;

       private int len;

       private int index = 0;

 

       public CircularSet(int size) {

              array = new int[size];

              len = size;

              // 初始化為-1

              for (int i = 0; i < size; i++) {

                     array[i] = -1;

              }

       }

 

       public synchronized void add(int i) {

              array[index] = i;

              // 如果數組滿后從頭開始填充,好比循環數組:

              index = ++index % len;

       }

 

       public synchronized boolean contains(int val) {

              for (int i = 0; i < len; i++) {

                     if (array[i] == val) {

                            return true;

                     }

              }

              return false;

       }

}

 

public class SerialNumberChecker {

       private static final int SIZE = 10;

       private static CircularSet serials = new CircularSet(1000);

       private static ExecutorService exec = Executors.newCachedThreadPool();

 

       static class SerialChecker implements Runnable {

              public void run() {

                     while (true) {

                            int serial = SerialNumberGenerator.nextSerialNumber();

                            if (serials.contains(serial)) {// 如果數組中存在則退出

                                   System.out.println("Duplicate: " + serial);

                                   System.exit(0);

                            }

                            serials.add(serial);// 如果不存在,則放入

                     }

              }

       }

 

       public static void main(String[] args) throws Exception {

              SerialChecker sc = new SerialChecker();

              // 啟動10線程

              for (int i = 0; i < SIZE; i++) {

                     exec.execute(sc);

              }

       }

}

 

 

 

 

public class Increament extends Thread {

       public static volatile int x = 0;

 

       public void run() {

//            synchronized (Increament.class) {

                     // x++ x = x + 1都不是原子操作

                     x++;

                     // x = x + 1;

//            }

       }

 

       public static void main(String[] args) throws Exception {

             

              Thread threads[] = new Thread[10000];

              for (int i = 0; i < threads.length; i++) {

                     threads[i] = new Increament();

              }

 

              for (int i = 0; i < threads.length; i++) {

                     threads[i].start();

              }

              for (int i = 0; i < threads.length; i++) {

                     // 等待計算線程運行完

                     threads[i].join();

              }

              System.out.println("n=" + Increament.x);

       }

}

如果對x的操作是原子級別的,最后輸出的結果應該為x=10000,而在執行上面積代碼時,很多時侯輸出的x都小於10000,這說明x++ 不是原子級別的操作。原因是聲明為volatile的簡單變量如果當前值由該變量以前的值相關,那么volatile關鍵字不起作用。

 

 

同一時刻只有一個線程能訪問synchronized塊,synchronized塊並不是一下子要執行完畢,CPU調試可能從synchronized塊中的某個語句切換到其它的線程,再其它線程執行完畢后再繼續執行該同步塊。切換到其他線程時是否釋放synchronized塊上的鎖,這要看切換所采用的方式:如果是CPU自動或調用Thread.yeild切換,則不會釋放;如果是調用wait,則會釋放;如果是調用的Thread.sleep,則不會;如果是調用的thread.join,則要看synchronized塊上的鎖是否是thread線程對象,如果不是,則不會釋放,如果是,則會釋放。

 

只能在同步控制方法或同步控制塊里調用wait()notify()notifyAll(),並且釋放操作鎖,但sleep()可以在非同步控制方法里調用,不會釋放鎖。

 

sleepyield都是Thread的靜態方法,join屬於Thread的非靜態方式,如果將它們放入在同步塊中調用時都不會釋放鎖。但wait屬於Object類的方法,在wait()期間對象鎖是釋放的。

 

在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖會釋放。

 

執行線程的suspend()方法會導致線程被暫停,並使用resume()可喚醒,但不會釋放鎖。

 

當線程在運行中執行了Thread類的yield()靜態方法,如果此時具有相同優先級的其他線程處於就緒狀態,那么yield()方法將把當前運行的線程放到可運行池中並使用中另一線程運行。如果沒有相同優先級的可運行進程,則該方法什么也不做。

 

sleep方法與yield方法都是Thread類的靜態方法,都會使當前處於運行的線程放棄CPU,把運行機會讓給另的線程。兩都的區別:

1.         sleep方法會給其他線程運行的機會以,而不考慮其他線程的優先級,因此會給較低優先級線程一個運行的機會;yield方法只會給相同或更高優先級的線程一個運行的機會。

2.         當線程執行了sleep方法后,將轉到阻塞狀態。當線程執行了yield方法后,將轉入就緒狀態。

3.         Sleep方法比yield方法具有更好的可移植性。不能依靠yield方法來提高程序的並發性能。對於大多數程序員來說,yield方法的唯一用途是在測試期間人為地提高程序的並發性能,以幫助發現一些隱藏的錯誤。

 

thread.join():當前線程調用另一線程thread.join()時,則當前運行的線程將轉到阻塞狀態,並且等待thread線程運行結束后,當前線程程才會恢復運行(從阻塞狀態到就緒狀態)。比如有3個線程在執行計算任務,必須等三個線程都執行完才能匯總,那么這時候在主線程里面讓三個線程join,最后計算結果既可:

public class JoinTest {

       public static void main(String[] args) {

              Rt[] ct = new Rt[3];

              for (int i = 0; i < ct.length; i++) {

                     ct[i] = new Rt();

                     ct[i].start();

                     try {

                            //主線等待三個線程終止后再繼續運行

                            ct[i].join();

                     } catch (InterruptedException e) {

                            e.printStackTrace();

                     }

              }

              int total = 0;

              for (int j = 0; j < ct.length; j++) {

                     total += ct[j].getResult();

              }

              System.out.println("total = " + total);

       }

}

 

class Rt extends Thread {

       private int result;

       public int getResult() {

              return result;

       }

       public void run() {

              try {

                     Thread.sleep(1000);

                     result = (int) (Math.random() * 100);

                     System.out.println(this.getName() + " result=" + result);

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

 

       }

}

 

join()只能由線程實例調用,如果thread.join()在同步塊中調用,並且同步鎖對象也是thread對象,由於thread.join()是調用thread.wait()來實現的,wait會釋放thread對象鎖,則thread.join()與在同步塊的鎖也會一並釋放;如果thread.join()在同步塊的鎖對象不是thread對象,則thread線程阻塞時不會釋放鎖:

public class JoinTest {

       public static void main(String[] args) throws InterruptedException {

              JThread t = new JThread();

              start(t, t);

              System.out.println("--------");

              t = new JThread();

              start(t, JThread.class);

       }

 

       static void start(JThread t, Object lock) {

              t.setLock(lock);

              t.start();

              try {

                     Thread.sleep(100);

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

              //如果鎖對象是JThread.class時,則主線程會一直阻塞

              t.f();

       }

}

 

class JThread extends Thread {

       private Object lock;

 

       void setLock(Object lock) {

              this.lock = lock;

       }

 

       public void run() {

              synchronized (lock) {

                     try {

                            System.out.println(Thread.currentThread().getName() + " - join before");

                            /*

                             * 當前線程阻塞,又要等待自己運行完,這是矛盾的,所以其實該線程永遠不會恢復執

                             * 行,除非使用 join(long millis)方式。實際上我們看this.join()源碼就會

                             * 看出,this.join()就是調用了this.wait()方法,因為了this.wait()會釋放

                             * this對象上的鎖,所以當lock對象是自身時,主線程不會被鎖住,所以第一個線程

                             * 會打印 "main - f()"。第二個線程的鎖對象是JThreadClass對象,由於join

                             * 時不會釋放JThread.class對象上的鎖, 第二個線程會一直阻塞,所以第二個線程

                             * 不會打印 "main - f()"

                             *

                             */

                            this.join();

                            /*

                             * 這樣可以正常結束整個程序,因為this線程一直會阻塞直到對方(也是this的線程)運行完

                             * 或者是對方沒有運行完等 1 毫秒后thsi線程繼續運行,所以以這樣的方式一定不會出現死鎖

                             * 現象

                             */

                            //this.join(1);

                            System.out.println(Thread.currentThread().getName() + " - join after");

                     } catch (InterruptedException e) {

                            e.printStackTrace();

                     }

              }

       }

 

       public void f() {

              synchronized (lock) {

                     System.out.println(Thread.currentThread().getName() + " - f()");

              }

       }

}

 

sleepjoin都會使當前線程處於阻塞狀態,而yield則是進行就緒狀態。

 

同步的靜態方法的鎖對象是方法所屬類的Class對象,而同步的非靜態方法的鎖對象是所調用方法實例所對應的this對象。

 

繼承RunnableThread的區別Thread類本身也實現了Runnable接口。 因此除了構造 Runnable對象並把它作為構造函數的參數傳遞給Thread類之外,你也可以生成Thread類的一個子類,通過覆蓋這個run方法來執行相應的操作。不過,通常最好的策略是把Runnable接口當作一個單獨的類來實現,並把它作為參數傳遞給個Thread的構造函數。通過將代碼隔離在單獨的類中可以使你不必擔心Runnable類中使用的同步方法和同步塊與在相應線程類中所使用的其他任何方法之間的潛在操行所帶來的影響。更一般地說,這種分離允許獨立控制相關的操作和運行這些操作的上下文,同一個Runnable對象既可以傳遞給多個使用不同方式初抬化的Thread對象,也可以傳遞給其他的輕量級執行者(executor)。同樣需要注意的是,繼承了Thread類的劉象不能再同時繼承其他類了。

 

如果線程被啟動並且沒有終止,調用方法isAlive將返回true。如果線程僅僅是因為某個原因阻塞,該方法也會返回true

 

通過調用線程tjoin方法將調用者掛起,直到目標線程t結束運行:t.join方法會在當t.isAlive方法的結果為false時返回。

 

有一些Thread類的方法只能應用於當前正在運行的那個線程中(也就是,調用Thread靜態方法的線程),為了強制實施,這些方法都被聲明為staticThread.currentThreadThread.interruptedThread.sleepThread.yield

 

Thread.yield:僅僅是一個建議——放棄當前線程去運行其他的線程,JVM可以使用自己的方式理解這個建議。盡管缺少保證,但yield方法仍舊可以在一些單CPUJVM實現上起到相應的效果,只要這些實現不使用分時搶占式的調用機制,在這種機制下,只有當一個線程阻塞時,CPU才會切換到其他線程上執行。如果在系統中線程執行了耗時的非阻塞計算任務的會占有更多的CPU時間,因而降低了應用程序的響應,為了安全起見,當執行非阻塞的計算任務的方法時,則可以在執行過程中插入yield方法(甚至是sleep方法)。為了減少不必要的影響,可以只在偶爾的情況下調用yield方法,比如一個包含如下語句的循環:

if(Math.random() < 0.01) Thread.yield();

使用搶占式調度機制的JVM實現,特別是在多處理器的情況下,yield才可能顯得沒有什么意義。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM