关于java:面试题系列第3篇Integer等号判断的内幕你可能不知道

42次阅读

共计 4639 个字符,预计需要花费 12 分钟才能阅读完成。

《Java 面试题系列》:对面试题中经典内容进行深刻开掘,剖析源码、汇总原理,造成公众号系列文章,面试与否均可晋升技能。欢送大家继续关注【程序新视界】。本篇为系列第 3 篇。

面试过程中对于 Integer 的比拟“==”的问题内容层出不穷,但无论怎么变动,只有理解了其中的底层原理,马上就能够得出答案,再也不必死记硬背考题了。

《阿里巴巴 Java 开发手册》中有这样一项强制要求:

“所有整形包装类对象之间值的比拟,全副应用 equals 办法比拟。阐明:对于 Integer var=?在 -128 到 127 范畴内的赋值,Integer 对象在 IntegerCache.cache 产生,会复用已有对象,这个区间的 Integer 值能够间接应用 == 进行判断,然而这个区间之外的所有数据都会在堆上产生,并不会复用已有对象,这是一个大坑,举荐应用 equals 办法进行判断。”

其实,如果将下面一段话背下来,那么你基本上曾经能够答对百分之五十(跟猜的概率差不多)的面试题了。但如果想理解深层原理和剩下的百分之五十的问题,就咱们就持续往下看。

面试题

先来看一道常见的面试题,对照下面的论断,看看可能答对几项。上面代码中打印后果为 true 的有几项?

@Test
public void test2() {
    Integer i1 = 64;
    int i2 = 64;

    Integer i3 = Integer.valueOf(64);
    Integer i4 = new Integer(64);

    Integer i5 = 256;
    Integer i6 = Integer.valueOf(256);

    System.out.println("A:" + (i1 == i2));
    System.out.println("B:" + (i1 == i3));
    System.out.println("C:" + (i3 == i4));
    System.out.println("D:" + (i2 == i4));
    System.out.println("E:" + (i3.equals(i4)));
    System.out.println("F:" + (i5 == i6));
}

执行下面的程序,打印后果为:

A:true
B:true
C:false
D:true
E:true
F:false

只有 C 和 F 项打印为 false。你是否纳闷为什么 i1 等于 i2,i1 等于 i3,i2 等于 i4,都为 true,那么依据等号的传递性,i3 应该等于 i4 啊?

为什么 i1 和 i3 相等,但 i5 和 i6 却不相等呢?

先保留疑难。上面,咱们从 int 及 Integer 在 JVM 中的存储构造来进行剖析。把握了底层存储构造,你会发现无论题面如何变动,都万变不离其宗。

变量在 JVM 中的存储

在彻底弄清楚上问题之前,咱们先来理解一下根底类型变量、援用类型变量在 JVM 中的存储。

通常变量分为局部变量和全局(成员)变量。局部变量是申明在办法内的变量;全局变量是申明在类中的成员变量。

根底类型的变量和值在调配的时候是在一起的,都在办法区或栈内存或堆内存。而援用类型的变量和值不肯定在一起。

局部变量存储在办法栈中

当办法被调用时,Java 虚拟机都同步创立一个栈帧,局部变量便存储在其中。当办法完结虚构机会开释办法栈,其中申明的变量随着栈帧的销毁而完结。因而,局部变量只能在办法中无效。

此过程中,根底类型和援用类型的存储有所区别:

(1)根本类型:变量和对应的值寄存在 JAVA 虚拟机的栈中;

(2)援用类型:变量存储在栈中,是一个内存地址,该地址值指向堆中的对象。

栈属于线程公有的空间,局部变量的生命周期和作用域个别都很短,为了进步 gc 效率,所以没必要放在堆外面。

全局变量存储在堆中

全局变量寄存在堆中,不会随着办法完结而销毁。同样在类中申明的变量也是分为根本类型和援用类型。

(1)根本类型:变量名和值寄存在堆内存中。

(2)援用类型:变量是一个援用地址,该地址指向所援用的对象。此时,变量和对象都在堆中。

举个简略的例子,如下代码:

public class Person {
    int age = 10;
    String name = "Tom";
}

对应的 age 和 name 的存储构造如下图:

联合下面的实践,咱们通过一段代码来剖析一下各种类型所存储的地位。

public class DemoTest {

 int y; // 变量和值均在堆上
 
 public static void main(String[] args) {

     int x = 1; // 变量和值调配在栈上
     
     String name = new String("cat"); // 数据在堆上,name 变量的指针在栈上
     
     String address = "北京"; // 数据在常量池,属于堆空间,指针在栈上
     
     Integer price = 4; // 包装类型为援用类型,编译时会主动装拆箱,数据在堆上,指针在栈
 }
}

