简介:一份面向安卓初学者的计算器小程序源代码文档,基于Android Studio开发环境编写,主要用于解决新手在界面布局、按钮事件监听与基础运算逻辑上的练习需求。文档以docx格式封装,仅含1个文件,压缩后约34KB,核心内容包括网格布局的配置文件,其中定义5列8行的网格、表达式与输入输出两个文本框,以及清除、退格、内存等运算按键;代码部分则对应按钮点击、菜单选择、对话框取消等事件处理,并附带通过系统浏览器打开网页的扩展功能。已吸引1518人学习,适合作为移动开发课程作业或入门练手项目。通过这份文档,读者可以掌握计算器界面从控件排列到右对齐文本显示的搭建思路,理解程序运行中的界面交互与事件回调机制,并在此基础上自行完善科学计算、异常输入处理与多屏幕适配等高级特性。
1. 这个计算器项目到底给了你什么:28 键、双 TextView 与双栈引擎
用 Android Studio 做计算器的人很多,翻开源码却发现一大半只给了布局文件,算法要么写死在一长串 if-else 里,要么只能算一次就不能继续。这份“Android studio计算器”源码不太一样:它把 28 键的计算器拆成了清晰的界面层和表达式解析层,界面用 GridLayout 排 5 列 8 行,解析用经典的运算符栈 + 数字栈,输入与表达式各占一个 TextView,MC/MR/MS/M+/M- 五个内存键也有闭环。适合三类人:正在学 Android 事件监听的初学者,需要一份能讲明白原理的课程作业源码的人,想研究 Stack 和运算符优先级表配合方式的人。下文把它从布局到算法拆开讲,容易翻车的位置也会标出来。
2. 界面层拆解:GridLayout 布局与 28 个按钮的事件绑定
拿到一份 Android 项目源码,我习惯先看布局再看逻辑。这个项目的界面文件是一个根 GridLayout,里面塞了 2 个 TextView 和 28 个 Button,没有任何嵌套容器。好处是布局层级浅,渲染快,坏处是 GridLayout 的排列规则和重量分配不直观,抄代码时容易排错位置。这一章先把界面层讲透,下一章再进算法。
2.1 5 列 × 8 行的 GridLayout 是怎么排布的
根布局的关键参数只有四个:columnCount=5、rowCount=8、宽度match_parent、高度wrap_content。GridLayout 的排列规则很简单:子控件按在 XML 里的书写顺序,从左到右、从上到下自动填格子;不想按顺序排的,可以用layout_column、layout_row手动指定,但这个项目里除了跨行跨列的按钮,其余全靠顺序。
把控件按行展开,是这个结构:
| 行序号 | 第 1 列 | 第 2 列 | 第 3 列 | 第 4 列 | 第 5 列 |
|---|---|---|---|---|---|
| 0 | expressionOutputView(跨 5 列) | ||||
| 1 | input_outputView(跨 5 列) | ||||
| 2 | MC | MR | MS | M+ | M- |
| 3 | Del | CE | C | +/- | sqrt |
| 4 | 7 | 8 | 9 | / | % |
| 5 | 4 | 5 | 6 | * | 1/x |
| 6 | 1 | 2 | 3 | - | =(rowSpan=2) |
| 7 | 0(columnSpan=2) | . | + | ← 等号延续至此 |
两个 TextView 通过layout_columnSpan="5"各占一整行,fill_parent的宽高配合layout_gravity="fill_horizontal|fill_vertical"才能撑满格子。这里有个细节:GridLayout 里直接写fill_parent不一定能铺满,必须让layout_gravity带上fill方向,否则控件会按照自身内容尺寸测量,看起来右边留白。
等号按钮的处理是底部排布的关键:它在第 6 行占据第 4 列,layout_rowSpan="2"让高度跨两行,再用layout_gravity="fill_vertical"把纵向填满。第 7 行剩下的位置由 0 和 . 、+ 填充,其中 0 占了columnSpan="2",于是底部恰好排满 5 列。这个“跨行 + 跨列”的组合是计算器布局里最容易抄错的地方,少写任何一个 span 或 gravity,整体就会错位。
2.2 两个 TextView:表达式框与输入输出框的分工
界面里两个 TextView 不是冗余,它们各自承担不同的显示职责。expressionOutputView在最上方,负责回显用户已经按过的完整表达式;它设了singleLine="true"和ellipsize="start",意味着文字超出宽度时,省略号出现在左边,保留右边最新输入的运算符和数字。
下方的input_outputView是当前输入值和计算结果的共用框,初始文本是 "0",maxLines="3"允许多行显示,同样是右对齐。这个双框设计在手机计算器上很常见,优点是用户在按长表达式时能同时看到“刚才按了什么”和“正在输入什么”。
两个框都是textAlignment="textEnd",也就是右对齐,这符合计算器显示习惯:数字从右往左增长,不会因为长度变化而跳动。字号统一用了 50sp,在模拟器上很合适,但真机窄屏上会放大溢出,这个坑后面避坑章节会专门讲。
2.3 一个监听器管 28 个按钮:内部类的设计
Java 端的按钮绑定没有写 28 行setOnClickListener,而是用了一个内部类统一处理:
public class MainActivity extends AppCompatActivity { private Button[] btn; private TextView ioView; private TextView expressionOutputView; private String expression = ""; private String input = "0"; private void initial() { btn = new Button[28]; ioView = (TextView) findViewById(R.id.input_outputView); expressionOutputView = (TextView) findViewById(R.id.expressionOutputView); ioView.setText("0"); btn[0] = (Button) findViewById(R.id.btn_0); // ... 依次绑定 28 个按钮 btn[27] = (Button) findViewById(R.id.btn_msub); MyOnClickHandler myOnClickHandler = new MyOnClickHandler(); for (Button i : btn) i.setOnClickListener(myOnClickHandler); } }MyOnClickHandler是 MainActivity 的内部类,能直接读写外部类的expression和input成员变量。这样设计的好处很明显:28 个按钮的事件处理逻辑共享同一组状态,不需要在 Activity 和 Handler 之间来回传值;而且键盘上的每个按钮只是在同一个状态机上触发不同的动作,写成一个类更便于统一收口。
内部类里还声明了几个关键的工具方法:getRet负责根据运算符计算两个数的结果,someOfStr_in_anotherStr负责从指定位置开始找最近的运算符,calculate是表达式求值入口,stripTrailingZeros负责清理结果里多余的尾零。从命名上能看出这个作者是按“工具方法 + 核心算法”来组织的,而不是把所有逻辑堆在onClick里。
3. 双栈计算引擎:表达式求值里最关键的三件事
如果说布局是皮,那这个项目的计算引擎是骨。它没有用 Android 自带的表达式求值库,也没有用 JavaScript 引擎桥接,而是纯靠 Java 的Stack类实现了一个经典的中缀表达式求值器。核心代码不算长,但包含了三个必须理解清楚的点:双栈模型、优先级表的语义、负数识别分支。
3.1 运算符栈 + 数字栈:中缀表达式求值的基本模型
calculate()的第一步是建立两个栈:
private void calculate() throws Exception { Stack<Character> opStack = new Stack<>(); Stack<Double> numStack = new Stack<>(); opStack.add('#'); String tempExpression = expression + "#"; int start = 0; int end; while (start < tempExpression.length()) { end = someOfStr_in_anotherStr(operator, tempExpression, start); // 解析操作数,这里省略了负数分支 numStack.add(Double.parseDouble(tempExpression.substring(start, end))); // 比较运算符优先级,决定是压栈还是弹栈计算 // ... 循环内核心逻辑 } }两个栈的分工很朴素:numStack存数字,opStack存运算符。表达式的处理方式是扫描字符串,每遇到一个运算符,就说明它前面夹着一段数字,把数字解析出来压进数字栈;然后再决定这个运算符是直接压栈,还是先把栈顶的运算符弹出来算一步。
为了处理边界,在栈底和表达式末尾各放了一个#哨兵字符。哨兵不进计算流程,只在两两相遇时表示表达式读完了,整个循环自然结束。这种方法比递归下降解析更容易读,也比“每次从头遍历找最高优先级运算符”的方式高效。
后半段循环原文没完整给出,但按这个结构,常见的补全逻辑是这样一段:
char nowOption = tempExpression.charAt(end); char topOption = opStack.peek(); int compareResult = compare[topOption][nowOption]; if (compareResult == 1) { opStack.add(nowOption); start = end + 1; } else if (compareResult <= 0) { double b = numStack.pop(); double a = numStack.pop(); char op = opStack.pop(); numStack.add(getRet(a, b, op)); // 注意:没有移动 start,继续处理当前运算符 }这里的核心在于:compareResult == 1时当前运算符压栈并读下一个字符;compareResult <= 0时先弹栈计算,但start不前进,让循环继续拿同一个运算符和新栈顶比较。当两个#相遇返回 0,循环结束,numStack栈顶就是最终结果。
3.2 compare 矩阵:6×6 优先级表怎么读才是对的
优先级比较不是用一堆 if 判断,而是查一张 6×6 的表。运算符集合是+-*/%#,矩阵定义如下:
private int[][] compare = { {-1, -1, 1, 1, 1, -1}, // 栈顶为 + {-1, -1, 1, 1, 1, -1}, // 栈顶为 - {-1, -1, -1, -1, -1, -1}, // 栈顶为 * {-1, -1, -1, -1, -1, -1}, // 栈顶为 / {-1, -1, -1, -1, -1, -1}, // 栈顶为 % {1, 1, 1, 1, 1, 0} // 栈顶为 # };行表示当前栈顶运算符,列表示当前读入的运算符。拿3 + 4 * 5举例:读入*时栈顶是+,查表compare[+][*] = 1,表示当前读入的*优先级更高,先把*压栈,等后面#到来时再逐层弹出,先算4 * 5再算3 + 20。
这里有个非常值得注意的细节:源码注释写的是“-1 代表低于,1 代表高于”,字面上容易理解成“返回值表示栈顶运算符的优先级关系”。但按实际运行行为反推,1的实际动作是“当前运算符压栈”,-1的实际动作是“弹出栈顶参与计算”。也就是说表是站在当前运算符的角度描述的。如果照注释直译一个优先级比较器,整个计算顺序会被带偏,2 + 3 * 4会算出错误结果。
同优先级运算符之间取-1,比如+遇+、+遇-,都会先弹栈计算,这正好实现从左到右的结合方向。只有栈顶和当前都是#时返回0,表示表达式扫描结束。这张表是理解整个计算器行为的关键,建议到手源码后先在纸上手推三四个表达式,再对照行为理解注释,能省不少绕弯的功夫。
3.3 负数识别:end - start < 1 分支到底在判断什么
扫描表达式时,someOfStr_in_anotherStr找到的end是最近一个运算符的下标,start到end之间应该是数字。但负数会使这个逻辑出现边界情况,比如-5 + 3一开头就是一个负号,这时end - start < 1,两个运算符之间没有数字。源码的处理是:
if (end - start < 1) { if (tempExpression.charAt(end) == '-') { start = end; end = someOfStr_in_anotherStr(operator, tempExpression, start + 1); } else { throw new Exception(ExpressionError); } }这段代码解决的是两个问题:一是一元负号,二是连续运算符场景,比如3 * -4 + 5。扫描到*之后,下一次从*后面找运算符,找到的是-,此时end - start为 0,发现紧邻的是-,就把它当作数字的符号,重新从-的下一个位置找真正的运算符,最终截取-4解析为数字。
这个分支只放行-,遇到3 * +4这类一元正号会直接抛“表达式错误”。这是设计取舍:不支持一元正号,但支持一元负号。实际使用中够用,因为正常用户很少在运算中间输入+4这种写法。
3.4 一元运算怎么落:sqrt、1/x、+/- 的处理思路
布局里有sqrt、1/x、+/-三个一元运算键,它们的处理方式和二元运算符完全不同:二元运算符要进表达式参与解析,一元运算更像是对“当前输入值”的即时变换。源码里calculate()只负责四则运算加取模,一元运算应该在onClick里走独立分支。
常见做法是这样:点击+/-时,把input取出转成 double,取负后再写回文本框;点击1/x时判断当前值是否为 0,不为 0 则求倒数并写回;点击sqrt时先判断负数,负数弹错误提示,非负数开方后写回。
private void handleUnary(char op) { double cur = Double.parseDouble(input); switch (op) { case 's': if (cur < 0) throw new IllegalArgumentException(SqrtByNegative); input = stripTrailingZeros(String.valueOf(Math.sqrt(cur))); break; case 'r': if (cur == 0) throw new ArithmeticException(DivByZeroError); input = stripTrailingZeros(String.valueOf(1.0 / cur)); break; case 'n': input = stripTrailingZeros(String.valueOf(-cur)); break; } ioView.setText(input); }这里容易忽略的是精度显示。Math.sqrt(2)算出来是1.4142135623730951,如果不做尾零清理,sqrt(4)会显示2.0而不是2。源码里出现的stripTrailingZeros就是为了处理这个问题,它把 double 转成字符串后去除末尾无意义的 0,让显示更干净。
一元运算执行后,通常还要把本次操作追加到表达式框,比如表达式框显示√(9),输出框显示3。这样用户能看到完整的操作历史,而不是莫名其妙蹦出一个数。
4. 内存键、菜单与对话框:把计算器从“能算”做到“能维护”
四则运算能跑通只是第一步。这份源码相比很多教学 demo 多了一排内存键 MC/MR/MS/M+/M-,还带了菜单和关于对话框的骨架。这一章讲功能闭环,也顺便指出代码里几个需要注意的细节。
4.1 saveNum 与 MC/MR/MS/M+/M- 的语义闭环
内存键的变量是内部类里的private double saveNum = 0.0,初始值为 0。五个键的语义按标准计算器定义如下:
| 按键 | 语义 | 对应操作 |
|---|---|---|
| MC | 清除内存 | saveNum = 0.0 |
| MR | 读取内存 | 把saveNum显示到输入输出框 |
| MS | 保存到内存 | 用当前输入值覆盖saveNum |
| M+ | 累加进内存 | saveNum += 当前输入值 |
| M- | 从内存减去 | saveNum -= 当前输入值 |
按这份源码 28 键的下标映射,btn[23]到btn[27]依次是 MC、MR、MS、M+、M-。常见的处理写法是这样:
private void handleMemoryKey(int index) { double cur = Double.parseDouble(input); switch (index) { case 23: // MC saveNum = 0.0; break; case 24: // MR input = stripTrailingZeros(String.valueOf(saveNum)); ioView.setText(input); break; case 25: // MS saveNum = cur; break; case 26: // M+ saveNum += cur; break; case 27: // M- saveNum -= cur; break; } }这里有个浮点上的老问题:saveNum是 double,连续几次 M+ 操作后,显示值可能带上浮点尾数。比如 0.1 加三次,saveNum内部是0.30000000000000004,MR 显示出来就露馅。所以 MR 读取时一定要走stripTrailingZeros或 BigDecimal 格式化,不能直接String.valueOf(saveNum)。双击 M+ 或连续 M+ 的场景,是测内存功能最容易暴露问题的地方。
4.2 一处注释与实现不符的按钮初始化
源码里初始化部分有一段:
btn[23].setClickable(false);//M+ btn[24].setClickable(false);//M-按钮下标映射在前文已经列过:btn[23]是btn_mc,btn[24]是btn_mr。也就是说这段代码实际禁用的是 MC 和 MR 键,但注释写的是禁用 M+ 和 M-。这是明显的复制粘贴问题。
从业务语义讲,初始状态只有 MS 是明确可用的,因为内存里还没有任何值,MC(清内存)和 MR(读内存)没有意义,M+/M- 也没有累加基础。所以更合理的初始化应该是禁用btn[23]、btn[24]、btn[26]、btn[27],只放行btn[25](MS)。等到用户第一次执行 MS 或 M+/M- 之后,再动态把 MC/MR 置为可点。如果只是照抄源码,运行后会发现 MR、MC 是灰的,而 M+、M- 能点,这和你预期的内存闭环是反的。具体修法放在避坑章节。
4.3 菜单与关于对话框:onOptionsItemSelected 的扩展点
源码里还留了一段onOptionsItemSelected,用Menu.FIRST系列常量区分菜单项,其中一个分支弹出一个“关于”对话框,确认后跳转外部页面,另一个分支把某个名为isPaused的字段设为 false。从字段命名看,这段逻辑大概率是从别的项目移植过来的,在这个计算器上下文里没有对应的 view 对象。
菜单的编程方式用的是旧式的Menu.FIRST + n索引,而不是菜单资源 ID。这种方式在现在的新项目里不推荐,但它展示了一个扩展点:如果要在计算器里加“帮助”“关于”“换肤”等功能,在onOptionsItemSelected里增加分支就可以。把这段代码接到界面上,需要先在 Activity 里重写onCreateOptionsMenu,给菜单添加三个条目,分别对应Menu.FIRST、Menu.FIRST + 1、Menu.FIRST + 2。Menu.FIRST + 2那段把isPaused置 false,逻辑上是“恢复”,对应的Menu.FIRST + 1预计是“暂停”,但源码没把前面的分支写全。
这个代码块的实用价值是演示了AlertDialog的链式写法:setTitle、setMessage、setPositiveButton依次设置,最后create().show()。对话框通过setCanceledOnTouchOutside(true)允许点击外部关闭,这是移动端对话框很常见的用户习惯,值得保留。
5. 常见问题与避坑:跑通这份源码最容易翻车的 5 个地方
部署一份别人的源码,最花时间的不是读代码,而是踩坑。这里把我在跑这个项目过程中真实遇到的几个问题按“现象 → 原因 → 解决”写出来。
5.1 现象:MC、MR 初始变灰,M+、M- 反而能点
运行后界面上 MC、MR 两个键是灰色不可点的,M+、M- 却能正常点击,和预期完全相反。查看代码发现initial()里写了btn[23].setClickable(false)和btn[24].setClickable(false),注释却写着 M+ 和 M-。
原因就是按钮下标映射和注释对不上,btn[23]是btn_mc,btn[24]是btn_mr,作者想禁用 M+/M- 却写错了下标。解决方法是按下标映射修正,同时考虑内存键的完整状态逻辑:
btn[23].setClickable(false); // MC btn[24].setClickable(false); // MR btn[26].setClickable(false); // M+ btn[27].setClickable(false); // M- // btn[25] MS 保持可用,作为内存功能的入口如果只是想让初始状态符合直觉,这一处修正就够了。更严谨的做法是在每次内存操作后同步更新按钮可用状态:执行过 MS 或 M+/M- 后,把 MC、MR、M+/M- 全部置为可点。
5.2 现象:3*+4 报“表达式错误”,3*-4 却正常
输入3*+4再按等号,直接弹出“表达式错误!”,但同样结构换成3*-4就能算出 -12。原因是calculate()的负数识别分支只放行-,遇到+、*、/等连续运算符会认为表达式不合法,抛出异常。
从设计角度讲这是可接受的行为,因为“一元正号”在数学上等价于省略,3*+4完全可以写成3*4。真正需要注意的是:如果你改代码支持一元正号,必须在if (tempExpression.charAt(end) == '-')的判断里把+也放进去,并且把start移到end + 1,否则+4里的 4 会丢失。这个分支很微妙,建议改完后把3*+4、3*-4、-5+3、3--4四组用例全部跑一遍。
5.3 现象:5.5 % 2 被拒绝,提示“不能对小数或负数进行取余数操作!”
取模键%是布局里的特色功能,但随便拿小数试一下就报错。原因在getRet里的取模分支:
if ((a != (int) a || b != (int) b) || (a < 0 || b < 0)) throw new Exception(ModByInt); ret = a - ((int) (a / b)) * b;作者故意把取模限制在“非负整数”范围内。原因是 Java 的%运算符对 double 的操作结果是浮点余数,对负数的处理又遵循向零取整规则,容易让初学者困惑。这里直接限制输入类型,把边界问题通过异常挡在门外。
如果要用原生 Java 取模,直接把ret = a % b即可,但要注意结果可能带小数尾数,比如5.5 % 2的结果是1.5。如果产品上允许小数取模,建议用BigDecimal.remainder()而不是 double 的%,能避免大部分精度问题。否则就要像源码一样,在 UI 上给用户明确提示,而不是抛一个笼统的异常文本。
5.4 现象:0.1+0.2 显示一长串浮点尾数
输入0.1+0.2=,输出框显示0.30000000000000004,虽然数学上不算错,但用户观感极差。原因不难猜:表达式里的数字都用Double.parseDouble解析,计算结果也直接用 double 参与运算,而 IEEE 754 浮点数无法精确表示 0.1 和 0.2。
源码里有一个stripTrailingZeros方法,它的作用是清理 double 转字符串后末尾的 0,比如让2.0显示成2。但它解决不了0.30000000000000004这种尾数问题,因为尾数不是 0。常见做法有两种:一是用BigDecimal.valueOf(ret).stripTrailingZeros().toPlainString()再转回字符串显示;二是限制小数位数,保留精度到一定位数后四舍五入。
需要注意BigDecimal的构造方法,用new BigDecimal(0.1)会把 double 的二进制近似值原样带进来,越转越乱,得用BigDecimal.valueOf(0.1)或字符串构造。这个项目如果只是交作业,用String.format("%.10f", ret)再清尾零是最省事的方案。
5.5 现象:50sp 大字号在窄屏上溢出,表达式框从头部省略
模拟器上界面显示良好,换到小屏真机,数字一长就会溢出到边界。原因有两个:input_outputView的textSize="50sp"是固定值,不随屏幕宽度缩放;expressionOutputView的ellipsize="start"在超长时会吞掉表达式开头的字符。
50sp 在 1080p 模拟器上能显示约 8 个字符,在 720p 屏上只能显示约 5 个,用户按一个较长的表达式根本看不全。解决思路有两种:一是改用 AndroidX 的 AutoSizeTextView,设置最小文本大小和最大文本大小,让字号随控件宽度自适应;二是在代码里动态计算,把 dp 转成 px 后按屏幕宽度等比缩放。
DisplayMetrics dm = getResources().getDisplayMetrics(); float textSize = Math.min(50f, dm.widthPixels / 20f); ioView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);表达式框的头部省略其实是可以接受的,因为用户关心的是最近输入的内容,最左边是已经确认过的运算符和数字。但如果你希望用户能回看完整表达式,可以把ellipsize改为marquee并开启滚动,或者把singleLine改成maxLines="2"换行显示。
6. 验证与进阶:用一张自测表把这套源码跑服
代码能编译不等于能交付。计算器这种工具类应用,边界情况多到足以让你怀疑人生。我的习惯是拿到任何计算器 demo,先建一张冒烟测试表,把常规运算、异常场景、内存功能分开跑一遍,跑过再谈优化。
| 编号 | 输入操作 | 期望结果 | 实际关注点 |
|---|---|---|---|
| 1 | 1+2= | 3 | 基本加法 |
| 2 | 2*3+4= | 10 | 乘法优先级高于加法 |
| 3 | 3+4*2= | 11 | 反序验证优先级不依赖输入顺序 |
| 4 | 9/0= | 除数不能为零! | 除零异常拦截 |
| 5 | 9%2= | 1 | 整数取模 |
| 6 | -5+3= | -2 | 负数开头 |
| 7 | 3*-4= | -12 | 运算符后紧跟负数 |
| 8 | 7 按 +/- | -7 | 正负号切换 |
| 9 | 9 按 sqrt | 3 | 开方且无尾零 |
| 10 | 10 按 1/x | 0.1 | 倒数 |
| 11 | 12 按 M+,再按 3 M+,MR | 15 | 内存累加 |
| 12 | 0.1+0.2= | 0.3 | 浮点显示是否干净 |
| 13 | 1÷3= | 0.3333333333 | 结果位数是否可接受 |
其中第 12 和第 13 条是浮点相关的硬关卡。很多计算器 demo 在整数运算里一切正常,一到小数就露馅。如果你拿到手的是原始 double 实现,建议先跑这两条,不合格就按前面 5.4 的 BigDecimal 方案改造。
验证环境上,模拟器和真机有差异。模拟器普遍分辨率高、密度大,50sp 的文字看起来刚好;真机如果是小屏,同样 sp 会显得偏大。所以布局相关的几条一定要在真机上过一遍,尤其是底部按钮宽度 80dp 是否会被系统缩放。还有等号按钮的rowSpan=2,在部分国产 ROM 上可能因为 GridLayout 版本差异出现高度测量不准,这时可以手动给等号按钮设一个最小高度兜底。
进阶方向上,这套双栈引擎最值得改的是支持括号。当前compare表只覆盖+-*/%#六种字符,要加括号得扩成 8×8 矩阵:读入(时无条件压栈,读入)时不断弹栈计算直到弹出匹配的(。改的时候注意,)之后的数字解析逻辑要跟着变,否则(1+2)*3这类表达式会扫描出“括号后紧跟数字”的异常情况。
另一个方向是把 double 换成 BigDecimal。改动面不小:Stack<Double>要变Stack<BigDecimal>,getRet的四则运算全部换成 BigDecimal 方法,解析数字的Double.parseDouble对应换成new BigDecimal(String)。但换来的是浮点问题彻底消失,加法和乘法都能精确落笔。
我第一次拿这套源码跑真机时,就是按着这张表敲到第 12 条,在 0.1+0.2 上被一长串浮点尾数糊了一脸,才意识到“能算”和“能交付”之间差着多少边界处理。从那以后我拿到任何计算器 demo,第一件事都是先过一遍小数精度和内存键状态,这两关没过就坚决不上线。希望帮到你。
本文还有配套的精品资源,点击获取