类型转换与数值精度
学习目标
- 能判断基本类型之间何时自动转换、何时必须显式强制转换,以及转换可能丢失什么。
- 理解二元数值运算的类型提升、整数溢出和
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 文本能力。
常用用法
扩大转换:安全提升整数范围
扩大转换通常不需要显式强转,但浮点类型的有效位有限,不能只看“语法允许”就认为精度不会损失。
// 语义:扩大转换通常不需要显式强转,但浮点类型的有效位有限,不能只看“语法允许”就认为精度不会损失。
// 初始状态: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 误差带入构造器。
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
}
}不常用但需要知道
窄化转换:显式承担范围风险
窄化转换会截断小数且可能溢出;如果这是业务输入,应先做范围校验而不是直接强转。
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,适合不允许静默回绕的计数和金额边界。
public class ExactMathDemo {
public static void main(String[] args) {
System.out.println(Math.addExact(20, 22));
// 输出:42
// 作用:通过 Math.addExact 溢出时立即失败。
}
}简单案例
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 码点数量和用户看到的字素簇数量可能不同。
课后小问
(int) 3.99的结果是 3 还是 4?为什么? 答案:结果是 3;从浮点类型窄化为整数会直接截去小数部分,不会四舍五入。 解析:若业务需要四舍五入,应先使用Math.round或明确的BigDecimal舍入策略,再转换为目标整数,并检查范围是否可接受。为什么
int total = 1_500_000_000 + 1_500_000_000可能得到负数? 答案:两个操作数都是int,加法先在 32 位整数范围内计算,结果超过最大值后发生溢出回绕。 解析:把其中一个操作数写成long(如1_500_000_000L)可以让运算提升到long;若溢出必须作为错误处理,则使用Math.addExact。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的码点数量。
阿源的知识库