• 作为JAVA程序员,你知道int 和 Integer 的区别吗?
  • 发布于 2天前
  • 32 热度
    0 评论
兄弟们,有没有过这样的经历?面试的时候,面试官突然抛出一个看似简单到不能再简单的问题:"说说 int 和 Integer 的区别,再看看这段代码的输出结果是什么?" 然后在白板上写下两行代码:
Integer a = 1;
Integer b = 10;
System.out.println(a == b);
System.out.println(a.equals(b));
你心里暗自窃喜,这不就是自动装箱嘛,int 和 Integer 的区别早就滚瓜烂熟了。可是当你自信满满地说出 "第一个输出 false,第二个输出 true" 的时候,面试官嘴角上扬,露出一丝神秘的微笑:"那如果是这样呢?" 接着又写下:
Integer c = 1;
Integer d = 1;
System.out.println(c == d);
Integer e = 128;
Integer f = 128;
System.out.println(e == f);
这时候你突然意识到事情没那么简单,刚才的答案可能有问题。看着面试官似笑非笑的表情,你开始怀疑人生:明明都是 int 装箱成 Integer,为什么有的用 == 比较是 true,有的又是 false?难道 1 和 10 有什么特殊魔力?今天咱们就来好好掰扯掰扯这个让无数程序员在面试中翻车的 "简单" 问题,看看背后藏着多少不为人知的细节。

一、从自动装箱说起:编译器背后的小魔术
首先,我们得搞清楚 int 和 Integer 之间的关系。在 Java 5 之后,引入了自动装箱(Autoboxing)和自动拆箱(Unboxing)的特性,让基本数据类型和对应的包装类之间可以自动转换。比如说:
Integer x = 5; // 自动装箱,相当于Integer x = Integer.valueOf(5);
int y = x; // 自动拆箱,相当于int y = x.intValue();
这个特性让我们在编写代码时可以更方便地使用包装类,不用频繁地手动调用 valueOf () 和 xxxValue () 方法。但是,自动装箱并不是简单地把 int 包装成 Integer 对象,背后涉及到一个重要的方法 ——Integer.valueOf(int i)。这个方法可是大有学问,面试题的玄机就藏在这里。我们先来看一下Integer.valueOf(int i)的源码(以 Java 8 为例):
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
看到没?这里有一个IntegerCache的缓存机制。当传入的 int 值在IntegerCache.low和IntegerCache.high之间时,不会创建新的 Integer 对象,而是直接从缓存中获取已经存在的对象。而默认情况下,IntegerCache.low是 - 128,IntegerCache.high是 127。也就是说,当我们将一个 int 值装箱成 Integer 时,如果值在 - 128 到 127 之间,会直接返回缓存中的对象,而不是新建一个对象;如果超过这个范围,才会新建一个 Integer 对象。这下明白了吧?刚才的例子中,Integer c = 1和Integer d = 1,因为 1 在缓存范围内,所以 c 和 d 指向的是同一个缓存中的对象,用 == 比较自然是 true;而Integer e = 128和Integer f = 128,128 超过了默认的缓存上限 127,所以会新建两个不同的 Integer 对象,用 == 比较就是 false。

但是等等,这里有个问题:面试官刚才的第一个问题中,a 是 1,b 是 10,都是在缓存范围内,为什么a == b是 false 呢?哦,对了,因为 a 和 b 是不同的对象,虽然都在缓存范围内,但缓存的是相同值的对象,而不是不同值的对象。也就是说,缓存是针对单个值的,每个值在缓存中只有一个对象。所以 1 对应的缓存对象和 10 对应的缓存对象是不同的,所以 a 和 b 指向不同的对象,== 比较自然是 false,而 equals 比较的是值,所以是 true。

二、Integer 缓存机制:面试官挖的第一个坑
刚才提到的IntegerCache是 Java 中为了优化性能而引入的一个缓存机制,用于缓存常用的小整数对象,避免频繁创建和销毁对象带来的性能开销。这个缓存的范围默认是 - 128 到 127,但是我们可以通过 JVM 参数来修改这个范围。比如,在启动程序时加上-XX:AutoBoxCacheMax=200,就可以将缓存的上限设置为 200,这样 200 以内的整数装箱时都会使用缓存中的对象。

