1. 简介

本篇文章我们来聊一聊 Java 中原始类型(primitive types)和其对应的包装类(wrapper classes)各自的优劣,帮助你在实际开发中做出更合理的选择。

2. Java 类型系统

Java 的类型系统可以分为两类:

  • 原始类型:如 intboolean
  • 引用类型:如 IntegerBoolean

每个原始类型都对应一个包装类。包装类是不可变(immutable)且被声明为 final 的,这意味着它们一旦创建就不能被修改,也不能被继承。

Java 在某些场景下会自动在原始类型和包装类之间进行转换,比如:

Integer j = 1;          // 自动装箱(autoboxing)
int i = new Integer(1); // 自动拆箱(unboxing)
  • 自动装箱:将原始类型转换为包装类
  • 自动拆箱:将包装类转换为原始类型

3. 优劣分析

选择原始类型还是包装类,通常取决于以下几个因素:

  • 应用性能需求
  • 可用内存大小
  • 是否需要处理默认值

如果你不关心这些细节,那可以忽略它们,但了解它们有助于写出更高效的代码。

3.1. 单个变量的内存占用

以下是常见原始类型的理论内存占用(bit):

  • boolean – 1 bit
  • byte – 8 bits
  • short, char – 16 bits
  • int, float – 32 bits
  • long, double – 64 bits

⚠️ 实际上,由于 JVM 实现的不同,这些值可能有所偏差。例如 Oracle JVM 中 boolean 实际上是按 int 存储的,占用 32 bits。

原始类型变量存储在 栈(stack) 上,访问速度快。

而包装类是对象,存储在 堆(heap) 上,访问速度相对较慢,且有额外的内存开销。

在以下 JVM 环境下测试:

java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

使用 Java Object Layout (JOL) 工具可以查看对象的内部结构。测试结果如下:

  • Boolean – 128 bits
  • Byte – 128 bits
  • Short, Character – 128 bits
  • Integer, Float – 128 bits
  • Long, Double – 192 bits

✅ 一个 Boolean 对象占用的内存相当于 128 个 boolean 原始变量,而一个 Integer 对象相当于 4 个 int 原始变量

3.2. 数组的内存占用

当我们创建数组时,情况变得更有趣。

下面是不同类型数组的内存占用随元素数量变化的图表:

plot memory bits

可以看到,原始类型数组在内存占用上被分为四组:

  • long, double: m(s) = 128 + 64 * s
  • short, char: m(s) = 128 + 64 * [s/4]
  • byte, boolean: m(s) = 128 + 64 * [s/8]
  • 其他类型: m(s) = 128 + 64 * [s/2]

其中 [] 表示向上取整。

⚠️ 意外的是,原始类型 longdouble 的数组比其包装类 LongDouble 占用更多内存。

✅ 除 longdouble 外,单元素原始类型数组通常比包装类数组更占内存

3.3. 性能对比

Java 代码的性能受多种因素影响:

  • 硬件
  • 编译器优化
  • JVM 状态
  • 系统中其他进程的活动

原始类型存储在栈上,访问速度快;而包装类在堆上,访问较慢。

我们用 JMH 做了一个简单测试:创建一个 500 万个元素的数组,查找最后一个不同的元素。

while (!pivot.equals(elements[index])) {
    index++;
}

结果如下图所示:

plot benchmark primitive wrapper

✅ 即使是简单的查找操作,原始类型也明显比包装类快。

对于加法、乘法、除法等更复杂的操作,性能差距会更大。

3.4. 默认值处理

  • 原始类型的默认值:

    • 数值型:0(如 0, 0.0d
    • booleanfalse
    • char\u0000
  • 包装类的默认值:null

⚠️ 这意味着原始类型只能表示其类型域内的值,而包装类可以为 null —— 这可能带来空指针风险。

虽然不建议使用未初始化的变量,但在某些场景下,我们可能需要在后续赋值。

  • 如果原始类型变量值为默认值(如 0),需要额外判断是否真的初始化过。
  • 而包装类变量为 null 时,一眼就能看出未初始化。

4. 使用建议

✅ 原始类型性能更好、内存占用更小,优先使用原始类型

但是,Java 的泛型、集合类(如 List<Integer>)和反射 API 都不支持原始类型,必须使用包装类。

如果你的应用需要处理大量数据,建议:

  • 使用原始类型数组
  • 尽量避免使用包装类集合(除非必须)

5. 总结

本文展示了 Java 中原始类型和包装类在性能和内存方面的差异:

  • 原始类型:速度快、内存小
  • 包装类:功能丰富、但开销大

在性能敏感的场景中,优先使用原始类型;在需要泛型或集合时,使用包装类。

如需查看文中代码示例,可以访问我们的 GitHub 仓库


原始标题:Java Primitives Versus Objects