項目Github地址:ExercisesGenerator
一、需求分析
需求描述 | 是否實現 |
---|---|
控制生成題目的個數 | 是 |
控制題目中數值范圍 | 是 |
計算過程不能產生負數,除法的結果必須是真分數,題目不能重復,運算符不能超過3個 | 是 |
生成的題目存入執行程序的當前目錄下的Exercises.txt文件 | 是 |
題目的答案存入執行程序的當前目錄下的Answers.txt文件 | 是 |
能支持一萬道題目的生成 | 是 |
支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計 | 是 |
統計結果輸出到文件Grade.txt | 是 |
具有圖形化的操作界面 | 是 |
二、開發計划
功能 | 描述 | 進度 |
---|---|---|
生成題目 | 隨機生成操作數和運算符,組成有效的四則運算表達式 | 完成 |
計算結果 | 根據生成的表達式,計算生成正確的結果 | 完成 |
批卷 | 根據指定的題目文件和答案文件,輸出成績結果 | 完成 |
UI界面設計 | 設計軟件的GUI界面 | 完成 |
UI界面實現 | 用Javafx實現GUI界面 | 完成 |
功能測試與故障修復 | 測試程序的功能,修復出現的故障 | 完成 |
性能分析與優化 | 分析程序執行的性能,優化性能表現 | 完成 |
三、實現方案
3.1 項目結構
結構說明:
上圖展示了程序中核心的類和方法,其中GenerateController負責生成題目的功能,CheckController負責批卷功能,兩者都依賴於底層的GenerateService,而GenerateService中進行四則運算的功能依賴於OperationService
3.2 代碼說明
3.2.1 出題功能代碼
/**
* 生成指定數目包含答案的有效題目
*/
@Override
public void generateExercises(int exercisesNum, int numRange) throws IOException {
int count = 0;
while (count < exercisesNum) {
Exercises exercises = generateQuestion(numRange);
generateAnswer(exercises);
//有效題目加入隊列
if (validate(exercises, exercisesSet)) {
//生成可以輸出的題目樣式
exercisesQueue.add(exercises);
count++;
//放入查重集合
exercisesSet.add(exercises.getSimplestFormatQuestion()); exercisesSet.add(CalculateUtil.getEqualsExpression(exercises.getSimplestFormatQuestion()));
}
}
}
/**
* 生成運算式
*/
private Exercises generateQuestion(int numRange) {
Exercises e = new Exercises();
int signalNum = CalculateUtil.getRandomNum(1, 3);
e.setOperatorNumber(signalNum);
for (int i = 0; i < e.getOperatorNumber(); i++) {
//添加運算符
e.addValue(i, generateOperator());
}
for (int i = e.getOperatorNumber(); i < 2 * e.getOperatorNumber() + 1; i++) {
//添加運算數
e.addValue(i, generateNum(numRange));
}
return e;
}
實現思路:
- 隨機生成運算符個數
- 根據運算符個數隨機生成運算符
- 根據運算符個數和限定的數值范圍隨機生成操作數
- 根據生成的表達式求解答案,求解過程發現表達式不合理就返回null
- 上一步合法的表達式,根據查重集合檢查是否重復,如果不合法,返回第1步
- 上一步合法的表達式,加入待輸出隊列和查重集合,如果重復,返回第1步
- 循環以上1-6,直到生成指定數量的題目
3.2.3 批卷功能代碼
/**
* 解析題目文件和答案文件
*/
public List<Exercises> readFile(File exercisesFile, File answerFile) throws Exception {
BufferedReader exercisesReader = FileUtil.getBufferedReader(exercisesFile);
BufferedReader answerReader = FileUtil.getBufferedReader(answerFile);
String question;
List<Exercises> exercisesList = new LinkedList<>();
while ((question = exercisesReader.readLine()) != null) {
String answer = answerReader.readLine();
//解析成Exercises對象
Exercises exercises = CalculateUtil.parseExercises(question);
//填入學生答案
exercises.setStudentAnswer(CalculateUtil.parseAnswer(answer));
exercisesList.add(exercises);
}
return exercisesList;
}
/**
* 批卷功能
*/
public Result checkAnswer(List<Exercises> exercises) throws IOException {
Result result = new Result();
for (Exercises e : exercises) {
generateAnswer(e);
//填入表格
CheckController.CHECK_EXERCISES_OBSERVABLE_LIST.add(e);
if (e.getAnswer().equalsIgnoreCase(e.getStudentAnswer())) {
result.getCorrectList().add(e.getNumber());
} else {
result.getWrongList().add(e.getNumber());
}
}
//將結果輸出到文件
writeCheckResultToFile(result);
return result;
}
實現思路:
- 讀取題目文件和學生答案,逐行解析為題目和答案
- 將解析出的題目和學生答案加入集合
- 從集合取出題目,計算正確答案
- 比較正確答案和學生答案
- 若上一步答案正確,把題號加入正確題目集合,若錯誤,把題號加入錯誤題目集合
- 循環以上1-5,直達文件讀取完畢
3.2.3 四則運算功能代碼
/**
* 生成答案
*/
@Override
public void generateAnswer(Exercises e) {
Queue<String> queue = new LinkedList<>();
ArrayList<String> eValueList = e.getValueList();
//將所有運算數進隊列
for (int i = 2 * e.getOperatorNumber(); i > e.getOperatorNumber() - 1; i--) {
queue.add(eValueList.get(i));
}
//取出每個運算符,再從隊列取出兩個數字進行運算,結果再放入隊尾中,直到取完所有運算符,此時隊列中的數字為最終答案
for (int i = e.getOperatorNumber() - 1; i >= 0; i--) {
String opSymbol = eValueList.get(i);
//從隊列取出兩個數字
String num1 = queue.remove();
String num2 = queue.remove();
//計算兩數運算后結果
String answer = OperatorEnum.getEnumByOpSymbol(opSymbol).op(num1, num2);
//計算過程出現不符合條件的數值,就返回null
if (answer == null) {
e.setAnswer(null);
return;
}
queue.add(answer);
}
e.setAnswer(queue.remove());
}
實現思路:
- 將題目的所有操作數加入隊列
- 如果還有剩余的運算符,取出題目的一個運算符,如果沒有,跳到第7步
- 從隊列頭部取出兩個操作數
- 將2和3取出的操作數和運算進行四則運算
- 如果四則運算返回的結果為null,則結束,返回答案為null
- 將4的結果加入到隊列中,返回第2步
- 取出隊列中元素作為答案返回(此時只剩一個元素)
四、效能分析
4.1 程序效能
這是先后兩次執行生成50000道題目功能時程序的資源消耗情況,可以得到以下結論:
- 內存占用:剛開始執行功能時占用內存短暫上升,但在功能執行結束后很快觸發了GC,內存得到回收
- 線程消耗:第一次執行時創建了線程,第二次執行時沒有創建新線程,說明線程池中線程得到重用
- 線程阻塞:整個過程只出現一次短暫的線程阻塞
- CPU占用:整個任務過程中CPU負載較高,屬於CPU密集型應用
4.2 性能優化
以下測試皆為程序執行生成10000道題目時的性能表現
4.2.1 優化前:(執行過程消耗時間為:74s)
這是優化前的程序執行生成10000道題目的性能表現,可以看到程序中性能消耗最大的函數是GenerateServiceImpl類的validate函數,其中執行List.contains方法的時間占用達到了96%
4.2.2 查重算法優化后:(執行過程消耗時間為:11s)
由於查重算法中,把題目是否重復的判斷寫在equals方法中,每次比較都要重新分析題目的運算次序,並且validate方法中直接調用了List.contain方法,從源碼來看,contain的內部是逐個遍歷再調用equals方法,效率很低,因此改成每次生成題目后,解析出題目的最簡式,用一個HashSet去保存題目的最簡式,查重時調用Set.contain方法,其內部實現時哈希尋址,因此效率得到提高。改進查重方法后,validate依然是消耗最大的函數,但占比已經下降到27%.
4.2.3 多線程並發優化后(執行過程消耗時間為:1s)
這次改進在查重算法改進的基礎至上,將生成題目和輸出到文件的方法進行分離,引入線程池和多線程並發執行,最終將生成10000道題的時間消耗降低至1s
五、測試報告
5.1 測試項:生成題目和答案是否符合要求
結果說明:
以上為測試生成10道數值在2以內的題目的截圖,可以看到題目符合要求且答案正確
5.2 測試項:批卷功能是否正確判斷答案正確與否
修改上一步中輸出的Answer.txt中奇數題號的答案,再使用批卷功能
查看輸出的Grade.txt文件:
結果說明:
其中標出來奇數編號被修改的題目為錯誤題號,結果符合預期
5.3 測試項:能否支持一萬道以上大量題目的生成
在出題模式下執行生成10000道數值范圍在5以內的題目的功能
查看生成的Exercises.txt文件
查看生成的Answer.txt文件
結果說明:
可以看到程序正確地執行了生成10000道題目的功能,消耗時間1s
六、PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 30 | 30 |
· Estimate | · 估計這個任務需要多少時間 | 30 | 30 |
Development | 開發 | 1755 | 2145 |
· Analysis | · 需求分析 (包括學習新技術) | 130 | 180 |
· Design Spec | · 生成設計文檔 | 60 | 35 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 5 | 5 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 10 | 5 |
· Design | · 具體設計 | 200 | 120 |
· Coding | · 具體編碼 | 1200 | 1500 |
· Code Review | · 代碼復審 | 30 | 120 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 120 | 180 |
Reporting | 報告 | 85 | 130 |
· Test Report | · 測試報告 | 60 | 30 |
· Size Measurement | · 計算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 15 | 90 |
合計 | 1870 | 2305 |
七、總結
獲得的經驗:
1.實現的過程中到了數據結構的知識,加深對數據結構的理解
2.對項目進行性能優化,加深對多線程知識的理解
3.增加了協作開發的經驗
不足的地方:
用Java來實現數據結構的效率不夠高,GUI界面沒有做響應式編程