一、128陷阱
1、经典面试真题
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,这个过程叫“自动装箱”,其底层实际执行的是:
Integer a = Integer.valueOf(127); // 自动装箱的本质128陷阱的关键,就藏在Integer.valueOf()方法的源码中。
2.Integer.valueOf()的源码逻辑
JDK 8中Integer.valueOf()的核心源码如下:
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类的静态内部类,负责维护缓存池。其核心源码如下:
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:基础结果判断(最常见)
问题:说出以下代码的运行结果,并解释原因。
Integer a = 100; Integer b = 100; Integer c = 200; Integer d = 200; System.out.println(a == b); // true System.out.println(c == d); // false解答:
1.a == b为true:100在-128~127缓存范围内,自动装箱时复用IntegerCache中的同一个对象,==比较地址相同;
2.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缓存的范围?如果可以,如何操作?有什么注意事项?
解答:
1. 可修改:缓存下限(-128)固定不可改,但上限可通过JVM启动参数配置;
2. 修改方式:通过-XX:AutoBoxCacheMax=<size>或-Djava.lang.Integer.IntegerCache.high=<size>指定上限,例如:
java -XX:AutoBoxCacheMax=512 MyApp // 缓存范围扩展为-128~5123. 注意事项:
自定义上限不能小于127(源码中通过
Math.max(i, 127)限制);修改仅对“自动装箱”(即
Integer.valueOf())生效,对new Integer()无效;多人协作或依赖第三方库的项目不建议修改,可能导致不可预期的兼容性问题。
考点4:Integer与int的比较(拆箱机制)
问题:说出以下代码的运行结果,并解释原因。
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()却能成功?
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"); } }解答:
1.==可能失败:list中添加128时,自动装箱会创建新对象;target=128也会创建新对象,两个对象地址不同,==比较失败;
2.equals()一定成功:Integer的equals()方法重写了Object的实现,专门用于比较数值是否相等(而非地址),源码如下:
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、总结:面试回答范式
1. 定义:128陷阱是Integer包装类的缓存机制导致的==比较异常——-128~127范围内的Integer对象会复用缓存,超出范围则新建对象,导致同数值对象==比较结果不同;
2. 成因:Java为优化性能,通过IntegerCache静态内部类提前缓存高频使用的小整数对象,自动装箱(Integer.valueOf())时复用缓存,超出范围则新建;
3. 避坑方案:比较数值用equals(),避免用==;必要时拆箱为int比较,注意null值安全;
4. 延伸:Byte/Short/Long等也有类似缓存,但范围或配置方式不同。
二、==和equals方法的区别
1、先看经典面试题
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 类源码):本质就是用==比较引用地址,源码如下:
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 是工具类,会先判空,源码如下):
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 大特性(自反性、对称性、传递性、一致性、非空性):
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,源码如下:
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 有类型 / 常量池特殊逻辑,避免空指针。