用91行Java代码实现一个Swing计算器:极简GUI编程实战 91行代码能写出一个什么样的Java计算器我最近把这个项目翻来覆去打磨了好几遍最后确认一个带完整Swing界面、支持四则运算、清除、退格、正负号、开根号、百分号的计算器完全可以压进91行。很多人觉得计算器项目太简单不值得动手但当你真的开始硬性控制代码量时就会发现这其实是验证Java基础功底的绝佳训练场。界面布局、事件监听、状态管理、字符串处理、异常防御全部被压缩在一个极小的范围里每一行代码都必须有它存在的理由。这篇分享就把这个91行Java基础计算器的完整实现过程拆开讲清楚包括界面设计、事件分发逻辑、运算状态管理以及我调试过程中踩过的几个真坑。适合学完Java基础语法、想动手做第一个图形界面程序的开发者参考也适合用来当课程作业或者面试前的快速练手项目。1. 项目概览这个计算器到底解决了什么问题1.1 为什么是91行约束倒逼设计91行这个数字不是拍脑袋定的它是我从“一个能用的GUI计算器的最小骨架”反推出来的。如果只做控制台版本输入一个表达式然后打印结果二三十行就能搞定但那只是模拟运算根本不叫“计算器项目”。如果没有行数限制大部分人会习惯性堆代码比如给每个按钮单独注册一个监听器或者把所有组件全部写成类字段最后轻松突破五六百行。代码越长初学者越难看清核心逻辑到底在哪里。91行的意义在于它逼着你做减法。你可以仔细审视每一个字段、每一个分支问自己一句“删掉它会不会出问题”。比如我用一个布尔变量start来标记“当前是否处于等待新数字输入的状态”如果删掉它连续输入数字时屏幕上就会拼出“12345”这样一串没法区分用户是在逐位输入还是在输入单独的数字。也就是说91行代码更像是一个筛子把可有可无的代码全部滤掉留下的全是必需品。这种“约束倒逼设计”的思路放到真实项目里同样适用——当资源有限时你才会真正学会排优先级。1.2 技术选型Swing不是老土而是最优解这个项目用的是Swing而不是JavaFX原因很朴素JDK自带不需要额外安装插件不需要处理module-info写完直接javac就能编译运行。对于大多数刚学完Java语法的朋友来说“能立刻看到窗口弹出来”是很重要的正反馈。JavaFX确实界面更现代但从配置SDK到配置模块化参数每一步都在消耗学习热情对基础练习来说完全没必要。还有人会问为什么不用AWT直接写AWT是Swing的前身组件是重量级组件跨平台外观有很大差异而且很多功能已经被Swing完整取代了。Swing的组件模型也足够经典——JFrame、JPanel、JButton、JTextField这四个类就能搭出一个完整的窗体应用。学这一个项目你对Java图形界面的组件层级、布局管理、事件模型就有了直观认识之后再去看JavaFX或者Android界面思路都是相通的。1.3 功能范围界定做减法也要知道减掉了什么这个91行版本明确支持的功能有这些数字0到9和小数点的输入加减乘除四则运算等号计算结果清除键C、退格键←正负号切换±开根号√百分号%它也明确不做这些事不处理运算符优先级1 2 * 3会得到9而不是7不支持括号不支持连续按等号重复上一次运算这种取舍在91行的前提下是合理的。我见过有人把这个版本“不支持优先级”当成缺陷来批判我觉得这是没搞清楚教学项目的定位。这个项目的核心目标是让人理解事件驱动编程和状态管理而不是复刻一个完整的计算器。运算符优先级解析本身就够单独写一个练手项目了硬塞进来只会让代码急速膨胀失去“基础练习”的意义。想清楚边界你才知道这个项目到底能教会你什么。2. 核心设计思路拆解91行怎么组织逻辑2.1 三个状态变量撑起整个运算状态机整个类的核心状态只有三个字段没有多余的东西。double result保存的是当前累计的运算结果。String op保存的是上一次按下的二元运算符。boolean start是最关键的一个标志位它表示“当前显示框里的内容是否已经是一个完整的数字可以参与下一次运算了”。我用一个生活化的类比来解释这三个变量的关系。可以把计算器想象成一条流水线result是工人手里攥着的半成品op是工人脑子里记住的“下一步要做什么操作”start则像是一个指示牌告诉工人“当前料已经可以传下去了还是正在组装中”。比如你按下5显示框显示“5”此时start为false表示这里有一个完整数字你再按工人把5收进result记下操作是加法然后把指示牌翻成starttrue意思是“下一个数字来了之后我会用新数字和result做加法”。这套状态模型是整个计算器所有按钮行为的底层逻辑理解它后面所有代码都顺理成章。2.2 一个actionPerformed分发所有按钮事件这个类实现了ActionListener接口所有按钮都通过addActionListener(this)注册到同一个监听器对象上。当任意按钮被点击actionPerformed方法就会被调用通过e.getActionCommand()拿到按钮上的文字再用字符串匹配去分发不同逻辑。有一个小技巧非常符合“91行”的气质判断数字键和小数点键用的不是一长串if-else而是0123456789..indexOf(cmd) 0。这个表达式写起来只有一行就能覆盖11个按键。如果不这么写你要写11个if (cmd.equals(1))、if (cmd.equals(2))这样的判断代码量直接翻倍。这就是在行数约束下自然逼出来的高效写法平时你可能不会想到用indexOf去判断按钮类型但在这个场景下它确实是最合适的。事件分发的顺序也经过设计先处理数字和小数点再处理一元操作符C、←、±、√、%最后处理二元运算符和等号。这个顺序保证了一元操作不会触发二元运算的合并逻辑也保证了数字输入优先于其他所有操作。2.3 顺序运算模型取舍与边界这个版本采用的是“顺序运算”模型也就是每按一次二元运算符如果当前显示框里已经有一个完整数字就先把result和当前数字做一次运算再把结果显示出来然后记录新的运算符把start置为true。举个例子输入1 2 3的完整过程是按1显示屏显示1按此时result变成1记录运算符为加号start变为true按2显示屏显示2start变为false再按此时执行12结果3显示出来同时记录新的运算符还是加号start再次变为true按3显示屏显示3按执行33最后显示6。整个过程就像边输入边计算非常直观。但这种模型天然没有优先级概念。如果要支持12*37就必须引入表达式解析比如把中缀表达式转成后缀表达式再用栈去计算或者用递归下降解析成语法树。这些方案单独写一个项目都很值得但随便实现一下就是两三百行打底完全不适合塞进一个91行的基础项目里。所以这里的选择很清晰先掌握简单状态机下一个项目再做解析器学习曲线会更平滑。我建议你先接受这个局限把当前版本跑通、理解透这才是最有价值的收获。3. 完整代码实现与逐段解析3.1 界面初始化从窗口到每一个按钮先看界面部分。Calculator继承JFrame在构造方法里完成所有初始化工作。第一件事是设置窗口标题、关闭行为以及布局方式。这里选择BorderLayout因为整个窗口只有上下两大块上面是显示框下面是一整块按钮面板用BorderLayout的NORTH和CENTER两个区域就能干净地分开。显示框是一个JTextField。它要设成不可编辑防止用户直接往输入框里敲文本破坏状态机逻辑同时把文字对齐方式设置为右对齐这样更接近日常计算器的显示习惯字体大小设成28像素保证长数字也能清晰展示。按钮面板用的是GridLayout(5, 4, 4, 4)表示5行4列组件之间水平间隔4像素、垂直间隔4像素。所有按键文字放在一个String[]数组里然后循环创建JButton、设置字体、注册监听器、添加到面板。这种“一维数组驱动界面生成”的写法非常省代码这也是91行能塞下20个按钮的关键。如果每个按钮都手动创建光按钮初始化就要二十多行。值得提醒一句GridLayout(5, 4, 4, 4)的第一个参数是行数第二个是列数很多初学者会搞反写成4行5列之后整个布局方向全乱了。这是一个我亲眼见过很多次的小坑。最后窗口要调用pack()来根据组件尺寸自动调整窗口大小再用setLocationRelativeTo(null)让窗口居中显示最后setVisible(true)把窗口展示出来。3.2 事件处理逻辑数字、运算符、功能键的分支设计事件处理是整个项目的核心actionPerformed方法里通过一系列分支把按钮分成几类。数字和小数点分支如果start为true说明当前显示的是上一轮运算的结果或者等待新输入的状态此时直接覆盖显示为按下的数字如果start为false说明正在逐位输入一个多位数那就把新数字追加到显示文本后面。紧接着把start置为false表示现在显示框里是一个完整的数字。另外我在数字分支开头加了一行防御代码if (cmd.equals(.) display.getText().contains(.)) return;连按小数点不会导致1..2这种非法数字出现。C清除分支把result、op、start三个状态全部重置显示框恢复为0。注意start必须重新变为true否则按清除后直接输入数字会拼接到残留文本后面。←退格分支删除显示文本的最后一个字符。但必须处理两个边界一是退到空字符串时显示0并把start置为true二是显示内容只剩一个负号-时同样恢复为0。这两个边界如果不处理后续解析数字时会非常痛苦。±正负号分支把当前显示内容解析成double取负后显示。这个分支不需要改start因为正负号被视为当前数字的一部分它的作用是修改数值本身而不是开启新数字。√开根号分支解析当前值调用Math.sqrt()显示结果。处理完以后要把start置为true否则用户接着输入数字就会把新数字追加在开方结果后面产生类似2.23606797749979后面再拼几个数字的混乱显示。%百分号分支解析当前值除以100后显示同样把start置为true。比如输入50再按%显示0.5接着输入3会重新开始显示3这更符合一般计算器的交互习惯。二元运算符和等号分支先判断!start也就是当前显示的是一个完整数字时才把显示值合并到result里。合并时根据op记录的上一次运算符对result和当前值做对应运算。如果op为空直接把当前值赋给result。做完之后无论实际按的是 - * /还是都统一记录为新的op并把start置为true。这里有个巧妙之处等号也被当作一种“运算符”处理。这样设计带来的好处是按完等号得到结果后再按运算符可以直接用结果继续运算。比如5 3 得到8接着按、按2、按就能得到10整个状态机不需要额外处理非常自然。3.3 主函数与Swing线程模型入口函数写得很简短就一行核心调用SwingUtilities.invokeLater(Calculator::new)。这行代码不是炫技它背后有一个很重要的原因Swing组件不是线程安全的UI组件的创建和操作必须在事件分发线程EDT上完成。invokeLater会把传入的任务投递到EDT的任务队列里由EDT负责执行从而保证所有组件都在正确的线程里初始化。当然在这么小的项目里直接new Calculator()通常也能跑起来但养成好习惯要从一开始。等你以后碰到真正复杂的并发界面应用时这个习惯能帮你躲开很多难以排查的线程问题。这也是为什么我建议即使代码行数紧张也要保留这一行的线程调度调用。3.4 完整源代码与运行方法下面是完整代码。我把界面初始化、事件处理、入口函数都放在一个类里去掉空行后大约在91行左右。这个体量对一个“带界面的完整计算器”来说已经是相当紧凑的写法了。import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Calculator extends JFrame implements ActionListener { private final JTextField display new JTextField(0); private double result 0; private String op ; private boolean start true; public Calculator() { setTitle(计算器); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); display.setEditable(false); display.setHorizontalAlignment(JTextField.RIGHT); display.setFont(new Font(Dialog, Font.BOLD, 28)); add(display, BorderLayout.NORTH); JPanel panel new JPanel(new GridLayout(5, 4, 4, 4)); 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(Dialog, Font.BOLD, 20)); button.addActionListener(this); panel.add(button); } add(panel, BorderLayout.CENTER); pack(); setLocationRelativeTo(null); setVisible(true); } Override public void actionPerformed(ActionEvent e) { String cmd e.getActionCommand(); if (0123456789..indexOf(cmd) 0) { if (cmd.equals(.) display.getText().contains(.)) return; if (start) display.setText(cmd); else display.setText(display.getText() cmd); start false; } else if (cmd.equals(C)) { result 0; op ; start true; display.setText(0); } else if (cmd.equals(←)) { String s display.getText(); if (!start s.length() 0) { display.setText(s.substring(0, s.length() - 1)); if (display.getText().isEmpty() || display.getText().equals(-)) { display.setText(0); start true; } } } else if (cmd.equals(±)) { double x Double.parseDouble(display.getText()); display.setText(String.valueOf(-x)); } else if (cmd.equals(√)) { double x Double.parseDouble(display.getText()); display.setText(String.valueOf(Math.sqrt(x))); start true; } else if (cmd.equals(%)) { double x Double.parseDouble(display.getText()); display.setText(String.valueOf(x / 100)); start true; } else { if (!start) { double x Double.parseDouble(display.getText()); if (op.equals()) result x; else if (op.equals(-)) result - x; else if (op.equals(*)) result * x; else if (op.equals(/)) result / x; else result x; display.setText(String.valueOf(result)); } op cmd; start true; } } public static void main(String[] args) { SwingUtilities.invokeLater(Calculator::new); } }运行方法很简单。把代码保存为Calculator.java在命令行执行javac Calculator.java java Calculator窗口弹出来就能正常使用了。4. 实操中的常见问题与排查技巧实录4.1 界面布局失控先检查这三个细节我调试这个项目时遇到过几次布局问题总结下来最常见的有三种。第一种是GridLayout的行列参数写反了。比如你想做5行4列结果写成了new GridLayout(4, 5, 4, 4)整个界面就变成了4行5列按钮顺序和预期完全不同。这种问题肉眼一眼就能看出来改参数即可。第二种是忘记把按钮面板加到窗口上。如果你写了JPanel panel new JPanel(...)也把所有按钮都往面板里加了但最后忘了add(panel, BorderLayout.CENTER)整个窗口只有一个光秃秃的输入框按钮全部不显示。这个错误很隐蔽因为代码不会报错只是界面不对。第三种是pack()和setSize()混用。pack()会根据组件的首选尺寸自动调整窗口大小而setSize()是强行指定窗口尺寸。如果先setSize(300, 400)再pack()后者会把前者覆盖导致你设置的尺寸失效。我建议这个项目里只用pack()让按钮和显示框的自然尺寸决定窗口大小简单省事。4.2 连续运算结果不对分清预期行为和bug很多人第一次跑这个计算器会发现1 2 * 3的结果是9而不是7然后觉得自己代码写错了。这个认知需要纠正这是顺序运算模型的正常行为不是bug。要支持四则运算优先级你需要的是表达式解析器而不是状态机。不过有一个现象需要警惕连续按等号。比如5 3 第二次按等号时因为start已经是true代码不会重新计算结果仍然是8不会变成11。如果你的需求是“连续等号重复上一次运算”那需要额外加两个字段去记忆上一次的操作数和运算符在按等号时优先复用这些记忆值。这不是基础版本该做的事但知道了原理扩展起来并不难。另外如果你输入5 3 结果是8而不是报错。这是因为第二个按下时start已经是true不会触发运算合并只是把op重新覆盖为加号。这个行为其实和很多实体计算器是一致的算是一个隐藏特性。4.3 小数点和空输入的两个隐藏坑小数点的问题我已经在代码里做了防御重复点击小数点不会产生1..2这样的非法字符串。如果你自己从零开始写很容易漏掉这个判断结果在按下运算符时触发NumberFormatException。这种异常的表现非常突然让人摸不着头脑但实际上就是在输入阶段埋下的隐患。退格键也有类似问题。如果显示框里只有一个字符比如5按退格后会变成空字符串紧接着继续输入数字就会得到7这样的显示吗不对我的代码里处理了空字符串的情况会把显示恢复成0并重置start。如果你没有这个处理空字符串被解析时直接抛异常。还有一个不太容易注意到的点当显示内容只剩一个负号-时也应该恢复成0。比如输入5按±变成-5再按退格会变成-此时如果再按数字正常的做法是重新开始输入而不是在负号后面追加所以同样需要重置。4.4 除零、负数开根号与其他边界值处理Java的double运算对除零的处理和数学上的直觉不一样。直接用5 / 0不会抛ArithmeticException而是得到Infinity。-9开根号也不会报错会得到NaN。这两个值如果出现在计算器屏幕上其实不会立即崩溃因为显示之后start已经变成了true下一次输入数字会直接覆盖显示内容。但有一个情况需要注意如果你把√分支里的start true漏了按了负数的开根号之后显示NaN此时start还是false继续输入数字就会把数字追加到NaN后面变成NaN5这种字符串再按运算符时Double.parseDouble(NaN5)就会抛异常。这就是为什么我反复强调一元操作之后一定要把start置为true它不仅是交互习惯问题更是异常防御的关键。4.5 从91行到完整计算器可以往哪些方向扩展这个91行版本完成后我建议你把它当作一个“地基”继续往下面几个方向扩展每个方向都是很好的独立练手项目。第一个方向是支持运算符优先级。你可以用两个栈分别存数字和运算符按优先级规则弹出计算或者先把中缀表达式转成后缀再计算。做完这一步1 2 * 3就能正确得到7计算器会从“基础版”变成“标准版”。第二个方向是支持键盘输入。给窗口添加键盘事件监听数字键和运算符键直接映射到对应的逻辑分支。这个扩展能让你完全脱离鼠标操作体验感和交互效率都会提升不少。第三个方向是加入括号支持。有了括号就需要更完整的表达式解析甚至可以考虑用递归下降法构建语法树这会把你从“状态管理”带到一个全新的“语法分析”领域。第四个方向是历史记录。用一个列表记录每次运算的表达式和结果点击某条历史可以重新载入。这个功能会涉及列表组件和集合的综合使用也很适合练习。我个人最推荐先做第一个方向因为优先级是计算器最核心的能力做完之后你会有非常明显的成就感。写在最后这个91行计算器项目我前前后后调试过很多遍最深刻的一个体会就是行数限制并不会闷死你的思路反而会逼出很多平时想不到的精巧写法。比如用indexOf判断数字键、用数组驱动按钮生成、用统一的二元操作分支处理等号和运算符这些技巧在几百行的冗余代码里反而不容易冒出来。最后再分享一个小细节。如果你把按钮文字设置成√、±这种特殊字符在某些操作系统自带的字体下可能会显示成方框这通常不是代码问题而是字体缺少对应字形。遇到这种情况可以给按钮统一换一个字体比如new Font(SansSerif, Font.BOLD, 20)SansSerif字体对特殊字符的覆盖率更高。换完字体再跑一次基本上就能正常显示了。这也是我在实际操作中踩过一次坑之后才总结出来的经验。