我對Java的面向對象的特性琢磨良久,自認為有所領悟,也開始有意識的運用OOP風格來寫程序,然而還是經常會覺得不知道應該怎樣提煉類,面對一個具體的問題的時候,會覺得腦子里千頭萬緒的,不知道怎么下手,一不小心,又會回到原來的思路上去。
舉個例子,要發廣告郵件,廣告郵件列表存在數據庫里面。倘若用C來寫的話,一般會這樣思考,先把郵件內容讀入,然后連接數據庫,循環取郵件地址,調用本機的qmail的sendmail命令發送。
然后考慮用Java來實現,既然是OOP,就不能什么代碼都塞到main過程里面,于是就設計了三個類:
一個類是負責讀取數據庫,取郵件地址,調用qmail的sendmail命令發送;
一個類是讀郵件內容,MIME編碼成HTML格式的,再加上郵件頭;
一個主類負責從命令讀參數,處理命令行參數,調用發email的類。
把一件工作按照功能劃分為3個模塊分別處理,每個類完成一件模塊任務。
仔細的分析一下,就會發現這樣的設計完全是從程序員實現程序功能的角度來設計的,或者說,設計類的時候,是自低向上的,從機器的角度到現實世界的角度來分析問題的。因此在設計的時候,就已經把程序編程實現的細節都考慮進去了,企圖從底層實現程序這樣的出發點來達到滿足現實世界的軟件需求的目標。
這樣的分析方法其實是不適用于Java這樣面向對象的編程語言,因為,如果改用C語言,封裝兩個C函數,都會比Java實現起來輕松的多,邏輯上也清楚的多。
我覺得面向對象的精髓在于考慮問題的思路是從現實世界的人類思維習慣出發的,只要領會了這一點,就領會了面向對象的思維方法。
舉一個非常簡單的例子:假使現在需要寫一個網頁計數器,客戶訪問一次頁面,網頁計數器加1,計數器是這樣來訪問的
http://hostname/count.cgi?id=xxx
后臺有一個數據庫表,保存每個id(一個id對應一個被統計訪問次數的頁面)的計數器當前值,請求頁面一次,對應id的計數器的字段加1(這里我們忽略并發更新數據庫表,出現的表鎖定的問題)。
如果按照一般從程序實現的角度來分析,我們會這樣考慮:首先是從HTTP GET請求取到id,然后按照id查數據庫表,獲得某id對應的訪問計數值,然后加1,更新數據庫,最后向頁面顯示訪問計數。
現在假設一個沒有程序設計經驗的人,他會怎樣來思考這個問題的呢?他會提出什么樣的需求呢?他很可能會這樣想:
我需要有一個計數器,這個計數器應該有這樣的功能,刷新一次頁面,訪問量就會加1,另外最好還有一個計數器清0的功能,當然計數器如果有一個可以設為任意值的功能的話,我就可以作弊了。
做為一個沒有程序設計經驗的人來說,他完全不會想到對數據庫應該如何操作,對于HTTP變量該如何傳遞,他考慮問題的角度就是我有什么需求,我的業務邏輯是什么,軟件應該有什么功能。
按照這樣的思路(請注意,他的思路其實就是我們平時在生活中習慣的思維方式),我們知道需要有一個計數器類 Counter,有一個必須的和兩個可選的方法:
getCount() // 取計數器值方法
resetCounter() // 計數器清0方法
setCount() // 設計數器為相應的值方法
把Counter類完整的定義如下:
public class Counter {
public int getCount(int id) {}
public void resetCounter(int id) {}
public void setCount(int id, int currentCount) {}
}
解決問題的框架已經有了,來看一下如何使用Counter。 在count.cgi里面調用Counter來計數,程序片斷如下:
// 這里從HTTP環境里面取id值
...
Counter myCounter = new Counter(); // 獲得計數器
int currentCount = myCounter.getCount(id); // 從計數器中取計數
// 這里向客戶瀏覽器輸出
...
程序的框架全都寫好了,剩下的就是實現Counter類方法里面具體的代碼了,此時才去考慮具體的程序語言實現的細節,比如,在getCount()方法里面訪問數據庫,更新計數值。
從上面的例子中看到,面向對象的思維方法其實就是我們在現實生活中習慣的思維方式,是從人類考慮問題的角度出發,把人類解決問題的思維方式逐步翻譯成程序能夠理解的思維方式的過程,在這個翻譯的過程中,軟件也就逐步被設計好了。
在運用面向對象的思維方法進行軟件設計的過程中,最容易犯的錯誤就是開始分析的時候,就想到了程序代碼實現的細節,因此封裝的類完全是基于程序實現邏輯,而不是基于解決問題的業務邏輯。
學習JDBC編程的經典錯誤問法是:“我怎樣封裝對數據庫的select操作?”
面向對象的設計是基于解決業務問題的設計,而不是基于具體編程技術的設計。我不會去封裝select語句的,我只封裝解決問題的業務邏輯,對數據庫的讀取是在業務邏輯的編碼實現階段才去考慮的問題。
回過頭看上面那個發廣告郵件的例子,應該如何應用面向對象的思維方法呢?
對于一個郵件來說,有郵件頭,郵件體,和郵件地址這三個屬性,發送郵件,需要一個發送的方法,另外還需要一個能把所有郵件地址列出來的方法。所以應該如下設計:
類JunkMail
屬性:
head
body
address
方法:
sendMail() // 發送郵件
listAllMail() // 列郵件地址
用Java來表示:
public class JunkMail {
private String head;
private String body;
private String address;
public JunkMain() { // 默認的類構造器
// 從外部配置文件讀郵件頭和郵件體
this.head=...;
this.body=...;
}
public static boolean sendMail(String address) {
// 調用qmail,發送email
}
public static Collection listAllMail() {
// 訪問數據庫,返回一個郵件地址集合
}
}
當把JunkMail設計好了以后,再調用JunkMail類完成郵件的發送,將是非常輕松的事情。
如果說傳統的面向過程的編程是符合機器運行指令的流程的話,那么面向對象的思維方法就是符合現實生活中人類解決問題的思維過程。
在面向對象的軟件分析和設計的時候,要提醒自己,不要一上來就去想程序代碼的實現,應該拋開具體編程語言的束縛,集中精力分析我們要實現的軟件的業務邏輯,分析軟件的業務流程,思考應該如何去描述和實現軟件的業務。畢竟軟件只是一個載體,業務才是我們真正要實現的目標。
但是在設計過程中,心里卻往往在擔心,如果我完全不去考慮程序代碼的實現的話,那么我怎么知道我的設計一定合理呢?我怎么知道我設計的類、接口一定可以實現呢?所以經??梢钥吹降默F象就是:
在設計過程中,雖然知道不能過早考慮代碼實現,但是每設計一個類,一個接口,心里都要不知不覺的用自己熟悉的編程語言大概的評估一下,看看能否編出來,因此,一不小心,就會又回到按照程序功能實現的思路進行設計的老路上去了。
舉個例子來說明,在做Web程序設計的時候,經常要遇到分頁顯示數據的情況。比如說需要把系統中所有的用戶都列出來這樣的功能。假設使用User類來表示用戶,增加用戶addUser(),刪除用戶deleteUser(),查詢所有用戶listUsers()方法。而數據庫中有一個user表,一條記錄是一個用戶的信息。下面考慮一下User類的方法的實現:
addUser()和deleteUser()方法都好實現,就是對數據庫增加記錄和刪除記錄。對于listUsers()方法,其實就是對user表的select,取出一個記錄集。但是該怎么從listUsers()方法中得到所有用戶的列表呢?
一個方法調用的返回值只有一個,沒有多個,所以很多情況下采用的辦法就是返回值定義為集合類型,比如Vector。這樣就可以在listUsers()方法的具體代碼實現的時候,從數據庫依次取出一個個記錄,插入到Vector里面來。在主程序里面,調用listUsers()方法可以返回一個Vector,然后再對Vector遍歷操作,就可以得到用戶列表了。
public class User {
public static void addUser(...) {
// 數據庫insert一條記錄
}
public static void deleteUser(...) {
// 數據庫delete一條記錄
}
public Vector listUsers(...) {
// 數據庫select結果放到一個集合里面
}
}
這樣的設計基本合理,但是仍然有點小問題。因為在設計的時候,就考慮到了用Java的集合類Vector來實現對不定長數據集的存放,因而違反了面向對象設計的一個原則:在設計的時候不應過早的考慮具體程序語言的實現。所以必須用抽象的方法,和具體實現無關的方法來表達業務邏輯。
我們知道,通常對具有集合特征的數據結構進行遍歷通常可以使用next和hasNext方法,next實現取下一個用戶,hasNext判斷是否還有元素。 因此我們定義一個接口Iterator,這個接口中定義兩個方法next和hasNext:
public interface Iterator {
public boolean hasNext() {}
public Object next() {}
}
而User類的listUses方法返回值改為Iterator接口的實現類:
public class User {
...
public Iterator listUsers() {
}
...
}
這樣就把User類的設計和具體的實現方法分離開了,因為此時任何實現了next()和hasNext()方法的類都可以做為listUsers的返回值,都可以被用來表達“用戶列表”,而不僅僅可以使用Vector而已。比如,我可以用ArrayList來表達用戶列表,因為ArrayList也實現了Iterator,當然我也可以自己專門寫一個類來存放用戶列表,只要實現next()和hasNext()方法就行了。
這樣在具體的編寫代碼的時候,程序員具有了最大的靈活性,可以根據具體的情況,采用不同的編程方法來存放用戶列表。特別是降低了程序的耦合度,提高了程序的可移植性。對于上面那個JunkMail的listAllMail()方法也同樣應該改為接口類型。
然后,在主程序里面就這樣來使用User類的listUsers方法:
User myUser = new User();
Iterator iterator = myUser.listUsers();
while (iterator.hasNext()) {
iterator.next();
}
這樣就可以完全不用考慮程序代碼實現了,從高層次上把功能抽象出來,定義成為接口,同時又可以把系統設計的很合理,完全根據業務的需求來進行設計。
結語
通過上面的幾個例子的設計說明,使用面向對象的思維方法,其實是一個把業務邏輯從具體的編程技術當中抽象出來的過程,而這個抽象的過程是自上而下的,非常符合人類的思維習慣,也就是先不考慮問題解決的細節,把問題的最主要的方面抽象成為一個簡單的框架,集中精力思考如何解決主要矛盾,然后在解決問題的過程中,再把問題的細節分割成一個一個小問題,再專門去解決細節問題。
因而一旦牢牢的抓住了這一點,你就會發現在軟件設計和開發過程中,你自己總是會不知不覺的運用面向對象的思維方法來設計和編寫程序,并且程序的設計和開發也變得不再那么枯燥,而一個合理運用面向對象技術進行設計和架構的軟件,更是具備了思維的藝術美感。
最后,愿面向對象的思維方法也能給您的程序設計之路帶來創作的樂趣。
一、目的
對于代碼,首要要求是它必須正確,能夠按照程序員的真實思想去運行;第二個的要求是代碼必須清晰易懂,使別的程序員能夠容易理解代碼所進行的實際工作。在軟件工程領域,源程序的風格統一標志著可維護性、可讀性,是軟件項目的一個重要組成部分。而目前還沒有成文的編碼風格文檔,以致于很多時候,程序員沒有一個共同的標準可以遵守,編碼風格各異,程序可維護性差、可讀性也很差。通過建立代碼編寫規范,形成開發小組編碼約定,提高程序的可靠性、可讀性、可修改性、可維護性、可繼承性和一致性,可以保證程序代碼的質量,繼承軟件開發成果,充分利用資源,使開發人員之間的工作成果可以共享。
本文在參考業界已有的編碼風格的基礎上,描述了一個基于 JBuilder 的項目風格,力求一種統一的編程風格,并從整體編碼風格、代碼文件風格、函數編寫風格、變量風格、注釋風格等幾個方面進行闡述。(這些規范并不是一定要絕對遵守,但是一定要讓程序有良好的可讀性)
二、整體編碼風格
1、縮進
縮進建議以4個空格為單位。建議在 Tools/Editor Options 中設置 Editor 頁面的Block ident為4,Tab Size 為8。預處理語句、全局數據、標題、附加說明、函數說明、標號等均頂格書寫。語句塊的"{"、"}"配對對齊,并與其前一行對齊,語句塊類的語句縮進建議每個"{"、"}"單獨占一行,便于匹對。JBuilder 中的默認方式是開始的"{"不是單獨一行,建議更改成上述格式(在 Project/Default Project Properties 中設置 Code Style 中選擇 Braces 為 Next line)。
2、空格
原則上變量、類、常量數據和函數在其類型,修飾名稱之間適當空格并據情況對齊。關鍵字原則上空一格,如:if ( ... ) 等。運算符的空格規定如下:"::"、"->;"、"["、"]"、"++"、"--"、"~"、"!"、"+"、"-"(指正負號)、"&"(引用)等幾個運算符兩邊不加空格(其中單目運算符系指與操作數相連的一邊),其它運算符(包括大多數二目運算符和三目運算符"?:"兩邊均加一空格,在作函數定義時還可據情況多空或不空格來對齊,但在函數實現時可以不用。","運算符只在其后空一格,需對齊時也可不空或多空格。不論是否有括號,對語句行后加的注釋應用適當空格與語句隔開并盡可能對齊。個人認為此項可以依照個人習慣決定遵循與否。
3、對齊
原則上關系密切的行應對齊,對齊包括類型、修飾、名稱、參數等各部分對齊。另每一行的長度不應超過屏幕太多,必要時適當換行,換行時盡可能在","處或運算符處,換行后最好以運算符打頭,并且以下各行均以該語句首行縮進,但該語句仍以首行的縮進為準,即如其下一行為“{”應與首行對齊。
變量定義最好通過添加空格形成對齊,同一類型的變量最好放在一起。如下例所示:
int Value;
int Result;
int Length;
DWORD Size;
DWORD BufSize;
個人認為此項可以依照個人習慣決定遵循與否。
4、空行
不得存在無規則的空行,比如說連續十個空行。程序文件結構各部分之間空兩行,若不必要也可只空一行,各函數實現之間一般空兩行,由于每個函數還要有函數說明注釋,故通常只需空一行或不空,但對于沒有函數說明的情況至少應再空一行。對自己寫的函數,建議也加上“//------”做分隔。函數內部數據與代碼之間應空至少一行,代碼中適當處應以空行空開,建議在代碼中出現變量聲明時,在其前空一行。類中四個“p”之間至少空一行,在其中的數據與函數之間也應空行。
5、注釋
注釋是軟件可讀性的具體體現。程序注釋量一般占程序編碼量的20%,軟件工程要求不少于20%。程序注釋不能用抽象的語言,類似于"處理"、"循環"這樣的計算機抽象語言,要精確表達出程序的處理說明。例如:"計算凈需求"、"計算第一道工序的加工工時"等。避免每行程序都使用注釋,可以在一段程序的前面加一段注釋,具有明確的處理邏輯。
注釋必不可少,但也不應過多,不要被動的為寫注釋而寫注釋。以下是四種必要的注釋:
A. 標題、附加說明。
B. 函數、類等的說明。對幾乎每個函數都應有適當的說明,通常加在函數實現之前,在沒有函數實現部分的情況下則加在函數原型前,其內容主要是函數的功能、目的、算法等說明,參數說明、返回值說明等,必要時還要有一些如特別的軟硬件要求等說明。公用函數、公用類的聲明必須由注解說明其使用方法和設計思路,當然選擇恰當的命名格式能夠幫助你把事情解釋得更清楚。
C. 在代碼不明晰或不可移植處必須有一定的說明。
D. 及少量的其它注釋,如自定義變量的注釋、代碼書寫時間等。
注釋有塊注釋和行注釋兩種,分別是指:"/**/"和"//"建議對A用塊注釋,D用行注釋,B、C則視情況而定,但應統一,至少在一個單元中B類注釋形式應統一。具體對不同文件、結構的注釋會在后面詳細說明。
6、代碼長度
對于每一個函數建議盡可能控制其代碼長度為53行左右,超過53行的代碼要重新考慮將其拆分為兩個或兩個以上的函數。函數拆分規則應該一不破壞原有算法為基礎,同時拆分出來的部分應該是可以重復利用的。對于在多個模塊或者窗體中都要用到的重復性代碼,完全可以將起獨立成為一個具備公用性質的函數,放置于一個公用模塊中。
7、頁寬
頁寬應該設置為80字符。源代碼一般不會超過這個寬度, 并導致無法完整顯示, 但這一設置也可以靈活調整. 在任何情況下, 超長的語句應該在一個逗號或者一個操作符后折行. 一條語句折行后, 應該比原來的語句再縮進2個字符.
8、行數
一般的集成編程環境下,每屏大概只能顯示不超過50行的程序,所以這個函數大概要5-6屏顯示,在某些環境下要8屏左右才能顯示完。這樣一來,無論是讀程序還是修改程序,都會有困難。因此建議把完成比較獨立功能的程序塊抽出,單獨成為一個函數。把完成相同或相近功能的程序塊抽出,獨立為一個子函數??梢园l現,越是上層的函數越簡單,就是調用幾個子函數,越是底層的函數完成的越是具體的工作。這是好程序的一個標志。這樣,我們就可以在較上層函數里容易控制整個程序的邏輯,而在底層的函數里專注于某方面的功能的實現了。
三、代碼文件風格
所有的 Java(*.java) 文件都必須遵守如下的樣式規則:
. 文件生成
對于規范的 JAVA 派生類,盡量用 JBuilder 的 Object Gallery 工具來生成文件格式,避免用手工制作的頭文件/實現文件。
. package/import
package 行要在 import 行之前,import 中標準的包名要在本地的包名之前,而且按照字母順序排列。如果 import 行中包含了同一個包中的不同子目錄,則應該用 * 來處理。
package hotlava.net.stats;
import java.io.*;
import java.util.Observable;
import hotlava.util.Application;
這里 java.io.* 使用來代替InputStream and OutputStream 的。
. 文件頭部注釋
文件頭部注釋主要是表明該文件的一些信息,是程序的總體說明,可以增強程序的可讀性和可維護性。文件頭部注釋一般位于 package/imports 語句之后,Class 描述之前。要求至少寫出文件名、創建者、創建時間和內容描述。JBuilder 的 Object Gallery 工具生成的代碼中會在類、工程文件中等自動添加注釋,我們也要添加一些注釋,其格式應該盡量約束如下:
/**
* Title: 確定鼠標位置類
* Description: 確定鼠標當前在哪個作業欄位中并返回作業號
* @Copyright: Copyright (c) 2002
* @Company: HIT
* @author: rivershan
* @version: 1.0
* @time: 2002.10.30
*/
. Class
接下來的是類的注釋,一般是用來解釋類的。
/**
* A class representing a set of packet and byte counters
* It is observable to allow it to be watched, but only
* reports changes when the current set is complete
*/
接下來是類定義,包含了在不同的行的 extends 和 implements
public class CounterSet
extends Observable
implements Cloneable
.Class Fields
接下來是類的成員變量:
/**
* Packet counters
*/
protected int[] packets;
public 的成員變量必須生成文檔(JavaDoc)。proceted、private和 package 定義的成員變量如果名字含義明確的話,可以沒有注釋。
. 存取方法
接下來是類變量的存取的方法。它只是簡單的用來將類的變量賦值獲取值的話,可以簡單的寫在一行上。(個人認為盡量分行寫)
/**
* Get the counters
* @return an array containing the statistical data. This array has been
* freshly allocated and can be modified by the caller.
*/
public int[] getPackets()
{
return copyArray(packets, offset);
}
public int[] getBytes()
{
return copyArray(bytes, offset);
}
public int[] getPackets()
{
return packets;
}
public void setPackets(int[] packets)
{
this.packets = packets;
}
其它的方法不要寫在一行上
. 構造函數
接下來是構造函數,它應該用遞增的方式寫(比如:參數多的寫在后面)。
訪問類型("public","private" 等.)和任何"static","final"或"synchronized"應該在一行中,并且方法和參數另寫一行,這樣可以使方法和參數更易讀。
public
CounterSet(int size)
{
this.size = size;
}
. 克隆方法
如果這個類是可以被克隆的,那么下一步就是 clone 方法:
public
Object clone()
{
try
{
CounterSet obj = (CounterSet)super.clone();
obj.packets = (int[])packets.clone();
obj.size = size;
return obj;
}
catch(CloneNotSupportedException e)
{
throw new InternalError("Unexpected CloneNotSUpportedException: "
+ e.getMessage());
}
}
. 類方法
下面開始寫類的方法:
/**
* Set the packet counters
* (such as when restoring from a database)
*/
protected final
void setArray(int[] r1, int[] r2, int[] r3, int[] r4)
throws IllegalArgumentException
{
//
// Ensure the arrays are of equal size
//
if (r1.length != r2.length || r1.length != r3.length || r1.length != r4.length)
throw new IllegalArgumentException("Arrays must be of the same size");
System.arraycopy(r1, 0, r3, 0, r1.length);
System.arraycopy(r2, 0, r4, 0, r1.length);
}
. toString 方法
無論如何,每一個類都應該定義 toString 方法:
public
String toString()
{
String retval = "CounterSet: ";
for (int i = 0; i < data.length(); i++)
{
retval += data.bytes.toString();
retval += data.packets.toString();
}
return retval;
}
. main 方法
如果main(String[]) 方法已經定義了, 那么它應該寫在類的底部.
四、函數編寫風格
. 函數的命名
通常,函數的命名也是以能表達函數的動作意義為原則的,一般是由動詞打頭,然后跟上表示動作對象的名詞,各單詞的首字母應該大寫。另外,還有一些函數命名的通用規則。如取數,則用Get打頭,然后跟上要取的對象的名字;設置數,則用Set打頭,然后跟上要設的對象的名字;而對象中為了響應消息進行動作的函數,可以命名為On打頭,然后是相應的消息的名稱;進行主動動作的函數,可以命名為Do打頭,然后是相應的動作名稱。類似的規則還有很多,需要程序員多讀優秀的程序,逐漸積累經驗,才能作出好的函數命名。
. 函數注釋
系統自動生成的函數,如鼠標動作響應函數等,不必太多的注釋和解釋;
對于自行編寫的函數,若是系統關鍵函數,則必須在函數實現部分的上方標明該函數的信息,格式如下:
/**
* 函數名:
* 編寫者:
* 參考資料:
* 功 能:
* 輸入參數:
* 輸出參數:
* 備 注:
*/
希望盡量遵循以上格式。
五、符號風格
. 總體要求
對于各種符號的定義,都有一個共通點,就是應該使用有實際意義的英文單詞或英文單詞的縮寫,不要使用簡單但沒有意義的字串,盡可能不使用阿拉伯數字,更切忌使用中文拼音的首字母。如這樣的名稱是不提倡的:Value1,Value2,Value3,Value4 …。
例如:
file(文件),code(編號),data(數據),pagepoint(頁面指針), faxcode(傳真號) ,address(地址),bank(開戶銀行),……
. 變量名稱
變量命名由(前綴+修飾語)構成?,F在比較流行的是一套由微軟的一個匈牙利軟件工程師首先使用,并且在微軟推廣開來,現在被稱之為匈牙利命名法的命名規則。匈牙利命名法規定,使用表示標識符所對應的變量類型的英文小寫縮寫作為標識符的前綴,后面在使用表示變量意義的英文單詞或縮寫進行命名。下面是匈牙利命名法中的一些命名方式:
(1)生存期修飾:用l(local)表示局域變量,p(public)表示全局變量,s(send)表示參數變量
(2)類型修飾:用s(AnsiString)表示字符串,c(char)表示字符,n(number)數值,i(intger)表示整數,d(double)表示雙精度,f(float)浮點型,b(bool)布爾型,d(date)表示日期型.
例如:
li_length表示整形的局域變量,是用來標識長度的.ls_code表示字符型的局域變量,用來標識代碼.
. 控件名稱
控件命名由(前綴+修飾語)構成。前綴即為控件的名稱。
按鈕變量 Button+Xxxxxxx 例如:ButtonSave,ButtonExit,ButtonPrint等
題標變量 Label+Xxxxxxxx 例如:LabelName,LabelSex等
數據表變量 Table+Xxxxxx 例如:TableFile,TableCount等
查詢變量 Query+Xxxxxx 例如:QueryFile,QueryCeneter等
數據源變量 DataSource+Xxx 例如:DataSourceFile,DataSourceCenter等
。。。。。。。。。。。。。。。。
(注:對于與表有關的控件“修飾語”部分最好直接用表名。)
. Package 的命名
Package 的名字應該都是由一個小寫單詞組成。
. Class 的命名
Class 的名字必須由一個或數個能表達該類的意思的大寫字母開頭而其它字母都小寫的單詞或縮寫組成,這樣能使這個 Class 的名稱能更容易被理解。
. Class 變量的命名
變量的名字必須用一個小寫字母開頭。后面的單詞用大寫字母開頭。對于類的成員變量,在對其標識符命名時,要加上代表member(成員)的前綴m_。例如一個標識符為m_dwFlag,則它表示的變量是一個類型為雙字的成員變量,它是代表一個標志。
. Static Final 變量的命名
Static Final 變量的名字應該都大寫,并且指出完整含義。
. 參數的命名
參數的名字必須和變量的命名規范一致。
. 數組的命名
數組應該總是用下面的方式來命名:
byte[] buffer;
而不是:
byte buffer[];
. 方法的參數
使用有意義的參數命名,如果可能的話,使用和要賦值的字段一樣的名字:
SetCounter(int size)
{
this.size = size;
}
. 神秘的數
首先要說什么是神秘的數。我們在程序里經常會用到一些量,它是有特定的含義的。例如,現在我們寫一個薪金統計程序,公司員工有50人,我們在程序里就會用50這個數去進行各種各樣的運算。在這里,50就是"神秘的數"。為什么稱它為神秘呢?因為別的程序員在程序里看到50這個數,不知道它的含義,只能靠猜了。
在程序里出現"神秘的數"會降低程序的可讀性,應該盡量避免。避免的方法是把神秘的數定義為一個常量。注意這個常量的命名應該能表達該數的意義,并且應該全部大寫,以與對應于變量的標識符區別開來。例如上面50這個數,我們可以定義為一個名為NUMOFEMPLOYEES的常量來代替。這樣,別的程序員在讀程序的時候就可以容易理解了。
六、程序編寫風格
. exit()
exit 除了在 main 中可以被調用外,其他的地方不應該調用。因為這樣做不給任何代碼代碼機會來截獲退出。一個類似后臺服務地程序不應該因為某一個庫模塊決定了要退出就退出。
. 異常
申明的錯誤應該拋出一個RuntimeException或者派生的異常。
頂層的main()函數應該截獲所有的異常,并且打印(或者記錄在日志中)在屏幕上。
. 垃圾收集
JAVA使用成熟的后臺垃圾收集技術來代替引用計數。但是這樣會導致一個問題:你必須在使用完對象的實例以后進行清場工作。比如一個prel的程序員可能這么寫:
...
{
FileOutputStream fos = new FileOutputStream(projectFile);
project.save(fos, "IDE Project File");
}
...
除非輸出流一出作用域就關閉,非引用計數的程序語言,比如JAVA,是不能自動完成變量的清場工作的。必須象下面一樣寫:
FileOutputStream fos = new FileOutputStream(projectFile);
project.save(fos, "IDE Project File");
fos.close();
. Clone
下面是一種有用的方法:
implements Cloneable
public
Object clone()
{
try
{
ThisClass obj = (ThisClass)super.clone();
obj.field1 = (int[])field1.clone();
obj.field2 = field2;
return obj;
}
catch(CloneNotSupportedException e)
{
throw new InternalError("Unexpected CloneNotSUpportedException: " + e.getMessage());
}
}
. final 類
絕對不要因為性能的原因將類定義為 final 的(除非程序的框架要求)
如果一個類還沒有準備好被繼承,最好在類文檔中注明,而不要將她定義為 final 的。這是因為沒有人可以保證會不會由于什么原因需要繼承她。
. 訪問類的成員變量
大部分的類成員變量應該定義為 protected 的來防止繼承類使用他們。
注意,要用"int[] packets",而不是"int packets[]",后一種永遠也不要用。
public void setPackets(int[] packets)
{
this.packets = packets;
}
CounterSet(int size)
{
this.size = size;
}
. byte 數組轉換到 characters
為了將 byte 數組轉換到 characters,你可以這么做:
"Hello world!".getBytes();
. Utility 類
Utility 類(僅僅提供方法的類)應該被申明為抽象的來防止被繼承或被初始化。
. 初始化
下面的代碼是一種很好的初始化數組的方法:
objectArguments = new Object[]
{
arguments
};
. 枚舉類型
JAVA 對枚舉的支持不好,但是下面的代碼是一種很有用的模板:
class Colour
{
public static final Colour BLACK = new Colour(0, 0, 0);
public static final Colour RED = new Colour(0xFF, 0, 0);
public static final Colour GREEN = new Colour(0, 0xFF, 0);
public static final Colour BLUE = new Colour(0, 0, 0xFF);
public static final Colour WHITE = new Colour(0xFF, 0xFF, 0xFF);
}
這種技術實現了RED, GREEN, BLUE 等可以象其他語言的枚舉類型一樣使用的常量。 他們可以用 '==' 操作符來比較。
但是這樣使用有一個缺陷:如果一個用戶用這樣的方法來創建顏色 BLACK
new Colour(0,0,0)
那么這就是另外一個對象,'=='操作符就會產生錯誤。她的 equal() 方法仍然有效。由于這個原因,這個技術的缺陷最好注明在文檔中,或者只在自己的包中使用。
. 混合使用 AWT 和 Swing 組件
如果要將 AWT 組件和 Swing 組件混合起來使用的話,請小心使用。實際上,盡量不要將他們混合起來使用。
. 滾動的 AWT 組件
AWT 組件絕對不要用 JscrollPane 類來實現滾動。滾動 AWT 組件的時候一定要用 AWT ScrollPane 組件來實現。
. 避免在 InternalFrame 組件中使用 AWT 組件
盡量不要這么做,要不然會出現不可預料的后果。
. Z-Order 問題
AWT 組件總是顯示在 Swing 組件之上。當使用包含 AWT 組件的 POP-UP 菜單的時候要小心,盡量不要這樣使用。
八、性能
在寫代碼的時候,從頭至尾都應該考慮性能問題。這不是說時間都應該浪費在優化代碼上,而是我們時刻應該提醒自己要注意代碼的效率。比如:如果沒有時間來實現一個高效的算法,那么我們應該在文檔中記錄下來,以便在以后有空的時候再來實現她。
不是所有的人都同意在寫代碼的時候應該優化性能這個觀點的,他們認為性能優化的問題應該在項目的后期再去考慮,也就是在程序的輪廓已經實現了以后。
. 不必要的對象構造
不要在循環中構造和釋放對象
. 使用 StringBuffer 對象
在處理 String 的時候要盡量使用 StringBuffer 類,StringBuffer 類是構成 String 類的基礎。String 類將 StringBuffer 類封裝了起來,(以花費更多時間為代價)為開發人員提供了一個安全的接口。當我們在構造字符串的時候,我們應該用 StringBuffer 來實現大部分的工作,當工作完成后將 StringBuffer 對象再轉換為需要的 String 對象。比如:如果有一個字符串必須不斷地在其后添加許多字符來完成構造,那么我們應該使用 StringBuffer 對象和她的 append() 方法。如果我們用 String 對象代替 StringBuffer 對象的話,會花費許多不必要的創建和釋放對象的 CPU 時間。
. 避免太多的使用 synchronized 關鍵字
避免不必要的使用關鍵字 synchronized,應該在必要的時候再使用她,這是一個避免死鎖的好方法。
rivershan 原創
在你的程序里使用hibernate必須有下面幾個步驟:
1、建立一個Hibernate configuration對象
2、使用Hibernate configuration對象來建立一個Hibernate factory對象。
3、使用Hibernate factory對象來建立一個Hibernate session對象。
4、使用Hibernate session對象來開始一個事務(可選)
5、使用Hibernate session對象來建立、讀取、更新、刪除數據庫里的數據
6、提交事務(可選)
7、關閉session
Hibernate最佳實踐是建立和緩存Hibernate factory來提高性能。所以我們最好在第一步和第二步建立一
個Struts plug-in 來在servlet context中緩存Hibernate factory。如List5所示:
Hibernate是一個功能非常強大的產品,還有一些未知的功能留給你們去發現。我們簡單的例子只是關于
讀這個行為,但是CRUD里的其它功能也是一樣的簡單。功能性的更新和讀取指定對象一樣簡單,調用
JavaBean setter,調用session的commit方法。Hibernate負責幫你生成SQL語句并且更新數據庫。一個刪
除也是非常的簡單—session.delete(element)便是所有要做的!最后建立只是需要初始化對象,調用
setters方法,然后調用session.save(element)。
Hibernate最佳實踐推薦緩存Hibernate factory對象。我們選擇通過Struts plug-in來建立并且緩存
factory。你也可以選擇使用其它方法在你的類里緩存它。
雖然這個摘錄能很好的滿足你的需要,它還有其它的一些缺點。第一,我們在Struts Action里使用了
Hibernate。遷移到其它的持久層框架上便將需要我們改變每個使用Hibernate的Action。第二,我們的持
久層緊密的與表示層連接。這種關聯使我們在其它表示層機制中沒有重新使用持久層邏輯的機會,例如批
處理程序。
雖然有許多改進的空間,當你不需要重用表現層的時候,這個摘錄還是很適合的。
3.應盡量避免在 where 子句中使用!=或<>操作符,否則將引擎放棄使用索引而進行全表掃描。
4.應盡量避免在 where 子句中使用 or 來連接條件,否則將導致引擎放棄使用索引而進行全表掃描,如:
select id from t where num=10 or num=20
可以這樣查詢:
select id from t where num=10
union all
select id from t where num=20
5.in 和 not in 也要慎用,否則會導致全表掃描,如:
select id from t where num in(1,2,3)
對于連續的數值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
6.下面的查詢也將導致全表掃描:
select id from t where name like '%abc%'
若要提高效率,可以考慮全文檢索。
7.如果在 where 子句中使用參數,也會導致全表掃描。因為SQL只有在運行時才會解析局部變量,但優化程序不能將訪問計劃的選擇推遲到運行時;它必須在編譯時進行選擇。然而,如果在編譯時建立訪問計劃,變量的值還是未知的,因而無法作為索引選擇的輸入項。如下面語句將進行全表掃描:
select id from t where num=@num
可以改為強制查詢使用索引:
select id from t with(index(索引名)) where num=@num
8.應盡量避免在 where 子句中對字段進行表達式操作,這將導致引擎放棄使用索引而進行全表掃描。如:
select id from t where num/2=100
應改為:
select id from t where num=100*2
9.應盡量避免在where子句中對字段進行函數操作,這將導致引擎放棄使用索引而進行全表掃描。如:
select id from t where substring(name,1,3)='abc'--name以abc開頭的id
select id from t where datediff(day,createdate,'2005-11-30')=0--‘2005-11-30’生成的id
應改為:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
10.不要在 where 子句中的“=”左邊進行函數、算術運算或其他表達式運算,否則系統將可能無法正確使用索引。
11.在使用索引字段作為條件時,如果該索引是復合索引,那么必須使用到該索引中的第一個字段作為條件時才能保證系統使用該索引,否則該索引將不會被使用,并且應盡可能的讓字段順序與索引順序相一致。
12.不要寫一些沒有意義的查詢,如需要生成一個空表結構:
select col1,col2 into #t from t where 1=0
這類代碼不會返回任何結果集,但是會消耗系統資源的,應改成這樣:
create table #t(...)
13.很多時候用 exists 代替 in 是一個好的選擇:
select num from a where num in(select num from b)
用下面的語句替換:
select num from a where exists(select 1 from b where num=a.num)
14.并不是所有索引對查詢都有效,SQL是根據表中數據來進行查詢優化的,當索引列有大量數據重復時,SQL查詢可能不會去利用索引,如一表中有字段sex,male、female幾乎各一半,那么即使在sex上建了索引也對查詢效率起不了作用。
15.索引并不是越多越好,索引固然可以提高相應的 select 的效率,但同時也降低了 insert 及 update 的效率,因為 insert 或 update 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數最好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有必要。
16.應盡可能的避免更新 clustered 索引數據列,因為 clustered 索引數據列的順序就是表記錄的物理存儲順序,一旦該列值改變將導致整個表記錄的順序的調整,會耗費相當大的資源。若應用系統需要頻繁更新 clustered 索引數據列,那么需要考慮是否應將該索引建為 clustered 索引。
17.盡量使用數字型字段,若只含數值信息的字段盡量不要設計為字符型,這會降低查詢和連接的性能,并會增加存儲開銷。這是因為引擎在處理查詢和連接時會逐個比較字符串中每一個字符,而對于數字型而言只需要比較一次就夠了。
18.盡可能的使用 varchar/nvarchar 代替 char/nchar ,因為首先變長字段存儲空間小,可以節省存儲空間,其次對于查詢來說,在一個相對較小的字段內搜索效率顯然要高些。
19.任何地方都不要使用 select * from t ,用具體的字段列表代替“*”,不要返回用不到的任何字段。
20.盡量使用表變量來代替臨時表。如果表變量包含大量數據,請注意索引非常有限(只有主鍵索引)。
21.避免頻繁創建和刪除臨時表,以減少系統表資源的消耗。
22.臨時表并不是不可使用,適當地使用它們可以使某些例程更有效,例如,當需要重復引用大型表或常用表中的某個數據集時。但是,對于一次性事件,最好使用導出表。
23.在新建臨時表時,如果一次性插入數據量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果數據量不大,為了緩和系統表的資源,應先create table,然后insert。
24.如果使用到了臨時表,在存儲過程的最后務必將所有的臨時表顯式刪除,先 truncate table ,然后 drop table ,這樣可以避免系統表的較長時間鎖定。
25.盡量避免使用游標,因為游標的效率較差,如果游標操作的數據超過1萬行,那么就應該考慮改寫。
26.使用基于游標的方法或臨時表方法之前,應先尋找基于集的解決方案來解決問題,基于集的方法通常更有效。
27.與臨時表一樣,游標并不是不可使用。對小型數據集使用 FAST_FORWARD 游標通常要優于其他逐行處理方法,尤其是在必須引用幾個表才能獲得所需的數據時。在結果集中包括“合計”的例程通常要比使用游標執行的速度快。如果開發時間允許,基于游標的方法和基于集的方法都可以嘗試一下,看哪一種方法的效果更好。
28.在所有的存儲過程和觸發器的開始處設置 SET NOCOUNT ON ,在結束時設置 SET NOCOUNT OFF 。無需在執行存儲過程和觸發器的每個語句后向客戶端發送 DONE_IN_PROC 消息。
29.盡量避免大事務操作,提高系統并發能力。
30.盡量避免向客戶端返回大數據量,若數據量過大,應該考慮相應需求是否合理。
JAVA的容器---List,Map,Set
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
Collection接口
Collection是最基本的集合接口,一個Collection代表一組Object,即Collection的元素(Elements)。一些 Collection允許相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接繼承自Collection的類,Java SDK提供的類都是繼承自Collection的“子接口”如List和Set。
所有實現Collection接口的類都必須提供兩個標準的構造函數:無參數的構造函數用于創建一個空的Collection,有一個 Collection參數的構造函數用于創建一個新的Collection,這個新的Collection與傳入的Collection有相同的元素。后一個構造函數允許用戶復制一個Collection。
如何遍歷Collection中的每一個元素?不論Collection的實際類型如何,它都支持一個iterator()的方法,該方法返回一個迭代子,使用該迭代子即可逐一訪問Collection中每一個元素。典型的用法如下:
Iterator it = collection.iterator(); // 獲得一個迭代子
while(it.hasNext()) {
Object obj = it.next(); // 得到下一個元素
}
由Collection接口派生的兩個接口是List和Set。
List接口
List是有序的Collection,使用此接口能夠精確的控制每個元素插入的位置。用戶能夠使用索引(元素在List中的位置,類似于數組下標)來訪問List中的元素,這類似于Java的數組。
和下面要提到的Set不同,List允許有相同的元素。
除了具有Collection接口必備的iterator()方法外,List還提供一個listIterator()方法,返回一個 ListIterator接口,和標準的Iterator接口相比,ListIterator多了一些add()之類的方法,允許添加,刪除,設定元素,還能向前或向后遍歷。
實現List接口的常用類有LinkedList,ArrayList,Vector和Stack。
LinkedList類
LinkedList實現了List接口,允許null元素。此外LinkedList提供額外的get,remove,insert方法在 LinkedList的首部或尾部。這些操作使LinkedList可被用作堆棧(stack),隊列(queue)或雙向隊列(deque)。
注意LinkedList沒有同步方法。如果多個線程同時訪問一個List,則必須自己實現訪問同步。一種解決方法是在創建List時構造一個同步的List:
List list = Collections.synchronizedList(new LinkedList(...));
ArrayList類
ArrayList實現了可變大小的數組。它允許所有元素,包括null。ArrayList沒有同步。
size,isEmpty,get,set方法運行時間為常數。但是add方法開銷為分攤的常數,添加n個元素需要O(n)的時間。其他的方法運行時間為線性。
每個ArrayList實例都有一個容量(Capacity),即用于存儲元素的數組的大小。這個容量可隨著不斷添加新元素而自動增加,但是增長算法并沒有定義。當需要插入大量元素時,在插入前可以調用ensureCapacity方法來增加ArrayList的容量以提高插入效率。
和LinkedList一樣,ArrayList也是非同步的(unsynchronized)。
Vector類
Vector非常類似ArrayList,但是Vector是同步的。由Vector創建的Iterator,雖然和ArrayList創建的 Iterator是同一接口,但是,因為Vector是同步的,當一個Iterator被創建而且正在被使用,另一個線程改變了Vector的狀態(例如,添加或刪除了一些元素),這時調用Iterator的方法時將拋出ConcurrentModificationException,因此必須捕獲該異常。
Stack 類
Stack繼承自Vector,實現一個后進先出的堆棧。Stack提供5個額外的方法使得Vector得以被當作堆棧使用。基本的push和pop 方法,還有peek方法得到棧頂的元素,empty方法測試堆棧是否為空,search方法檢測一個元素在堆棧中的位置。Stack剛創建后是空棧。
Set接口
Set是一種不包含重復的元素的Collection,即任意的兩個元素e1和e2都有e1.equals(e2)=false,Set最多有一個null元素。
很明顯,Set的構造函數有一個約束條件,傳入的Collection參數不能包含重復的元素。
請注意:必須小心操作可變對象(Mutable Object)。如果一個Set中的可變元素改變了自身狀態導致Object.equals(Object)=true將導致一些問題。
Map接口
請注意,Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個 value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。
Hashtable類
Hashtable繼承Map接口,實現一個key-value映射的哈希表。任何非空(non-null)的對象都可作為key或者value。
添加數據使用put(key, value),取出數據使用get(key),這兩個基本操作的時間開銷為常數。
Hashtable通過initial capacity和load factor兩個參數調整性能。通常缺省的load factor 0.75較好地實現了時間和空間的均衡。增大load factor可以節省空間但相應的查找時間將增大,這會影響像get和put這樣的操作。
使用Hashtable的簡單示例如下,將1,2,3放到Hashtable中,他們的key分別是”one”,”two”,”three”:
Hashtable numbers = new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));
要取出一個數,比如2,用相應的key:
Integer n = (Integer)numbers.get(“two”);
System.out.println(“two = ” + n);
由于作為key的對象將通過計算其散列函數來確定與之對應的value的位置,因此任何作為key的對象都必須實現hashCode和equals方法。hashCode和equals方法繼承自根類Object,如果你用自定義的類當作key的話,要相當小心,按照散列函數的定義,如果兩個對象相同,即obj1.equals(obj2)=true,則它們的hashCode必須相同,但如果兩個對象不同,則它們的hashCode不一定不同,如果兩個不同對象的hashCode相同,這種現象稱為沖突,沖突會導致操作哈希表的時間開銷增大,所以盡量定義好的hashCode()方法,能加快哈希表的操作。
如果相同的對象有不同的hashCode,對哈希表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時復寫equals方法和hashCode方法,而不要只寫其中一個。
Hashtable是同步的。
HashMap類
HashMap和Hashtable類似,不同之處在于HashMap是非同步的,并且允許null,即null value和null key。,但是將HashMap視為Collection時(values()方法可返回Collection),其迭代子操作時間開銷和HashMap 的容量成比例。因此,如果迭代操作的性能相當重要的話,不要將HashMap的初始化容量設得過高,或者load factor過低。
WeakHashMap類
WeakHashMap是一種改進的HashMap,它對key實行“弱引用”,如果一個key不再被外部所引用,那么該key可以被GC回收。
總結
如果涉及到堆棧,隊列等操作,應該考慮用List,對于需要快速插入,刪除元素,應該使用LinkedList,如果需要快速隨機訪問元素,應該使用ArrayList。
如果程序在單線程環境中,或者訪問僅僅在一個線程中進行,考慮非同步的類,其效率較高,如果多個線程可能同時操作一個類,應該使用同步的類。
要特別注意對哈希表的操作,作為key的對象要正確復寫equals和hashCode方法。
盡量返回接口而非實際的類型,如返回List而非ArrayList,這樣如果以后需要將ArrayList換成LinkedList時,客戶端代碼不用改變。這就是針對抽象編程。