根底类型的栈内存储

通过下面的实例,根本理解了不同类型的值的内存分配情况。上面咱们重点探讨局部变量。

上面先来看看在同一栈帧中,针对 int 类型的解决模式。

int a = 3;
int b = 3;

上述代码中 a 和 b 均为局部变量。假如编译器先解决 int a=3,此时会在栈中创立 a 的援用变量,而后查找栈中是否存在 3 这个值,如果没有就将 3 寄存进来,而后将 a 指向 3。

接着解决 int b=3,创立完 b 的援用变量后,同样进行查找。因为在栈中曾经有 3 这个值,便将 b 间接指向 3。

此时,a 与 b 同时指向 3 这个值,天然是相等的。

对于根底类型与援用类型的底层比拟,可略微延长一下:对于“==”操作符号,JVM 会依据其两边互相比拟的操作数的类型,在编译时生成不同的指令:

(1)对于 boolean,byte、short、int、long 这种整形操作数会生成 if_icmpne 指令。该指令用于比拟整形数值是否相等。

(2)如果操作数是对象的话,编译器则会生成 if_acmpne 指令,与 if_icmpne 相比将 i(int) 改成了 a(object reference)。

回归正题

学习了下面的底层理论知识,咱们基本上能够得出如下论断:(1)两个 int 类型比拟,间接应用双等号即可;(2)int 的包装类 Integer 对象比拟时,应用 equals 进行比拟即可。

但下面的后果只能说 E 我的项目是正确的。其比拟项还波及到整形的装箱拆箱操作、Integer 的缓存。咱们上面逐个剖析。

不同创立模式的比拟

先看 Integer 的初始化,依据 Integer 的外部实现,创立 Integer 有三种,别离是:

Integer a = new Integer(1); // 创立新的类

Integer b = Integer.valueOf(2);  

Integer c = 3; // 主动包装,会调用 valueOf 办法 

其中间接赋值底层会调用 valueOf 办法进行操作的,因而这两种操作成果是一样的。

因为通过 new 和 valueOf 创立的是齐全两个对象,那么针对题目中的 C 项,间接比拟两个对象的援用必定是不相等的,因而后果为 false。但 B 项为什么为 true 呢?前面咱们会讲到。

比拟中的拆箱

在题目中,咱们发现 A、D 都为 true,而且它们的比拟格局都是根底类型与包装类型的比照。

针对这种模式的比照,包装类型会进行主动拆箱,变成根底类型(int)。很显然,后果是相等的。

Integer 的缓存

为什么 i1 和 i3 相等,但 i5 和 i6 却不相等呢?对应题目中的 B 和 G 项。这里就波及到 Integer 的缓存机制。

咱们下面曾经晓得,Integer 间接赋值和 valueOf 是等效的,那先看一下 valueOf 及相干的办法。

public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch(NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

valueOf 办法判断数字是否大于 low(-128)并且小于 high(127),如果满足条件,则间接从 IntegerCache 中返回对应数字。

IntegerCache 用于存储一些罕用的数,避免反复创立,在 Integer 类装入内存时通过动态代码进行初始化。

所以只有是用 valueOf 或者 Integer 间接赋值的形式创立的对象,其值小于 127 且大于 -128 的,无论对其进行 == 比拟还是 equals 比拟,都是 true。

下面的源码及原理也解释了阿里 Java 开发手册中所阐明的起因。

为什么 equals 能够躲避问题

对于不满足 -128 到 127 范畴的数,无论通过什么形式创立,都会创立一个新的对象,只能通过 equals 进行比拟。接下来咱们再看看 equals 办法。

public boolean equals(Object obj) {if (obj instanceof Integer) {return value == ((Integer)obj).intValue();}
    return false;
}

equals 实现比较简单,先比拟类型是否统一,如果不统一,间接返回 false;否则,再比拟两者的值,雷同则返回 true。

小结

对于 Integer 的比拟外围点有以下三点:援用对象的存储构造、Integer 的缓存机制、主动装箱与拆箱。

Integer 在 == 运算时,总结一下:

(1)如果 == 两端有一个是根底类型 (int),则会产生主动拆箱操作,这时比拟的是值。

(2)如果 == 两端都是包装类型 (Integer),则不会主动拆箱,首先会面临缓存问题,即使在缓存范畴内的数据还会再次面临创立形式的问题,因而强烈建议应用 equals 办法进行比拟。

如果感觉文章写的还不错,就关注一下。下篇文章,咱们来讲讲 equals 和 hashcode 办法的重写底层逻辑。

<center> 程序新视界 :精彩和成长都不容错过 </center>

正文完
 0