Java计算器源代码:从界面布局到加减乘除的完整实现

AI绘画教程21小时前发布 jdkjadmin
0 00
Java计算器源代码:从界面布局到加减乘除的完整实现

Java计算器源代码:从界面布局到加减乘除的完整实现

本文从一个可运行的 Java 桌面程序出发,实现具备数字输入、小数点、加减乘除、退格和清空功能的计算器。示例采用 Swing 创建界面,以 BigDecimal 处理十进制数值,并明确处理除数为零、输入长度和结果过长等情况。代码不依赖第三方库,适合用来学习 Swing 布局、事件监听和简单的计算器状态管理。

这个示例面向练习、演示和普通的桌面计算需求,不是金融、科学计算或安全关键系统的数值引擎。代码中的长度限制和除法精度是为了让界面行为可控,不代表 Java 或 BigDecimal 本身的通用限制。实际使用时,应根据业务规则重新定义精度、舍入方式和输入范围。

适用场景与实现思路

小型计算器看似只需要给按钮绑定加减乘除,真正容易出错的地方却是输入状态:用户按下运算符后,接下来输入的数字应当开始一个新操作数;用户连续按两个运算符时,是替换运算符还是执行前一个运算;按等号后再输入数字,是接着编辑结果还是重新输入。若没有明确的状态规则,事件处理代码很快就会变得混乱。

本文的实现采用“累加器 + 当前运算符”的简单模型。display 保存屏幕上的当前输入,accumulator 保存待计算的左操作数,pendingOperator 保存尚未执行的运算符,startNewNumber 表示下一次数字输入是否要开始新的操作数。按下第二个运算符时,会先计算前一个运算,再保存新的运算符,因此支持按键顺序计算,例如按下“8 + 2 × 3 =”,结果是 30,而不是遵循常见算术优先级得到 14。

这种顺序计算方式适合入门演示,但不适合需要完整表达式语义的场景。如果要支持括号、运算优先级、变量或复杂表达式,需要另行设计表达式解析器,不能只靠一个累加器完成。

完整可运行代码

