Java计算器源代码详解:从界面搭建到运算逻辑

Java计算器源代码详解:从界面搭建到运算逻辑
计算器是练习 Java 桌面开发的经典小项目。它的界面看起来简单,却能集中练到 Swing 组件布局、事件监听、输入状态管理、十进制运算和异常处理。本文从需求与边界开始,逐步解释实现思路,并提供一份可直接编译运行的完整示例。
示例采用 Java Swing 实现,支持数字、小数点、加减乘除、正负号、清除、清除当前输入、退格和等号。运算按按键顺序立即执行,例如输入 2 + 3 × 4 =,结果为 20,而不是遵循通常的乘除优先级得到 14。这是许多基础计算器的行为;如果产品要求标准优先级,需要另行设计表达式解析器。
一、适用场景与实现边界
这份程序适合学习 Swing 事件驱动开发、制作简单桌面演示程序,或作为后续扩展的计算器原型。它不是科学计算器,也不是金融或安全关键系统的计算内核。程序仅接收按钮输入,不解析用户键入的任意表达式,因此没有表达式注入这一类输入路径;但这不代表它可以直接用于任何安全用途。涉及金额、计量、医疗或其他高后果场景时,必须根据业务单位、精度、舍入规则和验证要求单独设计并测试,不能把示例代码当作已经认证的计算组件。
- 输入长度:数字输入最多 30 个字符,限制的是正在编辑的输入文本,不是计算结果的位数。这个限制用于控制界面输入规模,不是 Java
BigDecimal的通用参数上限。 - 小数精度:加、减、乘使用
BigDecimal;除法结果保留 12 位小数,并按HALF_UP舍入。这是示例明确选择的显示精度,不代表适用于所有业务。 - 除数为零:显示错误状态,并要求用户重新开始,不返回无穷大或特殊数值。
- 运算顺序:每次按下新的运算符时,先完成已有的待处理运算,即从左到右连续计算。
- 键盘输入:示例只响应界面按钮。若要支持键盘,应增加键绑定,并复用同一套输入处理方法,而不是另写一套运算规则。
二、状态设计:按钮之外的核心
计算器不能只靠一个“当前显示值”完成所有操作。程序需要记住当前输入、上一阶段的结果以及等待执行的运算符。示例使用以下状态:
input:屏幕上显示的当前输入文本。使用字符串而非浮点数保存,可以保留用户正在输入的小数点,例如12.。storedValue:已经保存、等待参与下一次运算的数值。pendingOperator:尚未完成的运算符,例如+或×。startNewNumber:下一次输入数字时,是开始一个新数字,还是继续追加到当前数字。error:是否处于错误状态。发生除零后,除清除操作外,其他输入会被忽略。
例如,用户输入 8、按 +、再输入 5 时,程序先把 8 保存到 storedValue,将 + 保存到 pendingOperator,再把 5 作为当前输入。按等号后,才执行保存的运算。
三、完整可运行代码
将下面代码保存为 CalculatorApp.java。使用 JDK 8 或更高版本,在命令行运行 javac CalculatorApp.java 编译,再运行 java CalculatorApp 启动。Swing 程序应从事件分发线程创建界面,因此入口使用 SwingUtilities.invokeLater。
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 {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new CalculatorFrame().setVisible(true));
}
}
class CalculatorFrame extends JFrame {
private static final int MAX_INPUT_LENGTH = 30;
private static final int DIVISION_SCALE = 12;
private final JTextField display = new JTextField("0");
// 当前正在编辑的输入。用字符串保留末尾小数点等输入状态。
private String input = "0";
private BigDecimal storedValue;
private String pendingOperator;
private boolean startNewNumber = true;
private boolean error = false;
CalculatorFrame() {
super("Java 计算器");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(320, 420);
setLocationRelativeTo(null);
setLayout(new BorderLayout(8, 8));
display.setEditable(false);
display.setHorizontalAlignment(JTextField.RIGHT);
display.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
add(display, BorderLayout.NORTH);
String[] labels = {
"CE", "C", "退格", "÷",
"7", "8", "9", "×",
"4", "5", "6", "−",
"1", "2", "3", "+",
"±", "0", ".", "="
};
JPanel panel = new JPanel(new GridLayout(5, 4, 5, 5));
for (String label : labels) {
JButton button = new JButton(label);
button.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20));
button.addActionListener(event -> handle(label));
panel.add(button);
}
add(panel, BorderLayout.CENTER);
}
private void handle(String key) {
if ("C".equals(key)) {
clearAll();
return;
}
if ("CE".equals(key)) {
clearEntry();
return;
}
if (error) {
return;
}
if (isDigit(key)) {
enterDigit(key);
} else if (".".equals(key)) {
enterDecimalPoint();
} else if ("退格".equals(key)) {
backspace();
} else if ("±".equals(key)) {
toggleSign();
} else if (isOperator(key)) {
chooseOperator(key);
} else if ("=".equals(key)) {
calculateResult();
}
}
private boolean isDigit(String key) {
return key.length() == 1 && key.charAt(0) >= '0' && key.charAt(0) <= '9';
}
private boolean isOperator(String key) {
return "+".equals(key) || "−".equals(key)
|| "×".equals(key) || "÷".equals(key);
}
private void enterDigit(String digit) {
if (startNewNumber) {
input = digit;
startNewNumber = false;
} else {
if (input.length() >= MAX_INPUT_LENGTH) {
return;
}
if ("0".equals(input)) {
input = digit;
} else if ("-0".equals(input)) {
input = "-" + digit;
} else {
input += digit;
}
}
showInput();
}
private void enterDecimalPoint() {
if (startNewNumber) {
input = "0.";
startNewNumber = false;
} else if (!input.contains(".")) {
if (input.length() >= MAX_INPUT_LENGTH) {
return;
}
input += ".";
}
showInput();
}
private void backspace() {
if (startNewNumber) {
return;
}
if (input.length() <= 1 || (input.length() == 2 && input.startsWith("-"))) {
input = "0";
} else {
input = input.substring(0, input.length() - 1);
if ("-".equals(input)) {
input = "0";
}
}
showInput();
}
private void toggleSign() {
if ("0".equals(input) || "0.".equals(input)) {
return;
}
if (input.startsWith("-")) {
input = input.substring(1);
} else {
input = "-" + input;
}
startNewNumber = false;
showInput();
}
private void chooseOperator(String operator) {
BigDecimal current = parseInput();
if (pendingOperator != null && !startNewNumber) {
BigDecimal result = apply(storedValue, current, pendingOperator);
input = format(result);
storedValue = result;
showInput();
} else if (pendingOperator == null) {
storedValue = current;
}
pendingOperator = operator;
startNewNumber = true;
}
private void calculateResult() {
if (pendingOperator == null || startNewNumber) {
return;
}
try {
BigDecimal result = apply(storedValue, parseInput(), pendingOperator);
input = format(result);
storedValue = result;
pendingOperator = null;
startNewNumber = true;
showInput();
} catch (ArithmeticException ex) {
showError();
}
}
private BigDecimal apply(BigDecimal left, BigDecimal right, String operator) {
switch (operator) {
case "+":
return left.add(right);
case "−":
return left.subtract(right);
case "×":
return left.multiply(right);
case "÷":
if (right.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("division by zero");
}
return left.divide(right, DIVISION_SCALE, RoundingMode.HALF_UP);
default:
throw new IllegalArgumentException("未知运算符:" + operator);
}
}
private BigDecimal parseInput() {
return new BigDecimal(input);
}
private String format(BigDecimal value) {
BigDecimal normalized = value.stripTrailingZeros();
if (normalized.compareTo(BigDecimal.ZERO) == 0) {
return "0";
}
return normalized.toPlainString();
}
private void clearEntry() {
input = "0";
startNewNumber = true;
showInput();
}
private void clearAll() {
input = "0";
storedValue = null;
pendingOperator = null;
startNewNumber = true;
error = false;
showInput();
}
private void showInput() {
display.setText(input);
}
private void showError() {
error = true;
display.setText("错误:除数为零");
}
}
四、界面搭建与事件分发
CalculatorFrame 继承 JFrame,负责窗口和计算器状态。显示区使用只读的 JTextField,并设置右对齐,让数字的视觉位置更接近常见计算器。按钮放在 GridLayout(5, 4) 面板中;它会把组件排列成五行四列,适合这种固定矩阵布局。
每个按钮都绑定同一个监听器,将按钮文字传给 handle。统一入口有两个好处:界面创建代码简洁,所有输入又能经过相同的状态检查。按钮文字本身也是操作标识,例如界面上的 × 会传给运算逻辑,而不是使用 Java 表达式中的乘号字符。
Swing 组件应在事件分发线程中创建和更新。示例通过 SwingUtilities.invokeLater 启动窗口;后续如果加入耗时操作,不应在按钮监听器里执行长时间任务,否则窗口可能失去响应。当前计算只涉及少量本地十进制运算,直接在监听器中处理即可。
五、输入处理中的几个细节
1. 为什么输入先保存为字符串
如果每按一个数字都立即转成数值,用户输入 3. 时,数值本身无法表达“用户刚打完小数点、还没输入小数位”这一编辑状态。保留字符串可以分别处理开头的零、小数点、负号和退格,直到需要运算时再用 new BigDecimal(input) 转换。
示例只允许一个小数点,且按小数点时如当前是新数字,就生成 0.。这样单独按小数点后再按 5,得到的是 0.5。当输入长度达到 30 个字符时,程序拒绝继续追加;这个检查针对普通数字输入和小数点,不约束运算结果文本。
2. 退格与清除不是一回事
CE 将当前输入清为零,但保留尚未执行的运算状态;C 则连同保存值和待处理运算符一起清除。退格只在当前数字正在编辑时生效,若屏幕内容已经是一次计算的结果,按退格不会修改它。真实产品可以按需求调整这些按键语义,但应明确区分并测试。
± 只切换当前输入的正负号,零不做切换。它不是一元运算表达式解析器的一部分。示例允许当前输入包含负号,因此负数输入同样受 30 字符长度限制。
六、运算精度与除法处理
Java 的 double 使用二进制浮点表示,许多十进制小数无法精确表示。对于需要按十进制规则计算的场景,直接使用 BigDecimal 更容易明确精度策略。应从字符串创建它,例如 new BigDecimal("0.1");若先把十进制值放进 double 再构造,二进制浮点误差可能已经进入计算过程。
除法可能产生无限循环小数。对 BigDecimal.divide 不指定精度和舍入模式时,某些结果会抛出 ArithmeticException。示例先判断除数是否为零,然后调用带小数位数和舍入模式的重载方法:最多保留 12 位小数,采用四舍五入规则 HALF_UP。例如 1 ÷ 3 会得到有限位数的近似结果。
stripTrailingZeros 用于去掉结果尾部多余的零,toPlainString 则避免结果以科学计数法显示。零另行格式化为 0,避免出现不直观的负零或带指数的零。
这里的 12 位是显示与计算策略的一部分,并非所有计算器或所有 Java 程序的固定精度。若计算金额,应明确货币最小单位及每一步的舍入时机;若计算测量值,应明确有效数字和单位换算规则。仅仅换成 BigDecimal,并不自动保证业务结果正确。
七、运算状态的转移方式
按下运算符时,chooseOperator 会根据状态决定接下来做什么。如果已有待处理运算符,且用户已经输入了右侧数字,就先计算旧运算,再把结果作为新的保存值;如果是第一次按运算符,则保存当前输入。随后记录新的运算符,并把 startNewNumber 设为真。
按等号时,只有“存在待处理运算符且当前确实输入了右侧数字”才计算。因此在输入 9 + 后直接按等号,不会意外把某个未输入的操作数当成有效数字。计算成功后清除待处理运算符,结果留在屏幕上。示例没有实现重复按等号重复执行上一次运算,这是一项可按需求增加的功能。
连续运算采用顺序计算。输入 2 + 3 × 4 = 时,按下乘号的那一刻先算出 2 + 3,然后用结果乘以 4。若要求乘除优先于加减,不能只在当前逻辑上增加几条条件判断;通常应先把输入表示成运算符与操作数序列,再实现解析、优先级和括号规则。
八、常见错误与排查方法
- 用字符串直接比较数值。字符串
"2.0"与"2"文本不同,但数值相等。判断除零应将输入转换为BigDecimal后比较,示例使用compareTo。 - 用
double判断除数是否为零。混用浮点数与十进制输入,会使精度策略不清晰。整个运算链应采用一致的数值类型和舍入规则。 - 忽略输入状态。如果不区分“继续输入”和“开始新数字”,按完运算符后再输入数字可能变成拼接,例如把预期的
5错误接到前一个数字后面。 - 把显示文本当成唯一状态。显示区只是界面;待处理运算符、保存值和输入阶段都要单独维护。用屏幕文本反复推导全部状态,容易在退格、清除和连续运算时出错。
- 除法不处理精度或异常。无限小数可能导致异常;除数为零也必须明确处理。应在运算边界检查,并向用户显示可理解的反馈。
- 误以为按钮上的符号就是 Java 运算符。界面使用乘号
×、除号÷、减号−,与代码中的标准字符并不完全相同。比较时应统一使用界面传入的字符,或将界面标签映射到内部枚举。 - 在主线程创建界面。简单示例偶尔也能运行,但 Swing 推荐在事件分发线程中创建和操作组件,程序入口应使用
invokeLater。
九、建议验证的边界情况
运行后可按以下序列检查状态与边界是否符合预期:0.5 + 0.25 =;8 ÷ 4 =;1 ÷ 0 =;5 + 2 × 3 =;输入 12.3 后连续按两次小数点;输入数字后退格至零;输入超过 30 个字符;发生错误后按数字,再按 C 恢复。
测试时不仅要看最终结果,还应检查中间行为:输入运算符后是否能开始新数字,连续运算是否按预期从左到右,清除当前输入后待处理运算是否保留,以及除零后界面是否能通过清除恢复。对于边界条件,最好把运算逻辑进一步拆到独立类中编写单元测试;这样可以在不打开窗口的情况下覆盖精度、格式和异常规则。
十、后续扩展方向
这份实现刻意保持简单。扩展时可以考虑键盘映射、百分比、平方根、重复等号、历史记录、标准运算优先级和括号。增加功能前,建议先定义按键语义与状态转移,再补测试用例;否则功能之间容易互相影响。例如,百分比在不同计算器中的含义并不完全一致,不能只按按钮名称推断规则。
当运算逻辑逐渐复杂时,可将界面类与计算模型拆分:界面只负责显示和接收操作,模型负责输入状态及运算。进一步需要表达式优先级时,可使用词法分析与解析器,将表达式转换为语法树或逆波兰表达式。不要通过 eval 之类的动态执行方式计算用户输入;自行解析可以让可执行语法和边界行为保持明确。
一个可维护的计算器,关键不在按钮数量,而在于把输入、运算和显示三类职责分清楚,并对精度、顺序、除零和输入长度作出明确约定。示例代码提供了一个可运行的起点;将它用于实际业务前,应根据真实需求调整规则,并对边界与舍入结果进行充分验证。









