虛函數和抽象函數有什么區別
虛函數是有代碼的并明確允許子類去覆蓋,但子類也可不覆蓋,就是說可以直接用,不用重寫 ?
? 抽象函數是沒有代碼,子類繼承后一定要重寫
?
******************************************************************
在一個類中用虛函數: ?
? 是因為在超類中的有實際代碼的方法,但明確允許子類可以作重寫 ?
? 而且當子類重寫后,可以用子類實例超類;如果這樣,超類變量調用虛函數時,執行的是子類的方法 ?
? ?
? 在一個類中用抽象函數 ?
? 是在寫超類時不確定函數的代碼,讓子類去實現???
???
?
******************************************************************
抽象函數沒有方法體。
c+=4和c=c+4的區別。
String abc="aaa";
char c=abc.charAt(i);
c+=4;
以上如果把:c+=4; 改成:c=c+4;
就不正確。
********************************************************************************************************************************************************************************************************************************************************************
c+=4;
c=c+4;
當c是int類型的時候,這兩個表達式是一樣的,但是c不是int時,這兩個表達式是不一樣的。
這兩個表達式都被稱為賦值表達式。第二條語句使用的是簡單賦值操作符(=),而第一條語句使用的是復合賦值操作符。(復合賦值操作符包括 +=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和|=)Java語言規范中講到,復合賦值 E1 op= E2等價于簡單賦值E1 = (T)((E1)op(E2)),其中T是E1的類型,除非E1只被計算一次。
換句話說,復合賦值表達式自動地將它們所執行的計算的結果轉型為其左側變量的類型。
所以要讓c=c+4 編譯能通過,得
int a=c;
c=(char)(a+4);
System.out.println(c);
字符,字節和編碼
[原創文章,轉載請保留或注明出處:http://www.regexlab.com/zh/encoding.htm]
級別:初級
摘要:本文介紹了字符與編碼的發展過程,相關概念的正確理解。舉例說明了一些實際應用中,編碼的實現方法。然后,本文講述了通常對字符與編碼的幾種誤解,由于這些誤解而導致亂碼產生的原因,以及消除亂碼的辦法。本文的內容涵蓋了“中文問題”,“亂碼問題”。
引言
“字符與編碼”是一個被經常討論的話題。即使這樣,時常出現的亂碼仍然困擾著大家。雖然我們有很多的辦法可以用來消除亂碼,但我們并不一定理解這些辦法的內在原理。而有的亂碼產生的原因,實際上由于底層代碼本身有問題所導致的。因此,不僅是初學者會對字符編碼感到模糊,有的底層開發人員同樣對字符編碼缺乏準確的理解。
![]() | |||
|
1. 編碼問題的由來,相關概念的理解
1.1 字符與編碼的發展
從計算機對多國語言的支持角度看,大致可以分為三個階段:
? | 系統內碼 | 說明 | 系統 |
階段一 | ASCII | 計算機剛開始只支持英語,其它語言不能夠在計算機上存儲和顯示。 | 英文 DOS |
階段二 | ANSI編碼 (本地化) | 為使計算機支持更多語言,通常使用 0x80~0xFF 范圍的 2 個字節來表示 1 個字符。比如:漢字 '中' 在中文操作系統中,使用 [0xD6,0xD0] 這兩個字節存儲。 不同的國家和地區制定了不同的標準,由此產生了 GB2312, BIG5, JIS 等各自的編碼標準。這些使用 2 個字節來代表一個字符的各種漢字延伸編碼方式,稱為 ANSI 編碼。在簡體中文系統下,ANSI 編碼代表 GB2312 編碼,在日文操作系統下,ANSI 編碼代表 JIS 編碼。 不同 ANSI 編碼之間互不兼容,當信息在國際間交流時,無法將屬于兩種語言的文字,存儲在同一段 ANSI 編碼的文本中。 | 中文 DOS,中文 Windows 95/98,日文 Windows 95/98 |
階段三 | UNICODE (國際化) | 為了使國際間信息交流更加方便,國際組織制定了 UNICODE 字符集,為各種語言中的每一個字符設定了統一并且唯一的數字編號,以滿足跨語言、跨平臺進行文本轉換、處理的要求。 | Windows NT/2000/XP,Linux,Java |
字符串在內存中的存放方法:
在 ASCII 階段,單字節字符串使用一個字節存放一個字符(SBCS)。比如,"Bob123" 在內存中為:
42 | 6F | 62 | 31 | 32 | 33 | 00 |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
B | o | b | 1 | 2 | 3 | /0 |
在使用 ANSI 編碼支持多種語言階段,每個字符使用一個字節或多個字節來表示(MBCS),因此,這種方式存放的字符也被稱作多字節字符。比如,"中文123" 在中文 Windows 95 內存中為7個字節,每個漢字占2個字節,每個英文和數字字符占1個字節:
D6 | D0 | CE | C4 | 31 | 32 | 33 | 00 |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ||
中 | 文 | 1 | 2 | 3 | /0 |
在 UNICODE 被采用之后,計算機存放字符串時,改為存放每個字符在 UNICODE 字符集中的序號。目前計算機一般使用 2 個字節(16 位)來存放一個序號(DBCS),因此,這種方式存放的字符也被稱作寬字節字符。比如,字符串 "中文123" 在 Windows 2000 下,內存中實際存放的是 5 個序號:
2D | 4E | 87 | 65 | 31 | 00 | 32 | 00 | 33 | 00 | 00 | 00 | ???? ← 在 x86 CPU 中,低字節在前 |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ||||||
中 | 文 | 1 | 2 | 3 | /0 | ? |
一共占 10 個字節。
![]() | |||
|
1.2 字符,字節,字符串
理解編碼的關鍵,是要把字符的概念和字節的概念理解準確。這兩個概念容易混淆,我們在此做一下區分:
? | 概念描述 | 舉例 |
字符 | 人們使用的記號,抽象意義上的一個符號。 | '1', '中', 'a', '$', '¥', …… |
字節 | 計算機中存儲數據的單元,一個8位的二進制數,是一個很具體的存儲空間。 | 0x01, 0x45, 0xFA, …… |
ANSI 字符串 | 在內存中,如果“字符”是以 ANSI 編碼形式存在的,一個字符可能使用一個字節或多個字節來表示,那么我們稱這種字符串為ANSI 字符串或者多字節字符串。 | "中文123" (占7字節) |
UNICODE 字符串 | 在內存中,如果“字符”是以在 UNICODE 中的序號存在的,那么我們稱這種字符串為 UNICODE 字符串或者寬字節字符串。 | L"中文123" (占10字節) |
由于不同 ANSI 編碼所規定的標準是不相同的,因此,對于一個給定的多字節字符串,我們必須知道它采用的是哪一種編碼規則,才能夠知道它包含了哪些“字符”。而對于UNICODE 字符串來說,不管在什么環境下,它所代表的“字符”內容總是不變的。
![]() | |||
|
1.3 字符集與編碼
各個國家和地區所制定的不同 ANSI 編碼標準中,都只規定了各自語言所需的“字符”。比如:漢字標準(GB2312)中沒有規定韓國語字符怎樣存儲。這些 ANSI 編碼標準所規定的內容包含兩層含義:
- 使用哪些字符。也就是說哪些漢字,字母和符號會被收入標準中。所包含“字符”的集合就叫做“字符集”。
- 規定每個“字符”分別用一個字節還是多個字節存儲,用哪些字節來存儲,這個規定就叫做“編碼”。
各個國家和地區在制定編碼標準的時候,“字符的集合”和“編碼”一般都是同時制定的。因此,平常我們所說的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”這層含義外,同時也包含了“編碼”的含義。
“UNICODE 字符集”包含了各種語言中使用到的所有“字符”。用來給 UNICODE 字符集編碼的標準有很多種,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。
![]() | |||
|
2. 字符與編碼在程序中的實現
2.1 程序中的字符與字節
在 C++ 和 Java 中,用來代表“字符”和“字節”的數據類型,以及進行編碼的方法:
類型或操作 | C++ | Java |
字符 | wchar_t | char * |
字節 | char | byte |
ANSI 字符串 | char[] | byte[] |
UNICODE 字符串 | wchar_t[] | String |
字節串→字符串 | mbstowcs(), MultiByteToWideChar() * | string = new String(bytes, "encoding") |
字符串→字節串 | wcstombs(), WideCharToMultiByte() | bytes = string.getBytes("encoding") |
以上需要注意幾點:
- Java 中的 char 代表一個“UNICODE 字符(寬字節字符)”,而 C++ 中的 char 代表一個字節。
- MultiByteToWideChar() 和 WideCharToMultiByte() 是 Windows API 函數。
![]() | |||
|
2.2 C++ 中相關實現方法
聲明一段字符串常量:
// ANSI 字符串,內容長度 7 字節 char???? sz[20] ="中文123"; // UNICODE 字符串,內容長度 5 個 wchar_t(10 字節) wchar_t wsz[20] = L"/x4E2D/x6587/x0031/x0032/x0033"; |
UNICODE 字符串的 I/O 操作,字符與字節的轉換操作:
// 運行時設定當前 ANSI 編碼,VC 格式 setlocale(LC_ALL, ".936"); // GCC 中格式 setlocale(LC_ALL, "zh_CN.GBK"); // Visual C++ 中使用小寫 %s,按照 setlocale 指定編碼輸出到文件 // GCC 中使用大寫 %S fwprintf(fp, L"%s/n", wsz); // 把 UNICODE 字符串按照 setlocale 指定的編碼轉換成字節 wcstombs(sz, wsz, 20); // 把字節串按照 setlocale 指定的編碼轉換成 UNICODE 字符串 mbstowcs(wsz, sz, 20); |
在 Visual C++ 中,UNICODE 字符串常量有更簡單的表示方法。如果源程序的編碼與當前默認 ANSI 編碼不符,則需要使用 #pragma setlocale,告訴編譯器源程序使用的編碼:
// 如果源程序的編碼與當前默認 ANSI 編碼不一致, // 則需要此行,編譯時用來指明當前源程序使用的編碼 #pragma setlocale(".936") // UNICODE 字符串常量,內容長度 10 字節 wchar_t wsz[20] = L"中文123"; |
以上需要注意 #pragma setlocale 與 setlocale(LC_ALL, "") 的作用是不同的,#pragma setlocale 在編譯時起作用,setlocale() 在運行時起作用。
![]() | |||
|
2.3 Java 中相關實現方法
字符串類 String 中的內容是 UNICODE 字符串:
// Java 代碼,直接寫中文 String string = "中文123"; // 得到長度為 5,因為是 5 個字符 System.out.println(string.length()); |
字符串 I/O 操作,字符與字節轉換操作。在 Java 包 java.io.* 中,以“Stream”結尾的類一般是用來操作“字節串”的類,以“Reader”,“Writer”結尾的類一般是用來操作“字符串”的類。
// 字符串與字節串間相互轉化 // 按照 GB2312 得到字節(得到多字節字符串) byte [] bytes = string.getBytes("GB2312"); // 從字節按照 GB2312 得到 UNICODE 字符串 string = new String(bytes, "GB2312"); // 要將 String 按照某種編碼寫入文本文件,有兩種方法: // 第一種辦法:用 Stream 類寫入已經按照指定編碼轉化好的字節串 OutputStream os = new FileOutputStream("1.txt"); os.write(bytes); os.close(); // 第二種辦法:構造指定編碼的 Writer 來寫入字符串 Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"),"GB2312"); ow.write(string); ow.close(); /* 最后得到的 1.txt 和 2.txt 都是 7 個字節 */ |
如果 java 的源程序編碼與當前默認 ANSI 編碼不符,則在編譯的時候,需要指明一下源程序的編碼。比如:
E:/>javac -encoding BIG5 Hello.java |
以上需要注意區分源程序的編碼與 I/O 操作的編碼,前者是在編譯時起作用,后者是在運行時起作用。
![]() | |||
|
3. 幾種誤解,以及亂碼產生的原因和解決辦法
3.1 容易產生的誤解
? | 對編碼的誤解 |
誤解一 | 在將“字節串”轉化成“UNICODE 字符串”時,比如在讀取文本文件時,或者通過網絡傳輸文本時,容易將“字節串”簡單地作為單字節字符串,采用每“一個字節”就是“一個字符”的方法進行轉化。 而實際上,在非英文的環境中,應該將“字節串”作為 ANSI 字符串,采用適當的編碼來得到 UNICODE 字符串,有可能“多個字節”才能得到“一個字符”。 通常,一直在英文環境下做開發的程序員們,容易有這種誤解。 |
誤解二 | 在 DOS,Windows 98 等非 UNICODE 環境下,字符串都是以 ANSI 編碼的字節形式存在的。這種以字節形式存在的字符串,必須知道是哪種編碼才能被正確地使用。這使我們形成了一個慣性思維:“字符串的編碼”。 當 UNICODE 被支持后,Java 中的 String 是以字符的“序號”來存儲的,不是以“某種編碼的字節”來存儲的,因此已經不存在“字符串的編碼”這個概念了。只有在“字符串”與“字節串”轉化時,或者,將一個“字節串”當成一個 ANSI 字符串時,才有編碼的概念。 不少的人都有這個誤解。 |
第一種誤解,往往是導致亂碼產生的原因。第二種誤解,往往導致本來容易糾正的亂碼問題變得更復雜。
![]() | |||
|
3.2 常用的編碼簡介
簡單介紹一下常用的編碼規則,為后邊的章節做一個準備。在這里,我們根據編碼規則的特點,把所有的編碼分成三類:
分類 | 編碼標準 | 說明 |
單字節字符編碼 | ISO-8859-1 | 最簡單的編碼規則,每一個字節直接作為一個 UNICODE 字符。比如,[0xD6, 0xD0] 這兩個字節,通過 iso-8859-1 轉化為字符串時,將直接得到 [0x00D6, 0x00D0] 兩個 UNICODE 字符,即 "?D"。 反之,將 UNICODE 字符串通過 iso-8859-1 轉化為字節串時,只能正常轉化 0~255 范圍的字符。 |
ANSI 編碼 | GB2312, BIG5, Shift_JIS, ISO-8859-2 …… | 把 UNICODE 字符串通過 ANSI 編碼轉化為“字節串”時,根據各自編碼的規定,一個 UNICODE 字符可能轉化成一個字節或多個字節。 反之,將字節串轉化成字符串時,也可能多個字節轉化成一個字符。比如,[0xD6, 0xD0] 這兩個字節,通過 GB2312 轉化為字符串時,將得到 [0x4E2D] 一個字符,即 '中' 字。 “ANSI 編碼”的特點: 1. 這些“ANSI 編碼標準”都只能處理各自語言范圍之內的 UNICODE 字符。 2. “UNICODE 字符”與“轉換出來的字節”之間的關系是人為規定的。 |
UNICODE 編碼 | UTF-8, UTF-16, UnicodeBig …… | 與“ANSI 編碼”類似的,把字符串通過 UNICODE 編碼轉化成“字節串”時,一個 UNICODE 字符可能轉化成一個字節或多個字節。 與“ANSI 編碼”不同的是: 1. 這些“UNICODE 編碼”能夠處理所有的 UNICODE 字符。 2. “UNICODE 字符”與“轉換出來的字節”之間是可以通過計算得到的。 |
在這里,我們可以看到,前面所講的“誤解一”,即采用每“一個字節”就是“一個字符”的轉化方法,實際上也就等同于采用 iso-8859-1 進行轉化。因此,我們常常使用 bytes = string.getBytes("iso-8859-1") 來進行逆向操作,得到原始的“字節串”。然后再使用正確的 ANSI 編碼,比如 string = new String(bytes, "GB2312"),來得到正確的“UNICODE 字符串”。
![]() | |||
|
3.3 非 UNICODE 程序在不同語言環境間移植時的亂碼
非 UNICODE 程序中的字符串,都是以某種 ANSI 編碼形式存在的。如果程序運行時的語言環境與開發時的語言環境不同,將會導致 ANSI 字符串的顯示失敗。
比如,在日文環境下開發的非 UNICODE 的日文程序界面,拿到中文環境下運行時,界面上將顯示亂碼。如果這個日文程序界面改為采用 UNICODE 來記錄字符串,那么當在中文環境下運行時,界面上將可以顯示正常的日文。
由于客觀原因,有時候我們必須在中文操作系統下運行非 UNICODE 的日文軟件,這時我們可以采用一些工具,比如,南極星,AppLocale 等,暫時的模擬不同的語言環境。
![]() | |||
|
3.4 網頁提交字符串
當頁面中的表單提交字符串時,首先把字符串按照當前頁面的編碼,轉化成字節串。然后再將每個字節轉化成 "%XX" 的格式提交到 Web 服務器。比如,一個編碼為 GB2312 的頁面,提交 "中" 這個字符串時,提交給服務器的內容為 "%D6%D0"。
在服務器端,Web 服務器把收到的 "%D6%D0" 轉化成 [0xD6, 0xD0] 兩個字節,然后再根據 GB2312 編碼規則得到 "中" 字。
在 Tomcat 服務器中,request.getParameter() 得到亂碼時,常常是因為前面提到的“誤解一”造成的。默認情況下,當提交 "%D6%D0" 給 Tomcat 服務器時,request.getParameter() 將返回 [0x00D6, 0x00D0] 兩個 UNICODE 字符,而不是返回一個 "中" 字符。因此,我們需要使用 bytes = string.getBytes("iso-8859-1") 得到原始的字節串,再用 string = new String(bytes, "GB2312") 重新得到正確的字符串 "中"。
![]() | |||
|
3.5 從數據庫讀取字符串
通過數據庫客戶端(比如 ODBC 或 JDBC)從數據庫服務器中讀取字符串時,客戶端需要從服務器獲知所使用的 ANSI 編碼。當數據庫服務器發送字節流給客戶端時,客戶端負責將字節流按照正確的編碼轉化成 UNICODE 字符串。
如果從數據庫讀取字符串時得到亂碼,而數據庫中存放的數據又是正確的,那么往往還是因為前面提到的“誤解一”造成的。解決的辦法還是通過 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字節串,再重新使用正確的編碼轉化成字符串。
![]() | |||
|
3.6 電子郵件中的字符串
當一段 Text 或者 HTML 通過電子郵件傳送時,發送的內容首先通過一種指定的字符編碼轉化成“字節串”,然后再把“字節串”通過一種指定的傳輸編碼(Content-Transfer-Encoding)進行轉化得到另一串“字節串”。比如,打開一封電子郵件源代碼,可以看到類似的內容:
Content-Type: text/plain; ??????? charset="gb2312" Content-Transfer-Encoding: base64 sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg== |
最常用的 Content-Transfer-Encoding 有 Base64 和 Quoted-Printable 兩種。在對二進制文件或者中文文本進行轉化時,Base64 得到的“字節串”比 Quoted-Printable 更短。在對英文文本進行轉化時,Quoted-Printable 得到的“字節串”比 Base64 更短。
郵件的標題,用了一種更簡短的格式來標注“字符編碼”和“傳輸編碼”。比如,標題內容為 "中",則在郵件源代碼中表示為:
// 正確的標題格式 Subject: =?GB2312?B?1tA=?= |
其中,
- 第一個“=?”與“?”中間的部分指定了字符編碼,在這個例子中指定的是 GB2312。
- “?”與“?”中間的“B”代表 Base64。如果是“Q”則代表 Quoted-Printable。
- 最后“?”與“?=”之間的部分,就是經過 GB2312 轉化成字節串,再經過 Base64 轉化后的標題內容。
如果“傳輸編碼”改為 Quoted-Printable,同樣,如果標題內容為 "中":
// 正確的標題格式 Subject: =?GB2312?Q?=D6=D0?= |
如果閱讀郵件時出現亂碼,一般是因為“字符編碼”或“傳輸編碼”指定有誤,或者是沒有指定。比如,有的發郵件組件在發送郵件時,標題 "中":
// 錯誤的標題格式 Subject: =?ISO-8859-1?Q?=D6=D0?= |
這樣的表示,實際上是明確指明了標題為 [0x00D6, 0x00D0],即 "?D",而不是 "中"。
![]() | |||
|
4. 幾種錯誤理解的糾正
誤解:“ISO-8859-1 是國際編碼?”
非也。iso-8859-1 只是單字節字符集中最簡單的一種,也就是“字節編號”與“UNICODE 字符編號”一致的那種編碼規則。當我們要把一個“字節串”轉化成“字符串”,而又不知道它是哪一種 ANSI 編碼時,先暫時地把“每一個字節”作為“一個字符”進行轉化,不會造成信息丟失。然后再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢復到原始的字節串。
誤解:“Java 中,怎樣知道某個字符串的內碼?”
Java 中,字符串類 java.lang.String 處理的是 UNICODE 字符串,不是 ANSI 字符串。我們只需要把字符串作為“抽象的符號的串”來看待。因此不存在字符串的內碼的問題。
Java代碼編寫的30條建議
Java代碼編寫的30條建議
(1) 類名首字母應該大寫。字段、方法以及對象(句柄)的首字母應小寫。對于所有標識符,其中包含的所有單詞都應緊靠在一起,而且大寫中間單詞的首字母。例如:
ThisIsAClassName
thisIsMethodOrFieldName
若在定義中出現了常數初始化字符,則大寫static final基本類型標識符中的所有字母。這樣便可標志出它們屬于編譯期的常數。
Java包(Package)屬于一種特殊情況:它們全都是小寫字母,即便中間的單詞亦是如此。對于域名擴展名稱,如com,org,net或者edu等,全部都應小寫(這也是Java 1.1和Java 1.2的區別之一)。
(2) 為了常規用途而創建一個類時,請采取"經典形式",并包含對下述元素的定義:
equals()
hashCode()
toString()
clone()(implement Cloneable)
implement Serializable
(3) 對于自己創建的每一個類,都考慮置入一個main(),其中包含了用于測試那個類的代碼。為使用一個項目中的類,我們沒必要刪除測試代碼。若進行了任何形式的改動,可方便地返回測試。這些代碼也可作為如何使用類的一個示例使用。
(4) 應將方法設計成簡要的、功能性單元,用它描述和實現一個不連續的類接口部分。理想情況下,方法應簡明扼要。若長度很大,可考慮通過某種方式將其分割成較短的幾個方法。這樣做也便于類內代碼的重復使用(有些時候,方法必須非常大,但它們仍應只做同樣的一件事情)。
(5) 設計一個類時,請設身處地為客戶程序員考慮一下(類的使用方法應該是非常明確的)。然后,再設身處地為管理代碼的人考慮一下(預計有可能進行哪些形式的修改,想想用什么方法可把它們變得更簡單)。
(6) 使類盡可能短小精悍,而且只解決一個特定的問題。下面是對類設計的一些建議:
■一個復雜的開關語句:考慮采用"多形"機制
■數量眾多的方法涉及到類型差別極大的操作:考慮用幾個類來分別實現
■許多成員變量在特征上有很大的差別:考慮使用幾個類
(7) 讓一切東西都盡可能地"私有"--private。可使庫的某一部分"公共化"(一個方法、類或者一個字段等等),就永遠不能把它拿出。若強行拿出,就可能破壞其他人現有的代碼,使他們不得不重新編寫和設計。若只公布自己必須公布的,就可放心大膽地改變其他任何東西。在多線程環境中,隱私是特別重要的一個因素--只有private字段才能在非同步使用的情況下受到保護。
(8) 謹惕"巨大對象綜合癥"。對一些習慣于順序編程思維、且初涉OOP領域的新手,往往喜歡先寫一個順序執行的程序,再把它嵌入一個或兩個巨大的對象里。根據編程原理,對象表達的應該是應用程序的概念,而非應用程序本身。
(9) 若不得已進行一些不太雅觀的編程,至少應該把那些代碼置于一個類的內部。
(10) 任何時候只要發現類與類之間結合得非常緊密,就需要考慮是否采用內部類,從而改善編碼及維護工作(參見第14章14.1.2小節的"用內部類改進代碼")。
(11) 盡可能細致地加上注釋,并用javadoc注釋文檔語法生成自己的程序文檔。
(12) 避免使用"魔術數字",這些數字很難與代碼很好地配合。如以后需要修改它,無疑會成為一場噩夢,因為根本不知道"100"到底是指"數組大小"還是"其他全然不同的東西"。所以,我們應創建一個常數,并為其使用具有說服力的描述性名稱,并在整個程序中都采用常數標識符。這樣可使程序更易理解以及更易維護。
(13) 涉及構建器和異常的時候,通常希望重新丟棄在構建器中捕獲的任何異常--如果它造成了那個對象的創建失敗。這樣一來,調用者就不會以為那個對象已正確地創建,從而盲目地繼續。
(14) 當客戶程序員用完對象以后,若你的類要求進行任何清除工作,可考慮將清除代碼置于一個良好定義的方法里,采用類似于cleanup()這樣的名字,明確表明自己的用途。除此以外,可在類內放置一個boolean(布爾)標記,指出對象是否已被清除。在類的finalize()方法里,請確定對象已被清除,并已丟棄了從RuntimeException繼承的一個類(如果還沒有的話),從而指出一個編程錯誤。在采取象這樣的方案之前,請確定finalize()能夠在自己的系統中工作(可能需要調用System.runFinalizersOnExit(true),從而確保這一行為)。
(15) 在一個特定的作用域內,若一個對象必須清除(非由垃圾收集機制處理),請采用下述方法:初始化對象;若成功,則立即進入一個含有finally從句的try塊,開始清除工作。
(16) 若在初始化過程中需要覆蓋(取消)finalize(),請記住調用super.finalize()(若Object屬于我們的直接超類,則無此必要)。在對finalize()進行覆蓋的過程中,對super.finalize()的調用應屬于最后一個行動,而不應是第一個行動,這樣可確保在需要基礎類組件的時候它們依然有效。
(17) 創建大小固定的對象集合時,請將它們傳輸至一個數組(若準備從一個方法里返回這個集合,更應如此操作)。這樣一來,我們就可享受到數組在編譯期進行類型檢查的好處。此外,為使用它們,數組的接收者也許并不需要將對象"造型"到數組里。
(18) 盡量使用interfaces,不要使用abstract類。若已知某樣東西準備成為一個基礎類,那么第一個選擇應是將其變成一個interface(接口)。只有在不得不使用方法定義或者成員變量的時候,才需要將其變成一個abstract(抽象)類。接口主要描述了客戶希望做什么事情,而一個類則致力于(或允許)具體的實施細節。
(19) 在構建器內部,只進行那些將對象設為正確狀態所需的工作。盡可能地避免調用其他方法,因為那些方法可能被其他人覆蓋或取消,從而在構建過程中產生不可預知的結果(參見第7章的詳細說明)。
(20) 對象不應只是簡單地容納一些數據;它們的行為也應得到良好的定義。
(21) 在現成類的基礎上創建新類時,請首先選擇"新建"或"創作"。只有自己的設計要求必須繼承時,才應考慮這方面的問題。若在本來允許新建的場合使用了繼承,則整個設計會變得沒有必要地復雜。
(22) 用繼承及方法覆蓋來表示行為間的差異,而用字段表示狀態間的區別。一個非常極端的例子是通過對不同類的繼承來表示顏色,這是絕對應該避免的:應直接使用一個"顏色"字段。
(23) 為避免編程時遇到麻煩,請保證在自己類路徑指到的任何地方,每個名字都僅對應一個類。否則,編譯器可能先找到同名的另一個類,并報告出錯消息。若懷疑自己碰到了類路徑問題,請試試在類路徑的每一個起點,搜索一下同名的.class文件。
(24) 在Java 1.1 AWT中使用事件"適配器"時,特別容易碰到一個陷阱。若覆蓋了某個適配器方法,同時拼寫方法沒有特別講究,最后的結果就是新添加一個方法,而不是覆蓋現成方法。然而,由于這樣做是完全合法的,所以不會從編譯器或運行期系統獲得任何出錯提示--只不過代碼的工作就變得不正常了。
(25) 用合理的設計方案消除"偽功能"。也就是說,假若只需要創建類的一個對象,就不要提前限制自己使用應用程序,并加上一條"只生成其中一個"注釋。請考慮將其封裝成一個"獨生子"的形式。若在主程序里有大量散亂的代碼,用于創建自己的對象,請考慮采納一種創造性的方案,將些代碼封裝起來。
(26) 警惕"分析癱瘓"。請記住,無論如何都要提前了解整個項目的狀況,再去考察其中的細節。由于把握了全局,可快速認識自己未知的一些因素,防止在考察細節的時候陷入"死邏輯"中。
(27) 警惕"過早優化"。首先讓它運行起來,再考慮變得更快--但只有在自己必須這樣做、而且經證實在某部分代碼中的確存在一個性能瓶頸的時候,才應進行優化。除非用專門的工具分析瓶頸,否則很有可能是在浪費自己的時間。性能提升的隱含代價是自己的代碼變得難于理解,而且難于維護。
(28) 請記住,閱讀代碼的時間比寫代碼的時間多得多。思路清晰的設計可獲得易于理解的程序,但注釋、細致的解釋以及一些示例往往具有不可估量的價值。無論對你自己,還是對后來的人,它們都是相當重要的。如對此仍有懷疑,那么請試想自己試圖從聯機Java文檔里找出有用信息時碰到的挫折,這樣或許能將你說服。
(29) 如認為自己已進行了良好的分析、設計或者實施,那么請稍微更換一下思維角度。試試邀請一些外來人士--并不一定是專家,但可以是來自本公司其他部門的人。請他們用完全新鮮的眼光考察你的工作,看看是否能找出你一度熟視無睹的問題。采取這種方式,往往能在最適合修改的階段找出一些關鍵性的問題,避免產品發行后再解決問題而造成的金錢及精力方面的損失。
(30) 良好的設計能帶來最大的回報。簡言之,對于一個特定的問題,通常會花較長的時間才能找到一種最恰當的解決方案。但一旦找到了正確的方法,以后的工作就輕松多了,再也不用經歷數小時、數天或者數月的痛苦掙扎。我們的努力工作會帶來最大的回報(甚至無可估量)。而且由于自己傾注了大量心血,最終獲得一個出色的設計方案,成功的快感也是令人心動的。堅持抵制草草完工的誘惑--那樣做往往得不償失
(3) 對于自己創建的每一個類,都考慮置入一個main(),其中包含了用于測試那 個類的代碼。為使用一個項目中的類,我們沒必要刪除測試代碼。若進行了任 何形式的改動,可方便地返回測試。這些代碼也可作為如何使用類的一個示例 使用。
this is absolutly bad!
(4) 應將方法設計成簡要的、功能性單元,用它描述和實現一個不連續的類接 口部分。理想情況下,方法應簡明扼要。若長度很大,可考慮通過某種方式將 其分割成較短的幾個方法。這樣做也便于類內代碼的重復使用(有些時候,方 法必須非常大,但它們仍應只做同樣的一件事情)。
(5) 設計一個類時,請設身處地為客戶程序員考慮一下(類的使用方法應該是 非常明確的)。然后,再設身處地為管理代碼的人考慮一下(預計有可能進行 哪些形式的修改,想想用什么方法可把它們變得更簡單)。
(6) 使類盡可能短小精悍,而且只解決一個特定的問題。下面是對類設計的一 些建議:
■一個復雜的開關語句:考慮采用"多形"機制
■數量眾多的方法涉及到類型差別極大的操作:考慮用幾個類來分別實現
■許多成員變量在特征上有很大的差別:考慮使用幾個類
(7) 讓一切東西都盡可能地"私有"--private。可使庫的某一部分"公共化"(一個 方法、類或者一個字段等等),就永遠不能把它拿出。若強行拿出,就可能破 壞其他人現有的代碼,使他們不得不重新編寫和設計。若只公布自己必須公布 的,就可放心大膽地改變其他任何東西。在多線程環境中,隱私是特別重要的 一個因素--只有private字段才能在非同步使用的情況下受到保護。
not necessary , pretotect or package level also fine in most case
(8) 謹惕"巨大對象綜合癥"。對一些習慣于順序編程思維、且初涉OOP領域的新 手,往往喜歡先寫一個順序執行的程序,再把它嵌入一個或兩個巨大的對象 里。根據編程原理,對象表達的應該是應用程序的概念,而非應用程序本身。
(9) 若不得已進行一些不太雅觀的編程,至少應該把那些代碼置于一個類的內 部。
(10) 任何時候只要發現類與類之間結合得非常緊密,就需要考慮是否采用內部 類,從而改善編碼及維護工作(參見第14章14.1.2小節的"用內部類改進代 碼")。
(11) 盡可能細致地加上注釋,并用javadoc注釋文檔語法生成自己的程序文檔。
(12) 避免使用"魔術數字",這些數字很難與代碼很好地配合。如以后需要修改 它,無疑會成為一場噩夢,因為根本不知道"100"到底是指"數組大小"還是"其 他全然不同的東西"。所以,我們應創建一個常數,并為其使用具有說服力的描 述性名稱,并在整個程序中都采用常數標識符。這樣可使程序更易理解以及更 易維護。
(13) 涉及構建器和異常的時候,通常希望重新丟棄在構建器中捕獲的任何異常- -如果它造成了那個對象的創建失敗。這樣一來,調用者就不會以為那個對象已 正確地創建,從而盲目地繼續。
(14) 當客戶程序員用完對象以后,若你的類要求進行任何清除工作,可考慮將 清除代碼置于一個良好定義的方法里,采用類似于cleanup()這樣的名字,明確 表明自己的用途。除此以外,可在類內放置一個boolean(布爾)標記,指出 對象是否已被清除。在類的finalize()方法里,請確定對象已被清除,并已丟棄 了從RuntimeException繼承的一個類(如果還沒有的話),從而指出一個編程 錯誤。在采取象這樣的方案之前,請確定finalize()能夠在自己的系統中工作 (可能需要調用System.runFinalizersOnExit(true),從而確保這一行為)。
(15) 在一個特定的作用域內,若一個對象必須清除(非由垃圾收集機制處 理),請采用下述方法:初始化對象;若成功,則立即進入一個含有finally從 句的try塊,開始清除工作。
(16) 若在初始化過程中需要覆蓋(取消)finalize(),請記住調用 super.finalize()(若Object屬于我們的直接超類,則無此必要)。在對finalize() 進行覆蓋的過程中,對super.finalize()的調用應屬于最后一個行動,而不應是第 一個行動,這樣可確保在需要基礎類組件的時候它們依然有效。
(17) 創建大小固定的對象集合時,請將它們傳輸至一個數組(若準備從一個方 法里返回這個集合,更應如此操作)。這樣一來,我們就可享受到數組在編譯 期進行類型檢查的好處。此外,為使用它們,數組的接收者也許并不需要將對 象"造型"到數組里。
(18) 盡量使用interfaces,不要使用abstract類。若已知某樣東西準備成為一個 基礎類,那么第一個選擇應是將其變成一個interface(接口)。只有在不得不 使用方法定義或者成員變量的時候,才需要將其變成一個abstract(抽象) 類。接口主要描述了客戶希望做什么事情,而一個類則致力于(或允許)具體 的實施細節。
they are total diffrent ,
(19) 在構建器內部,只進行那些將對象設為正確狀態所需的工作。盡可能地避 免調用其他方法,因為那些方法可能被其他人覆蓋或取消,從而在構建過程中 產生不可預知的結果(參見第7章的詳細說明)。
(20) 對象不應只是簡單地容納一些數據;它們的行為也應得到良好的定義。
(21) 在現成類的基礎上創建新類時,請首先選擇"新建"或"創作"。只有自己的設 計要求必須繼承時,才應考慮這方面的問題。若在本來允許新建的場合使用了 繼承,則整個設計會變得沒有必要地復雜。
(22) 用繼承及方法覆蓋來表示行為間的差異,而用字段表示狀態間的區別。一 個非常極端的例子是通過對不同類的繼承來表示顏色,這是絕對應該避免的: 應直接使用一個"顏色"字段。
(23) 為避免編程時遇到麻煩,請保證在自己類路徑指到的任何地方,每個名字 都僅對應一個類。否則,編譯器可能先找到同名的另一個類,并報告出錯消 息。若懷疑自己碰到了類路徑問題,請試試在類路徑的每一個起點,搜索一下 同名的.class文件。
classpath is not that simple
(24) 在Java 1.1 AWT中使用事件"適配器"時,特別容易碰到一個陷阱。若覆蓋了 某個適配器方法,同時拼寫方法沒有特別講究,最后的結果就是新添加一個方 法,而不是覆蓋現成方法。然而,由于這樣做是完全合法的,所以不會從編譯 器或運行期系統獲得任何出錯提示--只不過代碼的工作就變得不正常了。
(25) 用合理的設計方案消除"偽功能"。也就是說,假若只需要創建類的一個對 象,就不要提前限制自己使用應用程序,并加上一條"只生成其中一個"注釋。 請考慮將其封裝成一個"獨生子"的形式。若在主程序里有大量散亂的代碼,用 于創建自己的對象,請考慮采納一種創造性的方案,將些代碼封裝起來。
(26) 警惕"分析癱瘓"。請記住,無論如何都要提前了解整個項目的狀況,再去 考察其中的細節。由于把握了全局,可快速認識自己未知的一些因素,防止在 考察細節的時候陷入"死邏輯"中。
(27) 警惕"過早優化"。首先讓它運行起來,再考慮變得更快--但只有在自己必須 這樣做、而且經證實在某部分代碼中的確存在一個性能瓶頸的時候,才應進行 優化。除非用專門的工具分析瓶頸,否則很有可能是在浪費自己的時間。性能 提升的隱含代價是自己的代碼變得難于理解,而且難于維護。
but know early and design better at first is always necesary, or else
you die
(28) 請記住,閱讀代碼的時間比寫代碼的時間多得多。思路清晰的設計可獲得 易于理解的程序,但注釋、細致的解釋以及一些示例往往具有不可估量的價 值。無論對你自己,還是對后來的人,它們都是相當重要的。如對此仍有懷 疑,那么請試想自己試圖從聯機Java文檔里找出有用信息時碰到的挫折,這樣 或許能將你說服。
(29) 如認為自己已進行了良好的分析、設計或者實施,那么請稍微更換一下思 維角度。試試邀請一些外來人士--并不一定是專家,但可以是來自本公司其他 部門的人。請他們用完全新鮮的眼光考察你的工作,看看是否能找出你一度熟 視無睹的問題。采取這種方式,往往能在最適合修改的階段找出一些關鍵性的 問題,避免產品發行后再解決問題而造成的金錢及精力方面的損失。
(30) 良好的設計能帶來最大的回報。簡言之,對于一個特定的問題,通常會花 較長的時間才能找到一種最恰當的解決方案。但一旦找到了正確的方法,以后 的工作就輕松多了,再也不用經歷數小時、數天或者數月的痛苦掙扎。我們的 努力工作會帶來最大的回報(甚至無可估量)。而且由于自己傾注了大量心 血,最終獲得一個出色的設計方案,成功的快感也是令人心動的。堅持抵制草 草完工的誘惑--那樣做往往得不償失
Java性能優化技巧集錦
一、通用篇
1.1 不用new關鍵詞創建類的實例
1.2 使用非阻塞I/O
1.3 慎用異常
1.4 不要重復初始化變量
1.5 盡量指定類的final修飾符
1.6 盡量使用局部變量
1.7 乘法和除法
二、J2EE篇
2.1 使用緩沖標記
2.2 始終通過會話Bean訪問實體Bean
2.3 選擇合適的引用機制
2.4 在部署描述器中設置只讀屬性
2.5 緩沖對EJB Home的訪問
2.6 為EJB實現本地接口
2.7 生成主鍵
2.8 及時清除不再需要的會話
2.9 在JSP頁面中關閉無用的會話
2.10 Servlet與內存使用
2.11 HTTP Keep-Alive
2.12 JDBC與Unicode
2.13 JDBC與I/O
1.14 內存數據庫
三、GUI篇
3.1 用JAR壓縮類文件
3.2 提示Applet裝入進程
3.3 在畫出圖形之前預先裝入它
3.4 覆蓋update方法
3.5 延遲重畫操作
3.6 使用雙緩沖區
3.7 使用BufferedImage
3.8 使用VolatileImage
3.9 使用Window Blitting
四、補充資料
===================================
正文:
===================================
一、通用篇
“通用篇”討論的問題適合于大多數Java應用。
1.1 不用new關鍵詞創建類的實例
用new關鍵詞創建類的實例時,構造函數鏈中的所有構造函數都會被自動調用。但如果一個對象實現了Cloneable接口,我們可以調用它的clone()方法。clone()方法不會調用任何類構造函數。
在使用設計模式(Design Pattern)的場合,如果用Factory模式創建對象,則改用clone()方法創建新的對象實例非常簡單。例如,下面是Factory模式的一個典型實現:
public static Credit getNewCredit() {
return new Credit();
}
?
改進后的代碼使用clone()方法,如下所示:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}
?
上面的思路對于數組處理同樣很有用。
1.2 使用非阻塞I/O
版本較低的JDK不支持非阻塞I/O API。為避免I/O阻塞,一些應用采用了創建大量線程的辦法(在較好的情況下,會使用一個緩沖池)。這種技術可以在許多必須支持并發I/O流的應用中見到,如Web服務器、報價和拍賣應用等。然而,創建Java線程需要相當可觀的開銷。
JDK 1.4引入了非阻塞的I/O庫(java.nio)。如果應用要求使用版本較早的JDK,在這里有一個支持非阻塞I/O的軟件包。
請參見Sun中國網站的《調整Java的I/O性能》。
1.3 慎用異常
異常對性能不利。拋出異常首先要創建一個新的對象。Throwable接口的構造函數調用名為fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,VM就必須調整調用堆棧,因為在處理過程中創建了一個新的對象。
異常只能用于錯誤處理,不應該用來控制程序流程。
1.4 不要重復初始化變量
默認情況下,調用類的構造函數時, Java會把變量初始化成確定的值:所有的對象被設置成null,整數變量(byte、short、int、long)設置成0,float和 double變量設置成0.0,邏輯值設置成false。當一個類從另一個類派生時,這一點尤其應該注意,因為用new關鍵詞創建一個對象時,構造函數鏈中的所有構造函數都會被自動調用。
1.5 盡量指定類的final修飾符
帶有final修飾符的類是不可派生的。在Java核心API中,有許多應用final的例子,例如java.lang.String。為String類指定final防止了人們覆蓋length()方法。
另外,如果指定一個類為final,則該類所有的方法都是final。Java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關)。此舉能夠使性能平均提高50%。
1.6 盡量使用局部變量
調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧(Stack)中,速度較快。其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢。另外,依賴于具體的編譯器/JVM,局部變量還可能得到進一步優化。請參見《盡可能使用堆棧變量》。
1.7 乘法和除法
考慮下面的代碼:
for (val = 0; val < 100000; val +=5) { alterX = val * 8; myResult = val * 2; }
?
用移位操作替代乘法操作可以極大地提高性能。下面是修改后的代碼:
for (val = 0; val < 100000; val += 5) { alterX = val << 3; myResult = val << 1; }
?
修改后的代碼不再做乘以8的操作,而是改用等價的左移3位操作,每左移1位相當于乘以2。相應地,右移1位操作相當于除以2。值得一提的是,雖然移位操作速度快,但可能使代碼比較難于理解,所以最好加上一些注釋。
二、J2EE篇
前面介紹的改善性能技巧適合于大多數Java應用,接下來要討論的問題適合于使用JSP、EJB或JDBC的應用。
2.1 使用緩沖標記
一些應用服務器加入了面向JSP的緩沖標記功能。例如,BEA的WebLogic Server從6.0版本開始支持這個功能,Open Symphony工程也同樣支持這個功能。JSP緩沖標記既能夠緩沖頁面片斷,也能夠緩沖整個頁面。當JSP頁面執行時,如果目標片斷已經在緩沖之中,則生成該片斷的代碼就不用再執行。頁面級緩沖捕獲對指定URL的請求,并緩沖整個結果頁面。對于購物籃、目錄以及門戶網站的主頁來說,這個功能極其有用。對于這類應用,頁面級緩沖能夠保存頁面執行的結果,供后繼請求使用。
對于代碼邏輯復雜的頁面,利用緩沖標記提高性能的效果比較明顯;反之,效果可能略遜一籌。
請參見《用緩沖技術提高JSP應用的性能和穩定性》。
2.2 始終通過會話Bean訪問實體Bean
直接訪問實體Bean不利于性能。當客戶程序遠程訪問實體Bean時,每一個get方法都是一個遠程調用。訪問實體Bean的會話Bean是本地的,能夠把所有數據組織成一個結構,然后返回它的值。
用會話Bean封裝對實體Bean的訪問能夠改進事務管理,因為會話Bean只有在到達事務邊界時才會提交。每一個對get方法的直接調用產生一個事務,容器將在每一個實體Bean的事務之后執行一個“裝入-讀取”操作。
一些時候,使用實體Bean會導致程序性能不佳。如果實體Bean的唯一用途就是提取和更新數據,改成在會話Bean之內利用JDBC訪問數據庫可以得到更好的性能。
2.3 選擇合適的引用機制
在典型的JSP應用系統中,頁頭、頁腳部分往往被抽取出來,然后根據需要引入頁頭、頁腳。當前,在JSP頁面中引入外部資源的方法主要有兩種:include指令,以及include動作。
include指令:例如<%@ include file="copyright.html" %>。該指令在編譯時引入指定的資源。在編譯之前,帶有include指令的頁面和指定的資源被合并成一個文件。被引用的外部資源在編譯時就確定,比運行時才確定資源更高效。
include動作:例如<jsp:include page="copyright.jsp" />。該動作引入指定頁面執行后生成的結果。由于它在運行時完成,因此對輸出結果的控制更加靈活。但時,只有當被引用的內容頻繁地改變時,或者在對主頁面的請求沒有出現之前,被引用的頁面無法確定時,使用include動作才合算。
2.4 在部署描述器中設置只讀屬性
實體Bean的部署描述器允許把所有get方法設置成“只讀”。當某個事務單元的工作只包含執行讀取操作的方法時,設置只讀屬性有利于提高性能,因為容器不必再執行存儲操作。
2.5 緩沖對EJB Home的訪問
EJB Home接口通過JNDI名稱查找獲得。這個操作需要相當可觀的開銷。JNDI查找最好放入Servlet的init()方法里面。如果應用中多處頻繁地出現EJB訪問,最好創建一個EJBHomeCache類。EJBHomeCache類一般應該作為singleton實現。
2.6 為EJB實現本地接口
本地接口是EJB 2.0規范新增的內容,它使得Bean能夠避免遠程調用的開銷。請考慮下面的代碼。
PayBeanHome home = (PayBeanHome)
javax.rmi.PortableRemoteObject.narrow
(ctx.lookup ("PayBeanHome"), PayBeanHome.class);
PayBean bean = (PayBean)
javax.rmi.PortableRemoteObject.narrow
(home.create(), PayBean.class);
第一個語句表示我們要尋找Bean的Home接口。這個查找通過JNDI進行,它是一個RMI調用。然后,我們定位遠程對象,返回代理引用,這也是一個 RMI調用。第二個語句示范了如何創建一個實例,涉及了創建IIOP請求并在網絡上傳輸請求的stub程序,它也是一個RMI調用。
要實現本地接口,我們必須作如下修改:
方法不能再拋出java.rmi.RemoteException異常,包括從RemoteException派生的異常,比如 TransactionRequiredException、TransactionRolledBackException和 NoSuchObjectException。EJB提供了等價的本地異常,如TransactionRequiredLocalException、 TransactionRolledBackLocalException和NoSuchObjectLocalException。
所有數據和返回值都通過引用的方式傳遞,而不是傳遞值。
本地接口必須在EJB部署的機器上使用。簡而言之,客戶程序和提供服務的組件必須在同一個JVM上運行。
如果Bean實現了本地接口,則其引用不可串行化。
請參見《用本地引用提高EJB訪問效率》。
2.7 生成主鍵
在EJB之內生成主鍵有許多途徑,下面分析了幾種常見的辦法以及它們的特點。
利用數據庫內建的標識機制(SQL Server的IDENTITY或Oracle的SEQUENCE)。這種方法的缺點是EJB可移植性差。
由實體Bean自己計算主鍵值(比如做增量操作)。它的缺點是要求事務可串行化,而且速度也較慢。
利用NTP之類的時鐘服務。這要求有面向特定平臺的本地代碼,從而把Bean固定到了特定的OS之上。另外,它還導致了這樣一種可能,即在多CPU的服務器上,同一個毫秒之內生成了兩個主鍵。
借鑒Microsoft的思路,在Bean中創建一個GUID。然而,如果不求助于JNI,Java不能確定網卡的MAC地址;如果使用JNI,則程序就要依賴于特定的OS。
還有其他幾種辦法,但這些辦法同樣都有各自的局限。似乎只有一個答案比較理想:結合運用RMI和JNDI。先通過RMI注冊把RMI遠程對象綁定到JNDI樹。客戶程序通過JNDI進行查找。下面是一個例子:
public class keyGenerator extends UnicastRemoteObject implements Remote {
private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey() throws RemoteException { return KeyValue++; }
2.8 及時清除不再需要的會話
為了清除不再活動的會話,許多應用服務器都有默認的會話超時時間,一般為30分鐘。當應用服務器需要保存更多會話時,如果內存容量不足,操作系統會把部分內存數據轉移到磁盤,應用服務器也可能根據“最近最頻繁使用”(Most Recently Used)算法把部分不活躍的會話轉儲到磁盤,甚至可能拋出“內存不足”異常。在大規模系統中,串行化會話的代價是很昂貴的。當會話不再需要時,應當及時調用HttpSession.invalidate()方法清除會話。HttpSession.invalidate()方法通常可以在應用的退出頁面調用。
2.9 在JSP頁面中關閉無用的會話
對于那些無需跟蹤會話狀態的頁面,關閉自動創建的會話可以節省一些資源。使用如下page指令:
<%@ page session="false"%>
2.10 Servlet與內存使用
許多開發者隨意地把大量信息保存到用戶會話之中。一些時候,保存在會話中的對象沒有及時地被垃圾回收機制回收。從性能上看,典型的癥狀是用戶感到系統周期性地變慢,卻又不能把原因歸于任何一個具體的組件。如果監視JVM的堆空間,它的表現是內存占用不正常地大起大落。
解決這類內存問題主要有二種辦法。第一種辦法是,在所有作用范圍為會話的Bean中實現HttpSessionBindingListener接口。這樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean使用的資源。
另外一種辦法就是盡快地把會話作廢。大多數應用服務器都有設置會話作廢間隔時間的選項。另外,也可以用編程的方式調用會話的 setMaxInactiveInterval()方法,該方法用來設定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計。
2.11 HTTP Keep-Alive
Keep-Alive功能使客戶端到服務器端的連接持續有效,當出現對服務器的后繼請求時,Keep-Alive功能避免了建立或者重新建立連接。市場上的大部分Web服務器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。對于提供靜態內容的網站來說,這個功能通常很有用。但是,對于負擔較重的網站來說,這里存在另外一個問題:雖然為客戶保留打開的連接有一定的好處,但它同樣影響了性能,因為在處理暫停期間,本來可以釋放的資源仍舊被占用。當Web服務器和應用服務器在同一臺機器上運行時,Keep- Alive功能對資源利用的影響尤其突出。
2.12 JDBC與Unicode
想必你已經了解一些使用JDBC時提高性能的措施,比如利用連接池、正確地選擇存儲過程和直接執行的SQL、從結果集刪除多余的列、預先編譯SQL語句,等等。
除了這些顯而易見的選擇之外,另一個提高性能的好選擇可能就是把所有的字符數據都保存為Unicode(代碼頁13488)。Java以Unicode形式處理所有數據,因此,數據庫驅動程序不必再執行轉換過程。但應該記住:如果采用這種方式,數據庫會變得更大,因為每個Unicode字符需要2個字節存儲空間。另外,如果有其他非Unicode的程序訪問數據庫,性能問題仍舊會出現,因為這時數據庫驅動程序仍舊必須執行轉換過程。
2.13 JDBC與I/O
如果應用程序需要訪問一個規模很大的數據集,則應當考慮使用塊提取方式。默認情況下,JDBC每次提取32行數據。舉例來說,假設我們要遍歷一個5000 行的記錄集,JDBC必須調用數據庫157次才能提取到全部數據。如果把塊大小改成512,則調用數據庫的次數將減少到10次。
在一些情形下這種技術無效。例如,如果使用可滾動的記錄集,或者在查詢中指定了FOR UPDATE,則塊操作方式不再有效。
1.14 內存數據庫
許多應用需要以用戶為單位在會話對象中保存相當數量的數據,典型的應用如購物籃和目錄等。由于這類數據可以按照行/列的形式組織,因此,許多應用創建了龐大的Vector或HashMap。在會話中保存這類數據極大地限制了應用的可伸縮性,因為服務器擁有的內存至少必須達到每個會話占用的內存數量乘以并發用戶最大數量,它不僅使服務器價格昂貴,而且垃圾收集的時間間隔也可能延長到難以忍受的程度。
一些人把購物籃/目錄功能轉移到數據庫層,在一定程度上提高了可伸縮性。然而,把這部分功能放到數據庫層也存在問題,且問題的根源與大多數關系數據庫系統的體系結構有關。對于關系數據庫來說,運行時的重要原則之一是確保所有的寫入操作穩定、可靠,因而,所有的性能問題都與物理上把數據寫入磁盤的能力有關。關系數據庫力圖減少I/O操作,特別是對于讀操作,但實現該目標的主要途徑只是執行一套實現緩沖機制的復雜算法,而這正是數據庫層第一號性能瓶頸通常總是 CPU的主要原因。
一種替代傳統關系數據庫的方案是,使用在內存中運行的數據庫(In-memory Database),例如TimesTen。內存數據庫的出發點是允許數據臨時地寫入,但這些數據不必永久地保存到磁盤上,所有的操作都在內存中進行。這樣,內存數據庫不需要復雜的算法來減少I/O操作,而且可以采用比較簡單的加鎖機制,因而速度很快。
三、GUI篇
這一部分介紹的內容適合于圖形用戶界面的應用(Applet和普通應用),要用到AWT或Swing。
3.1 用JAR壓縮類文件
Java檔案文件(JAR文件)是根據JavaBean標準壓縮的文件,是發布JavaBean組件的主要方式和推薦方式。JAR檔案有助于減少文件體積,縮短下載時間。例如,它有助于Applet提高啟動速度。一個JAR文件可以包含一個或者多個相關的Bean以及支持文件,比如圖形、聲音、HTML 和其他資源。
要在HTML/JSP文件中指定JAR文件,只需在Applet標記中加入ARCHIVE = "name.jar"聲明。
請參見《使用檔案文件提高 applet 的加載速度》。
3.2 提示Applet裝入進程
你是否看到過使用Applet的網站,注意到在應該運行Applet的地方出現了一個占位符?當Applet的下載時間較長時,會發生什么事情?最大的可能就是用戶掉頭離去。在這種情況下,顯示一個Applet正在下載的信息無疑有助于鼓勵用戶繼續等待。
下面我們來看看一種具體的實現方法。首先創建一個很小的Applet,該Applet負責在后臺下載正式的Applet:
import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable, AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求裝載的正式Applet
largeAppletName = getParameter("applet");
// “請稍等”提示信息
label = new Label("請稍等..." + largeAppletName);
add(label);
}
public void run(){
try {
// 獲得待裝載Applet的類
Class largeAppletClass = Class.forName(largeAppletName);
// 創建待裝載Applet的實例
Applet largeApplet = (Applet)largeAppletClass.newInstance();
// 設置該Applet的Stub程序
largeApplet.setStub(this);
// 取消“請稍等”信息
remove(label);
// 設置布局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 顯示正式的Applet
largeApplet.init();
largeApplet.start();
}
catch (Exception ex) {
// 顯示錯誤信息
label.setText("不能裝入指定的Applet");
}
// 刷新屏幕
validate();
}
public void appletResize(int width, int height) {
// 把appletResize調用從stub程序傳遞到Applet
resize(width, height);
}
}
?
編譯后的代碼小于2K,下載速度很快。代碼中有幾個地方值得注意。首先,PreLoader實現了AppletStub接口。一般地,Applet從調用者判斷自己的codebase。在本例中,我們必須調用setStub()告訴Applet到哪里提取這個信息。另一個值得注意的地方是, AppletStub接口包含許多和Applet類一樣的方法,但appletResize()方法除外。這里我們把對appletResize()方法的調用傳遞給了resize()方法。
3.3 在畫出圖形之前預先裝入它
ImageObserver接口可用來接收圖形裝入的提示信息。ImageObserver接口只有一個方法imageUpdate(),能夠用一次repaint()操作在屏幕上畫出圖形。下面提供了一個例子。
public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
if ((flags & ALLBITS) !=0 {
repaint();
}
else if (flags & (ERROR |ABORT )) != 0) {
error = true;
// 文件沒有找到,考慮顯示一個占位符
repaint();
}
return (flags & (ALLBITS | ERROR| ABORT)) == 0;
}
當圖形信息可用時,imageUpdate()方法被調用。如果需要進一步更新,該方法返回true;如果所需信息已經得到,該方法返回false。
3.4 覆蓋update方法
update()方法的默認動作是清除屏幕,然后調用paint()方法。如果使用默認的update()方法,頻繁使用圖形的應用可能出現顯示閃爍現象。要避免在paint()調用之前的屏幕清除操作,只需按照如下方式覆蓋update()方法:
public void update(Graphics g) {
paint(g);
}
更理想的方案是:覆蓋update(),只重畫屏幕上發生變化的區域,如下所示:
public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}
3.5 延遲重畫操作
對于圖形用戶界面的應用來說,性能低下的主要原因往往可以歸結為重畫屏幕的效率低下。當用戶改變窗口大小或者滾動一個窗口時,這一點通常可以很明顯地觀察到。改變窗口大小或者滾動屏幕之類的操作導致重畫屏幕事件大量地、快速地生成,甚至超過了相關代碼的執行速度。對付這個問題最好的辦法是忽略所有“遲到” 的事件。
建議在這里引入一個數毫秒的時差,即如果我們立即接收到了另一個重畫事件,可以停止處理當前事件轉而處理最后一個收到的重畫事件;否則,我們繼續進行當前的重畫過程。
如果事件要啟動一項耗時的工作,分離出一個工作線程是一種較好的處理方式;否則,一些部件可能被“凍結”,因為每次只能處理一個事件。下面提供了一個事件處理的簡單例子,但經過擴展后它可以用來控制工作線程。
public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) { // e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 獲得當前時間
lastOne.time = time;
(new Thread() {public void run() {
if (milliseconds > 0) {
try {Thread.sleep(milliseconds);} // 暫停線程
catch (Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件結束
if (lastOne.time != time) // 只處理最后一個事件
return;
}
}}).start();
}
private static Hashtable e_queue = new Hashtable();
private static class LastOne {
public long time=0;
public Object running = new Object();
}
3.6 使用雙緩沖區
在屏幕之外的緩沖區繪圖,完成后立即把整個圖形顯示出來。由于有兩個緩沖區,所以程序可以來回切換。這樣,我們可以用一個低優先級的線程負責畫圖,使得程序能夠利用空閑的CPU時間執行其他任務。下面的偽代碼片斷示范了這種技術。
Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);
3.7 使用BufferedImage
Java JDK 1.2使用了一個軟顯示設備,使得文本在不同的平臺上看起來相似。為實現這個功能,Java必須直接處理構成文字的像素。由于這種技術要在內存中大量地進行位復制操作,早期的JDK在使用這種技術時性能不佳。為解決這個問題而提出的Java標準實現了一種新的圖形類型,即BufferedImage。
BufferedImage子類描述的圖形帶有一個可訪問的圖形數據緩沖區。一個BufferedImage包含一個ColorModel和一組光柵圖形數據。這個類一般使用RGB(紅、綠、藍)顏色模型,但也可以處理灰度級圖形。它的構造函數很簡單,如下所示:
public BufferedImage (int width, int height, int imageType)
ImageType允許我們指定要緩沖的是什么類型的圖形,比如5-位RGB、8-位RGB、灰度級等。
3.8 使用VolatileImage
許多硬件平臺和它們的操作系統都提供基本的硬件加速支持。例如,硬件加速一般提供矩形填充功能,和利用CPU完成同一任務相比,硬件加速的效率更高。由于硬件加速分離了一部分工作,允許多個工作流并發進行,從而緩解了對CPU和系統總線的壓力,使得應用能夠運行得更快。利用VolatileImage可以創建硬件加速的圖形以及管理圖形的內容。由于它直接利用低層平臺的能力,性能的改善程度主要取決于系統使用的圖形適配器。VolatileImage的內容隨時可能丟失,也即它是“不穩定的(volatile)”。因此,在使用圖形之前,最好檢查一下它的內容是否丟失。VolatileImage有兩個能夠檢查內容是否丟失的方法:
public abstract int validate(GraphicsConfiguration gc);
public abstract Boolean contentsLost();
每次從VolatileImage對象復制內容或者寫入VolatileImage時,應該調用validate()方法。contentsLost()方法告訴我們,自從最后一次validate()調用之后,圖形的內容是否丟失。
雖然VolatileImage是一個抽象類,但不要從它這里派生子類。VolatileImage應該通過 Component.createVolatileImage()或者 GraphicsConfiguration.createCompatibleVolatileImage()方法創建。
3.9 使用Window Blitting
進行滾動操作時,所有可見的內容一般都要重畫,從而導致大量不必要的重畫工作。許多操作系統的圖形子系統,包括WIN32 GDI、MacOS和X/Windows,都支持Window Blitting技術。Window Blitting技術直接在屏幕緩沖區中把圖形移到新的位置,只重畫新出現的區域。要在Swing應用中使用Window Blitting技術,設置方法如下:
setScrollMode(int mode);
在大多數應用中,使用這種技術能夠提高滾動速度。只有在一種情形下,Window Blitting會導致性能降低,即應用在后臺進行滾動操作。如果是用戶在滾動一個應用,那么它總是在前臺,無需擔心任何負面影響。
Java編程思想讀書筆記(對象)
對象的存儲:
對象的存儲區域有:寄存器(Registers)、棧(Stack)、堆(Heap)、靜態存儲空間(Static Storage)、常量存儲空間(Constant storage)、Non-RAM存儲空間。
- 寄存器:寄存器位于處理器內部,由于寄存器個數有限,編譯器根據本身需求適當地分配寄存器使用。
- 棧:此里用來存儲對像的引用和基本型別的變量。基本型別包括:boolean,char,byte,short,int,long,float,doule,void。
- 堆:此里用來存儲所有的Java對象。棧里的所有關于對像的引用均指定堆里的具體對象。
- 靜態存儲空間:用來存儲對象內的特定靜態成員,此靜態成員是用static變量聲明的。但Java對象絕無可能置于靜態存儲空間中。
- 常量存儲空間:用來存儲常量,常量也可存于ROM只讀內存中。
- Non-RAM存儲空間:用來存儲串流化對象(streamed objects)和持久性對象(persistent objects)。
基本數據類型:
基本型別包括:boolean,char,byte,short,int,long,float,doule,void。(string 屬于對象,不屬于基本類別。)而其對應的外覆型分別是:Boolean,Character,Byte,Short,Integer,Long,Folat,Double,Void。
別外,Java還提供了兩個高精度計算的Classes:BigInteger(可以精確表示任意長度整數數值,不會在運算過程中喪失任何信息)和BigDecimal(提供任意精度的定點數)。雖然它們也可以視為外覆類,但兩者都沒有對應的基本型別。
缺省值:當Class的某個成員屬于基本型別時,即使沒有為它提供初值,Java仍保證它有一個缺省值,缺省值如下:
boolean: false
char :'/u0000/(null)
byte : (byte)0
short : (short)0
int : 0
long : 0L
float : 0.0f
double 0.0d
但是:只有當變量身份是“Class內的成員”時,Java才保證為該變量提供初值。但當變量屬于局域變量(如位于某個函數內時)時,Java并不提供初始值。如:int x 。x可能是任意值(和C/C++)中的一樣,不會被自動設為0.
所有數組的初始值為null。
垃圾回收:
當一個對象不再被引用后,其Reference會在棧內消失,當垃圾收集器在堆內檢測對象,發現有的對象不再有Reference引用指向它時,就會把它銷毀。
游戲框架設計Ⅰ—— 游戲中的事件機制
游戲框架設計Ⅰ—— 游戲中的事件機制
?
事件機制在很多高級程序設計語言中都有支持。譬如VB、C#(delegate)、C++Builder(并不屬于C++的范疇。C++Builder中的事件處理器必須用關鍵字closure<閉包>修飾)等等,甚至在HTML中也可以見到它的身影。事件機制的引入使軟件系統變得更加易于理解——它使一種語言(平臺)更加接近于這個世界的真相。事情的發展變得像現實世界中那樣順理成章。某一事件的產生引發了一系列其他事件的產生,這些事件要么是結果要么又會引發一系列事件的產生......如此這般,信息才得以在事件的新陳代謝中延續,世界才得以向前發展。在某些游戲設計過程中的一項重要任務就是模擬現實世界的某些特征,以期實現機器與用戶的更加親密的溝通。事件機制就是很好的一例。我們需要事件來使我們的系統更加人性化。
我想,在我繼續進行下面對討論之前,先簡單介紹一下"事件"這個東東。
?
?
1. 游戲中的事件機制
聯系是普遍存在的。事事有聯系、時時有聯系,整個世界是一個相互聯系的統一整體。一個人的行為、物的狀態的改變或事的進展過程的某一階段可以引發一個事件。一個事件的發生或許會引發另外的事件——通過人的感知、大腦的反映,然后作出決策,付諸行動——也或許,就這么蒸發掉,無人知曉。但無論如何,在這一過程中,我們總能抽象出一些實質性的東西來。就像下面的圖示:
?
?
在游戲中:
事件源——表示任何可以引發事件的對象。譬如,一個"人"、"坦克"、"建筑物"、"地面"。
事件——表示任何可以處理的事件。譬如,"感冒"、"射擊"、"倒塌"、"有對象經過"。
響應者——表示任何對某事件感興趣的對象。
響應器——表示對某事件感興趣的對象對某一確定事件作出的反應。
特別的,對于過程:
通知——發生在事件與響應者之間。我們把它分為兩種方式:有限聽眾式、廣播式。對事件感興趣的對象(響應者)只有確定的有限個(只有一個的情況下,可以叫做點對點式)的情況就是有限聽眾式。而對于廣播式,事件并不知道會有哪些(個)對象對自己感興趣。它向所有可以接收事件通知的對象廣播事件。
觸發——響應者發現自己對特定事件需要做出相應的行動時就會觸發事件處理器,并同時傳遞需要的事件信息給它。對于響應者,它也可以選擇沉默——自己了解事件但并不作出行動。因此這個過程的決定權在響應者手上。
2. 萬事之鼻祖 Event
我們需要一個類來表示所有事件的普遍性質。
public class Event {
???? // 屬性
???? public string Name { get;set; }// 獲取或設置事件的名稱
???? public string Message { get;set; }// 獲取或設置事件的簡單描述
???? EventTypes EventType { get;set; }// 獲取或設置事件類型(枚舉EventTypes)
???? ListenerCollection Listeners { get; } // 獲取響應者的集合
???? public bool PoolEvent { get;set; }// 獲取或設置事件的簡單描述
?
???? // 方法
???? void RaiseEvent(); // 通知響應者事件的發生
???? void AbandonListener( int index ); // 拋棄一個事件響應者,并把它從 Listeners 中移除。
???? void AbandonListener(); // 拋棄所有的事件響應者
}
?
3. 枚舉類型 EventTypes
這個枚舉類型指示事件通知過程的類型:有限聽眾式、廣播式。
public enum EventTypes {
???? LimitedListener ,
???? Broadcast
}
?
4. 響應者接口 IListener
該接口只有唯一的方法 EventArrived() 。事件發生時會調用這個方法并傳遞相關參數。這個參數必須是 EventArgs 或由它派生而來。
public interface IListener {
???? // 通知一個響應者事件的到達。
???? void EventArrived( EventArgs args );
}
?
5. EventPool
一個事件池。當且僅當需要事件廣播時我們才需要它。需要注意的是 AddEvent 方法。它把一個事件添加到池中,第二個參數指定是否將該事件已經指定的響應者亦添加到廣播的響應者中。事件添加后,其 Event::EventType 屬性會被設置為 EventTypes.Broadcast。
public class EventPool {
???? // 屬性
???? public ArrayList Events { get; }// 獲取池中所有的事件的集合
???? public ListnerCollection Listners { get; }// 獲取池中所有的響應者的集合
?
???? // 方法
???? void AddEvent( Event obj ,bool copyListners ); // 添加一個事件并把它作為廣播式事件
???? void RemoveEventAt( int index ); // 將一個事件從列表中移除
???? void RemoveEvent( Event listener ); // 將一個事件從列表中移除
???? void Broadcast( Event event ); // 向列表中的所有響應者廣播指定事件(可以是非池中的事件)
???? void BroadcastItemAt( int index ); // 向列表中的所有響應者廣播池中的指定事件
}
?
6. EventArgs
public class EventArgs {
???? public Event Event { get; } // 獲取傳遞這個參數的事件
???? public object Sender { get; } // 獲取事件源
}
?
7. UML Diagram
?
8. 響應者行為
響應者實現 IListener 接口后就可以響應事件了。在 EventArrived() 方法中,你可以直接處理事件,抑或是調用其它的事件處理器(響應器)。C#中有很好的解決方案——委托——替代函數指針的最有效的方法。在C++中也可以用虛擬函數表來模擬委托機制。總之,在響應器上的解決方案是很靈活的。在實際開發中,可以根據不同的環境做出不同的選擇。
9. 擴展機制
在一個游戲中,除了已經定義好的事件外,其劇情或功能可能會要求玩家自行定義一些事件。這就需要一種可擴展的方案。我們引入了 CustomEvent 類——繼承自 Event,以及 Condition 類。
public class CustomEvent : Event {
???? public CustomEvent( Condition condition ) {
????????? _Condition = condition;
???? }
?
???? public Condition TestCondition { get{ return _Condition; } }
?
???? Condition _Condition = null;
}
?
public abstract class Condition {
???? public Condition() {}
???? bool abstract Test();
}
?
初始化一個 CustomEvent 類時必須同時傳入一個 Condition 類。Condition 類必須被繼承。Test()方法在適當的時候被調用以檢測是否可以引發這個事件。
10. 后記
使用Java NIO提高服務端程序的性能
在前面的章節里,我們討論了Java NIO的基本概念,在這一節里,我們將結合具體的Java Socket編程,討論使用NIO提高服務端程序的性能的問題。????Java NIO增加了新的SocketChannel、ServerSocketChannel等類來提供對構建高性能的服務端程序的支持。 SocketChannel、ServerSocketChannel能夠在非阻塞的模式下工作,它們都是selectable的類。在構建服務器或者中間件時,推薦使用Java NIO。
????在傳統的網絡編程中,我們通常使用一個專用線程(Thread)來處理一個Socket連接,通過使用NIO,一個或者很少幾個Socket線程就可以處理成千上萬個活動的Socket連接。
????通常情況下,通過ServerSocketChannel.open()獲得一個ServerSocketChannel的實例,通過SocketChannel.open或者serverSocketChannel.accept()獲得一個SocketChannel實例。要使ServerSocketChannel或者SocketChannel在非阻塞的模式下操作,可以調用
????serverSocketChannel.configureBlocking (false);
????或者
????socketChannel.configureBlocking (false);
????語句來達到目的。通常情況下,服務端可以使用非阻塞的ServerSocketChannel,這樣,服務端的程序就可以更容易地同時處理多個socket線程。
????下面我們來看一個綜合例子,這個例子使用了ServerSocketChannel、SocketChannel開發了一個非阻塞的、能處理多線程的Echo服務端程序,見示例12-14。
????【程序源代碼】
1 // ==================== Program Discription ===================== 2 // 程序名稱:示例12-14 : SocketChannelDemo.java 3 // 程序目的:學習Java NIO#SocketChannel 4 // ============================================================== 5 6 7 import java.nio.ByteBuffer; 8 import java.nio.channels.ServerSocketChannel; 9 import java.nio.channels.SocketChannel; 10 import java.nio.channels.Selector; 11 import java.nio.channels.SelectionKey; 12 import java.nio.channels.SelectableChannel; 13 14 import java.net.Socket; 15 import java.net.ServerSocket; 16 import java.net.InetSocketAddress; 17 import java.util.Iterator; 18 19 public class SocketChannelDemo20 21 { 22 public static int PORT_NUMBER = 23;//監聽端口 23 ServerSocketChannel serverChannel; 24 ServerSocket serverSocket ; 25 Selector selector ; 26 private ByteBuffer buffer = ByteBuffer.allocateDirect (1024); 27 28 public static void main (String [] args) 29 throws Exception 30 { 31 SocketChannelDemo server=new SocketChannelDemo(); 32 server.init(args); 33 server.startWork(); 34 } 35 36 37 public void init (String [] argv)throws Exception 38 { 39 int port = PORT_NUMBER; 40 41 if (argv.length > 0) { 42 port = Integer.parseInt (argv [0]); 43 } 44 45 System.out.println ("Listening on port " + port); 46 47 // 分配一個ServerSocketChannel 48 serverChannel = ServerSocketChannel.open(); 49 // 從ServerSocketChannel里獲得一個對應的Socket 50 serverSocket = serverChannel.socket(); 51 // 生成一個Selector 52 selector = Selector.open(); 53 54 // 把Socket綁定到端口上 55 serverSocket.bind (new InetSocketAddress (port)); 56 //serverChannel為非bolck 57 serverChannel.configureBlocking (false); 58 59 // 通過Selector注冊ServerSocetChannel 60 serverChannel.register (selector, SelectionKey.OP_ACCEPT); 61 62 } 63 64 public void startWork()throws Exception65 66 { 67 while (true) { 68 69 int n = selector.select();//獲得IO準備就緒的channel數量 70 71 if (n == 0) { 72 continue; // 沒有channel準備就緒,繼續執行 73 } 74 75 // 用一個iterator返回Selector的selectedkeys 76 Iterator it = selector.selectedKeys().iterator(); 77 78 // 處理每一個SelectionKey 79 while (it.hasNext()) { 80 SelectionKey key = (SelectionKey) it.next(); 81 82 // 判斷是否有新的連接到達 83 if (key.isAcceptable()) { 84 //返回SelectionKey的ServerSocketChannel 85 ServerSocketChannel server = (ServerSocketChannel) key.channel(); 86 SocketChannel channel = server.accept(); 87 88 registerChannel (selector, channel, 89 SelectionKey.OP_READ); 90 91 doWork (channel); 92 } 93 94 // 判斷是否有數據在此channel里需要讀取 95 if (key.isReadable()) { 96 97 processData (key); 98 99 } 100 101 //刪除 selectedkeys 102 it.remove(); 103 } 104 } 105 } 106 protected void registerChannel (Selector selector, 107 SelectableChannel channel, int ops) 108 throws Exception 109 {110 if (channel == null) { 111 return; 112 } 113 114 115 channel.configureBlocking (false); 116 117 channel.register (selector, ops); 118 } 119 120 //處理接收的數據 121 protected void processData (SelectionKey key) 122 throws Exception 123 { 124 125 126 SocketChannel socketChannel = (SocketChannel) key.channel(); 127 int count; 128 129 buffer.clear(); // 清空buffer 130 131 // 讀取所有的數據 132 while ((count = socketChannel.read (buffer)) > 0) { 133 buffer.flip(); 134 135 // send the data, don′t assume it goes all at once 136 while (buffer.hasRemaining()) 137 { 138 //如果收到回車鍵,則在返回的字符前增加[echo]$字樣 139 if(buffer.get()==(char)13) 140 { 141 buffer.clear(); 142 buffer.put("[echo]___FCKpd___0quot;.getBytes()); 143 buffer.flip(); 144 145 } 146 socketChannel.write (buffer);//在Socket里寫數據 147 } 148 149 buffer.clear(); // 清空buffer 150 } 151 152 if (count < 0) { 153 // count<0,說明已經讀取完畢 154 socketChannel.close();155 } 156 } 157 158 159 private void doWork (SocketChannel channel)throws Exception 160 { 161 buffer.clear(); 162 buffer.put (" Hello,I am working,please input some thing,and i will echo to you! [echo] ___FCKpd___0quot;.getBytes()); 163 buffer.flip(); 164 channel.write (buffer); 165 } 166 167 } |
????使用:運行此程序,然后在控制臺輸入命令telnet localhost 23。
????【程序輸出結果】如圖12-1所示。