不过,需要注意的是,这个缓存机制只适用于自动装箱的情况,也就是通过Integer.valueOf(int i)方法来获取 Integer 对象的情况。如果我们直接使用 new Integer (int i) 来创建对象,不管值是多少,都会新建一个新的对象,不会使用缓存。比如:
Integer g = new Integer(1);
Integer h = new Integer(1);// 堆代码 duidaima.com
System.out.println(g == h); // 输出false,因为每次new都会创建新对象
System.out.println(g.equals(h)); // 输出true,因为值相同
另外,还有一个容易混淆的地方是,Integer 的缓存机制是在类加载的时候就已经初始化好了的,也就是说,当我们第一次使用 Integer 类的时候,缓存就已经创建好了,包含 - 128 到 127 之间的所有整数对象。所以,不管我们在程序的哪个地方装箱一个在这个范围内的整数,都会返回同一个缓存中的对象。这里还有一个有趣的现象:当我们将一个 Integer 对象赋值给 int 变量时,会发生自动拆箱,这时候比较的是值而不是对象引用。比如:
Integer i = 1;
int j = 1;
System.out.println(i == j); // 输出true,因为自动拆箱后比较的是值
这是因为当一个 Integer 对象和一个基本数据类型 int 进行比较时,Integer 会自动拆箱成 int,然后比较两个 int 的值,所以结果是 true。

三、== vs equals:面试官挖的第二个坑
接下来,我们来深入探讨一下 == 和 equals 方法的区别。这是 Java 面试中非常经典的问题,但很多人对它们的理解还停留在表面。首先,== 对于基本数据类型来说,比较的是值是否相等;对于引用数据类型来说,比较的是对象的内存地址是否相同,也就是是否指向同一个对象。而 equals 方法是 Object 类的一个实例方法,默认实现也是比较对象的内存地址,和 == 的效果一样。但是,很多类重写了 equals 方法,比如 String、Integer 等,重写后的 equals 方法比较的是对象的内容是否相等。

以 Integer 为例,它的 equals 方法源码如下:
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}
可以看到,Integer 的 equals 方法会先判断对象是否是 Integer 类型,如果是,就比较包装的 int 值是否相等。所以,当我们用 equals 比较两个 Integer 对象时,只要它们包装的 int 值相同,就会返回 true,不管它们是否是同一个对象。但是,这里需要注意一个问题:如果我们将一个 null 值和一个 Integer 对象用 equals 比较,会抛出 NullPointerException。而用 == 比较的话,null 和任何对象引用比较都是 false,不会抛出异常。

另外,还有一种常见的错误是,误以为所有包装类的 equals 方法都和 Integer 一样,只比较值。其实不然,比如 Double 和 Float 的 equals 方法在比较时,还会考虑 NaN 的情况。不过,这是另一个话题,我们今天先聚焦在 Integer 上。

回到最初的面试题,当面试官问a == b和a.equals(b)的结果时,我们需要分情况讨论:如果 a 和 b 都是通过自动装箱(即 Integer.valueOf ())得到的,并且值在缓存范围内(-128 到 127),那么当值相同时,a == b 为 true,否则为 false;而 a.equals (b) 只要值相同就为 true。如果 a 和 b 是通过 new Integer () 创建的,那么不管值是否相同,a == b 永远为 false,因为每次 new 都会创建新对象;而 a.equals (b) 只要值相同就为 true。

当一个是基本类型 int,一个是 Integer 对象时,== 比较会自动拆箱,比较值是否相同;而 equals 比较时,因为 int 会自动装箱成 Integer,所以和两个 Integer 对象比较一样,比较值是否相同。

四、哈希码与 equals:面试官可能追问的第三个坑
在 Java 中,哈希码(hash code)和 equals 方法有着密切的关系。根据 Java 的规范,两个对象如果 equals 方法返回 true,那么它们的哈希码(hashCode () 方法的返回值)必须相等;如果 equals 方法返回 false,它们的哈希码可以相等也可以不相等。Integer 类重写了 hashCode 方法,返回的是包装的 int 值。所以,两个值相同的 Integer 对象,它们的哈希码是相等的,这符合上述规范。

我们可以通过一个例子来验证:
Integer k = 100;
Integer l = 100;
System.out.println(k.hashCode()); // 输出100
System.out.println(l.hashCode()); // 输出100
System.out.println(k.equals(l)); // 输出true
Integer m = new Integer(100);
System.out.println(m.hashCode()); // 输出100,因为重写了hashCode方法,返回值本身
这里需要注意的是,如果我们自定义一个类,重写了 equals 方法,就必须同时重写 hashCode 方法,否则可能会违反上述规范,导致在使用哈希表(如 HashMap、HashSet)时出现问题。不过,这是另一个层面的问题,我们今天主要关注 Integer 类。另外,当我们将 Integer 对象作为 HashMap 的键时,需要注意如果对象被修改了(虽然 Integer 是不可变类,值不会被修改,但如果是自定义的可变类),哈希码可能会改变,导致无法正确获取对应的 value。不过,Integer 是不可变的,所以不用担心这个问题,但这是一个需要了解的知识点。

