1. 简介
本篇文章我们来聊一聊 Java 中原始类型(primitive types)和其对应的包装类(wrapper classes)各自的优劣,帮助你在实际开发中做出更合理的选择。
2. Java 类型系统
Java 的类型系统可以分为两类:
- 原始类型:如
int
、boolean
等 - 引用类型:如
Integer
、Boolean
等
每个原始类型都对应一个包装类。包装类是不可变(immutable)且被声明为 final
的,这意味着它们一旦创建就不能被修改,也不能被继承。
Java 在某些场景下会自动在原始类型和包装类之间进行转换,比如:
Integer j = 1; // 自动装箱(autoboxing)
int i = new Integer(1); // 自动拆箱(unboxing)
- 自动装箱:将原始类型转换为包装类
- 自动拆箱:将包装类转换为原始类型
3. 优劣分析
选择原始类型还是包装类,通常取决于以下几个因素:
- 应用性能需求
- 可用内存大小
- 是否需要处理默认值
如果你不关心这些细节,那可以忽略它们,但了解它们有助于写出更高效的代码。
3.1. 单个变量的内存占用
以下是常见原始类型的理论内存占用(bit):
boolean
– 1 bitbyte
– 8 bitsshort
,char
– 16 bitsint
,float
– 32 bitslong
,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 bitsByte
– 128 bitsShort
,Character
– 128 bitsInteger
,Float
– 128 bitsLong
,Double
– 192 bits
✅ 一个 Boolean
对象占用的内存相当于 128 个 boolean 原始变量,而一个 Integer
对象相当于 4 个 int 原始变量。
3.2. 数组的内存占用
当我们创建数组时,情况变得更有趣。
下面是不同类型数组的内存占用随元素数量变化的图表:
可以看到,原始类型数组在内存占用上被分为四组:
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]
其中 []
表示向上取整。
⚠️ 意外的是,原始类型 long
和 double
的数组比其包装类 Long
和 Double
占用更多内存。
✅ 除 long
和 double
外,单元素原始类型数组通常比包装类数组更占内存。
3.3. 性能对比
Java 代码的性能受多种因素影响:
- 硬件
- 编译器优化
- JVM 状态
- 系统中其他进程的活动
原始类型存储在栈上,访问速度快;而包装类在堆上,访问较慢。
我们用 JMH 做了一个简单测试:创建一个 500 万个元素的数组,查找最后一个不同的元素。
while (!pivot.equals(elements[index])) {
index++;
}
结果如下图所示:
✅ 即使是简单的查找操作,原始类型也明显比包装类快。
对于加法、乘法、除法等更复杂的操作,性能差距会更大。
3.4. 默认值处理
原始类型的默认值:
- 数值型:
0
(如0
,0.0d
) boolean
:false
char
:\u0000
- 数值型:
包装类的默认值:
null
⚠️ 这意味着原始类型只能表示其类型域内的值,而包装类可以为 null
—— 这可能带来空指针风险。
虽然不建议使用未初始化的变量,但在某些场景下,我们可能需要在后续赋值。
- 如果原始类型变量值为默认值(如 0),需要额外判断是否真的初始化过。
- 而包装类变量为
null
时,一眼就能看出未初始化。
4. 使用建议
✅ 原始类型性能更好、内存占用更小,优先使用原始类型。
但是,Java 的泛型、集合类(如 List<Integer>
)和反射 API 都不支持原始类型,必须使用包装类。
如果你的应用需要处理大量数据,建议:
- 使用原始类型数组
- 尽量避免使用包装类集合(除非必须)
5. 总结
本文展示了 Java 中原始类型和包装类在性能和内存方面的差异:
- 原始类型:速度快、内存小
- 包装类:功能丰富、但开销大
在性能敏感的场景中,优先使用原始类型;在需要泛型或集合时,使用包装类。
如需查看文中代码示例,可以访问我们的 GitHub 仓库。