将下面代码保存为 CalculatorApp.java。使用安装了 JDK 的环境编译、运行即可:先执行 javac CalculatorApp.java,再执行 java CalculatorApp。程序使用 Java 标准库中的 Swing 和 BigDecimal,无需下载依赖。

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.math.BigDecimal;
import java.math.RoundingMode;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class CalculatorApp {

    // 限制一次输入的字符数,包含小数点。
    private static final int MAX_INPUT_LENGTH = 16;

    // 限制屏幕结果的字符数,避免界面被超长结果撑开。
    private static final int MAX_DISPLAY_LENGTH = 32;

    // 除法结果最多保留 12 位小数,采用四舍五入。
    private static final int DIVISION_SCALE = 12;

    private final JTextField display = new JTextField("0");

    private BigDecimal accumulator = BigDecimal.ZERO;
    private Character pendingOperator = null;
    private boolean startNewNumber = true;
    private boolean errorState = false;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new CalculatorApp().createAndShow());
    }

    private void createAndShow() {
        JFrame frame = new JFrame("Java 计算器");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(320, 420);
        frame.setLocationRelativeTo(null);
        frame.setLayout(new BorderLayout(8, 8));

        display.setEditable(false);
        display.setHorizontalAlignment(JTextField.RIGHT);
        display.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 30));
        frame.add(display, BorderLayout.NORTH);

        JPanel panel = new JPanel(new GridLayout(5, 4, 6, 6));
        String[] keys = {
            "C", "⌫", "÷", "×",
            "7", "8", "9", "−",
            "4", "5", "6", "+",
            "1", "2", "3", "=",
            "±", "0", ".", "="
        };

        for (String key : keys) {
            JButton button = new JButton(key);
            button.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 22));
            button.addActionListener(event -> handleKey(key));
            panel.add(button);
        }

        frame.add(panel, BorderLayout.CENTER);
        frame.setVisible(true);
    }

    private void handleKey(String key) {
        if (isDigit(key)) {
            inputDigit(key);
            return;
        }

        switch (key) {
            case ".":
                inputDecimalPoint();
                break;
            case "+":
                chooseOperator('+');
                break;
            case "−":
                chooseOperator('-');
                break;
            case "×":
                chooseOperator('*');
                break;
            case "÷":
                chooseOperator('/');
                break;
            case "=":
                calculateResult();
                break;
            case "C":
                clearAll();
                break;
            case "⌫":
                backspace();
                break;
            case "±":
                toggleSign();
                break;
            default:
                // 未定义的按钮不执行操作。
                break;
        }
    }

    private boolean isDigit(String key) {
        return key.length() == 1 && key.charAt(0) >= '0'
                && key.charAt(0) <= '9';
    }

    private void inputDigit(String digit) {
        if (errorState) {
            clearAll();
        }

        String current = display.getText();

        if (startNewNumber) {
            display.setText(digit);
            startNewNumber = false;
            return;
        }

        if (current.length() >= MAX_INPUT_LENGTH) {
            return;
        }

        if ("0".equals(current)) {
            display.setText(digit);
        } else if ("-0".equals(current)) {
            display.setText("-" + digit);
        } else {
            display.setText(current + digit);
        }
    }

    private void inputDecimalPoint() {
        if (errorState) {
            clearAll();
        }

        if (startNewNumber) {
            display.setText("0.");
            startNewNumber = false;
            return;
        }

        if (!display.getText().contains(".")
                && display.getText().length() < MAX_INPUT_LENGTH) {
            display.setText(display.getText() + ".");
        }
    }

    private void chooseOperator(char operator) {
        if (errorState) {
            return;
        }

        try {
            BigDecimal current = readDisplay();

            if (pendingOperator != null && !startNewNumber) {
                accumulator = apply(accumulator, current, pendingOperator);
                showValue(accumulator);
            } else if (pendingOperator == null) {
                accumulator = current;
            }

            pendingOperator = operator;
            startNewNumber = true;
        } catch (ArithmeticException ex) {
            showError();
        }
    }

    private void calculateResult() {
        if (errorState || pendingOperator == null || startNewNumber) {
            return;
        }

        try {
            BigDecimal current = readDisplay();
            BigDecimal result = apply(accumulator, current, pendingOperator);

            showValue(result);
            accumulator = result;
            pendingOperator = null;
            startNewNumber = true;
        } catch (ArithmeticException ex) {
            showError();
        }
    }

    private BigDecimal apply(BigDecimal left, BigDecimal right, char operator) {
        switch (operator) {
            case '+':
                return checkedResult(left.add(right));
            case '-':
                return checkedResult(left.subtract(right));
            case '*':
                return checkedResult(left.multiply(right));
            case '/':
                if (right.compareTo(BigDecimal.ZERO) == 0) {
                    throw new ArithmeticException("除数不能为零");
                }
                return checkedResult(
                        left.divide(right, DIVISION_SCALE, RoundingMode.HALF_UP));
            default:
                throw new IllegalArgumentException("未知运算符:" + operator);
        }
    }

    private BigDecimal checkedResult(BigDecimal value) {
        String text = format(value);
        if (text.length() > MAX_DISPLAY_LENGTH) {
            throw new ArithmeticException("结果过长");
        }
        return value;
    }

    private BigDecimal readDisplay() {
        return new BigDecimal(display.getText());
    }

    private String format(BigDecimal value) {
        if (value.compareTo(BigDecimal.ZERO) == 0) {
            return "0";
        }
        return value.stripTrailingZeros().toPlainString();
    }

    private void showValue(BigDecimal value) {
        String text = format(value);
        if (text.length() > MAX_DISPLAY_LENGTH) {
            throw new ArithmeticException("结果过长");
        }
        display.setText(text);
    }

    private void toggleSign() {
        if (errorState) {
            clearAll();
            return;
        }

        String current = display.getText();
        if ("0".equals(current) || "0.".equals(current)) {
            return;
        }

        if (current.startsWith("-")) {
            display.setText(current.substring(1));
        } else {
            display.setText("-" + current);
        }
        startNewNumber = false;
    }

    private void backspace() {
        if (errorState) {
            clearAll();
            return;
        }

        if (startNewNumber) {
            display.setText("0");
            startNewNumber = false;
            return;
        }

        String current = display.getText();
        if (current.length() <= 1
                || (current.length() == 2 && current.startsWith("-"))) {
            display.setText("0");
        } else {
            display.setText(current.substring(0, current.length() - 1));
            if ("-".equals(display.getText())) {
                display.setText("0");
            }
        }
    }

    private void clearAll() {
        display.setText("0");
        accumulator = BigDecimal.ZERO;
        pendingOperator = null;
        startNewNumber = true;
        errorState = false;
    }

    private void showError() {
        display.setText("错误");
        accumulator = BigDecimal.ZERO;
        pendingOperator = null;
        startNewNumber = true;
        errorState = true;
    }
}

界面布局与按钮事件

程序使用一个 JFrame 作为窗口。顶部是不可编辑、右对齐的 JTextField,下方是采用 GridLayout 排列的按钮面板。网格共有五行四列,数字和运算符通过字符串数组创建,因此添加或调整按钮时,不必为每个按钮重复编写一套界面代码。

按钮的监听器将按钮文字传给 handleKey。该方法先识别数字,再用 switch 分发小数点、运算符、等号、清空、退格和正负号操作。这里选择用屏幕文字区分按钮功能,代码容易阅读;更大型的程序可以为按钮设置单独的命令或枚举,避免界面显示文案与业务逻辑耦合。

