1. 从零开始:为什么用Java Swing做计算器是个好主意
很多Java初学者在学完基础语法后,都会面临一个灵魂拷问:我能用Java做出什么像样的东西?课本上的例子太简单,企业级项目又太复杂,这时候,一个功能完整的科学计算器就成了绝佳的练手项目。我自己当年就是这么过来的,从只会写控制台程序,到做出第一个带界面的计算器,那种成就感至今难忘。
你可能觉得计算器很简单,不就是几个按钮加加减减吗?但真要自己动手实现一个多功能科学计算器,你会发现里面涉及的知识点非常全面。首先,你需要用Java Swing来构建图形界面,这能让你掌握事件驱动编程的核心思想。其次,计算器最核心的“大脑”——表达式计算,会逼着你去学习数据结构与算法,特别是栈(Stack)的应用。最后,为了让计算器足够“聪明”,能处理各种用户输入错误,你还得设计一套健壮的错误处理机制。
这个项目就像一块试金石,能检验你对Java基础、面向对象、GUI编程和算法的综合掌握程度。而且,它的成果是可视化的,你可以直接拿给朋友用,或者放到简历里,比干巴巴地写“熟悉Java语法”要有说服力得多。接下来,我就带你一步步拆解这个项目,从界面布局到核心算法,把每个环节的“坑”和技巧都讲清楚。
2. 界面设计:用Swing打造一个“看得见”的计算器
做计算器,第一步当然是让它“长得像”一个计算器。我们选择Java Swing这个经典的GUI工具包,它足够轻量,也完全能满足我们的需求。原始文章里用setBounds手动定位每个按钮,这确实是最直观的方式,尤其对于计算器这种按钮排列整齐、但布局又有点特殊的界面。用布局管理器当然也可以,但GridLayout或GridBagLayout在应对不同大小的按钮(比如那个超长的“=”键)时,反而会更麻烦。
我在这里踩过的坑是:不能直接把组件加到JFrame上。如果你把JFrame的布局设为null,然后直接往里面加按钮和文本框,很可能会发现界面一片空白或者显示异常。正确的做法是创建一个JPanel面板,把面板的布局设为null,然后把所有按钮、文本框都加到面板里,最后把这个面板加到JFrame的内容窗格中。这是因为JFrame本身有复杂的内部结构,直接操作容易出问题。
关于窗口尺寸,这里有个小细节:你设计的面板大小是600x400,但实际设置JFrame的大小时,需要额外加上边框和标题栏的像素。通常宽度加15,高度加35左右比较保险,也就是setSize(615, 435)。原始代码里是614x437,可能是根据特定系统主题微调的结果,大家在实际运行时可以根据自己电脑的效果再调整。
为了让界面更专业,我建议在原始代码基础上做两点优化。一是字体选择,计算器的显示屏通常使用等宽字体,比如Monospaced,这样数字对齐更美观。按钮上的文字可以用无衬线字体,如SansSerif,更清晰。二是颜色搭配,可以给数字键、运算符键、功能键(如sin、cos)设置不同的背景色,让用户一眼就能区分。比如,数字键用浅灰色,运算符用橙色,功能键用蓝色。这只需要在创建按钮后,调用button.setBackground(Color.ORANGE)即可实现。
下面是一个改进后的界面初始化代码片段,增加了颜色区分:
// 在创建按钮的循环中,根据按钮类型设置不同颜色
for (int i = 0; i < buttons.length; i++) {
buttons[i] = new JButton(strings[i]);
buttons[i].setFont(f);
buttons[i].setFocusable(false);
// 根据按钮文本内容设置颜色
String text = strings[i];
if (text.matches("[0-9.]")) {
buttons[i].setBackground(new Color(240, 240, 240)); // 数字键浅灰色
} else if (text.matches("[+\\-*/=]")) {
buttons[i].setBackground(new Color(255, 159, 0)); // 运算符橙色
buttons[i].setForeground(Color.WHITE);
} else if (text.matches("[()]")) {
buttons[i].setBackground(new Color(200, 200, 200)); // 括号中灰色
} else {
buttons[i].setBackground(new Color(70, 130, 180)); // 科学函数键蓝色
buttons[i].setForeground(Color.WHITE);
}
}
3. 核心算法:中缀表达式如何“变身”为后缀表达式
界面做好看了,接下来就是最核心、也最考验逻辑的部分:怎么让计算机理解我们输入的算式?我们人类习惯的写法,比如 3 + 5 * (2 - 1),在计算机科学里被称为中缀表达式。它的特点是运算符在操作数中间。但这种形式对计算机来说并不友好,因为运算符有优先级(乘除高于加减),还有括号会改变计算顺序。
为了让计算机能按正确的顺序计算,我们需要把中缀表达式转换成后缀表达式(也叫逆波兰表达式)。后缀表达式的特点是运算符在操作数后面,并且没有括号,计算顺序完全由表达式的顺序决定。比如,上面的中缀表达式转换成后缀表达式就是 3 5 2 1 - * +。
这个转换过程,栈(Stack) 数据结构就派上大用场了。栈的特点是“后进先出”,正好适合处理这种有优先级和括号的表达式。转换算法我总结为以下几个步骤,你可以对照着原始文章的houZhui方法来看:
- 初始化两个栈:一个运算符栈(
fuZhan),一个输出列表(list2,其实用列表或队列都可以)。 - 从左到右扫描中缀表达式的每个元素(数字或运算符)。
- 遇到数字:直接加入输出列表。
- 遇到运算符:
- 如果栈为空,或栈顶是左括号
(,或当前运算符优先级高于栈顶运算符,则直接入栈。 - 如果当前运算符优先级低于或等于栈顶运算符,则不断将栈顶运算符弹出并加入输出列表,直到栈为空、栈顶为左括号、或栈顶运算符优先级低于当前运算符,然后再将当前运算符入栈。
- 如果栈为空,或栈顶是左括号
- 遇到左括号
(:直接入栈。 - 遇到右括号
):不断将栈顶运算符弹出并加入输出列表,直到遇到左括号(,然后将左括号弹出(丢弃,不加入输出列表)。 - 扫描结束后:将运算符栈中剩余的所有运算符依次弹出并加入输出列表。
原始代码里用adv方法定义了运算符优先级,加减是1,乘除是2,乘方、函数等是4。这里有个可以优化的点:优先级定义可以更精细。比如,乘方^是右结合的(2^3^2应该计算为2^(3^2)),而加减乘除是左结合的。原始代码将所有高级运算符(^!gloct)都设为4,没有区分结合性。在实际更复杂的计算器中,我们需要为^单独设置优先级,并在遇到它时,只有优先级高于栈顶才入栈(对于右结合运算符),而左结合运算符是优先级高于或等于栈顶才入栈。不过对于初学者项目,原始代码的简化处理已经足够。
4. 后缀表达式计算:栈的第二次华丽登场
得到后缀表达式后,计算就变得异常简单了,栈再次成为主角。计算规则如下:
- 初始化一个操作数栈。
- 从左到右扫描后缀表达式。
- 遇到数字,就压入栈。
- 遇到运算符,就从栈中弹出两个操作数(注意顺序,对于减法和除法,先弹出的是右操作数),进行运算,将结果压回栈中。
- 扫描结束后,栈中剩下的那个数字就是最终结果。
原始文章的math方法就是这个逻辑。它处理了加减乘除、乘方、阶乘、开方、对数、三角函数等运算。这里我想重点说一下错误处理的细节。一个好的计算器不能一遇到错误就崩溃,而是要给出明确的提示。原始代码在math方法里做了几处关键检查:
- 除法时判断除数是否为零。
- 阶乘时判断是否为自然数(非负整数)。
- 自然对数
ln和以2为底的log判断参数是否大于零。 - 正切
tan判断参数是否会导致cos(x)为零(即π/2 + kπ)。
这些都是非常必要的。我建议在此基础上,还可以增加一些健壮性检查,比如在弹出栈元素进行计算前,先判断栈里是否有足够的操作数。对于单目运算符(如阶乘!、开方g、三角函数等),只需要弹出一个操作数。原始代码在遇到!、g、l等时,只弹出了一个数,这是正确的。但我们可以增加一个判断,如果栈为空,则说明表达式不合法,应报错。
5. 输入验证:给计算器穿上“防弹衣”
用户输入是不可预测的,他们可能会输入2++3、sin(2))、5/0或者12.34.56这样的非法表达式。如果不对输入进行校验,程序很可能在解析或计算时抛出异常,甚至崩溃。所以,在将字符串交给中缀转换和计算之前,我们必须先进行一轮严格的语法检查。
原始文章的estimate方法做了大量工作,列出了13条规则来检查输入合法性。这些规则非常细致,覆盖了开头结尾、运算符前后、数字格式、括号匹配等多个方面。我们来分析一下其中的核心思想:
- 状态机思想:本质上,它是在遍历字符串时,检查每一个字符及其前后字符的组合是否合法。这就像是一个简单的状态机,根据当前字符的类型(数字、运算符、括号、函数等)和上一个字符的类型,来判断下一个字符是否允许出现。
- 边界检查:特别关注字符串的第一个和最后一个字符,以及一些特殊字符(如小数点
.、数字0)的上下文环境。 - 括号匹配:在
zhanDui方法里,通过统计左右括号的数量和顺序来检查括号是否成对且嵌套正确。
在实际开发中,这种基于规则的检查虽然有效,但代码会显得比较冗长和难以维护。一个更优雅、更强大的方法是使用正则表达式来验证整个表达式的语法结构。我们可以先定义一个描述合法数学表达式的正则表达式模式,然后在用户点击等号时,用这个模式去匹配整个输入字符串。不过,正则表达式对于处理复杂的嵌套括号会有点力不从心。另一种更高级的方法是使用递归下降解析或语法分析器生成工具,但这对于计算器项目来说就有点杀鸡用牛刀了。
对于初学者,我建议理解并借鉴原始文章的检查思路,但可以尝试重构代码,使其更清晰。例如,可以将不同的检查规则封装成独立的方法,如checkOperatorSequence、checkNumberFormat、checkParentheses等,这样主方法estimate的逻辑会更清晰。
6. 事件处理:让按钮“活”起来
界面画好了,算法也准备好了,现在需要把两者连接起来,这就是事件监听器(ActionListener) 的工作。原始文章为33个按钮分别添加了监听器,每个监听器里更新显示文本框(jTextField1)和内部表达式字符串(str)。这是最直接的方式。
但这里有一个设计模式上的优化点:使用命令模式。我们可以为每一种计算器操作(数字输入、运算符输入、函数输入、等号、清空等)定义一个命令类。每个按钮绑定一个具体的命令对象。这样做的好处是,将用户界面的动作(点击按钮)与具体的业务逻辑(更新字符串、执行计算)解耦。如果以后要增加新的按钮功能,或者改变按钮的布局,只需要创建新的命令类或修改配置,而不需要去动那一大串actionPerformed方法。
不过,对于第一个版本的计算器,像原始文章那样为每个按钮写匿名内部类也是完全可以接受的,逻辑清晰直观。需要注意的是,在事件处理中,除了更新显示,更重要的是维护好内部状态。原始文章用str变量来记录用户输入的“原始”表达式,用jTextField1来显示“美化后”的表达式(比如str里存的是g,但显示的是√)。这是一个很好的做法,将内部逻辑表示和外部显示分离。
另一个细节是等号(=)按钮的处理。它触发了整个计算流程:先调用estimate()进行输入验证,再调用zhanDui()进行括号匹配和计算。如果计算成功,结果会同时显示在主显示屏(jTextField1)和副显示屏(jTextField2)上。这里可以增加一个功能:历史记录。每次成功计算后,可以把表达式和结果保存到一个列表里,然后提供一个“历史”按钮来查看,这会让计算器更实用。
7. 功能扩展:让你的计算器更“科学”
原始文章的计算器已经实现了三角函数、对数、阶乘、开方、乘方等科学计算功能,这已经很棒了。但如果你想挑战自己,或者让项目更出彩,这里有几个扩展方向:
- 角度与弧度切换:这是科学计算器的标配。可以增加一个“Deg/Rad”切换按钮,用一个布尔变量记录当前模式。在计算
sin、cos、tan时,如果处于角度制(Deg),需要先将输入值乘以π/180转换为弧度,再调用Math.sin等函数。 - 常数与存储器:增加
e和π按钮是第一步。还可以增加内存功能,如MC(内存清除)、MR(内存读取)、M+(加到内存)、M-(从内存减)。这需要额外定义一个double memory变量来存储值。 - 表达式历史与回退:实现一个栈或列表来存储之前计算过的表达式和结果。增加“上箭头”按钮可以调出上一次的输入或结果,这在连续计算时非常方便。
- 错误提示的友好性:原始代码在出错时只是简单显示“输入错误!”。可以做得更细致,比如提示“括号不匹配”、“除数不能为零”、“ln的参数必须大于0”,甚至用对话框高亮显示出错的大概位置。
- 键盘支持:让用户除了用鼠标点击,还可以用键盘输入数字和常用运算符(如
+,-,*,/,Enter作为等号),这会极大提升使用体验。这需要为JFrame添加KeyListener。
实现角度切换的代码示例可能如下:
// 在类中增加一个成员变量
private boolean isDegreeMode = true; // 默认角度制
// 在初始化按钮时,增加一个切换按钮
JButton degRadButton = new JButton("Deg");
degRadButton.addActionListener(e -> {
if (isDegreeMode) {
degRadButton.setText("Rad");
isDegreeMode = false;
} else {
degRadButton.setText("Deg");
isDegreeMode = true;
}
});
// 在计算三角函数的方法中,根据模式进行转换
private double computeSin(double value) {
if (isDegreeMode) {
value = Math.toRadians(value); // Java内置的转换方法,等同于 value * Math.PI / 180
}
return Math.sin(value);
}
// 同理修改cos, tan的计算方法
8. 项目总结与避坑指南
走完整个开发流程,我们再回头看看这个项目带给我们的收获。它绝不仅仅是一个计算器,而是一个综合性的Java练手项目。你实践了Swing GUI开发、事件处理、数据结构(栈)的应用、字符串处理、算法(中缀转后缀)、以及基本的软件工程思想(如错误处理、模块分离)。
结合我自己的经验,这里有几个容易踩的“坑”和调试建议:
- 布局错乱:如果界面显示不正常,首先检查是否将组件添加到了正确的容器(JPanel)中,以及JFrame的尺寸是否包含了边框。可以用
setBackground(Color.RED)临时给面板上色,看看它到底显示在哪儿、有多大。 - 表达式解析错误:这是最头疼的。建议在开发
zhongZhui(中缀分割)和houZhui(后缀转换)方法时,多使用System.out.println打印中间结果。比如,每扫描一个字符,就打印当前字符和解析出的列表;在转换后缀时,打印运算符栈的状态和输出列表。对比手动计算的步骤,很容易找到逻辑错误。 - 计算精度问题:Java的
double类型有精度限制,对于连续的浮点数运算,可能会产生极微小的误差。比如0.1 + 0.2的结果可能不是精确的0.3。对于显示,可以使用DecimalFormat格式化输出,限制小数位数。对于需要高精度计算的场景(如金融),应该考虑使用BigDecimal类。 - 事件响应问题:如果点击按钮没反应,首先检查
addActionListener是否被正确调用,以及监听器里的代码逻辑(特别是对str和文本框的更新)是否正确。确保没有在事件监听中阻塞了事件分发线程(EDT)。 - 代码复用与清晰度:原始文章的代码将所有功能写在一个大类里,对于学习是清晰的。但当功能扩展后,代码会变得很长。可以考虑将界面构建、表达式解析与计算、事件处理分离到不同的类中,比如
CalculatorView、ExpressionEvaluator、CalculatorController,这样符合MVC模式,更利于维护。
最后,把完整的代码跑起来,看到自己亲手打造的计算器能正确计算 sin(30) + log(8) / 2 这样的复杂表达式时,那种感觉是非常棒的。这个项目可以作为你Java学习路上的一个里程碑,它所涉及的思想和技能,在你未来学习更复杂的框架和系统时,依然会发挥重要作用。


被折叠的 条评论
为什么被折叠?



