從一道面試題了解Interge的原理實現

目錄

舉例

值傳遞和引用傳遞

源碼

拆箱和裝箱

IntegerCache


主要考察你對Interge裏面的緩存的實現機制,因爲這裏面很容易遇到一些坑。

舉例

  public static void main(String[] args) throws InterruptedException {
       Integer a=1,b=2;
        System.out.println("before=a"+a+"b="+b);
        swap(a,b);
        System.out.println("after=a"+a+"b="+b);
    }
    public static void swap(Integer i1,Integer i2){
        Integer temp;
        temp=i1;
        i1=i2;
        i2=temp;
    }
    
執行結果:    
before=a1b=2
after=a1b=2

我們發現我們通過這個方式去交換a和b值,沒有成功,但是我們這種思路又是沒有問題的,是正確的。

這個就是我們這裏 swap(Integer a,Integer b)傳過來的這個值進行交換,但是他不會對原來的會有影響。

那麼我們可能會有一個疑問了。爲什麼我們傳進來是一個引用對象的值,爲什麼改變這個值卻不會對原來的值造成影響呢?

那麼這裏就涉及到值傳遞和引用傳遞,這是java最基本的知識點。

值傳遞和引用傳遞

實際上呢,在java裏面,他只有一種參數傳遞機制,是按值傳遞。至於爲什麼會有值傳遞和引用傳遞的說法,實際上這裏取決於變量的類型,因爲我們知道變量類型分爲引用類型和基本類型。那麼當我們把這兩種的類型傳遞給一個方法的時候,那麼處理兩種類型的方式是相同的,對於我們jvm底層的處理方式是相同的。都是按照值去傳遞的,只是說,根據這兩種類型的不同;那麼我傳遞的是基本類型,那麼函數接收的是原始值的一個副本,因此如果函數改變了這個值,那麼函數改變的是副本的值,原始值不變。這是第一個。

那麼如果傳遞的是引用類型,那麼函數接收的是原始引用的內存地址,而不是副本,因此這個函數去修改這個參數的話,他會修改這個值的一個地址去影響到我們原本傳過來的值。

但是也許還有疑問:

爲什麼要取這麼去設計?

因爲我們知道對象類型是存儲在對堆裏面的,就是對象引用指向的一個地址存在堆內存裏面,引用地址是存在對棧裏面的。

那麼通過對象類型的一個地址存在堆裏面主要是爲了提升,我們的一個速度。因爲基本類型和對象類型,他們倆的內存副本的拷貝速度是不一樣的。第二個是:對象類型它本身佔用的內存空間比較大。如果從新去複製這樣對象的一個方法,他會比較浪費內存。

所以這是值傳遞和引用傳遞的區別。

但是這裏面還有第二個問題;

就是我們這裏傳遞的是一個Interge。他是一個引用類型,封裝類型。爲什麼他不能改變?

因爲在java裏面這種封裝類型的傳遞,都是傳遞他的副本值。這是一個規定。

那麼你一定 要去探究他的原理的話,你要去看他的源碼

源碼

在源碼中我們可以發現,Interge裏面的他的value值他是一個final的一個東西。

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

並且這個value還沒有對外的一個設置,因爲他是final的嘛。他是沒辦法改變。所以我們在傳遞過來的時候,他是無法改變他的值,所以我們只能改變他的副本。所以他只能拷貝他的一個副本來傳遞。

如果我們要來畫圖的話,我們先來畫一個棧空間和堆空間

原本我們定義了一個a和b,這兩個對象,這兩個封裝類型。他們仍然是引用類型,所以他們都在堆內存中存在一個地址。比如說a=1;b=2。

那麼我們在剛剛的那個傳遞過程中,他是這樣的,比如我們在另一個地方又定義了一個i1 ,i2。那麼實際上我我們傳遞的是這個對象的副本。那麼這個副本他就意味着他去在棧裏面重現分配一個局部變量的定義i1,i2

這是他在們棧針中的一個概念。那麼i1 i2他原本的值,應該是指向之前的。那麼此時我麼去交換,沒有問題,i1和i2的值進行交換了。但是他改變的是我們當前的副本的值。

