計(jì)算器課設(shè)源碼與報(bào)告:Swing表達(dá)式解析實(shí)戰(zhàn))
簡(jiǎn)介這份資源是廣東工業(yè)大學(xué)Java語言課程設(shè)計(jì)的科學(xué)計(jì)算器項(xiàng)目包含完整源碼與課設(shè)報(bào)告面向正在學(xué)習(xí)Java、需要完成課程設(shè)計(jì)或想通過實(shí)戰(zhàn)理解面向?qū)ο缶幊痰膶W(xué)生。壓縮包共7個(gè)文件約499KB以5個(gè)java源文件為核心另有1份doc課設(shè)報(bào)告和1個(gè)form界面文件源碼覆蓋類與對(duì)象設(shè)計(jì)、控制臺(tái)輸入輸出、異常處理等基礎(chǔ)內(nèi)容報(bào)告則記錄需求分析、設(shè)計(jì)與實(shí)現(xiàn)、測(cè)試調(diào)試及總結(jié)反思。項(xiàng)目實(shí)現(xiàn)了加減乘除及冪、平方根、對(duì)數(shù)等科學(xué)運(yùn)算并涉及表達(dá)式詞法分析、語法分析與運(yùn)算符優(yōu)先級(jí)處理還討論了策略模式、繼承多態(tài)等設(shè)計(jì)思路以及復(fù)數(shù)運(yùn)算、角度弧度轉(zhuǎn)換、三角函數(shù)等擴(kuò)展功能。已有902人學(xué)習(xí)下載適合對(duì)照源碼理解完整開發(fā)流程從需求分析到編碼實(shí)現(xiàn)再到測(cè)試維護(hù)可作為課設(shè)參考與Java進(jìn)階練習(xí)素材。1. 從一份課設(shè)作業(yè)說起為什么這個(gè) Java 科學(xué)計(jì)算器值得拆一遍很多計(jì)算機(jī)專業(yè)的學(xué)生都經(jīng)歷過類似的場(chǎng)景期末大作業(yè)要求做一個(gè)帶圖形界面的科學(xué)計(jì)算器功能要覆蓋三角函數(shù)、對(duì)數(shù)、階乘、冪運(yùn)算還得附一份課設(shè)報(bào)告。網(wǎng)上搜到的代碼要么只有界面沒有邏輯要么邏輯寫死了沒法擴(kuò)展要么干脆跑不起來。這份 Java 科學(xué)計(jì)算器源碼及課設(shè)報(bào)告解決的正是這個(gè)具體問題——它提供了一套可運(yùn)行的完整工程同時(shí)附帶一份結(jié)構(gòu)清晰的課設(shè)報(bào)告方便你對(duì)照理解設(shè)計(jì)思路。它適合兩類人一是正在做 Java 課程設(shè)計(jì)、需要參考完整實(shí)現(xiàn)的學(xué)生二是想通過一個(gè)中小型 Swing 項(xiàng)目練手、理解事件驅(qū)動(dòng)編程和表達(dá)式解析的開發(fā)者。項(xiàng)目本身不復(fù)雜但麻雀雖小五臟俱全從 UI 布局到計(jì)算核心再到異常處理該有的都有。2. 項(xiàng)目結(jié)構(gòu)與環(huán)境搭建從零把工程跑起來2.1 技術(shù)棧與目錄結(jié)構(gòu)這個(gè)項(xiàng)目用的是 Java SE 基礎(chǔ)技術(shù)棧沒有引入任何第三方框架。GUI 部分基于 Swing計(jì)算核心是純 Java 實(shí)現(xiàn)不依賴外部數(shù)學(xué)庫。這種選型的好處是零配置、跨平臺(tái)只要有 JDK 就能跑不需要額外配 Maven 或 Gradle。常見的目錄結(jié)構(gòu)是這樣的ScientificCalculator/ ├── src/ │ ├── main/ │ │ ├── CalculatorMain.java // 程序入口 │ │ ├── ui/ │ │ │ ├── CalculatorFrame.java // 主窗口 │ │ │ └── ButtonPanel.java // 按鈕面板 │ │ ├── core/ │ │ │ ├── ExpressionParser.java // 表達(dá)式解析 │ │ │ └── MathFunctions.java // 數(shù)學(xué)函數(shù)封裝 │ │ └── util/ │ │ └── NumberFormatter.java // 結(jié)果格式化 │ └── test/ │ └── CalculatorTest.java // 簡(jiǎn)單測(cè)試類 ├── lib/ // 空目錄預(yù)留 ├── doc/ │ └── 課設(shè)報(bào)告.docx // 課程設(shè)計(jì)報(bào)告 └── README.md如果你拿到的源碼沒有分這么細(xì)所有類都堆在一個(gè)包下也不影響運(yùn)行只是后續(xù)改起來會(huì)累一點(diǎn)。我一般會(huì)先按功能把類歸到ui、core、util三個(gè)包下再開始讀代碼。2.2 環(huán)境準(zhǔn)備與編譯運(yùn)行需要 JDK 8 或以上版本。檢查環(huán)境java -version javac -version兩個(gè)命令都能輸出版本號(hào)即可。如果提示找不到命令先把 JDK 的bin目錄加到系統(tǒng)環(huán)境變量PATH里。編譯和運(yùn)行有兩種方式。第一種是命令行直接操作# 進(jìn)入源碼根目錄 cd ScientificCalculator/src # 編譯所有 Java 文件到 out 目錄 javac -d ../out main/CalculatorMain.java main/ui/*.java main/core/*.java main/util/*.java # 運(yùn)行主類 java -cp ../out main.CalculatorMain第二種是用 IDE。Eclipse 或 IntelliJ IDEA 都行新建一個(gè) Java 項(xiàng)目把src目錄設(shè)為源碼根然后直接右鍵CalculatorMain.java運(yùn)行。IDEA 用戶注意把語言級(jí)別調(diào)到 8 以上否則某些 lambda 寫法會(huì)報(bào)錯(cuò)。提示如果編譯時(shí)報(bào)「找不到符號(hào)」且指向 Swing 相關(guān)類檢查一下是不是誤用了java.awt下的同名類Swing 組件要以javax.swing開頭。2.3 啟動(dòng)后的界面與基本操作程序啟動(dòng)后會(huì)彈出一個(gè)窗口上半部分是結(jié)果顯示區(qū)下半部分是按鈕矩陣。數(shù)字鍵 0-9、小數(shù)點(diǎn)、加減乘除是基礎(chǔ)操作另外還有 sin、cos、tan、log、ln、sqrt、x^y、n! 這些科學(xué)計(jì)算按鈕。操作邏輯是點(diǎn)擊按鈕輸入表達(dá)式按等號(hào)計(jì)算結(jié)果。比如算 30 度的正弦輸入sin(30)然后按結(jié)果區(qū)顯示0.5。注意角度和弧度的切換有些版本默認(rèn)用弧度算出來是-0.988別以為是 bug。3. 核心模塊拆解表達(dá)式解析與計(jì)算邏輯怎么寫的3.1 表達(dá)式解析器的設(shè)計(jì)思路科學(xué)計(jì)算器和普通計(jì)算器最大的區(qū)別在于它不能只做二元運(yùn)算得能處理帶括號(hào)、帶函數(shù)名的復(fù)合表達(dá)式。常見做法有兩種一種是基于棧的中綴轉(zhuǎn)后綴調(diào)度場(chǎng)算法另一種是遞歸下降解析。這個(gè)項(xiàng)目用的是第一種實(shí)現(xiàn)起來直觀調(diào)試也方便。核心類ExpressionParser暴露一個(gè)evaluate(String expr)方法內(nèi)部先把中綴表達(dá)式轉(zhuǎn)成后綴表達(dá)式再對(duì)后綴表達(dá)式求值。下面是我整理后的關(guān)鍵代碼public class ExpressionParser { // 運(yùn)算符優(yōu)先級(jí)數(shù)字越大優(yōu)先級(jí)越高 private static int priority(String op) { switch (op) { case : case -: return 1; case *: case /: return 2; case ^: return 3; default: return 0; } } // 中綴表達(dá)式轉(zhuǎn)后綴表達(dá)式 public static ListString infixToPostfix(String expr) { ListString output new ArrayList(); StackString ops new Stack(); // 按字符逐個(gè)掃描這里簡(jiǎn)化處理實(shí)際要處理多字符函數(shù)名 for (int i 0; i expr.length(); i) { char c expr.charAt(i); if (Character.isDigit(c) || c .) { // 數(shù)字直接輸出連續(xù)數(shù)字要拼起來 StringBuilder num new StringBuilder(); while (i expr.length() (Character.isDigit(expr.charAt(i)) || expr.charAt(i) .)) { num.append(expr.charAt(i)); } i--; output.add(num.toString()); } else if (c () { ops.push((); } else if (c )) { while (!ops.peek().equals(()) { output.add(ops.pop()); } ops.pop(); // 彈出左括號(hào) } else { String op String.valueOf(c); while (!ops.isEmpty() priority(ops.peek()) priority(op)) { output.add(ops.pop()); } ops.push(op); } } while (!ops.isEmpty()) { output.add(ops.pop()); } return output; } }這段代碼的邏輯說明priority方法定義了運(yùn)算符優(yōu)先級(jí)^最高乘除次之加減最低。infixToPostfix方法掃描輸入字符串遇到數(shù)字就拼成完整操作數(shù)輸出遇到左括號(hào)壓棧遇到右括號(hào)彈棧直到左括號(hào)遇到運(yùn)算符則比較優(yōu)先級(jí)決定是否彈出棧頂運(yùn)算符。最后把棧里剩余的運(yùn)算符全部彈出。參數(shù)方面expr是用戶輸入的原始表達(dá)式字符串返回值是后綴表達(dá)式列表。注意這里對(duì)函數(shù)名如sin、log的處理被簡(jiǎn)化了實(shí)際項(xiàng)目中需要在掃描時(shí)識(shí)別連續(xù)字母并作為整體處理。3.2 數(shù)學(xué)函數(shù)封裝與結(jié)果格式化后綴表達(dá)式求值階段遇到函數(shù)名就調(diào)用MathFunctions里對(duì)應(yīng)的靜態(tài)方法。這個(gè)類把 JDK 自帶的Math類包了一層主要做兩件事角度弧度轉(zhuǎn)換和異常處理。public class MathFunctions { // 角度制正弦輸入是角度值 public static double sin(double degree) { return Math.sin(Math.toRadians(degree)); } // 階乘只支持非負(fù)整數(shù) public static double factorial(int n) { if (n 0) { throw new IllegalArgumentException(階乘不支持負(fù)數(shù)); } double result 1; for (int i 2; i n; i) { result * i; } return result; } // 對(duì)數(shù)換底公式 public static double log(double value, double base) { if (value 0 || base 0 || base 1) { throw new IllegalArgumentException(對(duì)數(shù)參數(shù)不合法); } return Math.log(value) / Math.log(base); } }sin方法接收角度值內(nèi)部用Math.toRadians轉(zhuǎn)成弧度再調(diào) JDK 的Math.sin。factorial用循環(huán)累乘返回double是為了防止int溢出——20 的階乘已經(jīng)超出long的范圍了。log方法用換底公式支持任意底數(shù)同時(shí)做了參數(shù)校驗(yàn)。結(jié)果格式化交給NumberFormatter它負(fù)責(zé)把double結(jié)果轉(zhuǎn)成好看的字符串。比如0.30000000000000004要顯示成0.3整數(shù)結(jié)果不要帶.0。常見做法是用BigDecimal做四舍五入保留 10 位有效數(shù)字再去掉末尾多余的零。3.3 事件綁定與按鈕響應(yīng)Swing 的事件模型是觀察者模式每個(gè)按鈕注冊(cè)一個(gè)ActionListener點(diǎn)擊時(shí)觸發(fā)actionPerformed方法。這個(gè)項(xiàng)目里所有按鈕共用一個(gè)監(jiān)聽器通過getActionCommand()區(qū)分是哪個(gè)按鈕被按下。public class ButtonPanel extends JPanel { private JTextField display; private StringBuilder currentInput new StringBuilder(); public ButtonPanel(JTextField display) { this.display display; setLayout(new GridLayout(5, 4, 5, 5)); String[] labels { 7,8,9,/, 4,5,6,*, 1,2,3,-, 0,.,,, sin,cos,tan,sqrt }; for (String label : labels) { JButton btn new JButton(label); btn.addActionListener(new ButtonClickListener()); add(btn); } } private class ButtonClickListener implements ActionListener { Override public void actionPerformed(ActionEvent e) { String cmd e.getActionCommand(); if (.equals(cmd)) { // 調(diào)用解析器計(jì)算結(jié)果 try { double result ExpressionParser.evaluate(currentInput.toString()); display.setText(NumberFormatter.format(result)); } catch (Exception ex) { display.setText(錯(cuò)誤); } currentInput.setLength(0); } else { currentInput.append(cmd); display.setText(currentInput.toString()); } } } }ButtonClickListener是內(nèi)部類能直接訪問外部的currentInput和display。按等號(hào)時(shí)把累積的輸入串交給解析器捕獲異常后顯示「錯(cuò)誤」。其他按鈕直接把標(biāo)簽追加到輸入串末尾。這里有個(gè)細(xì)節(jié)sin、cos這些函數(shù)按鈕追加的是函數(shù)名加左括號(hào)實(shí)際項(xiàng)目中會(huì)在actionPerformed里做判斷自動(dòng)補(bǔ)括號(hào)。4. 避坑與常見問題跑不起來時(shí)先查這幾處4.1 編譯報(bào)錯(cuò)「編碼 GBK 的不可映射字符」現(xiàn)象javac編譯時(shí)提示某行有不可映射字符通常出現(xiàn)在中文注釋或中文字符串那一行。原因源碼文件保存成了 UTF-8但javac默認(rèn)用系統(tǒng)編碼Windows 中文版是 GBK去讀。解決編譯時(shí)顯式指定編碼javac -encoding UTF-8 -d ../out ...?;蛘咴?IDE 里把項(xiàng)目編碼統(tǒng)一設(shè)成 UTF-8文件編碼和編譯編碼保持一致。4.2 界面能出來但按鈕點(diǎn)了沒反應(yīng)現(xiàn)象窗口正常顯示按鈕也能按下去但顯示區(qū)沒變化。原因最常見的是ActionListener沒注冊(cè)上或者getActionCommand()返回的值和判斷條件對(duì)不上。比如按鈕標(biāo)簽是sin但判斷時(shí)寫的是Sin大小寫不一致就進(jìn)不了分支。解決在actionPerformed第一行加System.out.println(cmd)看實(shí)際輸出是什么再對(duì)照判斷條件改。另外檢查按鈕是否被其他組件遮擋Swing 里后添加的組件會(huì)蓋住先添加的。4.3 計(jì)算結(jié)果精度不對(duì)比如 sin(30) 算出負(fù)數(shù)現(xiàn)象輸入sin(30)期望0.5實(shí)際得到-0.988。原因把角度值直接傳給了Math.sin而Math.sin接收的是弧度。30 弧度約等于 1718 度正弦值自然是負(fù)的。解決確認(rèn)MathFunctions.sin里有沒有Math.toRadians轉(zhuǎn)換。如果沒有加上如果有檢查調(diào)用處是不是繞過了封裝直接用了Math.sin。4.4 連續(xù)運(yùn)算時(shí)結(jié)果被覆蓋現(xiàn)象算完123后想接著用 3 乘 4按*時(shí)顯示區(qū)清空了。原因按等號(hào)后currentInput被清空但沒把結(jié)果寫回去。下次輸入時(shí)從零開始。解決按等號(hào)計(jì)算出結(jié)果后把結(jié)果字符串重新賦給currentInput這樣后續(xù)操作可以基于上次結(jié)果繼續(xù)。同時(shí)用一個(gè)標(biāo)志位記錄「剛算完」下次按數(shù)字鍵時(shí)先清空再追加。4.5 階乘輸入大數(shù)導(dǎo)致界面卡死現(xiàn)象輸入1000!后程序無響應(yīng)。原因階乘結(jié)果是一個(gè)超過 2500 位的超大數(shù)double會(huì)溢出成Infinity循環(huán)本身也要跑 1000 次雖然不至于卡死但如果用遞歸實(shí)現(xiàn)且沒做限制棧會(huì)溢出。解決在factorial方法里加參數(shù)上限比如超過 170 就拋異常提示「數(shù)值過大」。170 是double能表示的最大階乘值再大就溢出了。5. 二次開發(fā)與改造技巧從能跑到好用5.1 把計(jì)算核心抽成獨(dú)立模塊如果你打算在這個(gè)項(xiàng)目基礎(chǔ)上做擴(kuò)展第一件事是把core包下的類抽出來做成不依賴 Swing 的純邏輯模塊。這樣你可以單獨(dú)寫單元測(cè)試也方便以后換 UI 框架——比如改成 JavaFX 或者命令行版本核心代碼一行不用動(dòng)。具體做法確保ExpressionParser和MathFunctions不 import 任何javax.swing或java.awt的類。檢查方法很簡(jiǎn)單在這兩個(gè)文件里搜import javax.swing有就刪掉把依賴倒過來——讓 UI 層調(diào) corecore 不感知 UI。5.2 增加歷史記錄與內(nèi)存功能科學(xué)計(jì)算器經(jīng)常需要復(fù)用上次結(jié)果。加一個(gè)Memory類內(nèi)部用一個(gè)double字段存值提供store、recall、clear三個(gè)方法。UI 上加三個(gè)按鈕 M、MR、MC分別對(duì)應(yīng)存、取、清。public class Memory { private double value 0; private boolean hasValue false; public void store(double v) { this.value v; this.hasValue true; } public double recall() { if (!hasValue) { throw new IllegalStateException(內(nèi)存為空); } return value; } public void clear() { value 0; hasValue false; } }hasValue標(biāo)志位用來區(qū)分「內(nèi)存里是 0」和「內(nèi)存里沒東西」避免用戶沒存過就取取出來一個(gè)默認(rèn) 0 還以為是之前存的。5.3 用表格對(duì)比改造前后的差異改造點(diǎn)改造前改造后收益核心模塊依賴core 依賴 uicore 獨(dú)立可單測(cè)、可換 UI內(nèi)存功能無M/MR/MC支持連續(xù)計(jì)算錯(cuò)誤提示統(tǒng)一顯示「錯(cuò)誤」區(qū)分「除數(shù)不能為0」「參數(shù)不合法」用戶知道哪錯(cuò)了結(jié)果精度直接輸出 doubleBigDecimal 保留 10 位有效數(shù)字顯示干凈5.4 課設(shè)報(bào)告的寫法建議報(bào)告部分別照抄代碼。老師想看的是你的設(shè)計(jì)思路為什么選后綴表達(dá)式而不是直接遞歸求值為什么用 Swing 而不是 JavaFX異常處理是怎么分層的。把「遇到什么問題、怎么解決的」寫清楚比堆砌代碼截圖得分高。我一般會(huì)按「需求分析 → 總體設(shè)計(jì) → 詳細(xì)設(shè)計(jì) → 測(cè)試 → 總結(jié)」五段來寫詳細(xì)設(shè)計(jì)里放類圖和關(guān)鍵方法說明測(cè)試部分列幾個(gè)典型用例和預(yù)期結(jié)果。這樣結(jié)構(gòu)清晰也方便答辯時(shí)對(duì)著講。從那以后我每次拿到一個(gè)課設(shè)項(xiàng)目都強(qiáng)制自己先把 core 層抽出來跑通單元測(cè)試再動(dòng) UI。這個(gè)習(xí)慣幫我省了很多來回調(diào)試界面的時(shí)間。希望這份拆解幫到你。本文還有配套的精品資源點(diǎn)擊獲取