圖12-1 輸出結果
????【程序注解】
????關于程序的解釋已經包含在程序里面了,在這里我們總結以下使用ServerSocket Channel開發服務端程序的過程:
????(1)分配一個ServerSocketChannel。
????(2)從ServerSocketChannel里獲得一個對應的ServerSocket。
????(3)生成一個Selector實例。
????(4)把ServerSocket綁定到端口上。
????(5)設置ServerSocketChannel為非block模式(可選)。
????(6)在Selector里注冊ServerSocetChannel。
????(7)用一個無限循環語句始終查看Selector里是否有IO準備就緒的channel。如果有,就執行對應的處理,如果沒有,繼續循環。
???? 小 結
????在本章我們主要介紹了Java中的網絡編程。Java一開始就是一種網絡編程語言,到后來才應用到各個方面,所以在Java中進行網絡編程遠比在C/C++中方便。
????我們介紹了幾個在網絡編程中很重要的類,如InetAddress、URL、URLConnection、Socket、 ServerSocket、DatagramSocket、DatagramPacket、MulticastSocket等。這些類包含了進行基本網絡編程的所有內容。要熟練地應用這些類,關鍵還是要多多練習。
????基于套接字的編程基本上是客戶/服務器模式,我們具體介紹了編寫這種模式的步驟。在實例方面,我們給出了一個基于TCP的套接字客戶/服務器程序,與此相對應,還給出了基于UDP的客戶/服務器程序。兩者的模式是很相似的,其實這也就是編寫客戶/服務器程序的一般模式。 (T111)