读完这篇,你能做什么
能推导常见表达式结果,并解释整数除法、转换与溢出带来的错误。
学习理由、准备要求与知识点
为什么学它
数值类型和求值规则决定计算是否正确,是条件、循环与业务运算的前提。
先读这些内容
知识点清单
- 八种基本类型
- 局部变量初始化
- 字面量与
long后缀 - 整数和浮点除法
- 数值提升与窄化转换
- 短路逻辑
- 整数溢出检测
- 表达式优先级
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
变量用一个带类型的名字保存当前值。int count = 3 是声明变量并给初值;后续 count = 4 是把值换成 4,不是数学等式。方法里的局部变量必须先明确初始化才能使用,字段和数组元素则有默认值。默认的 0 只说明语言做了初始化,不能据此认定业务数据已经录入。
Java 有八种基本类型:byte、short、int、long 保存整数,float、double 用浮点形式保存数值,可以处理小数但可能有近似误差;char 保存 UTF-16 的一个 16 位单位,叫代码单元;boolean 保存真假。
类型决定范围和能做的运算。一个 char 不一定就是用户看到的一个完整字符;boolean 也不能自动与 0、1 互换。保存对象引用的变量属于引用类型,不在这八种基本类型之内。
运算前先问操作数是什么类型。5 / 2 使用整数除法,结果为 2;5 / 2.0 才得到浮点结果。int 相加溢出不会自动扩大类型,long total = a + b 也可能先在 int 中算错再赋值;应在运算前扩大操作数,如 (long) a + b。金额不能简单依靠 double 精确表示十进制小数,先理解二进制浮点近似,再在金额篇选择 BigDecimal 与舍入规则。
文中这几个词,先认一认
- 数值提升
- 在需要数值提升的运算中,Java 会按规则转换参与运算的数字类型,例如两个 byte 相加,会先按
int计算。二元数值提升会把两边调整到共同类型,但不会因为结果太大就无限扩大类型。 - 溢出
- 计算结果超出类型能够表示的范围,例如两个
int相加后超出int上限。 - 短路
- 逻辑结果已经确定时,不再执行另一边,例如
false&& 后面的表达式不再求值。 - 赋值
- 把等号右边表达式的值存入左边指定的变量,不表示两边永远相等。
02看看它怎么工作
表达式按优先级和结合性组织,但可读性比炫耀记忆更重要。用括号表达业务分组,例如 (price + fee) * quantity,避免把赋值、递增和复杂条件揉在同一行。前置与后置递增返回的表达式值不同;如果只是增加计数,单独一行 count++ 最直观。Java 的操作数求值次序有规范保证,但依赖副作用组合仍容易让维护者误读。
byte、short 等较小的整数参与一般算术时,通常会先转成 int 来计算。所以 byte a = 1; byte b = 2; 的 a + b 并不是 byte。把较大范围的整数转成较小范围时,可能丢掉高位;复合赋值还包含隐含的类型转换,能编译通过不代表结果没有超出范围。
浮点数也有特殊规则,例如 NaN 的比较、正负零和无穷。入门不必背完整 IEEE 规则,但要知道 Java 浮点数的运算和比较,不能完全照搬数学中的实数规则。
短路与边界检查可以合作:text != null && text.length() > 0 中,左侧为假时不会执行右侧。换成 & 会改变求值行为,可能触发空引用异常。溢出也需要显式策略:计数允许失败时使用 Math.addExact,需要更大固定范围用 long,需要任意精度整数用 BigInteger。图展示先定范围、再选运算与校验的过程,而不是事后看到负数才补救。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["明确数值单位和范围"] --> B["选择类型"]
B --> C["确认操作数提升规则"]
C --> D["执行运算"]
D --> E{"结果是否必须精确且在范围内"}
E -->|"是"| F["使用精确检查或更大表示"]
E -->|"近似可接受"| G["记录误差与比较规则"]按顺序看,每一步做了什么
- 列出数值单位与可能范围。
- 确认字面量和操作数类型。
- 用括号显式表达分组。
- 明确溢出、近似和除法策略。
- 测试零、边界值和非法输入。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Math.java · java.lang.Math#addExact(int,int)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
public final class SourceModel {
public static int checkedAdd(int left, int right) {
// int 的任意两个数相加都能放进 long,先扩大再运算。
long wide = (long) left + right;
if (wide < Integer.MIN_VALUE || wide > Integer.MAX_VALUE) {
throw new ArithmeticException("integer overflow");
}
// 经过范围检查后,窄化不会丢失数值。
return (int) wide;
}
}本机 Math.addExact(int,int) 先做 int 加法,再通过符号位关系检测溢出。两个同号操作数得到相反符号结果才可能触发该检查;异号相加不会越过 int 两端。教学模型改用 long 暂存并比较上下界,便于初学者理解语义,明确不是 JDK 使用的那段位运算实现。
先扩大操作数非常关键。把 (long) 写在整个 int 加法结果外面已经太迟,因为窄类型中的溢出已经发生。模型中的 long 能容纳两个 int 之和,因此这个中间计算有范围保证;若把参数换成两个 long,就不能继续用同样理由宣称 long 中间结果安全。
抛异常能让调用者分清“结果超出表示范围”和“结果是合法负数”。普通加号只按固定宽度整数规则计算,不会主动判断业务是否允许这个结果。使用精确计算方法后,接口要明确溢出时怎么办:拒绝输入、换更大表示,还是把错误报告给调用方。
源码还可能标记可被虚拟机特殊优化的方法,不能从 Java 文件的语句数量直接估算机器指令或真实性能。这里的核验用于确认语义与边界,性能判断需要考虑调用环境、热身和输入分布。初学者先能预测边界输入的结果,再学习底层优化会更稳妥。
代码先调用谁,再调用谁
- 业务选择精确相加契约
Math.addExact计算候选结果- 检查符号关系是否表示溢出
- 返回合法
int或抛ArithmeticException
04动手写一个例子
在给定整数位宽内做饱和加法
输入两个 int;若数学和超出 int 范围,返回最近的端点,否则返回精确和。与 addExact 抛异常不同,饱和是本题明确规定的业务策略。
- 输入 1
2147483647 + 1- 输出 1
2147483647
- 输入 2
-2147483648 + -1- 输出 2
-2147483648
- 输入 3
7 + -3- 输出 3
4
直接做 int 加法后比较结果大小不可靠,因为结果可能已经绕回另一端。先把一个操作数转为 long,随后相加在 long 中完成。两个 int 的数学和都能被 long 表示,因此这个中间值足以作为可信比较对象;范围推导先于代码选择。
把数轴分成三个互斥区间:小于 int 最小值、处于合法范围、大于最大值。前后区间返回对应端点,中间区间安全转换。判断顺序简单且覆盖全部可能,避免多个相互重叠的特殊分支。饱和丢弃了超出边界的幅度,调用者必须事先接受这种信息损失。
测试不能只覆盖两个正数。正向溢出、负向溢出、异号相抵、零和合法端点都对应不同推理路径。通过测试说明该函数确实遵守饱和约定,但不声称这种约定适合所有金额或库存计算;需要拒绝超范围时,应采用另一种接口。
为什么这样写是对的分支判断前 wide 始终等于两个输入的精确数学和。
输入固定为两个 int,执行一次 long 加法、最多两次范围比较与一次转换,不遍历任何可变长度数据。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
static int saturatedAdd(int a, int b) {
// 先提升再相加,保证中间结果没有 int 溢出。
long wide = (long) a + b;
// 饱和策略把超范围数学结果映射到最近端点。
if (wide > Integer.MAX_VALUE) return Integer.MAX_VALUE;
if (wide < Integer.MIN_VALUE) return Integer.MIN_VALUE;
return (int) wide;
}
static void check(int expected, int a, int b) {
int actual = saturatedAdd(a, b);
if (actual != expected) throw new AssertionError(actual);
}
public static void main(String[] args) {
// 普通值、两端越界、相抵和边界不越界分别覆盖。
check(4, 7, -3);
check(Integer.MAX_VALUE, Integer.MAX_VALUE, 1);
check(Integer.MIN_VALUE, Integer.MIN_VALUE, -1);
check(-1, Integer.MIN_VALUE, Integer.MAX_VALUE);
check(0, 0, 0);
check(Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 极值与零相加仍返回原值。
- 两端越界采用饱和而非异常。
- 异号极值相加结果为 -1。
进阶选读 · 可选深入:任意精度与单位类型
场景累计金额或计数的理论范围超过 long,同时需要保留每一位。
做法选择 BigInteger 表示整数,并以专用类型或命名明确单位;分析数位增长,避免把任意精度误当成固定成本。
复杂度b 位整数加法通常需 O(b) 时间与结果存储。
好处与代价消除固定宽度溢出会增加内存与计算成本,仍需限制恶意超长输入。
05项目里怎么用
类型是数据契约的一部分。用户编号即使由数字组成,也可能不参与算术,保留字符串能避免前导零丢失。时间应明确秒还是毫秒,金额应明确元还是分;两个单位不同的 long 相加不会被编译器自动阻止。使用清晰命名或专用值类型,比在注释里暗示单位更可靠。
Java 算得对,不表示传到外部还能原样保存。数据库字段、JSON 接收方与 Java int、long 的范围可能不同。要检查序列化前后是否仍符合接口约定,特别是把很大的整数交给采用不同数值表示方式的客户端时。
数字超出范围时,要由业务决定怎么办。可以把它限制在最大或最小值,这叫饱和处理;也可以报错,或改用能保存更大整数的类型。音量和进度条可能适合限制到上限,账务计算通常不能悄悄截断。用户输错数字时给出清楚提示;程序内部本应成立的条件被破坏时,保留出错位置。不要一遇到算术异常就返回零,因为零也可能是一个正常结果。
容易踩的坑
- 在
int运算完成后才把结果转为long。 - 用
double的精确相等比较代替误差策略。 - 混淆 && 与 & 的求值行为。
- 把饱和截断当作所有溢出的通用修复。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 固定宽度普通运算 | 表示紧凑且直接 | 溢出不会自动报告 | 范围已证明且符合模运算语义 |
| 精确检查运算 | 失败路径明确 | 需要处理超范围情况 | 计数与业务约束必须成立 |
| 任意精度整数 | 保留更大整数结果 | 成本随数位增长 | 范围无法压入固定整数 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么 long x = 2_000_000_000 + 2_000_000_000 仍然错?
展开参考答案与推导
两个字面量是 int,先在 int 中相加,再把已溢出的结果转成 long。把其中一个操作数改成 long 字面量或在运算前强转,才能使加法本身采用更大范围。
02char 是否等于一个汉字或表情?
展开参考答案与推导
char 是 UTF-16 代码单元。部分字符需要一对代理代码单元表示,可见字符还可能由多个码点组合;字符统计与文本截断应在后续 Unicode 规则下处理。
03为什么饱和加法不能替代 addExact?
展开参考答案与推导
二者失败语义不同。饱和返回端点并丢失超出幅度,精确加法在不可表示时抛异常;调用者必须知道采用哪一种契约,才能判断结果是否可用于业务。
换个情况,还会处理吗
01库存相加后突然变成负数,先查什么?
展开参考答案与推导
先确认输入范围和实际运算类型,使用更大中间值或精确加法复现边界;同时核对单位与数据库类型。不能简单把负数取绝对值,那会把溢出掩盖成另一笔错误库存。
02零除法是否总会抛 ArithmeticException?
展开参考答案与推导
整数除零会抛该异常,浮点除零可能产生无穷或 NaN。首先确认操作数类型,然后依据业务拒绝零分母;不要仅凭表达式写着除号就假设同一种结果。
03条件左侧已检查非空却仍然空指针,为什么?
展开参考答案与推导
检查是否误用了非短路 &,以及右侧是否访问了另一个可能为空的值。在共享可变状态下重复读取字段也可能不是同一快照,必要时先读取到局部变量再判断。
写代码试一试
01写一个安全的 int 乘法
- 输入 / 条件
50000 × 50000- 输出 / 预期
抛 ArithmeticException
提示 先在 long 中相乘。
展开参考答案与推导
两个 int 的乘积可放入 long,先计算 (long)a * b,再比较 int 上下界,越界抛异常。不要先做 int 乘法再扩大,测试零、负数和最大乘积。
02预测两个除法结果
- 输入 / 条件
5/2 与 5/2.0- 输出 / 预期
2 与 2.5
提示 看运算时类型。
展开参考答案与推导
第一式是整数除法,向零截断;第二式包含 double,另一操作数经提升后进行浮点除法。赋给 double 的变量并不会倒推改变第一式已经完成的整数运算。
03为饱和算法增加负数正常路径
- 输入 / 条件
-7 + -3- 输出 / 预期
-10
提示 结果仍在范围内。
展开参考答案与推导
加入 check(-10, -7, -3)。这验证负结果本身并不表示溢出,真正判断依据是精确数学和是否越过下界;测试应同时保留负向越界用例。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能推导常见表达式结果,并解释整数除法、转换与溢出带来的错误。
本篇重点加练
重点加练数值运算的中间类型与范围,防止看似正确的公式在边界处改变含义。
- 整数和浮点除法
- 整数溢出检测
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为人数、是否启用、单个 UTF-16 单元和超过
int范围的计数选择基本类型,写出合法字面量与局部变量初始化样例;附一份不能编译的独立反例。- 提交内容
- 类型表、编译样例和错误记录。
- 怎样算完成
- 范围与业务含义对应
- 字面量类型和变量类型分开
练到的知识点八种基本类型 · 局部变量初始化 · 字面量与 long 后缀
- 作业 02代码推演
逐步推演 7
/2、7/2.0、(double)(7/2)、(byte)130 与 byte b=127 后的 b++,标注每个中间表达式的类型;再用最小程序核验。- 提交内容
- 类型推演表和运行记录。
- 怎样算完成
- 数值提升发生点明确
- 强制转换风险可解释
练到的知识点整数和浮点除法 · 数值提升与窄化转换
- 作业 03代码推演
计算每份 7 元的商品均摊给 3 人,分别制定“整数分”“小数元”和“余数另分”的业务含义,不要求使用尚未学习的金额类。
- 提交内容
- 单位、运算顺序与结果合同。
- 怎样算完成
- 先定义单位再写表达式
- 除法含义适配业务目标
练到的知识点整数和浮点除法
综合应用
- 作业 04排错实验
在一个 main 中使用布尔表达式和 counter++ 记录求值,分别改变 &&、|| 的条件顺序及括号;样本包含零分母风险,先预测哪些子表达式会求值再运行,不需要自定义判断方法。
- 提交内容
- Java 实验、自写表达式样本、预测与实际运行记录。
- 怎样算完成
- 求值顺序有独立证据
- 运算优先级与短路分别检验
练到的知识点短路逻辑 · 表达式优先级
- 作业 05编程实践
编写单个 main,边长直接在代码中赋值,计算长方形周长与面积;分别使用
int和long,并用本篇的精确算术 API 检查不可表示的结果。每次替换一组预先写好的正常、零值与大值样本,不要求交互输入或自定义方法。- 提交内容
- Java 文件、自写样本与结果核对表、逐组编译运行证据。
- 怎样算完成
- 记录中间表达式与结果类型
- 正常结果和数值失败均留证
- 数学结果与表示范围分别核验
练到的知识点字面量与 long 后缀 · 整数和浮点除法 · 数值提升与窄化转换 · 整数溢出检测
- 作业 06面试场景
促销程序把单价分数值乘以数量后再折扣,某些大订单结果异常。根据输入单价 1500000000、数量 3 设计排查记录,指出需要核对的每个中间类型和舍入位置。
- 提交内容
- 表达式分解、风险检查和改写验收案例。
- 怎样算完成
- 不只检查最终变量类型
- 整数与浮点精度分别考虑
练到的知识点整数和浮点除法 · 整数溢出检测 · 表达式优先级
- 作业 07排错实验
在单个 main 中独立执行整数与浮点除法实验,样本含零分母、极小非零浮点数以及
Integer.MIN_VALUE除以 -1;每个样例单独运行,记录结果或异常并手算核对表示范围,不新增循环或方法。- 提交内容
- Java 文件、自写样本核对表与逐组运行证据。
- 怎样算完成
- 每个操作数类型被标注
- 不同数值域的范围行为单独观察
练到的知识点整数和浮点除法 · 整数溢出检测
- 作业 08面试场景
某程序把
int乘法结果赋给long,开发者认为已经不会溢出。用具体类型标注检查该主张,构造可在单个 main 验证的输入,不增加循环。- 提交内容
- 表达式类型图和运行记录。
- 怎样算完成
- 每个中间值有类型说明
- 证据不限于最终变量范围
练到的知识点整数溢出检测
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成循环后:实现十进制非负整数解析,明确允许的字符、最大值和空输入政策,不直接调用现成 parse 方法;输入包含长数字串和非法字符。
- 提交内容
- Java 实现、自写边界与参考方法对照测试、运行证据。
- 怎样算完成
- 每一步累积有范围保证
- 非法输入定位清楚
- 复杂度按字符长度说明
练到的知识点八种基本类型 · 数值提升与窄化转换 · 整数溢出检测
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
同一计算从
int改为long,或从整数改为double。选择一个业务公式,分别评估范围、精度与除法政策发生了什么变化。- 提交内容
- 类型选择比较表和小规模样本。
- 怎样算完成
- 不只根据类型宽度下结论
- 仍需保持的业务合同明确
练到的知识点整数和浮点除法 · 整数溢出检测
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Dev.java:语言基础 (新标签页)↗
依次阅读变量、运算符、表达式与控制流入口,建立语法概念地图。
- 实践规范英文
Java 17 语言规范:类型、值与变量 (新标签页)↗
核对基本类型范围、引用类型、类型变量与可具体化类型,理解编译期约束。
- 深入规范英文
Java 17 语言规范:转换与上下文 (新标签页)↗
查阅数值提升、强制转换、装箱拆箱以及方法调用时的转换规则。
- 深入规范英文
Java 17 语言规范:表达式 (新标签页)↗
查阅运算符、求值顺序、短路、方法调用与 Lambda 的目标类型规则。
- 深入参考文档英文
OpenJDK 17u java.lang.Math (新标签页)↗
定位
addExact(int,int)的符号位检查,与教学模型的扩大范围方法对照。 - 深入参考文档英文
Java17 Math API (新标签页)↗
比较
addExact与普通算术的异常契约。