创建 Swing 窗口的代码通过 SwingUtilities.invokeLater 放到事件分派线程执行。Swing 组件通常应在该线程上创建和更新;把界面操作放到任意后台线程,可能导致难以复现的界面问题。本例的计算量很小,事件处理直接完成即可。若以后加入耗时任务,不应在事件分派线程中长时间执行,而应使用适当的后台任务机制。

计算状态如何变化

开始时屏幕显示 0,累加器为零,没有待执行的运算符。用户输入“8”,屏幕变为 8。按“+”后,当前值保存到累加器,待执行运算符设为加号,并将 startNewNumber 设为 true。随后输入“2”,由于标志为真,程序用新数字替换屏幕内容,而不是把它拼到 8 后面。

按“=”时,程序将屏幕内容解析为第二个操作数,调用 apply 执行运算,然后显示结果并清除待执行运算符。因而输入“8 + 2 =”会显示 10。等号之后的数字输入会开始新的输入;本实现不支持常见计算器中的重复等号行为,例如再按一次等号自动加上上次的第二个操作数。若产品需要这种行为,应保存上一次运算符和右操作数,并明确设计相应状态转换。

连续按运算符时也有具体规则。例如输入“8 + 2 ×”,按下乘号时会先计算前一项,累加器变成 10,然后等待新的操作数。最后输入“3 =”得到 30。这里不是数学表达式的运算优先级规则,而是按键顺序执行。实现时把这个规则写清楚很重要,否则用户容易将结果误认为程序计算错误。

如果用户输入“8 + × 3 =”,第二个运算符在还没有输入新的右操作数时会替换加号,不会先拿屏幕上原有的 8 再计算一次。这样的行为由 chooseOperator 中的 !startNewNumber 条件控制。小型界面可以采用这种直观规则;如果需要与某种特定计算器产品完全一致,应针对连续运算符、等号后输入和清空等行为编写测试并固定预期。

为什么这里使用 BigDecimal

Java 的 double 使用二进制浮点数表示。部分十进制小数无法精确表示,因此用 double 计算时可能看到类似 0.30000000000000004 的结果。对展示十进制输入结果的计算器来说,BigDecimal 更适合表达十进制数值。

代码通过 new BigDecimal(display.getText()) 从屏幕上的十进制字符串创建数值,而不是先转成 double 再创建。后一种写法会把二进制浮点近似值带入十进制计算,失去使用 BigDecimal 的一部分意义。

加法、减法和乘法在本例中按 BigDecimal 的规则计算。除法则通过 divide(right, DIVISION_SCALE, RoundingMode.HALF_UP) 指定最多保留 12 位小数,并使用 HALF_UP 舍入。由于许多除法结果是无限循环小数,例如 1 除以 3,必须明确指定精度或舍入规则;直接调用没有精度参数的 divide,在结果无法精确表示时会抛出算术异常。

12 位小数是本示例的产品选择,不是 BigDecimal 的固有限制。金融业务可能要求固定小数位、特定舍入规则,或按业务阶段分别舍入;科学计算可能需要不同精度。不能因为程序能显示很多位数字,就推断结果符合特定行业规范。

format 方法使用 stripTrailingZeros 去掉不必要的末尾零,并调用 toPlainString 避免输出科学计数法。这样整数结果不会显示成带小数点的形式,普通小数也不会出现多余的末尾零。对于数值零,单独返回字符串 0,避免负零或带很多小数位的零影响显示。

输入限制、异常与边界

  • 单次输入长度:MAX_INPUT_LENGTH 限制用户通过按钮输入的字符数,包含小数点。达到上限后,继续输入的数字或小数点会被忽略。这是界面级限制,不等于 BigDecimal 的最大位数,也不能代替对外部输入的校验。
  • 结果显示长度:MAX_DISPLAY_LENGTH 限制格式化结果的字符数。超出时程序显示“错误”并清除当前待计算状态。这是示例为了控制界面显示做的取舍;正式应用也可以改为横向滚动、缩放字体或提示用户缩小输入范围。
  • 除数为零:使用 compareTo(BigDecimal.ZERO) 判断数值是否为零。不能只比较字符串是否等于 "0",因为 0.0、0.00 等文本同样表示零。
  • 小数点:同一个操作数只允许输入一个小数点;直接按小数点会得到 0.,随后可以继续输入数字。该界面使用英文句点作为小数分隔符,不会根据系统地区自动转换逗号或其他格式。
  • 负数与退格:正负号按钮对当前输入取反,退格时会避免留下单独的负号。按退格的具体含义由代码定义:若当前处于等待新操作数状态,会把屏幕重置为零,而不是撤销已保存的运算符。

