這個作業屬於哪個課程 | 2021春軟件工程實踐|S班 |
---|---|
這個作業要求在哪里 | 軟工實踐寒假作業(2/2) |
這個作業的目標 | 1.閱讀《構建之法》提問 2.WordCount編程 |
其他參考文獻 | CSDN相關博客以及博客園相關博客 |
任務一 重新閱讀《構建之法》並提問
1. 問題一
4.5 結對編程 中 "結對編程讓兩個人所寫的代碼不斷地處於“復審”的過程,程序員們能夠不斷地審核,提高設計和編碼質量,可以及時發現並解決問題,避免把問題拖到后面的階段去。 "
在本次個人作業中,我與同學也曾為了實現一個功能連着麥修改了一下午的代碼,我想這也算是結對編程吧,但是在這個過程中我發現如果有一方主導意識較強,就容易將問題帶入一個死結。且在這個過程中我們還產生了分歧,那么這個時候是否應該結束結對編程,各自實現自己的想法呢?還是兩人應當按順序,一起先嘗試其中一個人的想法,再一起嘗試另一個思路,然后對比取更優呢?
2. 問題二
3.2 軟件工程師的職業發展 "邁克康奈爾把工程師分為8個級別(8—15),一個工程師要從一個級別升到另一個級別,
需要在各方面達到一定的要求。例如,要達到12級,工程師必須在三個知識領域
達到“帶頭人”水平。例如要到達“工程管理(知識領域)的熟練(能力)”水平,工程師必須要做到以下幾點。閱讀: 4—6個經典文獻的深入分析和閱讀工作經驗: 要參與並完成6個具體的項目課程: 要參加3個專門的課程有些級別"
到目前為止我看過的關於編程的書屈指可數,在學習新技術的時候我偏向於在網絡上學習,畢竟網絡上的技術文章是最新的,那么,閱讀經典文獻的必要性在哪呢?
3. 問題三
5.2.1 主治醫生模式 "在一些學校里,軟件工程的團隊模式往往從這一模式退化為“一個學生干活,其余學生跟着打醬油”"
這種情況的確很常見,但是如果在其他學生的水平都較低,對於那個水平高的學生來說,自己完成比教會他們再與他們合作效率不是高多了嗎? 但這種模式也是合理的吧,特別是對於高年級學生來說,如果參加競賽,對於隊伍中的新生,不就應該帶他們嗎?
4. 問題四
11.5.1 閉門造車 "小飛:我今天真失敗!在辦公室里坐了10個小時,但是真正能花在開發工作上的
時間可能只有3個
小時,然后我的工作進展大概只有兩個小時!
阿超:那你的時間都花到哪里去了?"
對於這個問題我深有體會,在完成個人項目的過程中,我常常一坐就是一整天,對着一個bug能改一個下午,但其實只是一個很小的錯誤,就很容易陷入這樣的迷惑中,不獨處呢,很難進入狀態工作,一個人呢,又會發散了思維,那我以后去公司里工作該怎么辦呢
5. 問題五
16.1 創新的迷思 "最近幾年,我們整個社會似乎對創新很感興趣,媒體上充斥了創新型的人才、創新型的學校、創新型的公司、創新型的城市、創新型的社會,等等名詞。有些城市還把“創新”當作城市的精神之一,還有城市要批量生產上千名頂級創新人才。"
一直有聽說前輩創業的事跡,但在進入專業學習了三年,我發現創新並不是那么容易的,你想實現的功能早就有人實現了並且已經失敗了,甚至找不到創新的方向,那些自稱創新型的事物,是否誇大其詞了。還有就是對於那些熱門的新技術方向,真正接觸了發現你能夠聽到的新技術,其實已經有許多先行者了。
任務二 WordCount編程
1. Github項目地址:
項目地址
2. PSP表格:
Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|
計划 | ||
預估這個任務需要多少時間 | 20 | 30 |
開發 | ||
需求分析(包括學習新技術) | 240 | 200 |
生成設計文檔 | 30min | 20min |
設計復審 | 30min | 15min |
代碼規范 | 30min | 40min |
具體設計 | 60min | 40min |
具體編碼 | 1000min | 1200min |
代碼復審 | 120min | 600min |
測試 | 60min | 30min |
測試報告 | 30min | 15min |
計算工作量 | 15min | 15min |
事后總結,並提出過程改進計划 | 30min | 10min |
總和 | 1665min | 2215min |
3. 代碼規范制定鏈接
4. 設計與實現過程
- 第一階段:復習git,復習java語法,編寫了我的代碼規范
這一階段的任務由於在寒假就有復習,因此進行的比較快。同時還根據《碼出高效_阿里巴巴Java開發手冊》結合我本人習慣,編寫我的代碼規范。由於之前未使用過GithubDesktop,在這個階段也安裝下載,並學習了如何使用。
-
第二階段:
- 程序需求分析:
- 獲取文件輸入
- 統計文件ascii碼字符數
- 統計符合規則的單詞數
- 統計文件的有效行數
- 統計出現次數最多的單詞及出現次數(輸出前十)
- 輸出結果到文件
- 程序需求分析:
-
我的類結構:
-
WordCount
- main
-
Lib
- readTxt
-
outputToTxt
- countChar
- countLine
- sortHashMap
- findLegal
- countWordNum
-
-
解題思路:
-
文件輸入
最開始我打算用BufferedReader去處理文件輸入,通過readLine()方法一次讀取一行,然后將讀取的字符串用換行符"\n"拼接起來。但在測試中發現,讀出的文本的ASCII碼比預期的少,又回去仔細閱讀了題目,發現文本中的換行並不是簡單的"\n",還有"\r"、"\r\n"這種情況,因此如果用readLine()可能會使得文本字符變少。通過百度查找資料后,我決定采用BufferedInputStream的read()函數來讀取,一次緩沖10m的文本。關鍵代碼如下:
byte[] bytes = new byte[BUFF_SIZE]; int len; while((len=bufferedInputStream.read(bytes)) != -1){ stringBuffer.append(new String(bytes, 0,len,StandardCharsets.UTF_8)); } ... } catch (IOException e){ ... } return stringBuffer.toString();
-
統計文件ASCII碼字符數
一開始沒認真審題,將問題復雜化了,通過正則匹配去統計。
String regex = "\\p{ASCII}"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(text); while (matcher.find()){ num++; }
后來發現由於題目說明給定的文本都是ASCII字符,因此只需返回讀取文件的字符串的長度即可。
-
統計符合規則的單詞數
- 單詞的規則:至少以4個英文字母開頭,跟上字母數字符號,不區分大小寫
- 我想到的辦法是先將文本用split方法分隔開,分隔用的正則表達式為:
[^ A-Za-z0-9_]|\\s+
,得到一個不含分隔符的字符串數組。再用一次循環用正則'[1]{4,}.*'去判斷是否為合法單詞。
將文本分割:
public static String[] splitWord(String text){ String[] words; String regexForSplit = "[^ A-Za-z0-9_]|\\s+"; words = text.split(regexForSplit); return words; }
- 判斷是否合法單詞
public static List<String> splitLegalWord(String[] words){ List<String> legalWords = new ArrayList<String>(); String regexForMatch = "^[a-zA-Z]{4,}.*"; for(int i=0 ; i<words.length; i++){ if(words[i].matches(regexForMatch)){ legalWords.add(words[i].toLowerCase()); } } return legalWords; }
-
統計文件的有效行數
用正則表達式去匹配空白字符(三種),然后用split將文本分割,就得到了每個字符串為一行的字符串數組,然后再遍歷過程中判斷是否空行,這里用trim是為了防止含有空格或tab制表符的無效行被視作有效行算入行數中。
String[] lines = text.split("\r\n|\r|\n"); for(int i=0; i<lines.length; i++){ if (!lines[i].trim().isEmpty()){ num++; } }
-
統計出現次數最多的單詞及出現次數(輸出前十)
將存放單詞和單詞出現次數的hashMap轉換為list,然后對list進行排序,重寫compare方法使得排序依據:從大到小排序,頻率相同的單詞,優先輸出字典序靠前的單詞,選取前十條記錄。
public static List<Map.Entry<String, Integer>> sortHashMap(HashMap<String, Integer> hashMap){ Set<Map.Entry<String, Integer>> entry = hashMap.entrySet(); List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(entry); Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { if(o1.getValue().equals(o2.getValue())){ return o1.getKey().compareTo(o2.getKey()); } return o2.getValue()-o1.getValue(); } }); //最多只取10條 if(list.size()>10) { return list.subList(0,10); } else{ return list; } }
- 輸出結果到文件
簡單地用BufferedWriteer按行寫入到文件中。
bufferedWriter.write("characters:"+num1+"\r\n"); bufferedWriter.write("words:"+num2+"\r\n"); bufferedWriter.write("lines:"+line+"\r\n"); for(int i=0; i<list.size()&&i<10; i++){ String key = list.get(i).getKey(); Integer value = list.get(i).getValue(); bufferedWriter.write(key+":"+value+"\r\n"); }
5. 性能改進
-
初次性能測試:
用於測試的文本大小為:95.3mb(100,000,000 字節,用來測試的文件由該文件[GenerateText][]隨機生成) 需要的運行時間為:54834ms 這個數字着實嚇了我一跳,因為其他人的運行時間是遠遠低於我的。 使用了緩沖區后 改進時間為50212ms。 -
算法優化
在對執行各個模塊的時間分析后,我發現耗時最高的是計算單詞數以及統計詞頻,由於算法不當以及一開始對各個方法獨立性的錯誤追求,在拆分單詞處進行了重復計算,在和洋藝同學交流后發現其實計算單詞的時候就可以用hashMap記錄詞頻的
-
改進前:先用split方法提取出獨立的字符串,存放在字符串數組中,然后再用 "[2]{4,}.*" 去匹配每一個字符串,用List
存放合法的單詞。用的是matches方法。據星源同學所說這兩個方法效率極低,否則我也沒意識到在這個地方有什么可以改進的地方,非常感謝他555。 -
改進后:直接對文本字符串進行匹配,通過Matcher的find方法取出符合規則的單詞,並且統計單詞的出現次數,存放到HashMap<String, Integer>里。不過這樣的正則表達式比較復雜,而且需要在文本字符串開頭添加一個空白字符。正則表達式:"([^ a-z0-9])([a-z]{4}[a-z0-9]*)"。關鍵代碼:
Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(text); HashMap<String, Integer> legalWords = new HashMap<String, Integer>(); //直接把單詞和出現頻率一起做了,放到hashMap里 while(matcher.find()){ wordNum++; String tmp = matcher.group(2).trim(); if(!legalWords.containsKey(tmp)){ legalWords.put(tmp,1); } else{ legalWords.put(tmp, legalWords.get(tmp)+1); } } return legalWords; }
-
對CountLine進行了優化,畢竟用split方法實在是太耗時且占用內存了,我用大小為476 MB (500,000,000 字節)的文本去跑,程序直接崩潰了,原因是堆溢出。在經過百度以及和鄒洋藝同學探討過后,決定采用正則匹配來計算行數。
public static int countLine(String text){ int num=0; String regex = "(.*)(\\s)"; Matcher matcher = regexUtils(regex, text); while (matcher.find()){ String tmp = matcher.group(1); if(!tmp.trim().isEmpty()){ num++; } } return num; }
-
-
-
多線程執行:
發現統計單詞詞頻和計算行數這兩個工作並不重復,而且耗時也都比較長,因此想到是否可以通過多線程來執行這兩個任務。由於兩個方法都需要返回值,因此實現的是Callabel接口
Callable callable1 = new Callable() { HashMap<String, Integer> legalWords; @Override public HashMap<String, Integer> call() { long startTime1 = System.currentTimeMillis(); try { Thread.sleep(5); }catch (Exception e){ e.printStackTrace(); } this.legalWords = SplitWord.findLegal(content); countDownLatch.countDown(); long endTime1 = System.currentTimeMillis(); System.out.println("線程1運行時間:"+(endTime1-startTime1)+"ms"); return legalWords; } }; futureTask1 = new FutureTask<HashMap<String, Integer>>(callable1); new Thread(futureTask1).start(); //執行線程
Callable callable2 = new Callable() { Integer line; @Override public Integer call() { long startTime2 = System.currentTimeMillis(); line = CountLine.countLine(content); countDownLatch.countDown(); long endTime2 = System.currentTimeMillis(); System.out.println("線程2運行時間:"+(endTime2-startTime2)+"ms"); return line; } }; futureTask2 = new FutureTask<Integer>(callable2); new Thread(futureTask2).start();
coutDownLatch使主線程等待兩個子線程都完成后才能繼續執行。
可以看到,兩個線程是並行的且主線程等兩個線程都執行完畢才繼續執行。性能改進后,測試95.3mb(100,000,000 字節)的文件,所需要的時間為:7053ms,相對於優化之前的50000多ms有了相當大的改進。
6. 單元測試
-
最開始的單元測試是很笨的通過main方法,寫好方法后在main中調用,並對方法的運行時間進行記錄。后來得知可以使用JUnit插件來進行單元測試,單元測試就變得簡單方便多了,也更加有針對性。在編程過程中,我進行了多次的單元測試,在確保功能正常后才進行下一步。
1.字符統計
@Test public void countChar() { String content = ReadTxt.readTxt(path+"input7.txt"); long startTime1 = System.currentTimeMillis(); System.out.println(CountAsciiChar.countChar(content)); long endTime1 = System.currentTimeMillis(); System.out.println("計算ASCII時間:"+(endTime1-startTime1)+"ms"); }
- 單詞計算
@org.junit.Test public void countWordNum() { num = CountWord.countWordNum(text); System.out.println(num); }
- 統計頻率
@Test public void sortHashMap() { list = CountFrequency.sortHashMap(legalWords); for(int i=0; i<list.size()&&i<10; i++){ String key = list.get(i).getKey(); Integer value = list.get(i).getValue(); System.out.println(key+":"+value+"\r\n"); } }
-
代碼覆蓋率測試
我的覆蓋率情況為:類的覆蓋率為100%,方法的覆蓋率為100%,代碼行覆蓋率為88%。
7. 異常處理說明
程序中主要會出現的異常是文件操作以及命令行輸入命令的錯誤。
在文件操作的相應代碼中都添加了try catch結構來捕獲異常
8. 心路歷程與收獲
- 心路歷程
早就聽聞軟件工程實踐的大名,真正上這門課的時候確實有害怕,怕自己編程能力太差,就和我一直對參加競賽有着莫名的恐懼一樣,害怕自己能力不足,無法在規定的時間內完成任務,特別是在規定時間內要完成新技術的學習,然后馬上投入應用。但是既然開始了,那也就只能克服恐懼了。
本次作業是個人編程任務,看到題目的時候我有點欣喜又有點迷茫,欣喜是因為這和我們以前編過的程序功能沒什么本質區別,迷茫則是作業要求里又有許多我沒接觸過的東西,什么 單元測試、性能改進,什么叫單元測試?又怎么去分析程序的性能呢?
剛開始的時候手忙腳亂的,雖然以前也用過git,但是並不熟練,只會簡單的pull和push,於是我的commit記錄里就多了一條提交測試hhhhh,這還導致我最后多commit了2次來刪去之前多提交的文件。
但隨着一個功能一個功能的實現我開始進入狀態了,連續好幾天都是從早上坐到晚上,有些代碼寫了改,改了刪,還會有些莫名其妙的bug,記得最深刻的是BufferedStream的read函數的一個用法錯誤,導致我讀取的文本內容產生了差錯,然后那個bug我從早上改到晚上,就是沒有改出來,最后發現是沒有指定每次從緩沖區讀取的長度,改出來的時候差點從椅子上跳起來哈哈哈哈哈,興奮又懊惱,懊惱自己怎么會犯這么低級的錯誤,而且效率還這么低。 我反思了一下,是因為缺少合理的休息,一直坐在電腦前是效率底下的,coding期間必須合理地休息。
在初次實現了功能后,我以為自己的程序很可以了...直到運行了大文件,我發現我的程序崩潰了...於是接下來就是各種百度,還有和同學交流探討,針對計算單詞這個功能,我甚至和洋藝同學連着麥,我倆邊交流改了一下午才改出來。看見其他同學的方法效率比我的高我就忍不住想問問如何實現的,但又怕被算作抄襲,同時時間也來不及了,就沒有更深的改進。
最后完成的時候感覺心里放下了一塊大石頭
- 收獲&不足
提高了自己的編程水平,抗壓力能力也變高了不少,畢竟這幾天頭發沒少掉,熬夜也是每天。
熟練使用git和gitdesktop,為以后的團隊合作打下基礎
效率過低,一個小bug由於思維誤區改了一下午
沒做好充分的准備就開始編寫代碼,導致代碼復審和性能分析的時候發現自己的算法過於差勁。其實應該在動手編寫代碼之前先找到最佳的算法,然后再動手實現。