qinghuan
← Java SE / 全部笔记

单元 03 / Java SE

P0 · 必会主线L0 · 零基础

变量、基本类型与运算符:先算对一个表达式

先理解变量保存什么,再检查表达式使用的类型,避免整数除法、数值溢出和转换顺序造成错误。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能推导常见表达式结果,并解释整数除法、转换与溢出带来的错误。

学习理由、准备要求与知识点

为什么学它

数值类型和求值规则决定计算是否正确,是条件、循环与业务运算的前提。

先读这些内容

知识点清单

  • 八种基本类型
  • 局部变量初始化
  • 字面量与 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["记录误差与比较规则"]

按顺序看,每一步做了什么

  1. 列出数值单位与可能范围。
  2. 确认字面量和操作数类型。
  3. 用括号显式表达分组。
  4. 明确溢出、近似和除法策略。
  5. 测试零、边界值和非法输入。

03对照实现代码

精确加法如何检测 int 范围 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Math.java · java.lang.Math#addExact(int,int)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
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 文件的语句数量直接估算机器指令或真实性能。这里的核验用于确认语义与边界,性能判断需要考虑调用环境、热身和输入分布。初学者先能预测边界输入的结果,再学习底层优化会更稳妥。

代码先调用谁,再调用谁

  1. 业务选择精确相加契约
  2. Math.addExact 计算候选结果
  3. 检查符号关系是否表示溢出
  4. 返回合法 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 始终等于两个输入的精确数学和。

时间 O(1)。额外空间 O(1)。

输入固定为两个 int,执行一次 long 加法、最多两次范围比较与一次转换,不遍历任何可变长度数据。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能推导常见表达式结果,并解释整数除法、转换与溢出带来的错误。

本篇重点加练

重点加练数值运算的中间类型与范围,防止看似正确的公式在边界处改变含义。

  • 整数和浮点除法
  • 整数溢出检测

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为人数、是否启用、单个 UTF-16 单元和超过 int 范围的计数选择基本类型,写出合法字面量与局部变量初始化样例;附一份不能编译的独立反例。

    提交内容
    类型表、编译样例和错误记录。
    怎样算完成
    • 范围与业务含义对应
    • 字面量类型和变量类型分开

    练到的知识点八种基本类型 · 局部变量初始化 · 字面量与 long 后缀

  2. 作业 02代码推演

    逐步推演 7/2、7/2.0、(double)(7/2)、(byte)130 与 byte b=127 后的 b++,标注每个中间表达式的类型;再用最小程序核验。

    提交内容
    类型推演表和运行记录。
    怎样算完成
    • 数值提升发生点明确
    • 强制转换风险可解释

    练到的知识点整数和浮点除法 · 数值提升与窄化转换

  3. 作业 03代码推演

    计算每份 7 元的商品均摊给 3 人,分别制定“整数分”“小数元”和“余数另分”的业务含义,不要求使用尚未学习的金额类。

    提交内容
    单位、运算顺序与结果合同。
    怎样算完成
    • 先定义单位再写表达式
    • 除法含义适配业务目标

    练到的知识点整数和浮点除法

综合应用

  1. 作业 04排错实验

    在一个 main 中使用布尔表达式和 counter++ 记录求值,分别改变 &&、|| 的条件顺序及括号;样本包含零分母风险,先预测哪些子表达式会求值再运行,不需要自定义判断方法。

    提交内容
    Java 实验、自写表达式样本、预测与实际运行记录。
    怎样算完成
    • 求值顺序有独立证据
    • 运算优先级与短路分别检验

    练到的知识点短路逻辑 · 表达式优先级

  2. 作业 05编程实践

    编写单个 main,边长直接在代码中赋值,计算长方形周长与面积;分别使用 int 和 long,并用本篇的精确算术 API 检查不可表示的结果。每次替换一组预先写好的正常、零值与大值样本,不要求交互输入或自定义方法。

    提交内容
    Java 文件、自写样本与结果核对表、逐组编译运行证据。
    怎样算完成
    • 记录中间表达式与结果类型
    • 正常结果和数值失败均留证
    • 数学结果与表示范围分别核验

    练到的知识点字面量与 long 后缀 · 整数和浮点除法 · 数值提升与窄化转换 · 整数溢出检测

  3. 作业 06面试场景

    促销程序把单价分数值乘以数量后再折扣,某些大订单结果异常。根据输入单价 1500000000、数量 3 设计排查记录,指出需要核对的每个中间类型和舍入位置。

    提交内容
    表达式分解、风险检查和改写验收案例。
    怎样算完成
    • 不只检查最终变量类型
    • 整数与浮点精度分别考虑

    练到的知识点整数和浮点除法 · 整数溢出检测 · 表达式优先级

  4. 作业 07排错实验

    在单个 main 中独立执行整数与浮点除法实验,样本含零分母、极小非零浮点数以及 Integer.MIN_VALUE 除以 -1;每个样例单独运行,记录结果或异常并手算核对表示范围,不新增循环或方法。

    提交内容
    Java 文件、自写样本核对表与逐组运行证据。
    怎样算完成
    • 每个操作数类型被标注
    • 不同数值域的范围行为单独观察

    练到的知识点整数和浮点除法 · 整数溢出检测

  5. 作业 08面试场景

    某程序把 int 乘法结果赋给 long,开发者认为已经不会溢出。用具体类型标注检查该主张,构造可在单个 main 验证的输入,不增加循环。

    提交内容
    表达式类型图和运行记录。
    怎样算完成
    • 每个中间值有类型说明
    • 证据不限于最终变量范围

    练到的知识点整数溢出检测

进阶挑战 · 掌握前置后选做

  1. 作业 09编程实践

    完成循环后:实现十进制非负整数解析,明确允许的字符、最大值和空输入政策,不直接调用现成 parse 方法;输入包含长数字串和非法字符。

    提交内容
    Java 实现、自写边界与参考方法对照测试、运行证据。
    怎样算完成
    • 每一步累积有范围保证
    • 非法输入定位清楚
    • 复杂度按字符长度说明

    练到的知识点八种基本类型 · 数值提升与窄化转换 · 整数溢出检测

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    同一计算从 int 改为 long,或从整数改为 double。选择一个业务公式,分别评估范围、精度与除法政策发生了什么变化。

    提交内容
    类型选择比较表和小规模样本。
    怎样算完成
    • 不只根据类型宽度下结论
    • 仍需保持的业务合同明确

    练到的知识点整数和浮点除法 · 整数溢出检测

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