細微的收獲
寫在前面
學習可以從模仿開始:如果對如何構建工程化代碼沒有頭緒,看看前人流傳下來的優秀工程代碼,試着模仿也許會有幫助。
jdk中的開源代碼就很值得閱讀,如果你正在使用eclipse且安裝了帶源碼版本的jdk,只需按住ctrl鍵,並點擊editor內某個類的聲明類名,即可查閱源代碼(對於自己構建的代碼同理)。
比如,在editor中有如下代碼:
String str = new String("test");
按住ctrl鍵:點擊前一個String將會打開String類的源代碼文件;點擊后一個String將打開文件,並跳轉到對應的構造方法。
下面列舉一些我認為目前值得學習的基礎類和其中的方法。
1. String
String 是一個final類,不可被繼承。
String類中主要維護了一個private final char value[],在一個String類被構造后這一數組的值就不會再改變。
| 方法 | 說明 | 關注 |
|---|---|---|
| public boolean equals(Object anObject) | 重寫了equals方法 |
判斷相等的條件是什么; 什么地方體現了“低耦合”思想; instanceof 的用法。 |
| public int compareTo(String anotherString) | 比較兩個字符串的序關系 | 判斷序關系的標准是什么;為什么要申明c1和c2。 |
| public String toLowerCase(Locale locale) | 將字符串轉換為小寫 | 什么是Local;String如何支持多國語言。 |
接觸JAVA后的一個感想是String的+太方便了。但是在瀏覽了所有String中的源代碼后,我也沒有發現對+的重載操作。查閱相關資料得知JAVA不支持運算符重載,對String類型+的“重載”是在編譯器中完成的。這樣的處理方式讓String類型對象有一定的特殊地位。(TODO:查閱OpenJDK javac 源代碼)
2. Integer
Integer類型是基礎類型int的包裝類。查看源代碼可以得知Integer的最大值和最小值。
| 方法 | 說明 | 關注 |
|---|---|---|
| public static int parseInt(String s, int radix) | 將String轉換為Integer類型 |
什么地方體現了“低耦合”思想;聲明為類的靜態方法有什么好處;如何使用thow。 |
| private static class IntegerCache | 緩沖小數據以節省內存開銷 | 這樣的處理方式有什么好處;將導致什么(考慮==) |
3.Vector
Vector(java.util.Vector) 在傳說中,是多線程安全的。翻閱代碼也可以發現其中基本所有操作方法都加上了synchronized標記,這個標記保證了在某一線程在執行該對象的這一方法的整個流程中不會有別的線程訪問這一方法用到的對象。
那么有這樣一個問題:
class RequestList {
private Vector<Request> requestList;
public boolean add(Request rq) {
int requestListLenth = requestList.size(); //獲取請求列表的長度
if(requestListLenth < 10) { //如果請求列表長度小於10
requestList.add(rq); //則在列表尾部添加
return true;
}
else {
return false;
}
}
}
RequestList中使用了多線程安全的Vector,而其中的add方法也使用了Vector中的多線程安全的方法size()和add(),那么add方法也是多線程安全的嗎?
synchronized標識符能使其所作用的代碼段(或方法)在多線程操作中“原子化”。“原子操作”在多線程中是安全的,不會被其他進程影響結果的。——佚名
這里先給出結論,以上的代碼在多線程運行過程中requestList的長度可能大於10。
詳細的閱讀筆記在另一篇博客中更新:經典研讀 JDK
第一次作業
第一次作業主要實現了多項式的加減操作。
類圖

其中各個方法都調用了Log類的靜態方法(圖中省略了與Log的關系),用傳遞枚舉變量的方式傳遞錯誤信息,這種方法的擴展性差,之后應該改進,考慮使用thow來傳遞錯誤信息。
其中主要的聚合關系是:Term -> Poly -> ComputePoly。這是處理問題的主要框架。
代碼質量分析
從類圖中就可以看出Term類下的方法太多了,其中的很多方法沒有存在的必要。在構建代碼時,我的考慮是所構建的類應該更具普遍性,而非單純完成這次作業,導致代碼量激增。事后看來完全沒有這個必要。
在保證代碼沒有太大問題的情況下 ,代碼量大不容易被Hack。——某OS助教
metric

可以看到總體質量一般。其中Cyclomatic complexity偏高:Logging類是用來處理控制台日志的,其中使用switch case來判定日志類型,導致復雜度升高。之后可以考慮在需要記錄日志的代碼段直接提供文本信息,而不使用枚舉類來管理。
Debug
測試方法
- 靜查
靜查包括了對主體業務邏輯,特殊情況處理等的審查。 - 細化bug樹構建測試樣例
細化bug樹是一項有技巧但是回報較高的工作,比如:“多逗號”一項,可以繼續細化,如下圖所示。