對於a和b來說他們是沒有任何影響的。

所以這一點驗證了,java中的封裝類型他傳遞的是副本不是原始值。這是一個固定的約定。

瞭解這個之後,在老看,如果這種方法沒有辦法改變的話,那麼還有什麼辦法去實現呢?

我麼看到源碼中定義的一個value屬性,他是用來保存我們a.b這兩的值的一個定義。

那麼我們可以通過什麼方式來改變一個私有的fianl的一個整型的成員變量呢?

那麼唯一的方法就是反射:

因爲反射可以改變我們原本定義的一些值,那麼怎麼實現呢

public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
   Integer a=1,b=2;
    System.out.println("before a="+a+"b="+b);
    swap(a,b);
    System.out.println("after a="+a+"b="+b);
}
public static void swap(Integer i1,Integer i2) throws NoSuchFieldException, IllegalAccessException {
  //通過一個反射去拿到Integer裏面的一個value的屬性
    Field  fiel=Integer.class.getDeclaredField("value");
    //拿到這屬性怎麼辦呢?然後給這個屬性去改變值
    int temp=i1.intValue();
    fiel.set(i1,i2.intValue());
    fiel.set(i2,temp);
}

我們發現,其實交換思想還是一樣的,只不過換了個方式,但是這裏我們知道私有成員和fianl成員是不允許我們去訪問的,所以運行他會報錯。

 Class com.syz.designMode.ThreadDemo can not access a member of class java.lang.Integer with modifiers "private final"

那麼怎麼去處理呢?

Field的裏面有一個field.setAccessible(true);

我們可以通過這種方式繞過他的一個檢查。

所以這也是我們面試中經常被問到了,我們怎麼去改變私有的成員變量的一個值,所以我們可以通過這個反射的機制去實現。

但是上面這個還會有一個問題:我麼看運行結果:

before a=1b=2
after a=2b=2

Process finished with exit code 0

我們發現a變過來了,但是b沒有變過來。這是第一個問題

那麼第二個問題是:通過這個setAccessible(true);他是怎麼繞過檢查的?

那麼我們先去分析setAccessible的源碼:

public void setAccessible(boolean flag) throws SecurityException {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
    setAccessible0(this, flag);
}

我們看到他調用setAccessible0這個方法。而且傳遞了一個flag參數。那麼這個flag是幹嘛呢?

private static void setAccessible0(AccessibleObject obj, boolean flag)
    throws SecurityException
{
    if (obj instanceof Constructor && flag == true) {
        Constructor<?> c = (Constructor<?>)obj;
        if (c.getDeclaringClass() == Class.class) {
            throw new SecurityException("Cannot make a java.lang.Class" +
                                        " constructor accessible");
        }
    }
    obj.override = flag;
}

我們可以看到他是去設計一個obj.override = flag;這個操作。由此可以看到obi就是我們AccessibleObject對象,而他裏面有一個成員屬性override,那麼他設置了這樣一個屬性,那麼他的目的是幹嘛的呢?

我們去看當我們field.set(i1,i2.intValue());這個值的時候他會去判斷這個屬性。

@CallerSensitive
public void set(Object obj, Object value)
    throws IllegalArgumentException, IllegalAccessException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    getFieldAccessor(obj).set(obj, value);
}

如果他設置爲true的話,他就不會走這個檢查邏輯。然後我們也能看到Field他實際上集成了AccessibleObject這個對象,所以他能夠訪問他的成員變量override。所以他通過這個繞過了安全檢查。這就是setAccessible的核心原理。

那麼第二個問題,他爲什麼改變了第一個值,第二個值沒有改變。也就是這個反射生效了,但是爲什麼他沒有全部的去生效呢?

這個就涉及到java中最核心的原理。拆箱和裝箱這樣一個概念。

拆箱和裝箱

那麼首先了解什麼是拆箱和裝箱。

首先:我們定義了一個Integer a=1.而這個1他實際是基本類型,也就是這int他int類型。這又定義了一個Integer類型。爲什麼編譯的時候他不會去報錯。那麼這裏就涉及到了裝箱操作。

那麼這個裝箱操作他做了什麼事情呢?

