一、128陷阱
1、经典面试真题
java
public class IntegerCacheDemo {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println("a == b: " + (a == b)); // 结果1
Integer c = 128;
Integer d = 128;
System.out.println("c == d: " + (c == d)); // 结果2
Integer e = new Integer(127);
Integer f = new Integer(127);
System.out.println("e == f: " + (e == f)); // 结果3
}
}
答案:结果1为true,结果2为false,结果3为false。
这就是典型的"128陷阱"场景。明明数值相同,为何比较结果不同?
2、什么是128陷阱?
128陷阱,本质是Java中Integer包装类的缓存机制导致的"==比较异常"现象:
当使用自动装箱(将int基本类型转为Integer包装类)创建Integer对象时,若数值在 -128~127范围内,Java会直接复用缓存池中的已有对象;若超出该范围,则会创建新的Integer对象。
由于==运算符对于引用类型比较的是"对象内存地址"而非"数值",就会出现:同数值的Integer对象,在-128~127范围内用==比较为true,超出范围则为false的"陷阱"。
核心结论:128陷阱的核心是"Integer缓存机制"与"==引用比较"的叠加效应。
3、为什么会有128陷阱?(底层成因)
128陷阱的产生,是Java对"常用小整数"的性能优化策略导致的,具体可从3个层面拆解:
1. 自动装箱的底层实现
当我们写Integer a = 127;时,编译器会自动将int转为Integer,这个过程叫"自动装箱",其底层实际执行的是:
java
Integer a = Integer.valueOf(127); // 自动装箱的本质
128陷阱的关键,就藏在Integer.valueOf()方法的源码中。
2.Integer.valueOf()的源码逻辑
JDK 8中Integer.valueOf()的核心源码如下:
java
public static Integer valueOf(int i) {
// 若数值在缓存范围(-128~127)内,直接返回缓存对象
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
// 超出范围,创建新的Integer对象
return new Integer(i);
}
源码逻辑很清晰:先判断数值是否在缓存范围内,命中则复用缓存对象,未命中则新建对象。
3.IntegerCache缓存池的设计
IntegerCache是Integer类的静态内部类,负责维护缓存池。其核心源码如下:
java
private static class IntegerCache {
static final int low = -128; // 缓存下限(固定不可改)
static final int high; // 缓存上限(可配置)
static final Integer cache[]; // 缓存数组,存储-128~high的Integer对象
static {
// 默认缓存上限为127
int h = 127;
// 读取JVM参数,可自定义缓存上限
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127); // 自定义上限不能小于127
// 防止数组超出Integer最大值范围
h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
} catch (NumberFormatException nfe) {
// 参数无效则忽略
}
}
high = h;
// 初始化缓存数组,长度 = 上限 - 下限 + 1
cache = new Integer[(high - low) + 1];
int j = low;
// 填充缓存数组:创建-128~high的Integer对象存入数组
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// 断言:确保缓存上限至少为127(JLS规范要求)
assert IntegerCache.high >= 127;
}
private IntegerCache() {} // 私有构造,禁止实例化
}
从源码可提炼出IntegerCache的核心特性:
-
缓存范围默认是 -128~127,下限固定,上限可通过JVM参数配置;
-
缓存数组在类加载时(静态代码块)初始化,提前创建好范围内的所有Integer对象;
-
缓存机制本质是"享元模式",通过复用对象减少内存占用和GC压力。
4.为什么默认缓存范围是-128~127?
这个范围不是随意定义的,而是JVM规范、性能优化和实际开发场景的综合考量:
-
与byte类型范围一致:byte是Java基本类型,范围正是-128~127,适配基础类型转换效率;
-
高频使用场景:开发中大部分整型常量(如下标、状态码、分页编号、枚举ID)都集中在该范围,缓存性价比最高;
-
内存与性能平衡:缓存范围过大则占用内存过多,过小则无法覆盖高频场景,-128~127是最优权衡。
4、128陷阱的延伸考点
考点1:基础结果判断(最常见)
问题:说出以下代码的运行结果,并解释原因。
java
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b); // true
System.out.println(c == d); // false
解答:
-
a == b为true:100在-128~127缓存范围内,自动装箱时复用IntegerCache中的同一个对象,==比较地址相同; -
c == d为false:200超出缓存范围,自动装箱时会通过new Integer()创建两个不同对象,地址不同,==比较结果为false。
考点2:new Integer()与自动装箱的区别
问题:为什么new Integer(127) == new Integer(127)的结果是false?
解答:
无论数值是否在缓存范围内,new Integer()都会强制创建新的Integer对象(直接操作堆内存)。两个新对象的内存地址不同,因此==比较结果为false。
核心区别:自动装箱可能复用缓存对象,new Integer()永远创建新对象。
考点3:缓存范围是否可修改?如何修改?
问题:能否修改Integer缓存的范围?如果可以,如何操作?有什么注意事项?
解答:
-
可修改:缓存下限(-128)固定不可改,但上限可通过JVM启动参数配置;
-
修改方式:通过
-XX:AutoBoxCacheMax=<size>或-Djava.lang.Integer.IntegerCache.high=<size>指定上限,例如:
java
java -XX:AutoBoxCacheMax=512 MyApp // 缓存范围扩展为-128~512
- 注意事项:
-
自定义上限不能小于127(源码中通过
Math.max(i, 127)限制); -
修改仅对"自动装箱"(即
Integer.valueOf())生效,对new Integer()无效; -
多人协作或依赖第三方库的项目不建议修改,可能导致不可预期的兼容性问题。
考点4:Integer与int的比较(拆箱机制)
问题:说出以下代码的运行结果,并解释原因。
java
Integer a = 128;
int b = 128;
System.out.println(a == b); // true
解答:
结果为true。当Integer与int比较时,Java会自动将Integer拆箱为int(调用intValue()方法),此时==比较的是"数值"而非"地址",因此128 == 128结果为true。
考点5:集合中的128陷阱
问题:以下代码中,为什么用==查找128时可能失败,用equals()却能成功?
java
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 150; i++) {
list.add(i); // 自动装箱
}
Integer target = 128;
for (Integer num : list) {
if (num == target) { // 可能失败
System.out.println("Found with ==");
}
if (num.equals(target)) { // 一定成功
System.out.println("Found with equals");
}
}
解答:
-
==可能失败:list中添加128时,自动装箱会创建新对象;target=128也会创建新对象,两个对象地址不同,==比较失败; -
equals()一定成功:Integer的equals()方法重写了Object的实现,专门用于比较数值是否相等(而非地址),源码如下:
java
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
考点6:其他包装类是否有类似陷阱?
问题:Byte、Short、Long等包装类是否存在类似的"缓存陷阱"?
解答:
存在类似缓存机制,但范围不同:
-
Byte:缓存范围-128~127(固定,不可修改),因为Byte的取值范围本身就是-128~127;
-
Short、Long:默认缓存范围-128~127,上限可通过JVM参数修改(与Integer类似);
-
Character:缓存范围0~127(ASCII码常用范围)。
核心差异:Integer的缓存上限可灵活配置,而Byte的缓存范围固定(因类型本身取值范围有限)。
5、总结:面试回答范式
-
定义:128陷阱是Integer包装类的缓存机制导致的
==比较异常-------128~127范围内的Integer对象会复用缓存,超出范围则新建对象,导致同数值对象==比较结果不同; -
成因:Java为优化性能,通过IntegerCache静态内部类提前缓存高频使用的小整数对象,自动装箱(
Integer.valueOf())时复用缓存,超出范围则新建; -
避坑方案:比较数值用
equals(),避免用==;必要时拆箱为int比较,注意null值安全; -
延伸:Byte/Short/Long等也有类似缓存,但范围或配置方式不同。
二、==和equals方法的区别
1、先看经典面试题
java
public class EqualsVsDoubleEqual {
public static void main(String[] args) {
// 场景1:基本类型比较
int a = 128;
int b = 128;
System.out.println(a == b); // 结果1
// 场景2:引用类型(未重写equals)
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1 == obj2); // 结果2
System.out.println(obj1.equals(obj2)); // 结果3
// 场景3:引用类型(重写equals)
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2); // 结果4
System.out.println(s1.equals(s2)); // 结果5
// 场景4:结合Integer缓存(128陷阱)
Integer i1 = 128;
Integer i2 = 128;
System.out.println(i1 == i2); // 结果6
System.out.println(i1.equals(i2)); // 结果7
}
}
答案:结果 1:true | 结果 2:false | 结果 3:false | 结果 4:false | 结果 5:true | 结果 6:false | 结果 7:true
2、核心定义:== 和 equals 分别是什么?
1. ==运算符:比较 "值 / 地址" 的双重逻辑
==是 Java 的运算符,其比较规则分两种场景,核心是 "看比较的是基本类型还是引用类型":
- 基本类型(byte/short/int/long/float/double/char/boolean) :比较的是实际数值(值相等则为 true);
- 引用类型(所有对象 / 包装类 / String 等) :比较的是对象在堆内存中的地址(只有指向同一个对象时才为 true)。
2. equals()方法:从 "地址比较" 到 "值比较" 的演变
equals()是Object类中定义的实例方法,其核心规则分 "默认实现" 和 "重写实现":
默认实现(Object 类源码) :本质就是用==比较引用地址,源码如下:
java
public boolean equals(Object obj) {
return (this == obj); // 直接比较对象地址
}
重写实现(如 String/Integer/Date 等):开发者重写后,改为比较 "对象的实际内容 / 数值"(这也是我们常用的场景)
核心结论:==的规则由 "数据类型" 决定,equals()的规则由 "是否重写" 决定;默认情况下,二者对引用类型的比较效果一致,重写后则完全不同。
3、底层本质:为什么会有这种区别?
==和equals()的区别,根源在于 Java 的 "数据类型体系" 和 "面向对象设计":
==的设计初衷:作为通用运算符,既要支持基本类型(无地址概念,只能比数值),也要支持引用类型(有地址,需区分 "同一个对象" 和 "内容相同的不同对象");equals()的设计初衷:Object 类作为所有类的父类,默认提供 "对象相等性判断"(即地址比较),但允许子类根据业务需求重写 ------ 比如 String 类需要判断 "字符内容是否相同",Integer 需要判断 "数值是否相同",而非判断 "是否是同一个对象"。
简单来说:==是 "底层物理层面" 的比较(值 / 地址),equals()是 "业务逻辑层面" 的比较(内容 / 语义)。
4、高频面试考点
面试官不会只考 "定义",更多是结合实际场景提问,以下是 6 类高频考点及标准解答:
考点 1:基本类型 vs 引用类型的==比较
问题 :为什么int a=128; int b=128; a==b为 true,而Integer i1=128; Integer i2=128; i1==i2为 false?解答:
int是基本类型,==比较的是数值,128=128 所以为 true;Integer是引用类型,==比较的是地址,128 超出 Integer 缓存范围,i1 和 i2 是两个不同对象,地址不同所以为 false。
考点 2:String 类的==和equals()对比
问题 :String s1="Java"; String s2="Java"; String s3=new String("Java"); 分析s1==s2、s1==s3、s1.equals(s3)的结果。解答:
s1==s2:true(字符串常量池复用,s1 和 s2 指向同一个对象);s1==s3:false(new String () 会在堆中创建新对象,地址不同);s1.equals(s3):true(String 重写了 equals,比较字符内容)。
考点 3:重写 equals 的注意事项(hashCode 联动)
问题 :重写 equals 时,为什么必须重写 hashCode?解答:这是 Java 的 "通用约定":如果两个对象的 equals () 返回 true,那么它们的 hashCode () 必须返回相同值(比如 HashMap/HashSet 等集合类依赖此规则)。
- 反例:若只重写 equals 不重写 hashCode,两个 "内容相等" 的对象会有不同的 hashCode,放入 HashMap 时会被判定为 "不同键",导致集合功能异常。
考点 4:null 值的比较场景
问题 :Integer i=null; i.equals(128)和Objects.equals(i,128)的结果分别是什么?有什么风险?解答:
i.equals(128):抛出NullPointerException(null 调用实例方法会空指针);Objects.equals(i,128):false(Objects.equals 是工具类,会先判空,源码如下):
java
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
面试提示 :比较可能为 null 的对象时,优先用Objects.equals(),避免空指针。
考点 5:自定义类的 equals 重写
问题 :如何正确重写自定义类的 equals 方法?解答:以 "用户类(id+name)" 为例,标准重写逻辑需满足 5 大特性(自反性、对称性、传递性、一致性、非空性):
解答:以 "用户类(id+name)" 为例,标准重写逻辑需满足 5 大特性(自反性、对称性、传递性、一致性、非空性):
java
class User {
private Integer id;
private String name;
// 正确重写equals
@Override
public boolean equals(Object o) {
// 1. 地址相同,直接返回true
if (this == o) return true;
// 2. 为null或类型不同,返回false
if (o == null || getClass() != o.getClass()) return false;
// 3. 强转后比较核心属性
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
// 必须同步重写hashCode
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
考点 6:包装类的 equals 特殊点
问题 :Integer i=128; Long l=128L; i.equals(l)的结果是什么?为什么?解答:结果为 false。因为 Integer 的 equals 方法会先判断 "参数是否是 Integer 类型",非 Integer 类型直接返回 false,源码如下:
java
public boolean equals(Object obj) {
if (obj instanceof Integer) { // 先判断类型
return value == ((Integer)obj).intValue();
}
return false;
}
面试提示:包装类的 equals 会严格校验类型,跨类型比较(如 Integer 和 Long)即使数值相同,结果也为 false。
5、面试回答范式
当面试官问 "==和 equals 的区别" 时,按以下逻辑回答,条理清晰且覆盖核心:
- 核心区别 :
==是运算符,基本类型比数值、引用类型比地址;equals 是 Object 的方法,默认比地址,重写后比内容; - 底层根源 :
==是物理层面的比较,equals 是业务逻辑层面的比较,子类可通过重写适配业务需求; - 使用场景 :基本类型用
==,引用类型比较内容用 equals(推荐 Objects.equals),比较对象身份用==; - 注意事项:重写 equals 必须重写 hashCode,包装类 / String 的 equals 有类型 / 常量池特殊逻辑,避免空指针。