五、序列化与反序列化:面试官可能深挖的第四个坑
Integer 作为 Java 的基本包装类,实现了 Serializable 接口,所以可以被序列化和反序列化。在序列化过程中,Integer 对象会被转换成字节流,反序列化时再恢复成对象。这里有一个有趣的现象:当反序列化一个 Integer 对象时,返回的对象是否来自缓存呢?答案是肯定的。因为反序列化过程中,会调用 Integer.valueOf () 方法来创建对象,所以如果值在缓存范围内,会返回缓存中的对象,而不是新建一个对象。

我们可以通过一个简单的例子来验证:
import java.io.*;

publicclass IntegerSerializationTest {
    public static void main(String[] args) throws Exception {
        Integer n = 100;
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("integer.ser"));
        oos.writeObject(n);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("integer.ser"));
        Integer m = (Integer) ois.readObject();
        ois.close();

        System.out.println(n == m); // 输出true,因为100在缓存范围内,反序列化使用了valueOf方法
    }
}
运行结果是 true,说明反序列化得到的 Integer 对象和缓存中的对象是同一个。而如果序列化的值是 128,反序列化得到的对象和新装箱的 128 是否是同一个呢?我们来试一下:
Integer p = 128;
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("integer2.ser"));
oos2.writeObject(p);
oos2.close();

ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream("integer2.ser"));
Integer q = (Integer) ois2.readObject();
ois2.close();

System.out.println(p == q); // 输出false,因为128不在默认缓存范围内,序列化时保存的是对象的二进制数据,反序列化时调用valueOf方法,128超过缓存上限,新建对象
System.out.println(p.equals(q)); // 输出true
这里输出 false,因为 128 不在默认的缓存范围内,反序列化时会调用 Integer.valueOf (128),而该方法会新建一个 Integer 对象,所以 p 和 q 是不同的对象,但值相同。

六、扩展思考:其他包装类的缓存机制
其实,不仅仅是 Integer 类有缓存机制,Java 中的其他基本包装类,如 Byte、Short、Long、Character 等,也都有类似的缓存机制,只不过缓存的范围可能不同:
Byte:缓存范围是 - 128 到 127,因为 Byte 的取值范围就是 - 128 到 127,所以所有值都会被缓存。
Short:默认缓存范围是 - 128 到 127,可以通过 JVM 参数修改上限。
Long:默认缓存范围是 - 128 到 127,可以通过 JVM 参数修改上限。
Character:默认缓存范围是 0 到 127,因为 Character 表示的是 Unicode 字符,0 到 127 对应 ASCII 字符,是比较常用的范围。
Double 和 Float:没有缓存机制,因为浮点数的范围太大,而且存在精度问题,缓存没有意义。

