軟件工程 作業二
1.碼雲項目地址
https://gitee.com/cxf1404445126/PersonalProject-Java/tree/master
2.PSP表格
PSP2.1 | 個人開發流程 | 預估耗費時間(分鍾) | 實際耗費時間(分鍾) |
---|---|---|---|
Planning | 計划 | 30 | 40 |
· Estimate | 明確需求和其他相關因素,估計每個階段的時間成本 | 30 | 40 |
Development | 開發 | 600 | 680 |
· Analysis | 需求分析 (包括學習新技術) | 60 | 100 |
· Design Spec | 生成設計文檔 | 60 | 50 |
· Design Review | 設計復審 | 30 | 15 |
· Coding Standard | 代碼規范 | 30 | 15 |
· Design | 具體設計 | 100 | 80 |
· Coding | 具體編碼 | 180 | 180 |
· Code Review | 代碼復審 | 30 | 20 |
· Test | 測試(自我測試,修改代碼,提交修改) | 70 | 180 |
Reporting | 報告 | 80 | 90 |
· | 測試報告 | 30 | 30 |
· | 計算工作量 | 20 | 20 |
· | 並提出過程改進計划 | 30 | 40 |
3.解題思路描述
- 首先當然先選擇好語言,嗯…Java無疑了。
- 下一步是理清題目中需要用到編程方面的知識點。
- 很明顯要求的重點是通過文本文件對其進行統計,那就是要考慮Java中關於文件的應用了;
- 其次是對讀取字符進行統計,包括大小寫、單詞等要求,那想來要用到到字符串匹配判斷方面的知識點;
- 接着統計好的單詞要進行分類存放,使用什么呢,Map?List?Set?嗯都先記下來好了,等到要使用的到了再根據具體情況決定。
- 大概總結完知識點,然后是開始進行類的划分,確定一下開始的方向。
- 最后就是根據划分出的組織順序開始代碼的編寫,至於資料,一般是等到需要用到的時候直接在網上進行查找,個人感覺這樣目的性比較明確,效率會高一些。
4.設計實現過程
一、相關類設計
- WordDeal類,用於存放統計字符數、行數、單詞數、詞頻等方法。
- FileDeal類作為文件處理類,用於讀取文件內容以及寫入文件。
- Main類,調用上面兩個類的方法,實現具體功能。
- Test類,用於進行單元測試。
二、相關函數設計
-
FIleDeal類中的方法
- FileToString方法實現讀取一個文件,並且返回文件內容,保存在一個字符串中。
- WriteToFile方法實現把統計結果寫入指定文件。
-
WordDeal類中的方法
- getCharCount()方法實現統計字符數。
- getLineCount()方法實現計算行數。
- getWordCount()方法實現計算單詞數。
- getWordFreq()方法實現統計每一個單詞的詞頻。
- sortMap()方法實現對Map中的數據進行排序,先按詞頻數后按字典順序。
- ListToArray()方法實現取出詞頻數前十的數據。
-
關鍵函數流程圖
5.代碼說明
1. getCharCount()函數
public int getCharCount() // 統計文件字符數(ascll碼(32~126),制表符,換行符,)
{
char c;
for (int i = 0; i < text.length(); i++) {
c = text.charAt(i);
if (c >= 32 && c <= 126 || c == '\r' || c == '\n'|| c == '\t') {
charNum++;
}
}
return charNum;
}
這個函數是用於統計字符的數量,主要實現方法就是將逐個字符進行遍歷,判斷它是否在有效字符內,若為有效字符則進行記錄。
2. getWordCount()函數
public int getWordCount() // 統計單詞總數(單詞:以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。)
{
String t = text;
t = t.replace('\n', ' ');
t = t.replace('\r', ' ');
t = t.replace('\t', ' ');
String[] spWord = t.split(" +"); // 對字符串進行分詞操作
for (int i = 0; i < spWord.length; i++) {
if (spWord[i].length() < 4) { // 判斷長度是否大於等於4
continue;
} else {
int flag = 1; // 判斷字符串的前四位是否是英文字母
char[] ch = spWord[i].toCharArray();
for (int j = 0; j < 4; j++) {
if (!(ch[j] >= 'A' && ch[j] <= 'Z' || ch[j] >= 'a' && ch[j] <= 'z')) {
flag = 0;
}
}
if (flag == 1) {
wordCount++;
}
}
}
return wordCount;
}
這個函數是用於判斷單詞的個數,因為文件中存在換行符制表符等分割符,所以方便分割字符,先統一將它們都替換成了空格,然后以空格為標志進行分割,接着按照單詞的要求進行單詞的判斷即可。
3. getLineCount()函數
public int getLineCount() { // 統計有效行數
String[] line = text.split("\r\n"); // 將每一行分開放入一個字符串數組
for (int i = 0; i < line.length; i++) { // 找出無效行,統計有效行
if (line[i].trim().length() == 0)
continue;
ValidLine = ValidLine + 1;
}
return ValidLine;
}
該函數用於統計有效行數,只需要將文件的每一行進行分割,存入字符數組,然后判斷有無空行,即對應的字符去掉空格后長度是否為0,然后記錄下有效行數並返回。
4. getWordFreq()函數
public Map getWordFreq() // 統計單詞詞頻(單詞:以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。)
{
wordFreq = new HashMap<String, Integer>();
String t = text;
t = t.replace('\n', ' ');
t = t.replace('\r', ' ');
t = t.replace('\t', ' ');
String[] spWord = t.split(" +"); // 對字符串進行分詞操作
for (int i = 0; i < spWord.length; i++) {
if (spWord[i].length() < 4) { // 判斷長度是否大於等於4
continue;
} else {
// System.out.println(spWord[i]+" "+spWord[i].length());
int flag = 1; // 判斷字符串的前四位是否是英文字母
char[] ch = spWord[i].toCharArray();
for (int j = 0; j < 4; j++) {
if (!(ch[j] >= 'A' && ch[j] <= 'Z' || ch[j] >= 'a' && ch[j] <= 'z')) {
flag = 0;
}
}
if (flag == 1) { // 將字符串轉化為小寫
spWord[i] = spWord[i].trim().toLowerCase();
if (wordFreq.get(spWord[i]) == null) { // 判斷之前Map中是否出現過該字符串
wordFreq.put(spWord[i], 1);
} else
wordFreq.put(spWord[i], wordFreq.get(spWord[i]) + 1);
}
}
}
return wordFreq;
}
該函數用於單詞詞頻的統計,之前的部分先按照單詞的判斷方法進行判斷一個詞是否是單詞,然后使用一個Map進行存儲,具體方法為先判斷Map中之前是否存過該數據,若沒有存過,則將其存入並置value值為1,若存過,則將該值的value值加1。
5. sortMap()函數
public List sortMap(Map wordCount) { // 對單詞詞頻的Map進行排序
List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(wordCount.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Entry<String, Integer> o1, Entry<String, Integer> o2) { //對Map中內容進行排序,先按詞頻后按字典順序
if (o1.getValue() == o2.getValue()) {
return o1.getKey().compareTo(o2.getKey());
}
return o2.getValue() - o1.getValue();
}
});
return list;
}
這個函數用於對Map詞頻進行排序,這里使用了Map類型的List來存放詞頻信息,然后通過重寫Comparator以及排序函數來實現相應要求的排序,最后返回排完序的List。
6.單元測試
這次的單元測試使用了Junit4,因為數量以及相似關系,僅展示WordDeal類中的一個函數的相關測試。
-
這里采用的是白盒測試法進行測試,白盒法考慮的是測試用例對程序內部邏輯的覆蓋程度,要求是要盡可能的使其覆蓋程度更大一些。
-
對於白盒法的覆蓋標准有如下幾個:
語句覆蓋:是一個比較弱的測試標准,它的含義是:選擇足夠的測試用例,使得程序中每個語句至少都能被執行一次。它是最弱的邏輯覆蓋,效果有限,必須與其它方法交互使用。
判定覆蓋(也稱為分支覆蓋):執行足夠的測試用例,使得程序中的每一個分支至少都通過一次。判定覆蓋只比語句覆蓋稍強一些,但實際效果表明,只是判定覆蓋,還不能保證一定能查出在判斷的條件中存在的錯誤。因此,還需要更強的邏輯覆蓋准則去檢驗判斷內部條件。
條件覆蓋:執行足夠的測試用例,使程序中每個判斷的每個條件的每個可能取值至少執行一次;條件覆蓋深入到判定中的每個條件,但可能不能滿足判定覆蓋的要求。
判定/條件覆蓋:執行足夠的測試用例,使得判定中每個條件取到各種可能的值,並使每個判定取到各種可能的結果。判定/條件覆蓋有缺陷。從表面上來看,它測試了所有條件的取值。但是事實並非如此。往往某些條件掩蓋了另一些條件。會遺漏某些條件取值錯誤的情況。為徹底地檢查所有條件的取值,需要將判定語句中給出的復合條件表達式進行分解,形成由多個基本判定嵌套的流程圖。這樣就可以有效地檢查所有的條件是否正確了。
條件組合覆蓋:執行足夠的例子,使得每個判定中條件的各種可能組合都至少出現一次。這是一種相當強的覆蓋准則,可以有效地檢查各種可能的條件取值的組合是否正確。它不但可覆蓋所有條件的可能取值的組合,還可覆蓋所有判斷的可取分支,但可能有的路徑會遺漏掉。測試還不完全。
1. getCharCount()函數
根據上面的函數流程圖,設計了以下四個文件,使其盡可能的覆蓋所有的分支。
** 測試數據:**
- text1.txt:空文件 字符數為0
- text2.txt:純中文文件 字符數為0
- text3.txt:純英文文件字符數為80
- text4.txt:包含了英文、中文、回車換行符、制表符的文件字符數為73
測試代碼:
@Test
public void testGetCharCount() throws IOException {
FileDeal fd = new FileDeal();
String text1 = fd.FileToString("text/text1.txt");
String text2 = fd.FileToString("text/text2.txt");
String text3 = fd.FileToString("text/text3.txt");
String text4 = fd.FileToString("text/text4.txt");
WordDeal wd1 = new WordDeal(text1);
WordDeal wd2 = new WordDeal(text2);
WordDeal wd3 = new WordDeal(text3);
WordDeal wd4 = new WordDeal(text4);
int cn1 = wd1.getCharCount();
int cn2 = wd2.getCharCount();
int cn3 = wd3.getCharCount();
int cn4 = wd4.getCharCount();
assertEquals(0, cn1);
assertEquals(0, cn2);
assertEquals(80, cn3);
assertEquals(73, cn4);
}
各個函數的測試結果:
2. 分支覆蓋率截圖:
7.效能分析
效能分析中選用了一篇大小為2MB左右的英文小說,在測試代碼中重點測試了字符串處理類中的五個函數的運行時間。性能測試的代碼如下:
public class Perf_analy {
FileDeal fd;
String text1;
WordDeal wd1;
Map<String, Integer> wf1;
List lis1;
@Before
public void setUp() throws Exception { // 初始化數據
fd = new FileDeal();
text1 = fd.FileToString("LittlePrince.txt");
wd1 = new WordDeal(text1);
wf1 = wd1.getWordFreq();
lis1 = wd1.sortMap(wf1);
}
@Test
public void testGetCharCount() throws IOException { // 測試統計字符數量函數
long startTime = System.currentTimeMillis();
wd1.getCharCount();
long endTime = System.currentTimeMillis();
System.out.println("getCharCount函數的運行時間為" + (endTime - startTime) + "毫秒");
}
@Test
public void testGetWordCount() throws IOException { // 測試統計單詞數量函數
long startTime = System.currentTimeMillis();
wd1.getWordCount();
long endTime = System.currentTimeMillis();
System.out.println("getWordCount函數的運行時間為:" + (endTime - startTime) + "毫秒");
}
………………
………………
………………
}
最后運行得到的每個函數的運行時間如下圖所示:
可以看出相比於其他函數,統計單詞個數的函數的運行時間要多得多。於是查看相應函數進行效能的改進:
public int getWordCount() // 統計單詞總數(單詞:以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。)
{
String t = text;
t = t.replace('\n', ' ');
t = t.replace('\r', ' ');
t = t.replace('\t', ' ');
String[] spWord = t.split(" +"); // 對字符串進行分詞操作
for (int i = 0; i < spWord.length; i++) {
if (spWord[i].length() < 4) { // 判斷長度是否大於等於4
continue;
} else {
int flag = 1; // 判斷字符串的前四位是否是英文字母
char[] ch = spWord[i].toCharArray();
for (int j = 0; j < 4; j++) {
if (!(ch[j] >= 'A' && ch[j] <= 'Z' || ch[j] >= 'a' && ch[j] <= 'z')) {
flag = 0;
}
}
if (flag == 1) {
wordCount++;
}
}
}
return wordCount;
}
對於上面代碼,在改進時考慮到在分詞操作時可以使用\\s來進行空白字符的匹配,這樣就可以省去使用replace()函數所需要的時間,再一個就是在循環判斷前四個字符是否為英文時使用了效率更高charAt代替了toCharArray(),改進后的代碼如下:
public int getWordCount() // 統計單詞總數(單詞:以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。)
{
String t = text;
String[] spWord = t.split("\\s"); // 對字符串進行分詞操作
for (int i = 0; i < spWord.length; i++) {
if (spWord[i].length() < 4) { // 判斷長度是否大於等於4
continue;
} else {
int flag = 1; // 判斷字符串的前四位是否是英文字母
char c;
for (int j = 0; j < 4; j++) {
c = spWord[i].charAt(j);
if (!(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
flag = 0;
}
}
if (flag == 1) {
wordCount++;
}
}
}
return wordCount;
}
再次運行效能分析文件,結果如下,可以看出運行時間上已經有了明顯的減少。
8.心得體會
這次的實驗其實暴露出了我的很多不足的地方,之前的作業我一般都是以代碼為核心,個人習慣一拿到題目就馬上開始寫代碼,但這次要求是要按照流程來進行一步步的操作。最開始的對PSP表格的估計就耗費了蠻多時間,因為之前沒有過按照規范來做的項目,所以對每一個模塊應該用多少時間在觀念上還是比較模糊,最后導致實際和預估時間相差還蠻大的。
在整個作業中錯誤處理和單元測試部分是我覺得最難的部分了,在上面花費的時間也是所有部分中最多的,但是最后的完成的效果還是不太滿意,對於異常,我一向是很少會考慮,在寫代碼的時候大部分也是按照理想的輸入狀態來,所以在對異常處理的部分還是有很多不足。至於單元測試,可能對於測試樣例的設計還是不太熟悉,所以會有冗余現象。
不過這次作業給我的收獲還是很大的,學到了比較規范的項目流程,發現了自己很多細節地方的欠缺,還了解到很多新知識。