数据类型与包装类
问题
Java 有哪些基本数据类型?包装类的作用是什么?自动装箱/拆箱是怎么回事?
答案
八大基本数据类型
Java 提供了 8 种基本数据类型(Primitive Types),分为 4 类:
| 类型 | 关键字 | 位数 | 默认值 | 范围 |
|---|---|---|---|---|
| 整型 | byte | 8 | 0 | -128 ~ 127 |
| 整型 | short | 16 | 0 | -32768 ~ 32767 |
| 整型 | int | 32 | 0 | -2^31 ~ 2^31-1 |
| 整型 | long | 64 | 0L | -2^63 ~ 2^63-1 |
| 浮点型 | float | 32 | 0.0f | IEEE 754 |
| 浮点型 | double | 64 | 0.0d | IEEE 754 |
| 字符型 | char | 16 | '\u0000' | 0 ~ 65535 |
| 布尔型 | boolean | 1* | false | true/false |
JVM 规范没有明确规定 boolean 的大小。在 HotSpot 中,单个 boolean 变量占 4 字节(当作 int 处理),而 boolean[] 数组中每个元素占 1 字节。
包装类(Wrapper Class)
每个基本类型都有对应的包装类:
| 基本类型 | 包装类 | 继承关系 |
|---|---|---|
byte | Byte | Number → Object |
short | Short | Number → Object |
int | Integer | Number → Object |
long | Long | Number → Object |
float | Float | Number → Object |
double | Double | Number → Object |
char | Character | Object |
boolean | Boolean | Object |
为什么需要包装类?
- 泛型不支持基本类型:
List<int>不合法,必须用List<Integer> - 集合框架只能存对象:
Map<String, Integer>中的 value 必须是对象 - 提供了丰富的工具方法:
Integer.parseInt()、Integer.toBinaryString()等 - 允许 null 值:数据库中的 NULL 无法用
int表示,但Integer可以
自动装箱与拆箱
Java 5 引入了自动装箱(Autoboxing)和自动拆箱(Unboxing),编译器自动插入转换代码:
// 自动装箱:编译器自动调用 Integer.valueOf(10)
Integer a = 10;
// 自动拆箱:编译器自动调用 a.intValue()
int b = a;
// 运算时自动拆箱
Integer x = 10;
Integer y = 20;
// x + y 实际执行的是 x.intValue() + y.intValue()
int sum = x + y;
自动拆箱时如果包装类为 null,会抛出 NPE,这是常见的坑:
Integer num = null;
// 编译通过,但运行时抛出 NullPointerException
int value = num; // 等价于 num.intValue()
在从数据库查询结果映射到实体类时尤其需要注意,数据库字段可能为 NULL。
Integer 缓存池(IntegerCache)
这是面试最高频的考点之一。Integer.valueOf() 对 -128 ~ 127 范围内的值使用缓存:
// valueOf 使用缓存,相同值返回同一个对象
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b); // true(同一个缓存对象)
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d); // false(超出缓存范围,创建了新对象)
// 自动装箱本质上调用的就是 Integer.valueOf()
Integer e = 127;
Integer f = 127;
System.out.println(e == f); // true
Integer g = 128;
Integer h = 128;
System.out.println(g == h); // false
缓存池源码(JDK 8):
public static Integer valueOf(int i) {
// 如果值在缓存范围内,直接返回缓存的对象
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
// 否则创建新的 Integer 对象
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high; // 默认 127,可通过 JVM 参数调整
static final Integer[] cache;
static {
int h = 127;
// 可通过 -XX:AutoBoxCacheMax=<size> 调整上限
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
}
Byte:全部缓存(-128 ~ 127,刚好覆盖全部值)Short:-128 ~ 127Long:-128 ~ 127Character:0 ~ 127Boolean:TRUE和FALSE两个静态常量Float/Double:没有缓存
== 与 equals 的区别
| 比较方式 | 基本类型 | 引用类型(包装类) |
|---|---|---|
== | 比较值 | 比较内存地址(引用) |
equals() | — | 比较值内容(包装类重写了 equals) |
// 基本类型用 == 比较值
int a = 10;
int b = 10;
System.out.println(a == b); // true
// 包装类用 == 比较的是引用地址
Integer x = new Integer(10);
Integer y = new Integer(10);
System.out.println(x == y); // false(两个不同的对象)
System.out.println(x.equals(y)); // true(值相等)
// 混合比较:包装类会自动拆箱
Integer m = 10;
int n = 10;
System.out.println(m == n); // true(m 拆箱后比较值)
包装类之间比较值时必须使用 equals(),不要用 ==。即使在缓存范围内 == 碰巧为 true,也不应依赖这个行为。
类型转换
自动类型转换(隐式)—— 小范围 → 大范围:
byte → short → int → long → float → double
↑
char
强制类型转换(显式)—— 大范围 → 小范围,可能有精度丢失:
double d = 3.14;
int i = (int) d; // 3,小数部分截断
long big = 130L;
byte small = (byte) big; // -126,溢出
整型字面量默认为 int,浮点字面量默认为 double:
// 编译错误:整型字面量默认是 int,超出 int 范围需要加 L
// long num = 2147483648; // 编译错误
long num = 2147483648L; // 正确
// 编译错误:浮点字面量默认是 double,赋值给 float 需加 F
// float f = 3.14; // 编译错误
float f = 3.14F; // 正确
常见面试问题
Q1: int 和 Integer 有什么区别?
答案:
| 维度 | int | Integer |
|---|---|---|
| 类型 | 基本数据类型 | 引用类型(包装类) |
| 存储位置 | 栈 / 对象字段内 | 堆(对象) |
| 默认值 | 0 | null |
| 泛型支持 | 不支持 | 支持 |
| 占用空间 | 4 字节 | 16 字节(对象头 12B + 数据 4B) |
| 比较方式 | == 比较值 | == 比较引用,equals() 比较值 |
Q2: Integer a = 127; Integer b = 127; a == b 的结果是什么?为什么?
答案:
结果是 true。自动装箱调用 Integer.valueOf(127),由于 127 在 IntegerCache 的缓存范围 [-128, 127] 内,两次调用返回的是同一个缓存对象,所以 == 比较的引用地址相同。如果换成 128,结果就是 false,因为超出缓存范围会创建新对象。
Q3: 自动装箱在什么场景需要特别注意?
答案:
- 循环中的自动装箱:频繁创建包装类对象,导致性能下降和 GC 压力
// 反例:每次 += 都会创建新的 Long 对象
Long sum = 0L;
for (int i = 0; i < 1000000; i++) {
sum += i; // 拆箱 → 加法 → 装箱
}
// 正例:使用基本类型
long sum = 0L; - 拆箱时的 NPE:包装类为 null 时拆箱会抛 NullPointerException
- 集合中的比较:
List.contains()内部使用equals(),但如果不小心用了==做判断会出错
Q4: float 能精确表示所有 int 值吗?
答案:
不能。float 有 32 位,但其中 1 位符号位、8 位指数位、23 位尾数,只能精确表示尾数在 23 位以内的整数(约 )。超过这个范围的 int 值赋给 float 会有精度丢失:
int big = 16777217; // 2^24 + 1
float f = big;
System.out.println((int) f); // 16777216,精度丢失
Q5: short s = 1; s = s + 1; 和 short s = 1; s += 1; 有什么区别?
答案:
s = s + 1;编译错误。s + 1中1是int类型,s会提升为int,结果是int,赋值回short需要强制转换。s += 1;编译通过。复合赋值运算符+=隐含了强制类型转换,等价于s = (short)(s + 1);。
Q6: switch 语句支持哪些数据类型?
答案:
| Java 版本 | 支持类型 |
|---|---|
| 1.0 ~ 1.4 | byte、short、char、int |
| 5.0 | 新增对应的包装类(自动拆箱)、enum |
| 7 | 新增 String |
| 14+ (preview) / 17 (preview) / 21 | 模式匹配(Pattern Matching for switch) |
不支持 long、float、double、boolean。
Q7: 为什么 Java 不支持无符号整数?
答案:
Java 设计者 James Gosling 认为无符号整数容易引发混淆和 Bug(尤其是有符号与无符号混合运算时)。Java 8 开始提供了 Integer.toUnsignedLong()、Integer.divideUnsigned() 等方法来处理无符号需求,但底层表示仍然是有符号的。
Q8: 基本类型存在栈上还是堆上?
答案:
不绝对,取决于上下文:
- 局部变量中的基本类型:存在栈帧的局部变量表中
- 对象的字段中的基本类型:随对象存在堆上
- 数组中的基本类型:数组对象在堆上,元素也在堆上
另外 JIT 编译器的逃逸分析可能把未逃逸的对象标量替换到栈上。