基本类型与包装类
Java 同时提供基本类型和包装类型。基本类型追求简单高效,包装类型让基本值可以作为对象参与泛型、集合、反射、注解、序列化等机制。
这一节要解决几个非常常见的问题:
int和Integer到底有什么区别。- 自动装箱和拆箱是怎么发生的。
- 为什么
Integer a = 127; Integer b = 127; a == b可能是true。 - 为什么
Integer拆箱会抛NullPointerException。 - 金额为什么不能用
double。
# 1. 基本类型
Java 有 8 种基本类型。
| 类型 | 位数 | 默认值 | 取值特点 |
|---|---|---|---|
byte | 8 | 0 | 小范围整数 |
short | 16 | 0 | 短整数 |
int | 32 | 0 | 最常用整数 |
long | 64 | 0L | 大整数 |
float | 32 | 0.0f | 单精度浮点 |
double | 64 | 0.0d | 双精度浮点 |
char | 16 | '\u0000' | Unicode 字符单元 |
boolean | 未规定具体位数 | false | 布尔值 |
局部变量没有默认值,必须先赋值再使用:
public void test() {
int value;
System.out.println(value); // 编译失败
}
成员变量才有默认值:
public class Counter {
private int count; // 默认 0
}
# 2. 包装类型
每种基本类型都有对应包装类。
| 基本类型 | 包装类型 |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
包装类型是对象,可以为 null,可以放进集合和泛型中。
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
不能写:
List<int> numbers = new ArrayList<>(); // 编译失败
泛型要求类型参数是引用类型,因此必须使用 Integer。
# 3. 基本类型与包装类型对比
| 对比项 | 基本类型 | 包装类型 |
|---|---|---|
| 是否对象 | 否 | 是 |
是否可为 null | 否 | 是 |
| 存储开销 | 小 | 更大,有对象头 |
| 适合场景 | 高频计算、局部变量、数组 | 集合、泛型、可空字段、反射 |
| 比较方式 | == | 优先 equals |
| 默认值 | 有固定默认值 | 成员变量默认 null |
内存关系简化图:
int value = 10
┌────────────┐
│ value: 10 │ 直接保存数值
└────────────┘
Integer number = 10
┌──────────────┐
│ number 引用 │
└──────┬───────┘
▼
┌──────────────┐
│ Integer 对象 │
│ value = 10 │
└──────────────┘
实际 JVM 会做很多优化,但从语言语义上可以这样理解。
# 4. 自动装箱与拆箱
自动装箱:基本类型自动转换为包装类型。
Integer value = 10;
大致等价于:
Integer value = Integer.valueOf(10);
自动拆箱:包装类型自动转换为基本类型。
Integer value = 10;
int number = value;
大致等价于:
int number = value.intValue();
转换流程:
int 10
│ boxing
▼
Integer.valueOf(10)
│
▼
Integer 对象
│ unboxing
▼
intValue()
│
▼
int 10
# 5. 拆箱空指针
包装类型为 null 时,自动拆箱会抛 NullPointerException。
Integer value = null;
int number = value; // NullPointerException
真实发生的是:
int number = value.intValue();
开发中最常见场景:
public boolean isEnabled(Boolean enabled) {
return enabled; // enabled 为 null 时 NPE
}
更安全的写法:
public boolean isEnabled(Boolean enabled) {
return Boolean.TRUE.equals(enabled);
}
或者在入参处明确默认值。
# 6. 包装类缓存
部分包装类会缓存常用值。以 Integer 为例,默认缓存 -128 到 127。
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
原因:
Integer.valueOf(127)
│
├─ 命中缓存:返回已有 Integer 对象
▼
Integer.valueOf(128)
│
├─ 超出默认缓存:创建新对象
▼
不要依赖 == 比较包装类型数值,正确写法是:
Objects.equals(a, b);
或拆箱后比较,但要确保不为 null。
常见缓存:
| 包装类 | 常见缓存 |
|---|---|
Boolean | TRUE、FALSE |
Byte | 全部缓存 |
Short | 通常缓存 -128 到 127 |
Integer | 默认 -128 到 127 |
Long | 通常缓存 -128 到 127 |
Character | 通常缓存 0 到 127 |
Float、Double | 不缓存 |
# 7. 数值转换
# 7.1 自动类型提升
小范围整数可以自动转成大范围整数:
int a = 10;
long b = a;
整数运算中,byte、short、char 通常会提升为 int。
byte a = 1;
byte b = 2;
// byte c = a + b; // 编译失败
int c = a + b;
# 7.2 强制类型转换
大范围类型转小范围类型需要强转,可能溢出。
int value = 130;
byte b = (byte) value;
System.out.println(b); // -126
转换过程不是“报错”,而是截断高位。
int 130
二进制低 8 位保留
│
▼
byte -126
开发中需要对外部输入做范围校验,不要盲目强转。
# 8. 浮点数精度
float 和 double 使用二进制浮点表示,不能精确表示很多十进制小数。
System.out.println(0.1 + 0.2); // 0.30000000000000004
金额计算不要使用 double:
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3
不要这样创建金额:
new BigDecimal(0.1); // 不推荐
推荐:
new BigDecimal("0.1");
BigDecimal.valueOf(0.1);
# 9. char 与 Unicode
char 是 16 位 Unicode 代码单元,不一定等于一个完整字符。
char c = '中';
对于超出基本多文种平面的字符,一个视觉字符可能由两个 char 组成。
String emoji = "😀";
System.out.println(emoji.length()); // 2
这也是处理 Emoji、短信长度、昵称截断时容易出错的原因。
# 10. boolean
Java 的 boolean 只有 true 和 false,不能和数字互转。
boolean flag = true;
// int value = flag; // 编译失败
这与 C/C++ 不同,避免了很多隐式转换问题。
包装类型 Boolean 可以为 null,常用于数据库三态字段:
true = 是
false = 否
null = 未设置
但用于条件判断前必须处理 null。
# 11. 开发建议
| 场景 | 建议 |
|---|---|
| 局部计数、循环变量 | 使用基本类型 |
| DTO 可空字段 | 使用包装类型 |
| 数据库字段映射 | 使用包装类型表达 null |
| 集合元素 | 使用包装类型 |
| 高频数值计算 | 尽量避免频繁装箱 |
| 金额计算 | 使用 BigDecimal |
| 包装类型比较 | 使用 Objects.equals |
避免隐式装箱带来的性能问题:
Long sum = 0L;
for (long i = 0; i < 1000000; i++) {
sum += i; // 频繁拆箱、装箱
}
更好:
long sum = 0L;
for (long i = 0; i < 1000000; i++) {
sum += i;
}
# Tips 快问快答
Q:为什么 Java 要有基本类型? A:基本类型更简单高效,避免所有数值都变成对象造成额外内存和性能成本。
Q:为什么集合不能放 int?
A:泛型类型参数必须是引用类型,所以要使用 Integer。
Q:自动装箱一定会创建新对象吗?
A:不一定。Integer.valueOf 可能命中缓存。
Q:包装类型能用 == 比较吗?
A:不建议。== 比较引用,缓存范围外结果可能和预期不同。
Q:Integer 拆箱为什么会 NPE?
A:拆箱本质是调用 intValue(),如果引用为 null 就会空指针。
Q:金额为什么不用 double?
A:二进制浮点无法精确表示很多十进制小数,金额应使用 BigDecimal 或以分为单位的整数。
Q:new BigDecimal(0.1) 为什么不推荐?
A:传入的 0.1 已经是近似的 double 值,会把误差带进 BigDecimal。
Q:char 能表示所有字符吗?
A:不能保证。某些字符需要两个 char 组成的代理对表示。
Q:Boolean 用在 if 中安全吗?
A:如果可能为 null 就不安全,会自动拆箱并抛 NPE。
Q:什么时候字段用包装类型? A:当需要表达“未设置”或数据库 null 时,用包装类型更合适。