4.1 暂住证:包装类与自动装箱


4.1 暂住证:包装类与自动装箱

本节摘要:八个基本类型不进对象世界,包装类是它们进城办事的暂住证;自动装箱拆箱由编译器代劳,代价藏在使用 == 比较与 null 拆箱两处。Integer 对小值区间有缓存,缓存内 == 为真、缓存外为假——同一行代码前后行为不同的事故由此而来。本节用三组实验验证缓存区间与拆箱空指针,并给出工程口径。

为什么 int 进不了集合

第 1 章立的规矩还记得:对象是堆里的档案,变量是指向档案的引用。而 int、double 这些基本类型为了性能直接把值存在栈上,不是引用、没有档案、不继承 Object。这套设计在纯计算里跑得飞快,但撞上对象世界就出问题:集合框架只收引用(第 5 章马上看到),泛型的类型参数也不接受基本类型。总不能为"存一组整数"重写一套集合。

包装类就是解法:八种基本类型各对应一张"暂住证"——Byte、Short、Integer、Long、Float、Double、Character、Boolean。证上除了装着值,还带了一批静态服务:解析字符串、比较大小、进制转换、取上下限。

public class WrapDemo { public static void main(String[] args) { Integer a = 5; // 自动装箱:编译器改为 Integer.valueOf(5) int b = a; // 自动拆箱:编译器改为 a.intValue() Integer c = Integer.valueOf(7); // 装箱的实质 int d = c.intValue(); // 拆箱的实质 System.out.println(a + "," + b + "," + d); // 输出:5,5,7 Integer e = a + c; // 混算:先拆箱相加 再装箱 System.out.println(e); // 输出:12 int x = Integer.parseInt("1024"); // 字符串转整数 最常用的静态服务 System.out.println(x + 1); // 输出:1025 System.out.println(Integer.MAX_VALUE); // 输出:2147483647 int 的上限 System.out.println(Integer.toBinaryString(10)); // 输出:1010 十进制转二进制串 } }

自动装箱拆箱不是运行时机制,是编译期的语法糖:编译器把 Integer a = 5 翻译成 Integer.valueOf(5),把 int b = a 翻译成 a.intValue()。糖很甜,但两处代价必须知道——都在下面两段代码里。

实验一:缓存区间,同一行代码两种结果

先看一段"见了鬼"的代码。前一组 == 为真,后一组为假,唯一区别是数值大小:

public class CacheDemo { public static void main(String[] args) { Integer m = 100, n = 100; Integer p = 200, q = 200; System.out.println(m == n); // 输出:true System.out.println(p == q); // 输出:false —— 同样的写法 结果变了 } }

原因在 valueOf 的实现:Integer 把负一百二十八到正一百二十七这个区间的对象提前造好、放进缓存数组,装箱时值落在区间内就返回缓存里那个共享对象,落在区间外才新建对象。100 两次装箱拿到的是同一个缓存对象,== 比引用自然为真;200 各自新建,两份档案两个编号,== 为假。

图 4-1 Integer 缓存区间:共享对象与新建对象的分界

图 4-1 Integer 缓存区间:共享对象与新建对象的分界

这个事故阴险在哪?测试数据常常是小数字——单元测试全用 100 以内的值,== 全过;上线后真实数据是金额、是编号,落在区间外,条件判断悄悄失效。八个包装类里,Integer、Short、Byte、Character、Long 有缓存(区间基本同上,Character 是零到一百二十七),Double、Float 没有缓存——浮点值无穷多,缓存没有意义。工程口径一句话:包装类型判等永远用 equals,== 留给基本类型。

实验二:拆箱空指针

第二处代价来自包装类的一个特性:它是引用类型,可以为 null。null 的证没有值,硬要拆箱(当成 int 用),运行时对着 null 调方法,抛空指针异常:

public class NullDemo { public static void main(String[] args) { Integer score = null; // 比如来自数据库 未填分的字段 // int s = score; // 拆箱 null 运行时 NullPointerException if (score != null) { // 判空后再用 int s = score; System.out.println(s); } else { System.out.println("分数缺失 按缺考处理"); // 输出这一行 } // 三元表达式里的混用更隐蔽 两个分支类型不同时都会拆箱 Integer a = null; int r = true ? 1 : a; // 编译器统一按 int 处理 a 被拆箱 抛异常 System.out.println(r); } }

最后那个三元表达式是真实项目里的高发坑:条件为真似乎用不到 a,但编译器要求两个分支类型一致,把 Integer 分支拆成 int,null 直接炸。判空在包装类上是标配动作,尤其在数据库映射、外部接口解析这些"值可能缺失"的场景。

基本类型还是包装类:选型口径

什么时候用哪个,工程上有稳定共识。默认用基本类型:快、省、不会为 null,局部变量、循环计数、算术参数一律用它。三类场景必须上包装类:一是集合与泛型的类型参数(第 5 章 List 可以、List 不行);二是实体字段——数据库列可空时,null 与零语义不同("未登记分数"和"零分"是两回事),Integer 的 null 恰好表达"缺失";三是需要静态服务的场合——解析、进制转换、取上下限都挂在包装类上。此外的比较用工具方法更稳:两个包装对象判等用 equals;数值比较先拆箱再用比较器或大小判断,不要依赖 == 的歧义行为。

本节要点回顾

  • 包装类是基本类型的暂住证:八种各对应一张,证上带值与静态服务;自动装箱拆箱是编译期语法糖,实质是 valueOf 与 intValue
  • 缓存区间是 == 事故的根源:Integer 缓存负一百二十八到正一百二十七,区间内共享对象、区间外新建;Double、Float 无缓存
  • 包装类判等一律 equals:测试数据小、生产数据大,是这类事故最阴险的组合
  • null 拆箱抛空指针:数据库与接口的"缺失值"用 null 表达,用前判空;三元表达式混用分支会隐性拆箱
  • 选型口径:默认基本类型,集合泛型、可空字段、静态服务三场景上包装类

值能进城了,文本呢?下一节看 Java 里被讨论最多的一个类:String 为什么把字刻在石头上,以及刻石与草稿纸(StringBuilder)之间的性能账。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U