天天看點

提高程式設計思想

虛函數是有代碼的并明确允許子類去覆寫,但子類也可不覆寫,就是說可以直接用,不用重寫  

  抽象函數是沒有代碼,子類繼承後一定要重寫

******************************************************************

在一個類中用虛函數:  

  是因為在超類中的有實際代碼的方法,但明确允許子類可以作重寫  

  而且當子類重寫後,可以用子類執行個體超類;如果這樣,超類變量調用虛函數時,執行的是子類的方法  

  在一個類中用抽象函數  

  是在寫超類時不确定函數的代碼,讓子類去實作   

抽象函數沒有方法體。

String abc="aaa";

char c=abc.charAt(i);

c+=4;

以上如果把:c+=4; 改成:c=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]

級别:初級

摘要:本文介紹了字元與編碼的發展過程,相關概念的正确了解。舉例說明了一些實際應用中,編碼的實作方法。然後,本文講述了通常對字元與編碼的幾種誤解,由于這些誤解而導緻亂碼産生的原因,以及消除亂碼的辦法。本文的内容涵蓋了“中文問題”,“亂碼問題”。

“字元與編碼”是一個被經常讨論的話題。即使這樣,時常出現的亂碼仍然困擾着大家。雖然我們有很多的辦法可以用來消除亂碼,但我們并不一定了解這些辦法的内在原理。而有的亂碼産生的原因,實際上由于底層代碼本身有問題所導緻的。是以,不僅是初學者會對字元編碼感到模糊,有的底層開發人員同樣對字元編碼缺乏準确的了解。

提高程式設計思想
提高程式設計思想

回頁首

從計算機對多國語言的支援角度看,大緻可以分為三個階段:

系統内碼

說明

系統

階段一

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

提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想

在 UNICODE 被采用之後,計算機存放字元串時,改為存放每個字元在 UNICODE 字元集中的序号。目前計算機一般使用 2 個位元組(16 位)來存放一個序号(DBCS),是以,這種方式存放的字元也被稱作寬位元組字元。比如,字元串 "中文123" 在 Windows 2000 下,記憶體中實際存放的是 5 個序号:

2D

4E

87

65

     ← 在 x86 CPU 中,低位元組在前

提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想

一共占 10 個位元組。

提高程式設計思想
提高程式設計思想

了解編碼的關鍵,是要把字元的概念和位元組的概念了解準确。這兩個概念容易混淆,我們在此做一下區分:

概念描述

舉例

字元

人們使用的記号,抽象意義上的一個符号。

'1', '中', 'a', '$', '¥', ……

位元組

計算機中存儲資料的單元,一個8位的二進制數,是一個很具體的存儲空間。

0x01, 0x45, 0xFA, ……

ANSI

字元串

在記憶體中,如果“字元”是以 ANSI 編碼形式存在的,一個字元可能使用一個位元組或多個位元組來表示,那麼我們稱這種字元串為ANSI 字元串或者多位元組字元串。

"中文123"

(占7位元組)

在記憶體中,如果“字元”是以在 UNICODE 中的序号存在的,那麼我們稱這種字元串為 UNICODE 字元串或者寬位元組字元串。

L"中文123"

(占10位元組)

由于不同 ANSI 編碼所規定的标準是不相同的,是以,對于一個給定的多位元組字元串,我們必須知道它采用的是哪一種編碼規則,才能夠知道它包含了哪些“字元”。而對于UNICODE 字元串來說,不管在什麼環境下,它所代表的“字元”内容總是不變的。

提高程式設計思想
提高程式設計思想

各個國家和地區所制定的不同 ANSI 編碼标準中,都隻規定了各自語言所需的“字元”。比如:漢字标準(GB2312)中沒有規定南韓語字元怎樣存儲。這些 ANSI 編碼标準所規定的内容包含兩層含義:

使用哪些字元。也就是說哪些漢字,字母和符号會被收入标準中。所包含“字元”的集合就叫做“字元集”。

規定每個“字元”分别用一個位元組還是多個位元組存儲,用哪些位元組來存儲,這個規定就叫做“編碼”。

各個國家和地區在制定編碼标準的時候,“字元的集合”和“編碼”一般都是同時制定的。是以,平常我們所說的“字元集”,比如:GB2312, GBK, JIS 等,除了有“字元的集合”這層含義外,同時也包含了“編碼”的含義。

“UNICODE 字元集”包含了各種語言中使用到的所有“字元”。用來給 UNICODE 字元集編碼的标準有很多種,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

提高程式設計思想
提高程式設計思想

在 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 函數。

提高程式設計思想
提高程式設計思想

聲明一段字元串常量:

// 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() 在運作時起作用。

提高程式設計思想
提高程式設計思想

字元串類 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 操作的編碼,前者是在編譯時起作用,後者是在運作時起作用。

提高程式設計思想
提高程式設計思想

對編碼的誤解

誤解一

在将“位元組串”轉化成“UNICODE 字元串”時,比如在讀取文本檔案時,或者通過網絡傳輸文本時,容易将“位元組串”簡單地作為單位元組字元串,采用每“一個位元組”就是“一個字元”的方法進行轉化。

