Skip to content

类型转换与数值精度 ​

学习目标 ​

  • 能判断基本类型之间何时自动转换、何时必须显式强制转换,以及转换可能丢失什么。
  • 理解二元数值运算的类型提升、整数溢出和 Math.addExact 的边界。
  • 能解释浮点数为什么不适合直接比较金额,并用 BigDecimal 表达精确十进制计算。
  • 区分 char、UTF-16 码元、Unicode 码点和用户可见字符。

核心知识点 ​

专业术语 ​

  • 扩大转换(widening conversion):从表示范围或精度较小的类型转向较大的类型,通常可自动完成,如 int 到 long。
  • 窄化转换(narrowing conversion):从较宽类型转向较窄类型,必须显式强转,可能截断、溢出或丢失精度。
  • 数值提升(numeric promotion):算术运算前把参与者提升到共同类型;byte、short 和 char 通常先提升为 int。
  • 整数溢出(integer overflow):结果超出整数类型范围后按位回绕,Java 默认不会自动抛异常。
  • 二进制浮点(binary floating point):float/double 用二进制近似表示实数,部分十进制小数无法精确存储。
  • BigDecimal:以十进制数值和精度规则进行计算的类,适合金额和需要可审计舍入的场景。
  • UTF-16 码元/Unicode 码点:char 表示一个 16 位码元,补充平面字符可能由两个码元组成一个码点。

白话解释与边界 ​

自动扩大转换 ​

自动扩大转换可以把 int 放进 long,因为目标类型能表示源类型的所有整数值;但 long 到 float 虽然语法上允许,浮点有效位有限,仍可能丢失整数精度。窄化转换必须写 (int) value,它不会四舍五入,也不会检查业务范围:小数会截去小数部分,超出范围的整数会按低位重新解释。

数值提升与窄化转换 ​

一个表达式的类型由参与运算的类型共同决定。byte + byte 的结果是 int,int + long 的结果是 long,出现 double 时通常提升为 double。赋值给更窄的变量时,常量表达式在编译器能证明范围安全的情况下可能被接受,但变量表达式仍需要显式转换;不要把这种语法便利当作运行时校验。

整数溢出与 Math.*Exact ​

整数溢出是确定的回绕行为,例如 Integer.MAX_VALUE + 1 得到负数。金额、计数上限和索引等业务数据应先定义范围;需要溢出时立即失败可用 Math.addExact、Math.multiplyExact,捕获 ArithmeticException 后给出明确处理。

浮点误差与 BigDecimal ​

double 保存的是二进制近似值,所以 0.1 + 0.2 可能不等于精确的 0.3。科学计算通常接受误差并使用误差范围比较;金额计算使用 new BigDecimal("0.10") 这样的字符串构造,并在除法或格式化时明确 RoundingMode。new BigDecimal(0.1) 会把 double 已有的误差带进十进制对象。

char、UTF-16 与 Unicode 码点 ​

char 的范围是一个 UTF-16 码元,不等于“一个用户看到的字符”。例如 🙂 的一个 Unicode 码点占两个 char;用 String.codePointCount 统计码点,用 codePoints() 遍历码点。字素簇(用户可见字符)还可能由多个码点组合,复杂文本处理应使用更高层的 Unicode 文本能力。

常用用法 ​

扩大转换:安全提升整数范围 ​

扩大转换通常不需要显式强转,但浮点类型的有效位有限,不能只看“语法允许”就认为精度不会损失。

java
// 语义:扩大转换通常不需要显式强转,但浮点类型的有效位有限,不能只看“语法允许”就认为精度不会损失。
// 初始状态:count 初始为 7;total 初始为 count。
public class WideningDemo {
    public static void main(String[] args) {
        int count = 7;
// 关键变化:count 接收表达式 7 的计算结果。
// 初始状态:count 的初始值为 7。
        long total = count;
        System.out.println(total);
        // 输出:7
    }
}

BigDecimal.valueOf:处理金额小数 ​

金额等精确小数优先使用字符串或 valueOf 构造 BigDecimal,不要直接把二进制 double 误差带入构造器。

java
import java.math.BigDecimal;

public class DecimalDemo {
    public static void main(String[] args) {
        BigDecimal total = BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2));
        // 初始状态:total 当前为 BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2))。
        // 作用:金额等精确小数优先使用字符串或 valueOf 构造 BigDecimal,不要直接把二进制 double 误差带入构造器。
        System.out.println(total);
        // 输出:0.3
    }
}

不常用但需要知道 ​

窄化转换:显式承担范围风险 ​

窄化转换会截断小数且可能溢出;如果这是业务输入,应先做范围校验而不是直接强转。

java
public class NarrowingDemo {
    public static void main(String[] args) {
        double source = 3.9;
// 关键变化:source 接收表达式 3.9 的计算结果。
// 初始状态:source 的初始值为 3.9。
        int result = (int) source;
        System.out.println(result);
        // 输出:3
    }
}