我们以 Short 为例,来看一下它的 valueOf 方法源码:
public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    return new Short(s);
}
可以看到,Short 的缓存范围也是 - 128 到 127,和 Integer 类似。而 Character 的缓存范围是 0 到 127,源码如下:
public static Character valueOf(char c) {
    if (c <= 127) { // must cache
        return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}
所以,当我们使用这些包装类时,也要注意它们的缓存机制,避免在面试中被问到类似的问题时翻车。

七、面试陷阱总结:这些坑你都踩过吗?
现在,我们来总结一下面试中关于 int 和 Integer 的常见问题和陷阱:
自动装箱 / 拆箱的原理:知道是通过 valueOf () 和 xxxValue () 方法实现的,尤其是 valueOf () 方法的缓存机制。
== 和 equals 的区别:基本类型比较值,引用类型比较地址;equals 方法在 Integer 中比较的是值,但要注意 null 的情况。
Integer 缓存范围:默认 - 128 到 127,可通过 JVM 参数修改,new Integer () 不会使用缓存。
哈希码与 equals 的关系:重写 equals 必须重写 hashCode,Integer 的 hashCode 返回值本身。
序列化问题:反序列化使用 valueOf () 方法,所以在缓存范围内会返回缓存对象。
其他包装类的缓存:Byte、Short、Long、Character 有缓存,Double 和 Float 没有。

为了帮助大家更好地理解,我们再来看几个经典的面试题例子:

例子 1:
Integer a1 = 127;
Integer a2 = 127;
System.out.println(a1 == a2); // 输出true,127在缓存范围内

Integer b1 = 128;
Integer b2 = 128;
System.out.println(b1 == b2); // 输出false,128超出缓存范围
例子 2:
Integer c1 = new Integer(100);
Integer c2 = new Integer(100);
System.out.println(c1 == c2); // 输出false,new创建新对象
System.out.println(c1.equals(c2)); // 输出true,值相同
例子 3:
Integer d1 = 100;
int d2 = 100;
System.out.println(d1 == d2); // 输出true,d1自动拆箱成int,比较值
例子 4:
Integer e1 = null;
Integer e2 = 100;
// System.out.println(e1 == e2); // 输出false,不会抛异常
// System.out.println(e1.equals(e2)); // 抛NullPointerException
例子 5:
Integer f1 = Integer.valueOf(100);
Integer f2 = Integer.valueOf(100);
System.out.println(f1 == f2); // 输出true,使用缓存对象
八、为什么面试官喜欢问这个问题?
看到这里,可能有人会问:不就是一个自动装箱和缓存的问题吗?为什么面试官总是揪着不放?其实,这个问题虽然看似简单,但背后涉及到 Java 的很多核心概念:

基本类型与包装类的区别:值类型和引用类型的本质区别,栈内存和堆内存的存储方式。
自动装箱拆箱的实现原理:理解编译器如何处理基本类型和包装类的转换,背后的方法调用。
对象池技术(缓存机制):Java 中为了优化性能而采用的常见技术,如 String 常量池、Integer 缓存池等,理解性能优化的思路。
== 和 equals 的语义:深入理解 Java 中对象比较的规则,避免在实际开发中出现逻辑错误。
不可变类的设计:Integer 是不可变类,一旦创建值就不能改变,理解不可变类的优点和应用场景。
这些知识点都是 Java 程序员必须掌握的基础,尤其是在涉及到对象比较、集合操作(如 HashMap 的键)、性能优化等场景时,对这些细节的理解会直接影响代码的正确性和效率。

九、实际开发中的注意事项
虽然面试中经常考察这些细节,但在实际开发中,我们应该如何正确使用 int 和 Integer 呢?优先使用基本类型:如果不需要对象功能(如 null 值、方法调用等),优先使用 int、double 等基本类型,因为它们更高效,占用内存更小。
注意 null 值处理:当使用 Integer 时,要注意可能为 null 的情况,避免空指针异常。比如,在数据库查询中,整数类型的字段可能返回 null,这时候需要合理处理。
谨慎使用 == 比较对象:除非你确定两个引用指向同一个对象(如来自缓存或同一个 new 操作),否则应该使用 equals 方法比较值,尤其是在涉及自动装箱的情况下。
了解框架的处理方式:很多框架(如 Spring、MyBatis)在处理数据类型转换时,会涉及到自动装箱拆箱,了解这些机制可以帮助我们更好地调试和优化代码。
性能敏感场景的优化:在高频调用的代码中,如循环内部,如果需要创建大量小整数的 Integer 对象,使用自动装箱(利用缓存)会比 new Integer () 更高效,因为避免了对象创建和垃圾回收的开销。

十、总结:细节决定成败
回到最初的面试场景,为什么一个看似简单的 int 和 Integer 的问题会成为滑铁卢?因为很多程序员只停留在表面知识,知道自动装箱拆箱,知道 == 和 equals 的区别,但没有深入理解背后的实现原理,尤其是 Integer 的缓存机制。而面试官通过这个问题,实际上是在考察候选人对 Java 基础的掌握程度,是否注重细节,是否有深入钻研的习惯。

技术面试的本质,不是考察你会不会某个冷门的 API,而是考察你对基础原理的理解深度,以及能否将这些原理应用到实际开发中。就像 Integer 的缓存机制,看似只是一个小细节,但背后涉及到性能优化、对象池设计、语言特性实现等多个层面的知识。所以,各位程序员朋友们,下次遇到类似的问题,不要轻敌,多问自己几个为什么:为什么会有自动装箱?为什么 Integer 要设计缓存机制?缓存范围为什么是 - 128 到 127?修改 JVM 参数会有什么影响?只有把这些问题都搞清楚,才能在面试中从容应对,避免滑铁卢。
用户评论