PDF
GitHub
- PSP表格
- 需求分析
- 解題思路
- 代碼規范
- 設計說明
- 總體設計簡述
- 類圖及流程圖
- 模塊設計
- 計數模塊
- 模塊說明
- 類說明
- CharCounter
- WordCounter
- LineCounter
- WordsFrequencyCounter
- 計數模塊
- 關鍵代碼
- 異常處理
- 性能分析
- 單元測試
- 代碼覆蓋率
- 感想
- 參考鏈接
PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 65 | 68 |
· Estimate | · 估計這個任務需要多少時間 | 65 | 68 |
Development | 開發 | 510 | 558 |
· Analysis | · 需求分析 (包括學習新技術) | 150 | 180 |
· Design Spec | · 生成設計文檔 | 30 | 24 |
· Design Review | · 設計復審 | 20 | 11 |
· Coding Standard | · 代碼規范(為目前的開發制定合適的規范) | 10 | 5 |
· Design | · 具體設計 | 30 | 10 |
· Coding | · 具體編碼 | 120 | 158 |
· Code Review | · 代碼復審 | 40 | 26 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 120 | 144 |
Reporting | 報告 | 90 | 65 |
· Test Report | · 測試報告 | 40 | 18 |
· Size Measurement | · 計算工作量 | 20 | 14 |
· Postmortem & Process Improvement Plan | · 事后總結, 并提出過程改進計劃 | 30 | 33 |
合計 | 665 | 691 |
需求分析
基本功能點:
- 程序可通過命令行讀取輸入文件;
- 程序可統計文件的字符數,具體要求:
- 只需要統計Ascll碼,漢字不需考慮;
- 空格,水平制表符,換行符,均算字符;
- 程序可統計文件的單詞數,具體要求:
- 單詞4:至少以4個英文字母1開頭,跟上字母數字符號2,單詞以分隔符3分割,不區分大小寫;
- 程序可統計文件的有效行數,具體要求:
- 任何包含非空白字符的行,都需要統計;
- 程序可統計文件的單詞詞頻,具體要求:
- 最終只輸出頻率最高的10個;
- 頻率相同的單詞,優先輸出字典序靠前的單詞;
- 按照字典序輸出結果至文件result.txt,具體要求:
- 輸出的單詞統一為小寫格式;
- 需按格式5輸出.
非功能性需求:
- 對三個核心功能統計字符數、統計單詞數、統計最多的10個單詞及其詞頻進行封裝;
- 使用Github進行源代碼管理,代碼有進展即簽入Github。根據需求劃分功能后,每做完一個功能,編譯成功后,應至少commit一次;
- 至少應采用白盒測試用例設計方法來設計測試用例,并設計至少10個測試用例.
備注:
軟件工程實踐報告、1、英文字母:A-Z,a-z;
2、字母數字符號:A-Z, a-z,0-9;
3、分割符:空格,非字母數字符號;
4、例:file123是一個單詞,123file不是一個單詞。file,File和FILE是同一個單詞;
5、輸出格式示例:
characters: number
words: number
lines: number
<word1>: number
<word2>: number
...
解題思路
看到這個題目后,我其實第一想法是用MapReduce....這也算是MapReduce的Hello World了。不過題目是在單機上測試,所以用分布式框架毫無意義(之后應該會補充基于MapReduce的WordCount)。
這次需要實現的功能其實主要是兩個部分:字詞計數和文件讀寫。下面進行具體描述:
對于文件讀寫,因為很多計數處理在讀文件時可以一起完成,所以我選擇將文件讀取放進計數模塊中。而寫文件則獨立出來,避免過多功能寫在一起顯得太臃腫。
對于核心的計數模塊,其實字符和行數還是比較好實現的。但在字符計數中也碰到了一個問題,用readLine讀取文件時,無法將換行符讀取進來,更改成read一個一個讀就沒問題了。對于單詞的讀取,我一開始想直接用split進行切分,但又有些擔心正則的效率。。經過測試,最后還是選擇了stringTokenizer進行切分,正則用來匹配。不過官方并不推薦用stringTokenizer,,但簡單切分還是蠻好用的。
軟件工程期末大作業、關于怎么做詞頻排序,我起初想了幾個方案:轉換為list直接sort、建堆、BFPTR加快排。實測BFPTR加快排還是會比堆快一點的。但最終實現時,我還是用了sort,寫起來干凈方便。。其實也是有些地方沒修好,因為很少用java寫算法,所以雖然能跑起來,但中間冗余部分還是有點多,看著非常別扭,于是棄用了。很難說這樣扯出來的代碼性能究竟怎么樣,因為時間有限,所以沒有再進行對比測試,之后修復好還是得多試試。
代碼規范
代碼規范我用的是實驗室的代碼規范:阿里巴巴的碼出高效,并加上了一些補充。
設計說明
- 總體設計簡述
- 類圖及流程圖
- 模塊設計
- 計數模塊
- 模塊說明
- 類說明
- CharCounter
- WordCounter
- LineCounter
- WordsFrequencyCounter
- 計數模塊
總體設計簡述
整體由一個計數模塊提供字詞計數功能,分為字符計數、單詞計數、行數計數、詞頻計數四個部分.
類圖及流程圖
類圖
工學交替實踐報告。流程圖
模塊設計
- 計數模塊
- 模塊說明
- 類說明
- CharCounter
- WordCounter
- LineCounter
- WordsFrequencyCounter
計數模塊
- 模塊說明
- 類說明
- CharCounter
- WordCounter
- LineCounter
- WordsFrequencyCounter
模塊說明
通過傳入文件名,提供統計字符總數、單詞總數、總行數和總詞頻的功能.
類說明
- CharCounter
- WordCounter
- LineCounter
- WordsFrequencyCounter
CharCounter
(1) countChar(String fileName):long
功能:計算字符數
輸入:fileName:文件名
輸出:文件總字符數
WordCounter
(1) countWord(String fileName):long
功能:計算單詞數
輸入:fileName:文件名
輸出:文件總單詞數
LineCounter
寒假工算社會實踐嗎,(1) countLine(String fileName):long
功能:計算行數
輸入:fileName:文件名
輸出:文件總行數
WordsFrequencyCounter
(1) countWordsFrequency(String fileName):long
功能:計算單詞詞頻
輸入:fileName:文件名
輸出:各單詞詞頻
(2) topTenFrequentWords(HashMap<String, Long> wordMap):ArrayList<HashMap.Entry<String, Long>>
功能:求出頻率最高的10個單詞
輸入:wordMap:各單詞詞頻
輸出:頻率最高的10個單詞
關鍵代碼
詞頻計算器部分,使用StringTokenizer分詞,然后用regex匹配,存入HashMap中,再轉換為ArrayList進行排序。
/*** 詞頻計算器,包括計算文件中各單詞詞頻,只輸出頻率最高的10個.* 頻率相同的單詞,優先輸出字典序靠前的單詞.** @author xyy* @version 1.0 2018/9/12* @since 2018/9/11*/
public class WordsFrequencyCounter {/*** 讀取并計算文件詞頻.** @param fileName 文件名* @return 各單詞詞頻*/public static HashMap<String, Long> countWordsFrequency(String fileName) {InputStreamReader inputStreamReader = null;BufferedReader bufferedReader = null;String in = null;String regex = "[a-zA-Z]{4,}[a-zA-Z0-9]*";String delim = " ,.!?-=*/()[]{}\\\"\\';:\\n\\r\\t“”‘’·——…()【】{}\\0";String word = "";HashMap<String, Long> wordMap = new HashMap<String, Long>(16);//讀入文件try {inputStreamReader = new InputStreamReader(new FileInputStream(fileName));} catch (FileNotFoundException e) {System.out.println("找不到此文件");e.printStackTrace();}if (inputStreamReader != null) {bufferedReader = new BufferedReader(inputStreamReader);}//計算單詞詞頻try {while ((in = bufferedReader.readLine()) != null) {in = in.toLowerCase();//根據分隔符分割StringTokenizer tokenizer = new StringTokenizer(in, delim);while (tokenizer.hasMoreTokens()) {word = tokenizer.nextToken();//匹配單詞if (word.matches(regex)) {if (wordMap.get(word) != null) {wordMap.put(word, wordMap.get(word) + 1);} else {wordMap.put(word, 1L);}}}}} catch (IOException e) {e.printStackTrace();} finally {try {inputStreamReader.close();} catch (IOException e) {e.printStackTrace();}}return wordMap;}/*** 求頻率最高的10個單詞** @param wordMap 各單詞詞頻* @return 頻率最高的10個單詞*/public static ArrayList<HashMap.Entry<String, Long>> topTenFrequentWords(HashMap<String, Long> wordMap) {ArrayList<HashMap.Entry<String, Long>> wordList =new ArrayList<HashMap.Entry<String, Long>>(wordMap.entrySet());Collections.sort(wordList, new Comparator<HashMap.Entry<String, Long>>() {public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {if (o1.getValue() < o2.getValue()) {return 1;} else {if (o1.getValue().equals(o2.getValue())) {if (o1.getKey().compareTo(o2.getKey()) > 0) {return 1;} else {return -1;}} else {return -1;}}}});return wordList;}
}
Main部分,建立線程池,并行運行四個任務,然后輸出至文件。
/*** 主函數類,包括提交計數任務、打印結果.** @author xyy* @version 1.0 2018/9/12* @since 2018/9/11*/
public class Main {public static void main(final String[] args) {ExecutorService executor = Executors.newCachedThreadPool();//計算字符數Future<Long> futureChar = executor.submit(new Callable<Long>() {public Long call() {return CharCounter.countChar(args[0]);}});//計算單詞數Future<Long> futureWord = executor.submit(new Callable<Long>() {public Long call() {return WordCounter.countWord(args[0]);}});//計算行數Future<Long> futureLine = executor.submit(new Callable<Long>() {public Long call() {return LineCounter.countLine(args[0]);}});//計算單詞詞頻Future<ArrayList<HashMap.Entry<String, Long>>> futureWordFrequnency = executor.submit(new Callable<ArrayList<HashMap.Entry<String, Long>>>() {public ArrayList<HashMap.Entry<String, Long>> call() {return WordsFrequencyCounter.topTenFrequentWords(WordsFrequencyCounter.countWordsFrequency(args[0]));}});//輸出至文件try {FilePrinter.printToFile("result.txt",futureChar.get(), futureWord.get(), futureLine.get(), futureWordFrequnency.get());executor.shutdown();} catch (ExecutionException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}}
}
異常處理
寒假工實踐報告2000字。對于各個異常情況都會打印異常信息,如讀取文件時,如果找不到對應文件:
try {inputStreamReader = new InputStreamReader(new FileInputStream(fileName));
} catch (FileNotFoundException e) {System.out.println("找不到此文件");e.printStackTrace();
}
性能分析
可見最大開銷來源于多線程并行以及單詞計數部分。
單元測試
暑假工實踐內容及成果,單元測試框架用的是JUnit4。
我總共設計了十一個單元測試,其中Main一個,三個字詞計數部分各三個,詞頻計數部分一個。
單元測試 | 測試項 | 被測試代碼 |
---|---|---|
CharCounterTest | 分別測試普通字符、換行符和空格 | CharCounter.java |
WordCounterTest | 分別測試普通單詞、特殊單詞和大小寫單詞 | WordCounter.java |
LineCounterTest | 分別測試普通行、空白行和混合行 | LineCounter.java |
WordFrequencyCounterTest | 測試混合單詞 | WordFrequencyCounter.java |
MainTest | 測試空白文件 | Main.java |
代碼覆蓋率
檢測覆蓋率使用的是IDEA的Coverage,截圖如下:
因為異常處理并沒有單獨提出來,而是當場處理了,所以總的代碼覆蓋率并不高。尤其是功能比較簡單的字詞行計數部分,許多代碼都用來處理讀寫文件異常了。
軟功是什么。
感想
這次最大的感想就是差點沒趕上deadline。。雖然時間預估看上去沒有出現太多問題,但這實際上算是用工程質量的下降換來的,有許多地方沒有達到原先預想的水平。因為之前有了幾次做小項目的經驗,所以我很重視需求分析和設計文檔,事前也做了許多學習,但實際上手時,還是遇到比較多的問題。很多問題還是源于我對java編程和各個工具的使用還不夠熟練,特別是異常處理和單元測試部分,非常不滿意。。
也因為還不熟練,很多知識需要當場查閱學習,浪費了很多時間。最后實際編碼時間其實不長,一次編碼中也遺留了一些小問題,到測試時才再一一解決。
通過這次的作業,我也對單元測試有了個大概的理解。之前做測試都是手動編寫一些樣例進行測試,就像做算法一樣。不過比較糟糕的是我是在編碼結束后才編寫單元測試的。。在學習相關內容時,我才了解到單元測試最好在設計時就寫好,或者至少也應該跟程序一起寫了。而且我編寫的單元測試也比較簡單,有許多用法還在學習。
網工和軟工哪個好,還有一點就是對GitHub的使用,其實也是對代碼的管理。我之前是不常用Git的,常常是按自己的習慣在本地進行保存和版本管理。做實驗室的項目時,也沒有很好地利用svn,經常是完成了幾個部分才一起提交,但并這不符合實際軟件工程的要求。而且我還學會了怎么更好地書寫commit message,對比之前慘不忍睹的提交記錄。。。
這次也算是第一次像點樣子的完成了整個軟件開發的工程,深感自己在編碼和時間把控上還非常不足,希望在之后的結對和組隊中能夠有所提高。
參考鏈接
git commit 規范指南
現代軟件工程講義 2 開發技術 - 單元測試 & 回歸測試
在IntelliJ IDEA中查看代碼覆蓋率結果
IDEA 單元測試覆蓋技巧
Java 比較字符串之間大小
BFPRT算法O(n)解決第k小的數
Java的簡單單元測試例子
Java正則表達式的語法與示例
正則表達式匹配解析過程探討分析(正則表達式匹配原理)