而實際上,在非英文的環境中,應該将“位元組串”作為 ANSI 字元串,采用适當的編碼來得到 UNICODE 字元串,有可能“多個位元組”才能得到“一個字元”。

通常,一直在英文環境下做開發的程式員們,容易有這種誤解。

誤解二

在 DOS,Windows 98 等非 UNICODE 環境下,字元串都是以 ANSI 編碼的位元組形式存在的。這種以位元組形式存在的字元串,必須知道是哪種編碼才能被正确地使用。這使我們形成了一個慣性思維:“字元串的編碼”。

當 UNICODE 被支援後,Java 中的 String 是以字元的“序号”來存儲的,不是以“某種編碼的位元組”來存儲的,是以已經不存在“字元串的編碼”這個概念了。隻有在“字元串”與“位元組串”轉化時,或者,将一個“位元組串”當成一個 ANSI 字元串時,才有編碼的概念。

不少的人都有這個誤解。

第一種誤解,往往是導緻亂碼産生的原因。第二種誤解,往往導緻本來容易糾正的亂碼問題變得更複雜。

提高程式設計思想
提高程式設計思想

簡單介紹一下常用的編碼規則,為後邊的章節做一個準備。在這裡,我們根據編碼規則的特點,把所有的編碼分成三類:

分類

編碼标準

單位元組字元編碼

ISO-8859-1

最簡單的編碼規則,每一個位元組直接作為一個 UNICODE 字元。比如,[0xD6, 0xD0] 這兩個位元組,通過 iso-8859-1 轉化為字元串時,将直接得到 [0x00D6, 0x00D0] 兩個 UNICODE 字元,即 "ÖÐ"。

反之,将 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 字元串”。

提高程式設計思想
提高程式設計思想

非 UNICODE 程式中的字元串,都是以某種 ANSI 編碼形式存在的。如果程式運作時的語言環境與開發時的語言環境不同,将會導緻 ANSI 字元串的顯示失敗。

比如,在日文環境下開發的非 UNICODE 的日文程式界面,拿到中文環境下運作時,界面上将顯示亂碼。如果這個日文程式界面改為采用 UNICODE 來記錄字元串,那麼當在中文環境下運作時,界面上将可以顯示正常的日文。

由于客觀原因,有時候我們必須在中文作業系統下運作非 UNICODE 的日文軟體,這時我們可以采用一些工具,比如,南極星,AppLocale 等,暫時的模拟不同的語言環境。

提高程式設計思想
提高程式設計思想

當頁面中的表單送出字元串時,首先把字元串按照目前頁面的編碼,轉化成位元組串。然後再将每個位元組轉化成 "%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") 重新得到正确的字元串 "中"。

提高程式設計思想
提高程式設計思想

通過資料庫用戶端(比如 ODBC 或 JDBC)從資料庫伺服器中讀取字元串時,用戶端需要從伺服器獲知所使用的 ANSI 編碼。當資料庫伺服器發送位元組流給用戶端時,用戶端負責将位元組流按照正确的編碼轉化成 UNICODE 字元串。

如果從資料庫讀取字元串時得到亂碼,而資料庫中存放的資料又是正确的,那麼往往還是因為前面提到的“誤解一”造成的。解決的辦法還是通過 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的位元組串,再重新使用正确的編碼轉化成字元串。

提高程式設計思想
提高程式設計思想

當一段 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],即 "ÖÐ",而不是 "中"。

提高程式設計思想
提高程式設計思想

非也。iso-8859-1 隻是單位元組字元集中最簡單的一種,也就是“位元組編号”與“UNICODE 字元編号”一緻的那種編碼規則。當我們要把一個“位元組串”轉化成“字元串”,而又不知道它是哪一種 ANSI 編碼時,先暫時地把“每一個位元組”作為“一個字元”進行轉化,不會造成資訊丢失。然後再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢複到原始的位元組串。

Java 中,字元串類 java.lang.String 處理的是 UNICODE 字元串,不是 ANSI 字元串。我們隻需要把字元串作為“抽象的符号的串”來看待。是以不存在字元串的内碼的問題。

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小節的"用内部類改進代 碼")。

(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章的詳細說明)。

(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();

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。值得一提的是,雖然移位操作速度快,但可能使代碼比較難于了解,是以最好加上一些注釋。

前面介紹的改善性能技巧适合于大多數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)

(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操作,而且可以采用比較簡單的加鎖機制,因而速度很快。

這一部分介紹的内容适合于圖形使用者界面的應用(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;

// 檔案沒有找到,考慮顯示一個占位符

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(),隻重畫螢幕上發生變化的區域,如下所示:

g.clipRect(x, y, w, h);

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會導緻性能降低,即應用在背景進行滾動操作。如果是使用者在滾動一個應用,那麼它總是在前台,無需擔心任何負面影響。

對象的存儲:

對象的存儲區域有:寄存器(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 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。

    【程式源代碼】

    使用:運作此程式,然後在控制台輸入指令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)

提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想
提高程式設計思想