基本类型与包装类
本章定位:Java的8种基本类型各自有一个对应的包装类(Wrapper Class),将基本类型"装箱"为对象,使得int也能放进集合、float也能享受泛型的便利。JDK 5引入的自动装箱拆箱大大简化了代码,但Integer的-128~127缓存机制也成了面试的经典陷阱。白歌在Code Review中抓到过小崔用
==比较两个Integer导致线上BUG,这一课就是为此准备的。
8种基本类型与包装类对照
| 基本类型 | 包装类 | 大小 | 默认值(基本/包装) | 缓存范围 |
|---|---|---|---|---|
byte(字节型) | Byte | 8 bit | 0 / null | -128 ~ 127(全部) |
short(短整型) | Short | 16 bit | 0 / null | -128 ~ 127 |
int(整型) | Integer | 32 bit | 0 / null | -128 ~ 127(可配置) |
long(长整型) | Long | 64 bit | 0L / null | -128 ~ 127 |
float(单精度浮点型) | Float | 32 bit | 0.0f / null | 无缓存 |
double(双精度浮点型) | Double | 64 bit | 0.0d / null | 无缓存 |
char(字符型) | Character | 16 bit | \u0000 / null | 0 ~ 127(ASCII范围) |
boolean(布尔型) | Boolean | 未定义 | false / null | TRUE/FALSE(全覆盖) |
为什么要用包装类?
深度原理:装箱、拆箱与缓存
自动装箱(Autoboxing)与自动拆箱(Unboxing)
JDK 5引入的语法糖,编译器在编译时自动插入装箱/拆箱代码。
// 源码(你写的)
Integer i = 100; // 自动装箱
int j = i; // 自动拆箱
Integer k = i + j; // 先拆箱相加,再装箱
// 编译后(等价于)
Integer i = Integer.valueOf(100);
int j = i.intValue();
Integer k = Integer.valueOf(i.intValue() + j);
Integer缓存机制——面试重灾区
完整代码示例
示例一:飞翔科技——包装类的集合应用与缓存陷阱
/**
* 飞翔科技 - 包装类与缓存机制演示
* 场景:飞翔科技的员工绩效分存入List,并演示Integer缓存陷阱
*/
import java.util.ArrayList;
import java.util.List;
public class WrapperDemo {
public static void main(String[] args) {
System.out.println("========== 包装类的集合应用 ==========");
// 基本类型不能存入List
// List<int> scores = new ArrayList<>(); // 编译错误!
// 使用包装类 Integer
List<Integer> performanceScores = new ArrayList<>();
performanceScores.add(100); // 自动装箱:int → Integer
performanceScores.add(95);
performanceScores.add(88);
performanceScores.add(72);
// 遍历并自动拆箱参与运算
int sum = 0;
for (Integer score : performanceScores) {
sum += score; // 自动拆箱:Integer → int
}
double average = (double) sum / performanceScores.size();
System.out.println("绩效分列表:" + performanceScores);
System.out.println("总分:" + sum);
System.out.println("平均分:" + String.format("%.1f", average));
System.out.println("\n========== 字符串解析为基本类型 ==========");
// 包装类的 parseXxx 静态方法
int quantity = Integer.parseInt("1000");
long orderId = Long.parseLong("20251201123456");
float price = Float.parseFloat("188.88");
double moonDistance = Double.parseDouble("384400.12345");
boolean isPaid = Boolean.parseBoolean("true");
System.out.println("数量:" + quantity);
System.out.println("订单号:" + orderId);
System.out.println("单价:¥" + price);
System.out.println("地月距离:" + moonDistance + " km");
System.out.println("支付状态:" + isPaid);
}
}
运行输出:
========== 包装类的集合应用 ==========
绩效分列表:[100, 95, 88, 72]
总分:355
平均分:88.8
========== 字符串解析为基本类型 ==========
数量:1000
订单号:20251201123456
单价:¥188.88
地月距离:384400.12345 km
支付状态:true
示例二:Integer缓存陷阱——白歌给大翔的工资对比
/**
* 飞翔科技 - Integer 缓存陷阱演示
* 场景:大翔的基本工资用Integer存储,== 比较引发线上BUG
*/
public class IntegerCacheDemo {
public static void main(String[] args) {
System.out.println("========== Integer 缓存陷阱 ==========");
// 缓存范围内(-128 ~ 127):同一对象
Integer salary1 = 100; // Integer.valueOf(100) → 缓存命中
Integer salary2 = 100; // Integer.valueOf(100) → 缓存命中
System.out.println("100 == 100 : " + (salary1 == salary2)); // true!
System.out.println("100 equals 100 : " + salary1.equals(salary2)); // true
// 缓存范围外(128):每次创建新对象
Integer salary3 = 200; // new Integer(200)
Integer salary4 = 200; // new Integer(200)
System.out.println("200 == 200 : " + (salary3 == salary4)); // false!
System.out.println("200 equals 200 : " + salary3.equals(salary4)); // true
// 飞翔科技场景:大翔的工资
// 基本工资8888和绩效18888都超出缓存范围
Integer basicSalary = 8888;
Integer performanceBonus = 18888;
Integer expectedTotal = 27776; // 8888 + 18888 = 27776
Integer actualTotal = basicSalary + performanceBonus; // 拆箱相加 → 重新装箱
System.out.println("\n====== 大翔的工资陷阱 ======");
System.out.println("基本工资:" + basicSalary);
System.out.println("绩效奖金:" + performanceBonus);
System.out.println("期望总额:" + expectedTotal);
System.out.println("实际总额:" + actualTotal);
// ❌ 危险!用 == 比较
System.out.println("== 比较:" + (actualTotal == expectedTotal)); // false!
// ✅ 正确!用 .equals()
System.out.println("equals比较:" + actualTotal.equals(expectedTotal)); // true
System.out.println("\n====== 经验教训 ======");
System.out.println("Integer用==比较的是引用地址,不是值!");
System.out.println("在缓存范围[-128,127]内可能'碰巧'相等");
System.out.println("必须用equals()或先拆箱为int再用==");
}
}
运行输出:
========== Integer 缓存陷阱 ==========
100 == 100 : true
100 equals 100 : true
200 == 200 : false
200 equals 200 : true
====== 大翔的工资陷阱 ======
基本工资:8888
绩效奖金:18888
期望总额:27776
实际总额:27776
== 比较:false
equals比较:true
====== 经验教训 ======
Integer用==比较的是引用地址,不是值!
在缓存范围[-128,127]内可能'碰巧'相等
必须用equals()或先拆箱为int再用==
易错场景
反例一:用==比较两个Integer
// ❌ 经典线上BUG:Integer用==比较
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存内,碰巧相等)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出缓存,不同对象)
// ✅ 正确做法
System.out.println(a.equals(b)); // true(值比较)
System.out.println(c.equals(d)); // true(值比较)
// 或者:先拆箱为int
System.out.println((int)c == (int)d); // true(基本类型比较)
反例二:自动拆箱时的NullPointerException
// ❌ 危险:包装类为null时自动拆箱抛空指针
Integer score = null;
int s = score; // NullPointerException!自动拆箱调用score.intValue()
if (score > 60) { // NullPointerException!score拆箱为null
// ✅ 正确:先判空
if (score != null && score > 60) {
// 安全
}
// 或使用Optional(JDK 8)
// Optional.ofNullable(score).filter(s -> s > 60).isPresent();
反例三:三目运算符中的自动拆箱
// ❌ 危险:三目运算符的类型提升导致NPE
Integer value = null;
// int result = true ? value : 0;
// NullPointerException!因为三目运算符两个分支类型不同,
// value被自动拆箱为int,null.intValue()抛异常
// ✅ 正确
Integer result = true ? value : Integer.valueOf(0); // 返回Integer,可以null
反例四:在循环中大量装箱导致性能问题
// ❌ 性能问题:循环中频繁装箱
Integer sum = 0;
for (int i = 0; i < 100000; i++) {
sum += i; // 每次循环:sum拆箱 → 相加 → 装箱 → 新的Integer对象
}
// ✅ 正确:用基本类型计算,最后装箱
int sumPrimitive = 0;
for (int i = 0; i < 100000; i++) {
sumPrimitive += i;
}
Integer result = sumPrimitive; // 只装箱一次
各包装类的核心方法速查
| 包装类 | valueOf | parseXxx | 常量 | 特殊说明 |
|---|---|---|---|---|
Byte(字节型包装类) | Byte.valueOf(byte) / Byte.valueOf(String) | Byte.parseByte(String) | MIN_VALUE, MAX_VALUE | 全缓存 |
Short(短整型包装类) | Short.valueOf(short) / Short.valueOf(String) | Short.parseShort(String) | MIN_VALUE, MAX_VALUE | -128~127缓存 |
Integer(整型包装类) | Integer.valueOf(int) / Integer.valueOf(String) | Integer.parseInt(String) | MIN_VALUE, MAX_VALUE | -128~127缓存(可调) |
Long(长整型包装类) | Long.valueOf(long) / Long.valueOf(String) | Long.parseLong(String) | MIN_VALUE, MAX_VALUE | -128~127缓存 |
Float(单精度浮点型包装类) | Float.valueOf(float) / Float.valueOf(String) | Float.parseFloat(String) | NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY | 无缓存 |
Double(双精度浮点型包装类) | Double.valueOf(double) / Double.valueOf(String) | Double.parseDouble(String) | 同Float | 无缓存 |
Character(字符型包装类) | Character.valueOf(char) | — | MIN_VALUE, MAX_VALUE | 0~127缓存 |
Boolean(布尔型包装类) | Boolean.valueOf(boolean) / Boolean.valueOf(String) | Boolean.parseBoolean(String) | TRUE, FALSE | 全缓存 |
面试考点
Q1:Integer i = 100 和 Integer j = new Integer(100) 有什么区别?
Integer i = 100编译后调用Integer.valueOf(100),该值在-128~127缓存范围内,返回缓存中已存在的Integer对象。new Integer(100)每次都创建新对象,不会使用缓存。因此i == Integer.valueOf(100)为true,而j == new Integer(100)为false。从JDK 9开始,new Integer()被标记为@Deprecated,推荐使用valueOf()。
Q2:Integer的缓存范围可以修改吗?
可以。通过JVM参数
-XX:AutoBoxCacheMax=<size>可以调整Integer缓存的上限(仅正向调整,不能小于127)。例如-XX:AutoBoxCacheMax=200会将缓存范围扩展到-128~200。但其他包装类的缓存是固定的,无法修改。
Q3:自动装箱和拆箱是如何实现的?
是编译器的语法糖。自动装箱编译后调用
valueOf()方法(如Integer.valueOf(int)),自动拆箱编译后调用xxxValue()方法(如Integer.intValue())。这发生在编译阶段,字节码中已包含对应的方法调用,JVM无需特殊处理。
Q4:int和Integer在方法参数传递上有区别吗?
int是基本类型,传递时复制值,方法内修改不影响外部。Integer是引用类型(不可变类),传递时复制引用,方法内param = newValue只改本地引用,不影响外部。但由于Integer不可变,你无法"原地"修改Integer的值——看起来和int的行为一致。实际开发中用int更符合直觉,用Integer需要警惕null。
Q5:JDK 8的Optional<Integer>和Integer有什么区别?
Integer可以表示一个整数或null("无值"),但null的含义不明确(是"还没有"还是"不存在"?)。Optional<Integer>显式表达"值可能存在也可能不存在"的语义,是JDK 8推荐的null处理范式。但注意不要在字段或方法参数中使用Optional(这是反模式),它最适合作为方法返回值。