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