本人代碼
由於每個方法的代碼量都不大,在構建的過程中不太容易出現邏輯漏洞,在測試后沒有發現bug。
對方代碼
- 輸入:所測試的代碼使用類似狀態機的方法來判定輸入的合法性並解析指令中的相關信息。總體代碼量大,靜查過於耗費精力,在粗略檢查控制台輸入部分的代碼后,我開始細化bug樹構建測試用例,最終發現了一些細節問題。
- 主體邏輯:這部分的代碼量比較大,我細化bug樹構建測試用例找到了細節問題。
- 輸出:邏輯清晰簡單,沒有與Readme不符的地方。
總結來看,在此次測試代碼中單個方法分支多,邏輯復雜的部分比較容易出現bug,此時可以使用bug樹進行測試。
第二次作業
第二次作業主要實現了一部功能簡單的電梯。
類圖

同樣,其中幾乎每個類都調用了Logger的靜態方法(圖中省略了與Logger的關系),這種調用方式的可擴展性太差。其余架構設計與推薦架構沒有太大區別。
代碼質量分析
metric

其中代碼圈復雜度較高的是Floor.parseRequest()函數:
public static Request parseRequset(String str) {
String pattern = "\\(FR,([+]?[0-9]{1,}),([A-Z]{1,}),([+]?[0-9]{1,})\\)";
Pattern reg = Pattern.compile(pattern);
ButtonType bt;
int floor;
long timeStamp;
try {
Matcher m = reg.matcher(str);
if(m.find()) {
floor = Integer.parseInt(m.group(1));//throw
if(floor>this.maxFloorNO
||floor<this.minFloorNO)return null;
if(m.group(2).equals("DOWN"))bt = ButtonType.DOWN;
else bt = ButtonType.UP;
if(floor == this.minFloorNO && bt.equals(ButtonType.DOWN)
||floor == this.maxFloorNO && bt.equals(ButtonType.UP)) return null;
timeStamp = Long.parseLong(m.group(3));//throw
if(timeStamp>this.maxTimeStamp) return null;
return new Request(RequestType.FLOOR, floor, bt, timeStamp);
}else return null;
}catch(Throwable e) {
return null;
}
}
其中的驗證的邏輯分支數較多,可以考慮用別的函數檢驗,簡化邏輯。
Debug
本人代碼
未發現bug。
對方代碼
對此次得到的測試代碼使用使用與第一次相同的方式測試,沒有找到漏洞。但是發現了一處會導致crash的寫法:
String str=s.nextLine();
////////////
int count=0;
while(!str.equals("RUN")) {
//////////////////
count++;
if(count>=100) {
break;
}
else {
str=s.nextLine();
}
}
s.close();
由於測試中要求不測試終止符,故上述代碼不會出錯,但是s.nextLine()在遇到終止符時會拋出異常,應當有相應的處理辦法。
第三次作業
在第二次作業的基礎上使用了新的調度規則。細節復雜度有較的提升。
類圖

代碼質量分析
metric

其中調度代碼的邏輯比較復雜,在規划優秀架構和短時間完成開發的取舍問題上,在這次的作業中我稍稍偏向於短時間完成開發對架構的設計沒有投入足夠的精力。
Debug
本人代碼
未發現bug。
對方代碼
- 靜態數組:在動態場景下使用靜態數組往往在邊界處理的地方比較容易出錯。
//class RequestList
RequestList() {
RList = new Request[1000];
head = 0;
tail = -1;
}
///void Main
Scanner s = new Scanner(System.in);
String str = "";
int count = 0;
while (s.hasNextLine()) {
/////
if (req.destination == -1) {
////////
}
else{
////////
count++;
flag = true;
}
if (count > 1000) {
System.out.println("INVALID: we can only handle 1000 requests");
return;
}
}
這樣的寫法將產生數組訪問越界錯誤,恰好比數組結尾下標多出1。
2. 核心代碼復雜:實現主要業務邏輯的代碼太長,這里暫不引用。其中的圈復雜度高達113,在細化bug樹自動化構建測試集后,發現了兩個邏輯問題。
總結
從這三次作業來看,遇到的問題主要有:對Java語言的特性以及基礎知識不夠了解;核心邏輯不夠精簡,驗證性代碼和功能性代碼混雜;風格不夠成熟,擴展性,維護性不好。
有關風格我在面向對象先導課程——PART3中有所討論,這里補充一點,如果對抽象的Java開發手冊不太感冒,可以考慮閱讀優秀的源碼,不止在風格方面的學習,對語言特性,常用套路方面的學習也會有所幫助。所以才有了一開始的寫在前面。
