1. 因為一個小小的Integer問題導(dǎo)致阿里一面沒過,遺憾!

        共 4473字,需瀏覽 9分鐘

         ·

        2021-11-23 14:46


        面試題:new Integer(112)和Integer.valueOf(112)的區(qū)別

        面試官考察點猜想

        這道題,考察的是對Integer這個對象原理的理解,關(guān)于這道題的變體有很多,我們會一一進行分析。

        理解這道題,對于實際開發(fā)過程中防止出現(xiàn)意想不到的Bug很有用,建議大家認真思考和解讀。

        背景知識詳解

        關(guān)于Integer的實現(xiàn)

        Integer是int的一個封裝類,它的構(gòu)造實現(xiàn)如下。

            /**
        * The value of the {@code Integer}.
        *
        * @serial
        */
        private final int value;

        /**
        * Constructs a newly allocated {@code Integer} object that
        * represents the specified {@code int} value.
        *
        * @param value the value to be represented by the
        * {@code Integer} object.
        */
        public Integer(int value) {
        this.value = value;
        }

        Integer中定義了一個int類型的value屬性。由于該屬性是final類型,因此需要通過構(gòu)造方法來賦值。這個邏輯非常簡單,沒有太多要關(guān)注的。

        結(jié)論:當通過new關(guān)鍵字構(gòu)建一個Integer實例時,和所有普通對象的實例化相同,都是在堆內(nèi)存地址中分配一塊空間。

        Integer.valueOf

        Integer.valueOf方法,是把一個字符串轉(zhuǎn)換為Integer類型,該方法定義如下

        public static Integer valueOf(String s) throws NumberFormatException {
        return Integer.valueOf(parseInt(s, 10));
        }

        這個方法調(diào)用另外一個重載方法,該方法定義如下。

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

        從這段代碼中發(fā)現(xiàn),如果i的值是在IntegerCache.lowIntegerCache.high這個區(qū)間范圍,則通過下面這段代碼返回Integer對象實例。

        IntegerCache.cache[i+(-IntegerCache.low)];

        否則,使用new Integer(i)創(chuàng)建一個新的實例對象。

        IntegerCache是什么?

        從它的命名來看,不難猜出它應(yīng)該和緩存有關(guān)系,簡單猜測就是:如果i的值在某個區(qū)間范圍內(nèi),則直接從緩存中獲取對象。

        IntegerCache的代碼定義如下。

        private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[]; //定義一個緩存數(shù)組

        static {
        // high value may be configured by property
        int h = 127;
        //high的值允許通過系統(tǒng)屬性來調(diào)整
        String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        //如果配置了high的屬性值,則取兩者中最大的一個值作為IntegerCache的最高區(qū)間值。
        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;
        //創(chuàng)建一個數(shù)組容器
        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() {}
        }

        上述代碼的實現(xiàn)邏輯非常簡單:

        1. IntegerCache的取值區(qū)間為:IntegerCache.low=-128, IntegerCache.hign=127,其中hign是可以通過系統(tǒng)參數(shù)來調(diào)整。

        2. 創(chuàng)建一個Integer數(shù)組,循環(huán)初始化這個區(qū)間中的每一個值。

        Integer為什么這么設(shè)計?但凡涉及到Cache的,一定和性能有關(guān),在Integer這個對象中,常用的數(shù)值區(qū)間是在-128到127之間,所以為了避免對這個區(qū)間范圍內(nèi)的數(shù)據(jù)頻繁創(chuàng)建和銷毀對象,所以構(gòu)建了一個緩存。意味著后續(xù)只要不是通過new關(guān)鍵字創(chuàng)建的Integer實例,在這個區(qū)間內(nèi)的數(shù)值都會從IntegerCache中獲取。

        問題解答

        面試題:new Integer(112)和Integer.valueOf(112)的區(qū)別

        理解了上面的原理后,再來解答這個問題就很容易了。

        • new Integer,是創(chuàng)建一個Integer對象實例。

        • Integer.valueOf(112),Integer默認提供了Cache機制,在-128到127區(qū)間范圍內(nèi)的數(shù)據(jù),通過valueOf方法不需要創(chuàng)建新的對象實例,只需要從緩存中獲取即可。

        問題總結(jié)

        Integer這個對象的變形面試題比較多,其中一個面試題比較典型。

        有兩個Integer變量a,b,通過swap方法之后,交換a,b的值,請寫出swap的方法。

        public class SwapExample {

        public static void main(String[] args){
        Integer a=1;
        Integer b=2;
        System.out.println("交換前:a="+a+",b="+b);
        swap(a,b);
        System.out.println("交換后:a="+a+",b="+b);
        }

        private static void swap(Integer a,Integer b){
        //doSomething
        }
        }

        基礎(chǔ)不是很好的同學,可能會很直接的按照”正確的邏輯“來編寫程序,可能的代碼如下。

        private static void swap(Integer a,Integer b){
        Integer temp=a;
        a=b;
        b=temp;
        }

        程序邏輯,理論上是沒問題,定義一個臨時變量存儲a的值,然后再對ab進行交換。而實際運行結(jié)果如下

        交換前:a=1,b=2
        交換后:a=1,b=2

        Integer對象的重新賦值思考

        Integer作為封裝對象類型,通過函數(shù)傳遞該引用以后,理論上來說,main方法中定義的ab,以及傳遞到swap方法中的a、和b,指向同一個內(nèi)存地址,那么按照上述代碼的實現(xiàn),理論上來說也是成立的。

        Java中有兩種參數(shù)傳遞類型。

        • 值傳遞,傳遞的是數(shù)據(jù)的副本,方法執(zhí)行中形式參數(shù)值的改變不影響實際參數(shù)的值。

        • 引用傳遞,傳遞的是內(nèi)存地址的引用,在方法執(zhí)行中,由于引用對象的地址指向同一塊內(nèi)存,所以對于對象數(shù)據(jù)的修改,會影響到引用了該地址的變量。

        這么設(shè)計的好處,是為了減少內(nèi)存的占用,提升訪問效率和性能。

        那么Integer作為封裝類型,為什么傳遞的是副本,而不是引用呢?

        我們來看一下Integer中value值得定義,可以發(fā)現(xiàn)該屬性是final修飾,意味著是不可更改。

            /**
        * The value of the {@code Integer}.
        *
        * @serial
        */
        private final int value;

        結(jié)論:在Java中,只有一種參數(shù)傳遞方式,就是值傳遞。但是,當參數(shù)傳的是基本類型時,傳的是值的拷貝,對拷貝變量的修改不影響原變量;當傳的是引用類型時,傳的是引用地址的拷貝,但是拷貝的地址和真實地址指向的都是同一個真實數(shù)據(jù),因此可以修改原變量中的值;當傳的是Integer類型時,雖然拷貝的也是引用地址,指向的是同一個數(shù)據(jù),但是Integer的值不能被修改,因此無法修改原變量中的值。

        因此,上述代碼之所以沒有交換成功,是因為傳遞到swap方法中的ab,會創(chuàng)建一個變量副本,這個副本中的值雖然發(fā)生了交換,但不影響原始值。

        了解了這塊知識之后,我們的問題就變成了,如何對一個修飾了final關(guān)鍵字的屬性進行數(shù)據(jù)修改。那就是通過反射來實現(xiàn),實現(xiàn)代碼如下.

        public class SwapExample  {

        public static void main(String[] args){
        Integer a=1;
        Integer b=2;
        System.out.println("交換前:a="+a+",b="+b);
        swap(a,b);
        System.out.println("交換后:a="+a+",b="+b);
        }

        private static void swap(Integer a,Integer b){
        try {
        Field field=Integer.class.getDeclaredField("value");
        Integer temp= a;
        field.setAccessible(true); //針對private修飾的變量,需要通過該方法設(shè)置。
        field.set(a,b);
        field.set(b,temp);
        } catch (NoSuchFieldException e) {
        e.printStackTrace();
        } catch (IllegalAccessException e) {
        e.printStackTrace();
        }
        }
        }

        那這段代碼運行完是否能達到預(yù)期呢?上述程序運行結(jié)果如下:

        交換前:a=1,b=2
        交換后:a=2,b=2

        從結(jié)果來看,確實是發(fā)生了變化,但是變化并不完整,因為b=1這個預(yù)期值并沒有出現(xiàn)。為什么呢?其實還是和今天分享的主題有關(guān)系,我們來逐步看一下。

        1. Integer temp=a這個地方,基于IntegerCache的原理,這里并不會產(chǎn)生一個新的temp實例,意味著temp變量和a變量指向的內(nèi)存地址是同一個。

        2. 當通過field.set方法,把a內(nèi)存地址的值通過反射修改成b以后,那么此時a的值應(yīng)該是2。注意:由于內(nèi)存地址的值變成了2,而temp這個變量又指向該內(nèi)存地址,因此temp的值自然就變成了2.

        3. 接著使用filed.set(b,temp)修改b屬性的值,此時temp的值時2,所以得到的結(jié)果b也變成了2.

        private static void swap(Integer a,Integer b){
        try {
        Field field=Integer.class.getDeclaredField("value");
        Integer temp= a;
        field.setAccessible(true); //針對private修飾的變量,需要通過該方法設(shè)置。
        field.set(a,b);
        field.set(b,temp);
        } catch (NoSuchFieldException e) {
        e.printStackTrace();
        } catch (IllegalAccessException e) {
        e.printStackTrace();
        }
        }

        理解了原理后,我們只需要修改Integer temp=a這段代碼,改成下面這種寫法。保證temp變量是一個獨立的實例。

        Integer temp=new Integer(a);

        修改以后運行結(jié)果如下

        交換前:a=1,b=2
        交換后:a=2,b=1


        瀏覽 35
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 中文字幕一区二区三区人妻电影 | 操的好舒服啊 | 成人黄色性爱视频 | 婷婷丁香五月综合精品 | 豆花视频入口 |