欢迎光临
我们一直在努力

Java自动装箱拆箱,明明是偷懒神器,咋就坑哭无数程序员?

刚学 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开发者, 或者是后端开发者, 又或者是全栈开发者, 那就关注我一块儿前行。
 

赞(0)
未经允许不得转载:171主机测评 » Java自动装箱拆箱,明明是偷懒神器,咋就坑哭无数程序员?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址