如果想去看的話,可以去看他的字節碼:可以通過命令:javap -c Swap.class


public class com.syz.designMode.Swap {
  public com.syz.designMode.Swap();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return


  public static void main(java.lang.String[]) throws java.lang.InterruptedException, java.lang.NoSuchFieldException, java.lang.IllegalAccessException;
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: iconst_2
       6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       9: astore_2
      10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      13: new           #4                  // class java/lang/StringBuilder
      16: dup
      17: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      20: ldc           #6                  // String before a=
      22: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: aload_1
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      29: ldc           #9                  // String b=
      31: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: aload_2
      35: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      38: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      41: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      44: aload_1
      45: aload_2
      46: invokestatic  #12                 // Method swap:(Ljava/lang/Integer;Ljava/lang/Integer;)V
      49: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      52: new           #4                  // class java/lang/StringBuilder
      55: dup
      56: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      59: ldc           #13                 // String after a=
      61: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      64: aload_1
      65: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      68: ldc           #9                  // String b=
      70: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      73: aload_2
      74: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      77: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      80: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      83: return

找到我們main方法裏面的變量對應的字節碼:

我們發現我們定義的一個常量 iconst_1 這是他的字節碼指令。但我們發現他去做:

 1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

的時候,他會有一個Integer.valueOf,他實際上是int的value去做一個裝箱。也就是意味着:

Integer a=1 就相當於Integer a=Integer.valueOf(1); 他們是等價的。這個就是他的裝箱操作。

瞭解了這個之後,我們看valueOf他裏面做了什麼事情?

IntegerCache

我們去Integer.java類中去搜索一下這個方法:

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

這裏面他去判斷了i >= IntegerCache.low && i <= IntegerCache.high

這個操作。然後返回IntegerCache.cache[i + (-IntegerCache.low)。

我們通過Cache這個名字知道他是一個緩存。意思是如果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() {}

這個緩存定義了low = -128 。int h = 127;

所以就是在-128和127範圍之內他會去做一件什麼事呢?

從源碼看他會去初始化緩存:cache = new Integer[(high - low) + 1];

然後把這個範圍的值放入緩存裏。

這就意味着他在運行的時候,-128到127這個範圍的值就應經被佔用了。

那麼這麼設計的好處是,因爲他提升了效率。因爲Integer我們用的最多的就是在這個範圍之內的數據。所以他可以很好的去減少內存的分配,去提升他的一個效率。

那麼怎麼去驗證Integer的緩存的概念呢?如案例:

Integer i1 = 1;
Integer i2 = 1;
System.out.println(i1 == i2);

按照我們對於java裏面的原理,就是兩個對象去直接== 去相等的話,這是不可取的。因爲他比較的是兩個內存地址的值。所以對於對象去進行==對比的話,他們結果是不相等的。所以他這個地方應該打印的是false.。但是呢;我們上面說過Integer裏面他去做了一個緩存的概念。所以發現運行的結果是:true。

那麼如果我們超過了他的範圍之後呢?

加入是i1,i2=129.那麼他就會導致一個問題。他會走我們源碼中的另一個方法:

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

這裏面的return new Integer(i);這個方法。

凡事帶new的操作,他都會去分配一個內存地址。所以這時候i1.i2他們兩個的內存地址是不一樣的。所以他的比較是不相等的。

所以這就緩存的概念。

那麼爲什麼要了解緩存的概念和裝箱拆箱的概念呢?

因爲這個就是產生之前數據交換爲什麼i2的值沒有變的結果了。

那麼我們在來回顧那段代碼:

int temp = i1.intValue(); 這段代碼是沒有問題。他就是去拿到i1的整數值。

field.set(i1, i2.intValue());但是這裏他就會有問題了。

因爲這個set方法,他的兩個參數值都是Object類型的

public void set(Object obj, Object value){}

我們這裏傳的i1是沒問題的,他是Object類型;但是i2.intValue()這個就有問題了。我們給他傳的是一個int類型的值了。所以這個他就涉及到裝箱操作。引入你int類型怎能讓我們Object去接受呢?所以他就行了一個自裝箱操作。

所以實際上i1的值的變化應該是 i1= Integer.valueOf(i2.intValue()).intValue()

那麼:;這地方也會涉及到裝箱,那麼i2=Integer.valueOf(temp).intValue()

那麼這個地方的問題就來了;valueOf()操作他涉及到了緩存;那麼Integer.valueOf()他是根據temp的值去拿到下標的位置。而這個下標的位置i1的值變成了2.所以這裏temp的值也變成了2.

那麼這裏應該又有一個疑問:temp的值怎麼會變成2呢?

System.out.println(Integer.valueOf(temp)); 我們去輸出一下temp。

因爲我們剛介紹到.valueOf()他是從緩存的下標去拿值。我們會發現,temp他本來的值應該是1.拿到的是基本的值。但是現在他經歷了裝箱,又變成了從緩存內存地址去拿,就是從下標位置去拿。所以運行這個輸出值:發現temp=2。 就是:temp原本是數值數據1,但是現在進行裝箱,通過valueOf(1) 就是取緩存中1下邊對應的值了。這個下標1對應的值爲2,所以Integer.valueof(temp)=2所以理解temp的值變成了2.

所以現在temp=2.有設置給了i2(field.set(i2, temp)) (temp=Integer.valueOf(temp).intValue()=》temp=Integer.valueOf(1).intValue() 緩存下表1的值)。所以理所當然a等於2,b也等於2,他們是去替換,只是替換了結果。但是由於緩存的原因導致了最終出現了這樣的結果。

通過代碼理解:

public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
    Integer a=1,b=2;
    System.out.println("Integer獲取緩存中的值 start");
    System.out.println("000="+Integer.valueOf(0));
    System.out.println("111="+Integer.valueOf(1));
    System.out.println("222="+Integer.valueOf(2));
    System.out.println("333="+Integer.valueOf(3));
    System.out.println("444="+Integer.valueOf(4));
    System.out.println("555="+Integer.valueOf(5));
    System.out.println("Integer獲取緩存中的值 end");
    System.out.println("before a="+a+"b="+b);
    swap(a,b);
    System.out.println("after a="+a+"b="+b);
}


public static void swap(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException {
    //通過一個反射去拿到Integer裏面的一個value的屬性
    Field field = Integer.class.getDeclaredField("value");
    //拿到這屬性怎麼辦呢?然後給這個屬性去改變值
    field.setAccessible(true);
    int temp = i1.intValue();
    System.out.println("temp0="+temp);
    System.out.println("temp1="+Integer.valueOf(temp));
    field.set(i1, i2.intValue()); // Integer.valueOf(i2.intValue()).intValue()
    System.out.println("temp2="+temp);
    System.out.println("temp3="+Integer.valueOf(temp));
    field.set(i2, temp);
}


輸出值:
Integer獲取緩存中的值 start
000=0
111=1
222=2
333=3
444=4
555=5
Integer獲取緩存中的值 end
before a=1b=2
temp0=1
temp1=1
temp2=1
temp3=2
after a=2b=2

那麼我們該怎樣改變這個值,避免他的裝箱還拆箱操作。

避免的方法比如:

方法一:改變temp的值:Integer temp=new Integer(i1.intValue());

這下我們temp的內存地址跟我們的i1就完全區分開。這時候我們就不因爲temp的值產生影響了

public static void swap(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException {
    //通過一個反射去拿到Integer裏面的一個value的屬性
    Field field = Integer.class.getDeclaredField("value");
    //拿到這屬性怎麼辦呢?然後給這個屬性去改變值
    field.setAccessible(true);
    Integer temp=new Integer(i1.intValue());
   // int temp = i1.intValue();
    field.set(i1, i2.intValue());
    field.set(i2, temp);
    System.out.println(Integer.valueOf(temp));
}

這時候b的值就會變化了。

因爲這裏我們走了內存空間,並沒有走i1.intValue() 去走相同的內存地址。因爲我們在這個位置Integer.valueOf(i2.intValue()).intValue()把i1的內存地址給改成2,所以我們再去拿相同地址i1的時候變成了2。

方法二:set改成setInt

field.setInt(i1, i2.intValue());
field.setInt(i2, temp);

去避免裝箱操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章