程序将可预期的算术问题,例如除以零和结果过长,转换成界面上的“错误”状态,而不是让异常直接中断用户操作。出现错误后,数字输入会先清空状态再开始新输入;按清空按钮则立即恢复初始状态。示例没有展示异常的详细原因,也没有记录日志。真实产品可以把错误提示得更具体,但应避免把内部堆栈信息直接暴露给普通用户。

常见错误与排查方式

1. 使用 double 直接累加

若业务要求输入的十进制数按十进制方式展示,直接使用 double 容易出现浮点误差。改用 BigDecimal 时,应从字符串创建,并为除法明确指定小数位数和舍入方式。若只是普通物理或图形计算,浮点数可能更合适;选择应取决于数值模型和精度需求,而不是简单地认为某种类型总是更准确。

2. 把显示文本当作计算状态

屏幕文本是界面的一部分,不应承担全部计算状态。只修改显示字符串而不更新累加器或运算符,会造成显示看似正确、下一步计算却使用旧值的情况。本文分开保存屏幕内容、累加器、待执行运算符和新输入标志,方便理解每次按键对状态的影响。

3. 忘记连续按键的状态规则

运算符连续输入、等号后输入数字、按退格时恰好剩下负号等情况,都需要提前定义行为。不能只测试“2 + 3 =”这一条路径。至少应手动检查空白状态按运算符、连续运算符、连续小数点、除以零、负数退格和结果过长等情况。

4. 除法没有规定精度

BigDecimal.divide 在结果无法精确表示时可能抛出异常。正确做法不是捕获后悄悄返回零,而是根据需求指定精度和舍入模式,并把该规则作为计算器行为的一部分说明。本示例使用 12 位小数和 HALF_UP,并不适用于所有业务。

5. 只在输入时做限制,忽略运算结果

限制输入字符数不能保证计算结果永远较短。乘法和连续运算可能产生越来越长的数值,所以示例在结果格式化后再次检查屏幕长度。实际应用可选择拒绝、截断显示或采用科学计数法,但“显示截断”和“数值截断”不是一回事,不应在未告知用户的情况下改变计算结果。

6. 在错误的线程创建 Swing 界面

Swing 界面应在事件分派线程中创建和更新。本例在 main 方法中使用 SwingUtilities.invokeLater。如果只在单线程的小程序里偶然不使用它,程序可能仍能运行,但养成正确习惯有助于避免后续扩展时产生线程问题。

运行验证建议

编译成功后,可以按下面的序列检查关键行为:

  • 输入 8 + 2 =,应显示 10。
  • 输入 8 + 2 × 3 =,按键顺序计算,应显示 30。
  • 输入 1 ÷ 3 =,应显示有限位数的小数,而不是无限循环。
  • 输入 5 ÷ 0 =,应显示“错误”;之后输入数字应能重新开始。
  • 输入 0.0 作为除数,也应按零处理。
  • 连续输入两个小数点,第二个小数点应被忽略。
  • 输入负数后多次退格,不应留下单独的负号。

这些验证是行为检查,不替代正式的自动化测试。扩展功能后,建议把纯计算逻辑从 Swing 界面中拆出来,针对加减乘除、零值比较、舍入和异常边界编写单元测试;界面层则测试按钮事件是否正确调用逻辑层。这样比在一个窗口类中同时混写布局、状态和全部计算规则更容易维护。

安全与使用范围

这个示例只响应程序内置的按钮输入,不读取文件、网络或环境变量,也不执行用户输入的表达式。它不使用 eval 一类动态代码执行方式。BigDecimal 适合表示十进制数值,但并不自动让整个程序具备安全性,也不会替代输入校验、访问控制或业务审计。

如果把计算器改成网页接口、命令行服务或接收外部文本的程序,必须在服务端校验输入格式、长度和允许范围,并考虑请求频率、资源消耗和错误处理。不能只依赖前端按钮限制,因为调用方可以绕过界面直接提交数据。对于涉及付款、计费、税务或账务的用途,还应依照业务规则确定小数精度、舍入时机、最大值、溢出策略和结果复核方式,并通过测试和审查验证;本文代码没有经过此类用途的认证或合规验证。

总结

一个可用的基础计算器,不只是把四个运算符绑定到按钮上。它还需要明确界面状态、数字输入规则、运算顺序、除法精度、错误恢复和显示边界。本文给出的 Swing 程序可以直接编译运行,适合作为入门示例,也展示了用 BigDecimal 从十进制字符串进行计算的基本方式。

后续可以在此基础上增加键盘输入、括号和运算优先级、历史记录或本地化显示。不过每增加一种能力,都应同时补充对应的状态规则和边界测试。将界面与计算逻辑逐步分离,再明确业务精度与安全要求,才是从练习程序走向可维护应用的关键。

© 版权声明

相关文章