Math.addExact:溢出时立即失败 ​

addExact、multiplyExact 等方法在溢出时抛出 ArithmeticException,适合不允许静默回绕的计数和金额边界。

java
public class ExactMathDemo {
    public static void main(String[] args) {
        System.out.println(Math.addExact(20, 22));
// 输出:42
        // 作用:通过 Math.addExact 溢出时立即失败。
    }
}

简单案例 ​

java
import java.math.BigDecimal;
import java.math.RoundingMode;

public class ConversionDemo {
    public static void main(String[] args) {
        int count = 7;
        long widened = count;                 // 自动扩大:int -> long
        double narrowedSource = 9.75;
        int truncated = (int) narrowedSource; // 显式窄化:截去小数
        byte left = 100;
        byte right = 30;
        int promoted = left + right;          // byte 参与算术先提升为 int
        byte wrapped = (byte) promoted;       // 130 -> -126

        System.out.println(widened + "," + truncated + "," + promoted + "," + wrapped);
        // 输出:7,9,130,-126

        int overflow = Integer.MAX_VALUE + 1;
        int checked = Math.addExact(40, 2);
// 返回:Math.addExact(40, 2) 计算得到 42;overflow 则按 int 回绕为 -2147483648。
        System.out.println("overflow=" + overflow + ", checked=" + checked);
        // 输出:overflow=-2147483648, checked=42

        double approximate = 0.1 + 0.2;
        BigDecimal exact = new BigDecimal("0.10").add(new BigDecimal("0.20"));
// 返回:new BigDecimal("0.10").add(new BigDecimal("0.20")) 得到精确值 0.30。
        BigDecimal quotient = new BigDecimal("1")
                .divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
// 返回:divide(new BigDecimal("3"), 2, HALF_UP) 按两位小数舍入为 0.33。
        System.out.println("double=" + approximate);
        // 输出:double=0.30000000000000004(常见 JDK 输出)
        System.out.println("decimal=" + exact + ", quotient=" + quotient);
        // 输出:decimal=0.30, quotient=0.33

        String emoji = "🙂";
        System.out.println("chars=" + emoji.length()
                + ", codePoints=" + emoji.codePointCount(0, emoji.length()));
        // 输出:chars=2, codePoints=1
    }
}

案例把“能写出来”和“结果可靠”分开:扩大转换只改变表示类型,窄化转换明确承认信息可能丢失;整数检查用 Math.addExact,金额用字符串构造的 BigDecimal,Unicode 文本则按码点而不是只看 char 数量。

易混点 ​

  • 自动转换不等于绝对无损:long 转 float 可能失去整数精度,窄化转换还可能截断或回绕。
  • byte + byte 的结果是 int,因为二元数值提升发生在算术运算之前。
  • 强制转换不会四舍五入,也不会把已经溢出的结果恢复成原值。
  • double 的误差不是输出格式问题;要精确十进制就从字符串构造 BigDecimal 并定义舍入。
  • char 数量、Unicode 码点数量和用户看到的字素簇数量可能不同。

课后小问 ​

  1. (int) 3.99 的结果是 3 还是 4?为什么? 答案:结果是 3;从浮点类型窄化为整数会直接截去小数部分,不会四舍五入。 解析:若业务需要四舍五入,应先使用 Math.round 或明确的 BigDecimal 舍入策略,再转换为目标整数,并检查范围是否可接受。

  2. 为什么 int total = 1_500_000_000 + 1_500_000_000 可能得到负数? 答案:两个操作数都是 int,加法先在 32 位整数范围内计算,结果超过最大值后发生溢出回绕。 解析:把其中一个操作数写成 long(如 1_500_000_000L)可以让运算提升到 long;若溢出必须作为错误处理,则使用 Math.addExact。

  3. new BigDecimal(0.1) 和 new BigDecimal("0.1") 为什么不应混用? 答案:前者接收的是已经近似存储的 double,会把二进制浮点误差带进去;后者直接按十进制文本构造精确值。 解析:金额、税率等可审计数据应从字符串或整数最小单位构造,并在除法、展示和比较时统一精度与舍入规则。

本节小结 ​

  • 扩大转换常可自动完成,窄化转换需显式写出,并可能造成截断、溢出或精度损失。
  • 算术表达式会先做数值提升,整数溢出默认回绕,安全检查可使用 Math.*Exact。
  • float 和 double 是二进制近似值;精确十进制计算应使用字符串构造的 BigDecimal。
  • char 是 UTF-16 码元,处理补充平面字符时应按 Unicode 码点遍历和计数。

快速回顾 ​

  • 能判断一次赋值是扩大转换还是需要强制转换,并说出可能的损失。
  • 能解释 byte + byte 为什么得到 int,以及整数溢出的结果来源。
  • 能写出可靠的 BigDecimal 构造和带舍入的除法。
  • 能区分 String.length() 的码元数量与 codePointCount 的码点数量。

继续阅读 ​