刚学 Java 的时候,很多人都会有点别扭。
既然已然存在 int、long这个样子了, 为何还要再度出现一套 、Long、、呢?
看着像重复设计,写多了还容易出事。
比如:
到此许多人学到之后呢竟然会将包装类视作Java所遗留下来的历史包袱。
这话只说对了一半。
它确实带着很强的历史痕迹,但它也不是无缘无故存在的。
说到底, 包装类是Java搭建的一座桥, 这座桥位于两套世界之间, 一边是基本类型, 其追求效率, 另一边是面向对象体系, 该体系内万物讲对象。
先说最根上的原因:int 不是对象
Java 里有一组非常特殊的类型:
int
long
double
float
short
byte
char
boolean
这些叫基本类型。
和普通类不同的是, 它们没法被调方法, 如果是int, 不能够进行这种操作。并且, 它们不存在继承某个父类这么一回事。它们有着极具直接性的存在价值, 分别是更轻盈, 能够运行时间提升速度到更快, 占用内存也更加的节省。
比如一个 int,本质上就是一个数值;
但存在这样一个情况, 它涉及对象, 此对象带着对象头, 还有引用产生的意义。另外, 它的出现很有可能把那些装箱、拆箱以及缓存这些额外的机制被牵扯进来。
要是 Java 将全部数字都设定为对象, 那么语言会呈现出“更统一”的状况, 然而运行成本会大幅提高很多。
在 Java 诞生的那个时候呀, 特别是, 内存相较于如今更为敏感, 性能也是如此, 并且 JVM 并不具备现阶段这么激进的优化能力, 就是这样。
因此, Java作出了保留基本类型的选择, 这一行为本身是不存在问题的, 甚至于还能够表述为是极为务实的。
问题在于,Java 又偏偏是一门典型的面向对象语言。
而面向对象世界里,很多地方只认对象。
这时候裂缝就出来了。
包装类,本质上是给基本类型补一个“对象入口”
要是仅有int, 不存在, 那么Java的诸多基础能力将会径直断掉一部分。
最常见的就是集合。
List list = new ArrayList<>();
list.add(1);
这里你能写 ,却不能写:
List list = new ArrayList<>();
并非是集合存心要为难你, 然而泛型这样的一套内容, 原本就是在围绕着引用类型来进行设计构建的。
int 不是对象,放不进去。
于是 Java 需要一个“对象版的 int”,这就是 。
同理:
你可以把包装类理解成:
让基本类型拥有一张进入对象世界的门票。
没有这张门票,很多 API 根本没法玩。
为什么说它不是可有可无
包装类真正解决的,不只是“集合能不能放”。
它解决的是这几类现实问题。
1. 集合、泛型、反射、框架参数传递,都更偏对象世界
Java 大量基础设施都是按对象设计的。
例如集合, 泛型, 反射, 注解处理, 序列化, 诸多 ORM 和 Web 框架的参数绑定, 本质上都更倾向于与对象进行交互, 是这样的。
不妨试着去想象一番, 要是不存在包装类, 接下来的这些场景都会变得特别别扭:
将整套类库改成两套平行体系, 这对于Java来说是不可能的, 仅仅是为了几个基本类型, 就去做这样的改变, 绝无可能实现。
所以最自然的做法,就是给它们准备“对象形态”。
2. 基本类型不能表达“没有值”,包装类可以
这个问题在业务代码里特别常见。
int age = 0;
这里的 0 到底是什么意思?
是年龄真的为 0,还是“暂时不知道”?
在很多业务场景里,这两件事完全不是一回事。
而包装类可以表达这个差异:
Integer age = null;
这表示“没有值”。
有一种能力, 在数据库字段可空之处, 在前端表单未填写之时, 在接口参数非必填之地, 到处都对其有很大的依靠。
所以, 包装类并非仅仅是“将数字转化为对象”这般简单, 它还顺便肩负起了一项颇为实际的职责, 那便是: 去表达 语义。
3. 包装类顺便承载了很多工具能力
基本类型自己没有方法,但包装类有。
Integer.parseInt("123");
Integer.valueOf(10);
Double.compare(1.2, 2.3);
Boolean.TRUE
这让 Java 不需要额外再发明一堆零散工具函数。
针对某一种类别的通常具备的能力, 径直往包装类别上进行挂载, 这般既做到了顺其自然, 同时对记忆而言也颇为简便, 是这样的情形。
所以包装类其实还承担了“类型工具箱”的角色。
包装类好用,但它从来不是“纯值”
诸多在线上出现的差错之处, 并非是因为包装类型有没有发挥用处而造成的, 反而是当人们将它视作纯粹的数字情况时才产生的。
它不是。
看起来像数字,实际上是对象。
既然身为对象, 那它便会带来对象世界之中的那一系列独一无二的特性, 这些特性包含引用, 包含null, 包含比较规则, 包含缓存, 包含装箱拆箱。
此同样是众多Java初学者切实开启领会“值”以及“对象并非同一回事”之处。
一个最经典的坑:null 拆箱
Integer x = null;
int y = x; // NPE
很多人第一次看到都会愣一下。
x 不是整数吗,怎么会空指针?
因为这里发生了自动拆箱。编译器会帮你把它变成类似下面这样:
int y = x.intValue();
而 x 是 null,当然直接炸。
也就是说,包装类能表达“没有值”,这是它的优势;
但一旦你又把它当成基本类型来用,这个优势会立刻变成风险。
线上最容易出现的形式通常不是上面这段,而是这种:
Integer status = null;
if (status == 0) {
…
}
看上去像普通比较,实际上已经在偷偷拆箱了。
另一个高频坑:== 不比较值,比较的是不是同一个对象
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
原因不复杂:
但麻烦的是, 还有缓存池。
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
再次变为成真状态, 是由于处于负一百二十八至一百二十七这段范围, 一般情况下会进行缓存复用。
这就很容易把人带沟里:
你以为 == 能用,结果只是刚好踩在缓存区间里。
所以只要是比较包装类的值,规矩很简单:
Objects.equals(a, b)
或者至少:
a.equals(b)
别拿 == 去赌运气。
与此同时,这同样是致使, a等于128, b等于128, 然而a等于b却并非是true这般情况出现的缘由。
性能上,包装类确实更重
这件事也没必要替它洗。
就是比 int 重。
不但并非仅仅是“概念里更为复杂”这一情形, 而且是实实在在地比之前更占用内存空间, 还比较轻易地就会触动引发额外的分配操作, 并且也相对更易于导致让CPU缓存的局部性状况变得更差些。
比如你做大量数值存储时:
再加上自动装箱和拆箱,循环里频繁来回转换,成本也不算小。
所以工程上要有一个很朴素的判断:
包装类不是不能用,而是别把它用在它不擅长的位置上。
说到底,包装类是 Java 的一次折中
Java 从来都不是“极端纯粹”的语言。
它没有像某些语言那样,干脆所有东西都做成对象;
也没有像更底层的语言那样,完全只围绕裸值和内存布局思考。
它的路子一直是折中:
要点一点性能, 也要点一点统一性, 优先实用主义, 把理论上的绝对整洁往后放置。
包装类就是这种折中的典型产物。
代价也很明显:
语言会多一层理解成本,开发者也得多记几条规则。
可是倘若并未拥有这一套设计, 那么当今Java中很多你认为理所当然的代码, 实际上都是没法编写出来的。
真正麻烦的从来不是包装类本身,而是“忘了它是对象”
包装类极易使人陷入误区的所在之处, 并非因其具备复杂性, 而是因其外貌与基本类型极为相像。
你看到,脑子里常常浮现的是“数字”;
可 JVM 看到的,是对象。
一边是值语义直觉,一边是对象语义现实。
很多 bug,就是在这条缝里长出来的。
所以我自己更愿意把包装类理解成一句话:
它并非是 int 的替身, 然而却是存在于对象世界当中的 int 的代理人。
理解到这里,很多现象就顺了:
Java 并不是无缘无故多造了一套类型。
它只是把“快”和“通用”这两件事,硬生生接到了一起。
而包装类,就是那道接缝。
为达成弥补这些缺陷以及自身对于高性能的追求, 进而便产生了项目。Java 27存在出现项目预览的可能性,在下一篇之中我将会紧接着撰写未来篇, 去探究其解决这些历史遗留问题的方式手段。
假如你身为Java开发者, 或者是后端开发者, 又或者是全栈开发者, 那就关注我一块儿前行。




