寒假作業(2/2)
| 這個作業屬於哪個課程 | 2021春軟件工程實踐S班 |
|---|---|
| 這個作業要求在哪里 | 作業要求 |
| 這個作業目標 | 深度閱讀構建之法;學會提問題;完成WordCount編程;了解學習單元測試 |
| 其他參考文獻 | 百度、github、CSDN、博客園 |
🚩PART①:閱讀《構建之法》並提問
《構建之法》3.2 軟件工程師的思維誤區
“不分主次,想解決所有依賴問題:想馬上動手解決所有主要問題和次要問題,而不是根據現有條件找到一個足夠好的方案”
疑問&思考🤔:
我們在編寫代碼的時候確實會遇到一個問題,但是因為代碼都是有相關聯性的,所以往往會牽扯出一鏈子的問題。那么這時候難道不是順着這條思路去改代碼嗎?什么叫做根據現有條件找到一個足夠好的方法呢?
就還是以作者舉的小飛的例子,小飛本來要去自習,發現自行車沒氣去借打氣筒,借打氣筒要送圍巾,就開始織圍巾。確實這個結果偏離了最開始的計划,或許最開始小飛發現自行車沒氣決定走路去自習是不是作者認為的足夠好的方法呢?
那么我的問題和思考歸結於是不是要評估這個問題所依賴的一鏈子問題是不是過於復雜?對於不復雜的例子就可以馬上動手順着思路去解決,對於復雜的問題就先放着,看看有沒有更快捷的方法?
《構建之法》4 兩人合作
疑問&思考🤔:
閱讀《構建之法》第一次知道結對編程,感覺結對編程這種形式對我來很新穎,兩個人一起寫一個代碼工作量直接減少一半欸:),但我覺得也有些局限。首先結對的兩個人水平相差怎么樣,一個人很牛一個人相對的菜。會不會導致牛人寫的時候,另一個人看的時候覺得“哇,牛,這個好,寫的好快,性能也好...”。等到這個人寫的時候,厲害的人在旁邊看的覺得這不行這不對,開始指導最后恨不得自己把鍵盤鼠標搶過來。那么對於結對編程的人是不是要有一個基本的水平要求?
《構建之法》9 項目經理
疑問&思考🤔:
通過對第九章的閱讀了解了PM,同時知道PM需要的能力很多。我在網絡上收集了一下PM需要的基本技能:
1.研發/測試 2.運營3.設計4.市場5.職能部門6.其他技能比如word、excel、ppt等基礎技能還要有思維、管理、溝通能力7.產品,熟悉高效的產品工具:Auxre、墨刀、Xmind、ProcessON7.多關注各網站和APP。多看行業報告和商業計划書,多看別人的產品。
那么PM是由程序員逐漸去往PM培養成長起來的呢還是一開始的職業目標就向PM方向發展?比如軟件工程的大學生發現對於寫代碼開發不怎么感冒,能不能就輕於寫代碼開發,而盡早去點亮作為PM的技能樹呢?
《構建之法》13 軟件測試
疑問&思考🤔:
書中認為測試人員測試的軟件功能100%符合要求,測試人員也都按照SPEC去測試。但是如果用戶恨你的軟件,那么就說明是測試人員的責任。我想請問的是這里是不是把測試人員的責任看的太大,首先我認同作者的好的測試人員要做易用性測試,去站在用戶的角度考慮,但是我也考慮到用戶在運用軟件發現有問題或者不好的地方,是不是說明這個軟件的需求分析之類的做的不夠好,而不是去指責測試的不到位。像易用性測試這樣的更像是一個附加的測試條件,有點像之前提到的Ad hoc Test。
比如之前的微信,我們很經常會用微信打開別人分享的鏈接,之后在鏈接里看到另一篇文章也不錯,再打開看看,然后就在微信里面開始循環的瀏覽信息。這個時候突然有一條微信進來,我要去看看。結果回復完信息之后找不到之前循環瀏覽的網頁了。這是我之前使用微信的一個痛點,但是驚喜的是后來微信增加了一個float window的功能,就解決了這個問題。請問這樣的是測試的責任嗎?
《構建之法》2 個人技術和流程
疑問&思考🤔:
這部分有涉及到單元測試,正好這次的實踐的作業WordCount里也有單元測試的部分,里面對於好的單元測試的標准對於這次實踐真的是很實用。但是里面提到單元測試應該是可重復的,用隨機數去單元測試不好,又提到也要用隨機數出去增加測試真實性,但不是在單元測試中,那請問是在什么時候用隨機數去測試呢?在這個部分我也有上網搜索,但是結果都是程序中含有隨機數生成器怎么進行單元測試,並沒有對於什么時候可以使用隨機數去測試的解答。但是也有以外的收獲,現在確實很多機器算法啟發式算法都又用到隨機數的生成,而我在網上搜索到別人的分享,比如說可以用樁對象或者是模擬對象。
冷知識
1946年,第一台電子計算機ENIAC在美國誕生,從此實際上一些最聰明,最有創造力的人開始進入這個行業,在他們身上形成了一種獨特的技術文化,這種文化的發展過程中涌現了很多“行話”。20世紀60年代初,麻省理工學院有一個學生團體叫做“鐵路模型技術俱樂部”(簡稱TMRC),他們把解決難題的方法稱為hack。
這里的hack有兩個意思,既可以指很巧妙的很便捷的解決方法(cool hack 或neat hack),也可以指比較笨拙,不那么優雅的解決方法(ugly hack 或 quick hack)。hack的字典意思是砍木頭,在這些學生看來,解決一個計算機難題就好像砍到一顆大樹。那么相應的,完成這種hack的過程就被成為hacking,而從事hacking的人就是hacker,也就是黑客。
這個詞被發明的時候,“黑客”完全是正面意義上的稱呼。TMRC使用這個詞是帶有敬意的,因為在他們看來,如果要完成一個hack,就必然包含着高度的革新、獨樹一幟的風格、精湛的技藝。最能干的人會自豪地稱自己為黑客。
真正的黑客致力於改變世界,讓世界運轉的更好。媒體對黑客的定義過於片面,而且影響了大眾對黑客的看法。而那些惡意入侵計算機系統的人應該被成為cracker(入侵者)。
參考來源
我認為我們要區分hacker和cracker的區別,其實我們現實當中都把cracker也定義為hacker,但是我認為真正的黑客是有黑客精神的,而不是一味的去進行破壞。黑客是實干家,是優秀的程序員,人活着的意義在於創造,那么黑客是激進的創造者。 我還了解到黑客的口頭禪是:
Talk is cheap, show me the code .
:)少廢話,放碼過來哈哈哈哈哈哈哈哈
真正的黑客一定是個優秀的程序員,他們追求分享,進步、計算機的自由使用。他們也理解原理,推崇技術,想方設法解決問題,這正是我們需要學習的地方。
🚩PART②:WordCount編程
項目地址
hannah-shaw /PersonalProject-Java
PSP表格
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | 30 | 43 |
| ·Estimate | ·估計這個任務需要多少時 | 30 | 43 |
| ·Development | ·開發 | 780 | 1030 |
| ·Analysis | ·需求分析(包括學習新技術) | 120 | 260 |
| ·Design Spec | ·生成設計文檔 | 30 | 60 |
| ·Design Review | ·設計復審 | 10 | 15 |
| ·Coding Standard | ·代碼規范 | 30 | 25 |
| ·Design | ·具體設計 | 90 | 110 |
| ·Coding | ·具體編碼 | 360 | 400 |
| ·Code Review | ·代碼復審 | 20 | 10 |
| ·Test | ·測試(自我測試,修改代碼,提交修改) | 120 | 150 |
| Reporting | 報告 | 135 | 145 |
| ·Test Repor | ·測試報告 | 90 | 100 |
| ·Size Measurement | ·計算工作量 | 15 | 10 |
| ·Postmortem & Process Improvement Plan | ·事后總結, 並提出過程改進計划 | 30 | 35 |
| 合計 | 1030 | 1217 |
解題思路描述
一、需求分解
首先根據作業要求,把大需求分解成以下小part
- 讀取文件內數據
- 統計文件字符數
- 統計文件單詞總數
- 統計文件有效行數
- 統計出現頻率最高的10個單詞
- 以UTF-8格式輸出到指定文件
二、讀取文件數據
用BufferedReader中包裝InputStreamReader類讀取數據再存儲到字符串,方便之后統計。
三、統計文件字符數
將讀取文件數據部分生成的字符串轉化為字符數組,然后按題目要求判斷是不是Ascii碼:32~126,空格,水平制表符,換行符,是的話就增加計數。
四、統計文件單詞數
首先單詞不分大小寫,那可以先把所有字母轉為小寫。
因為非字母數字符號也屬於分隔符,可以統一先轉化為空格,便於之后判斷。
現在得到的就是只由小寫字母與數字加上空格的數據,把數據按空格拆分,組成字符串的集合。按照要求判斷就可以得到合法的單詞總數。
因為后面還有要求統計出現頻率最高的單詞,所以這里可以把合法單詞和出現次數統計出來,於是可以使用MAP來保存鍵值對。
五、統計文件有效行數
我的想法是統計出所有行數減去空白行就是有效行數。
六、出現頻率最高的10個單詞
基於前面已經統計好的MAP里的數據,排序輸出前十到list。
代碼規范制定鏈接
設計與實現過程
一、類構建組織
為了實現功能獨立,我將需求2、3、4、5封裝在WordCountMethods類,進行字符數量、行數、單詞數、詞頻的統計;將需求1、6封裝進WordCountIO類,:進行文件內容的讀取,以及處理結果的寫入;最后設計一個WordCount類調用以上兩個類的方法去實現WordCount功能。
二、主要方法設計
2.1 WordCountIO/讀取文件轉化為string形式
FileInputStream fileinputstream=new FileInputStream(filePath);
InputStreamReader inputstreamreader=new InputStreamReader(fileinputstream);
br = new BufferedReader(inputstreamreader);
int c;
while ((c = br.read()) != -1) {
strBud.append((char) c);
}
br.close();
return strBud.toString();
2.1.1 解釋思路
最開始我是想到用BufferedReader中包裝InputStreamReader類,但是一開始程序用的是BufferedReader類的readline(),后期單元測試的時候發現問題,readline當遇到換行符('\n'),回車符('\r')時會終止讀取表示該行文字讀取完畢且返回該行文字(不包含換行符和回車符),就會導致無法統計換行符、回車符,於是后來改用read(),讀取1個或多個字節,返回一個字符,當讀取到文件末尾時,返回-1。(具體改動版本可以參見我的README.md
2.1.2 好處
- BufferedReader會一次性從物理流中讀取8k字節內容到內存, 如果外界有請求,就會到這里存取,如果內存里沒有才到物理流里再去讀。即使讀,也是再8k。 而直接讀物理流,是按字節來讀,對物理流的每次讀取,都有IO操作。IO操作是最耗費時間的。 參考來源
- StringBuilder 為動態數組可以有效的降低字符串拼接的損耗,避免頻繁使用 s = s+"sss",對於stirng的"+="操作只適用於不在循環內的拼接。參考來源
2.2 WordCountMethods/統計文件有效行數
InputStream inpStr = new FileInputStream(filePath);
BufferedReader br = new BufferedReader(new InputStreamReader(inpStr));
//空白行的正則匹配器
Pattern blankLinePattern = Pattern.compile(BLANK_LINE_REGEX);
String line = null;
while ((line = br.readLine()) != null) {
if (blankLinePattern.matcher(line).find()) {
//是空白行就計數
blankLine++;
}
allLine++;
}
//有效行是總行數減去空白行數
validLine = allLine-blankLine;
return validLine;
2.2.1 解釋思路
我的想法是統計出所有行數減去空白行就是有效行數.但是如何判斷哪一行是空白行是個關鍵問題。
我經過上網搜索查閱資料,發現可以使用正則表達式"\s+"
正則表達式中\s+匹配任何空白字符,包括空格、制表符、換頁符等等, 等價於[ \f\n\r\t\v]
\f -> 匹配一個換頁
\n -> 匹配一個換行符
\r -> 匹配一個回車符
\t -> 匹配一個制表符
\v -> 匹配一個垂直制表符
2.3 WordCountMethods/統計合法單詞數
int words = 0;
//先全部轉小寫
String lowerStr = str.toLowerCase();
//匹配非單詞非數字的字符
Pattern pat = Pattern.compile(UN_ALPHABET_NUM_REGEX);
Matcher mat = pat.matcher(lowerStr);
//轉換為空格
lowerStr = mat.replaceAll(" ");
//按規定的分隔符拆分
String[] word = lowerStr.split("\\s+");
for (int i = 0; i < word.length; i++) {
String tw = word[i];
//判斷是不是要求的單詞
if (tw.matches(FIRST_FOUR_APLH_REGEX)) {
words++;
if (!map.containsKey(tw)) {
map.put(tw, 1);
}
else {
int num = map.get(tw);
map.put(tw, num + 1);
}
}
}
2.3.1 解釋思路
但是對於MAP的選擇我也是選擇和修改了很久,一個是Treemap另一個是hashmap。(最后是基於單元測試和性能分析,用比較大的數據去跑用treemap的代碼和用hashmap的代碼,發現treemap更快)(具體改動版本可以參見我的README.md
Treemap是基於紅黑樹,時間復雜度為O(logn),但是結果是排好序的,對於后面的統計出現頻率最高的單詞比較友好不用再排序。hashmap是基於哈希表,hashmap的時間復雜度為O(1),但是可以優化HashMap空間的使用調優初始容量和負載因子。
Java中HashMap和TreeMap的區別深入理解
2.4 WordCountMethods/統計出現頻率最高的單詞
Collections.sort(list,new Comparator<Map.Entry<String, Integer>>(){
//Treemap只要比較出現次數,不用再比較字典序
public int compare(Map.Entry<String, Integer> word1, Map.Entry<String, Integer> word2) {
return word2.getValue() - word1.getValue();
}
2.4.1 解釋思路
基於上面選擇的Treemap存儲到的list當中,因為在map中的數據已經是按字典序排序的了,只要比較頻率就可以,那么這里可操作性的點在於怎么取出方便的得到鍵值對。
3.4.2 好處
這里我使用Map.Entry,它是一個接口,他的用途是表示一個映射項(里面有Key和Value),而Set< Map.Entry<K,V>>表示一個映射項的Set。方法entrySet()返回值就是這個map中各個鍵值對映射關系的集合。可使用它對map進行遍歷。
Map.Entry里有相應的getKey和getValue方法,讓我們能夠從一個項中取出Key和Value。entrySet的方式整體都是比keySet方式要高一些;
另一種遍歷Map的方式: Map.Entry 和 Map.entrySet()
map遍歷的幾種方式和效率問題
性能改進
- 使用treemap存儲鍵值對
首先是測試3000個單詞使用treemap和hashmap在小數據測試下發現兩種方法耗時差不多
之后測試300000個單詞,可以看出treemap在計算單詞數,把單詞和次數的鍵值對存入map時花費時間較大,之后的輸出頻率最高的10個單詞花費時間小。但是相反hashmap在存儲的時間比較短相應增加最后輸出排序的時間。
- 使用 BufferedReader和StringBuilder
具體詳見 3.1 WordCountIO/讀取文件轉化為string形式
單元測試
一、模塊正確性測試
| 單元測試名稱 | 測試內容 |
|---|---|
| testCountChars() | 測試字符數(含有字母數字以及兩種換行符 |
| testCountLines() | 測試有效行數(含有空行有效行 |
| testCountWords() | 測試單詞數含有大寫小寫字母數字以及分隔符(包含有效單詞和無效單詞 |
| testHighFreqWord() | 測試輸出頻率最高的10個單詞(包含大小寫,非法單詞,以及不同單詞相同出現次數 |
二、整體正確性測試
| 單元測試名稱 | 測試內容 |
|---|---|
| testFile1() | 測試正常含有字母、字符、換行符以及空行的文件 |
| testFile2() | 測試文件中只有換行和空格 |
| testFile3() | 測試正常含有字母、字符、換行符以及空行的文件(最后一行是空行,存在某行以空格加回車結尾 |
| testEmptyFile() | 測試空文件 |
三、異常處理測試
| 單元測試名稱 | 測試內容 |
|---|---|
| testNotFoundFile() | 測試輸入文件不存在 |
| testParamOne() | 測試輸入參數不足兩個 |
四、單元測試代碼展示
模塊測試單詞數
含有大寫小寫字母數字以及分隔符
包含有效單詞和無效單詞
包含大小寫屬於同單詞
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++) {
stringBuilder.append("FILE").append(i).append(",");
}
for (int i = 0; i < 10; i++) {
stringBuilder.append("file").append(i).append("\n");
}
for (int i = 0; i < 10; i++) {
stringBuilder.append("a").append(i).append("\r");
}
for (int i = 0; i < 10; i++) {
stringBuilder.append(i).append("FILE").append("\r");
}
int countWords = WordCountMethods.countWords(stringBuilder.toString());
assertEquals(20,countWords);
模塊測試輸出頻率最高的10個單詞
包含大小寫,以及相同出現次數按字典序排序
StringBuilder stringBuilder = new StringBuilder();
for (int i = 1991; i < 2001; i++) {
for (int j = 0;j < 10; j++) {
stringBuilder.append("windows").append(i).append(",");
}
}
for (int i = 9; i >= 0; i--) {
for (int j = 0;j < 10; j++) {
stringBuilder.append("LINUX").append(i).append("\n");
}
}
stringBuilder.append("windows").append("2000").append(",");
stringBuilder.append("windows").append("2000").append(",");
stringBuilder.append("windows").append("1999").append(",");
/*
處理string,生成map(省略
*/
String[] key = {"windows2000","windows1999","linux0","linux1",
"linux2","linux3","linux4","linux5","linux6","linux7"
};
Integer[] values = {12,11,10,10,10,10,10,10,10,10};
for (int i = 0; i < hotWords.size(); i++) {
Map.Entry<String, Integer> temp = hotWords.get(i);
assertEquals(key[i],temp.getKey());
assertEquals(values[i],(Integer)temp.getValue());
}
五、單元測試結果

六、測試覆蓋率

沒到100%的原因是有一些關於文件流關閉打開的錯誤信息拋出無法測試。
異常處理說明
一、輸入參數少於兩個
如果參數兩個以上,默認選最先輸入的兩個
if (args.length<2) {
System.out.println("ERROR:參數至少為兩個,例如 java WordCount input.txt output.txt");
return;
}
二、讀文件無法正常運行
try {
/*
用BufferedReader從文件讀取內容(省略
*/
int c;
while ((c = br.read()) != -1) {
strBud.append((char) c);
}
br.close();
} catch (FileNotFoundException e) {
System.out.println("ERROR:文件未找到...\n");
e.printStackTrace();
} catch (IOException e) {
System.out.println("ERROR:字符輸入流出錯...\n");
e.printStackTrace();
}
三、寫文件無法正常運行
/*
構建包含輸出結果的StringBuffer(省略
*/
BufferedWriter writer = createFileWriter(filePath);
try {
writer.write(str.toString());
} catch (IOException e){
System.out.println("ERROR:寫文件出錯...\n");
e.printStackTrace();
} finally {
if (writer != null){
try {
writer.flush();
writer.close();
} catch (IOException e) {
throw new RuntimeException("ERROR:關閉輸出流出錯..."); }
}
}
心路歷程與收獲
一、項目之前
首先是重讀《構建之法》。這次作業有點像是一個小小小型的項目,不像是之前的作業只要提交代碼就可以了而且也沒有那么多的規范,在作業的要求里的很多步驟發現和書中章節結合的很好,比如說像在之前的學習和編寫代碼過程中從來沒有接觸過單元測試和性能分析 ,但是讀了老師的書和博客之后也是有了些基本的了解。 所以對於一些要求也不會不知所雲。
第二是關於學習Git和Github,因為之前也沒有做過什么大項目,所以對於這個的使用也是很茫然的。所以在PSP表格里的需求分析(包括學習新技術)這一項我的預估時間給了2個小時,但是結果學習時間還是超出了預期,但是還是收獲滿滿。
二、項目之中
第一是養成完成一個小進展就commit一次的習慣,確實很好用,在本地庫就可以隨時看到我比較之前的代碼有什么變動,以及也會保存之前的代碼:)。
第二是代碼的編寫,其實看這個程序的要求不算多也不算是太復雜,但是一個學期沒寫Java代碼確實是有點生疏,有些函數的使用也是邊寫邊查發現還有更好的邊改進。
第三是關於單元測試,最初由於是對於單元測試的不熟悉,所以本能的想用普通的方法在寫代碼的過程中去打開input.txt,寫一些測試的例子,再一項一項的用命令行去測試:(,花費了很多時間。
最后是代碼都基本完成,才開始用單元測試的插件和具體方法。所幸我有封裝每個方法以及分成不同的類,以至於最后的單元測試沒有花太多的時間去拆分單元。
第四是性能測試,對於這個也是和單元測試一樣我對其是完全陌生的。我之前要是想得到這個方法的運行時間,都是使用Date類在方法首尾得到相減時間long time = endTime - begintime;
在這次項目中我使用的是Jprofiler,確實是很好用可以看出CPU在哪一個函數哪一個方法上運行的時間最多,以及內存的占用情況,方便於對於不同方法的測試和改進。
三、項目結尾
首先是最后審閱這個作業和作業要求的時候發現,自己漏看了commit message的推薦閱讀,導致自己這一次的commit message編寫不規范qaq。吸取一個教訓,以后關於參考的知識要先都看一遍,對於之后的工作也是事半功倍。
第二通過PSP表格,確實這個看起來是一個會比以后實戰小得多的項目比預期花了我挺多的時間,主要是花在學習新知識,還有養成習慣上。因為第一次使用PSP表格,有些時間記錄的也不是很准確,但是可以看出有一個好的規划是很重要的,寫代碼不是埋頭苦寫。
