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碼,漢字不需考慮;
- 空格,水平制表符,換行符,均算字符;
- 程序可統計文件的單詞數,具體要求:
- 程序可統計文件的有效行數,具體要求:
- 任何包含非空白字符的行,都需要統計;
- 程序可統計文件的單詞詞頻,具體要求:
- 最終只輸出頻率最高的10個;
- 頻率相同的單詞,優先輸出字典序靠前的單詞;
- 按照字典序輸出結果至文件result.txt,具體要求:
- 輸出的單詞統一為小寫格式;
- 需按格式5輸出.
非功能性需求:
- 對三個核心功能統計字符數、統計單詞數、統計最多的10個單詞及其詞頻進行封裝;
- 使用Github進行源代碼管理,代碼有進展即簽入Github。根據需求划分功能后,每做完一個功能,編譯成功后,應至少commit一次;
- 至少應采用白盒測試用例設計方法來設計測試用例,並設計至少10個測試用例.
備注:
[1、英文字母:A-Z,a-z;](#header1) [2、字母數字符號:A-Z, a-z,0-9;](#header1) [3、分割符:空格,非字母數字符號;](#header1) [4、例:file123是一個單詞,123file不是一個單詞。file,File和FILE是同一個單詞;](#header1) [5、輸出格式示例:](#header1) ```java characters: number words: number lines: number解題思路
看到這個題目后,我其實第一想法是用MapReduce....這也算是MapReduce的Hello World了。不過題目是在單機上測試,所以用分布式框架毫無意義(之后應該會補充基於MapReduce的WordCount)。
這次需要實現的功能其實主要是兩個部分:字詞計數和文件讀寫。下面進行具體描述:
對於文件讀寫,因為很多計數處理在讀文件時可以一起完成,所以我選擇將文件讀取放進計數模塊中。而寫文件則獨立出來,避免過多功能寫在一起顯得太臃腫。
對於核心的計數模塊,其實字符和行數還是比較好實現的。但在字符計數中也碰到了一個問題,用readLine讀取文件時,無法將換行符讀取進來,更改成read一個一個讀就沒問題了。對於單詞的讀取,我一開始想直接用split進行切分,但又有些擔心正則的效率。。經過測試,最后還是選擇了stringTokenizer進行切分,正則用來匹配。不過官方並不推薦用stringTokenizer,,但簡單切分還是蠻好用的。
關於怎么做詞頻排序,我起初想了幾個方案:轉換為list直接sort、建堆、BFPTR加快排。實測BFPTR加快排還是會比堆快一點的。但最終實現時,我還是用了sort,寫起來干凈方便。。其實也是有些地方沒修好,因為很少用java寫算法,所以雖然能跑起來,但中間冗余部分還是有點多,看着非常別扭,於是棄用了。很難說這樣扯出來的代碼性能究竟怎么樣,因為時間有限,所以沒有再進行對比測試,之后修復好還是得多試試。
代碼規范
代碼規范我用的是實驗室的代碼規范:阿里巴巴的碼出高效,並加上了一些補充。
設計說明
總體設計簡述
整體由一個計數模塊提供字詞計數功能,分為字符計數、單詞計數、行數計數、詞頻計數四個部分.
類圖及流程圖
類圖

流程圖

模塊設計
計數模塊
模塊說明
通過傳入文件名,提供統計字符總數、單詞總數、總行數和總詞頻的功能.
類說明
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();
}
}
}
異常處理
對於各個異常情況都會打印異常信息,如讀取文件時,如果找不到對應文件:
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正則表達式的語法與示例
正則表達式匹配解析過程探討分析(正則表達式匹配原理)
