包裝類型和基本類型

包裝類型和基本類型

  1. Java中有八種基本數據類型,分別對應着八大包裝類型,因爲包裝類型的實例都存在於堆中,所以包裝類型也稱爲引用類型。
    在這裏插入圖片描述

  2. 基本類型屬於原始數據類型,變量中存儲的就是原始值。包裝類型屬於引用數據類型,變量中存儲的是存儲原始值的地址的引用。

    • 基本類型中,局部變量存在方法虛擬機棧的局部變量表中,而類中聲明的的變量存在堆裏。
    • 包裝類型中,無論局部變量還是類中聲明的變量均存在堆中,而方法內的局部包裝類型變量,其也存在局部變量表中,不過期值爲該變量在堆中的地址。
  3. 手動裝箱 / 手動拆箱

 	Integer b = valueOf(1);
    int a = b.intValue();
  1. 自動裝箱 / 自動拆箱
   Integer b = 1;
   int a = b;

Java SE5 爲了減少開發人員的工作,提供了自動裝箱與自動拆箱的功能,3等價4。
5. == and equals

  • == 比較的是內存地址
  • equals 比較的是值
  1. 代碼實例
 int a=0;
 int b=0;
 System.out.println(a==b);

按照==的分析,此處應該輸出false,然而結果是true,原因在於:

當定義b時,JVM會先檢查局部變量表中是否已經有了0這個值,如果沒有,則創建,如果有(如之前已經執行過int a= 0),則不會再創建,而是直接將變量b指向變量a所在的局部變量表的地址,就好像執行的語句是int b = a。換句話說,a和b最終指向的內存空間,其實還是一致的

   int a = 0;
   int b = 0;
   b = b + 1;
   System.out.printlt(a == 1)

首先jvm會先創建一個常量 0,然後把a的引用指向0,再把b的引用指向0,當執行b=b+1的時候,運算出結果爲1,jvm先不創建1這個量,而是在局部變量表中去查找是否有這個值,有就返回,無就創建,此時b的值就被指向了1,所以輸出結果自然是false,當我們看下面代碼

  	int a = 0;
    int b = 0;
    int c=1;
    b = b + 1;
    System.out.println(b==c);

我們已經聲明瞭1,並把c指向這個地址,然後運行b=b+1時,jvm查找有1這個值,就把這個值的局部變量表地址賦值給了b,所以這裏判斷b==c應該是true;

  1. 裝箱拆箱詳解
    • 裝箱:裝箱是通過 valueOf()方法來實現自動裝箱的,我們來看看源碼:
public static Integer valueOf(int i) {
           if (i >= IntegerCache.low && i <= IntegerCache.high)
               return IntegerCache.cache[i + (-IntegerCache.low)];
           return new Integer(i);
       }

這裏面有個IntegerChche即Integer類型的緩存,來看看內部類IntegerCache:

private static class IntegerCache {
           static final int low = -128;
           static final int high;
           static final Integer cache[];
   
           static {
              ...
               
               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() {}
       }

這裏省略了部分內容,這個類存在的意義是將較爲常用的數字存到緩存中,int類型的是 -128 - +127,我們可以看到有一個final類型、Integer類型的cache數組,然後在static塊中分別對這個數組賦值,即把-128 - +127這256個數據存到cache數組裏,且索引是0-255,注意,這裏的cache數組是在類加載過程的初始化階段確定的,因爲在static塊中,並且由於是常量,會存在元空間內 ,又由於cache這實際上是一個對象數組,所以常量池中有cache這一項引用,其指向了再堆中的數組,但是又因爲是對象數組,其每一項都指向了堆中的具體的Integer對象.

然後再看valueOf,如果要裝箱的值不在-128-+127之間,那麼它會返回一個新的對象,這個對象是存在堆裏,如果是方法內調用,那麼對象的引用會存在方法的局部變量表內。

即:當我們開始運行程序的時候,-128-+127這256個數字就已經存在了堆中,用cache進行管理,如果新建一個Integer對象,其值是在此範圍內,就會直接返回cache中的相對於的信息即堆中的信息,如果不在此範圍內,就會新建立一個Integer對象,放在堆中,然後將其引用存在局部變量表裏。

  • 拆箱:拆箱是通過intValue來實現的
  	  Integer a = 0;
      int b = a.intValue();

其中intValue:

   public int intValue() {
             return value;
         }

從JVM來看,先創建了一個Integer類型的變量a,其引用指向堆,當我們實現拆箱的時候,JVM會先判斷
在局部變量表中是否有這個值,如果有,直接放回在局部變量表中的引用,如果沒有則新建再返回。

  1. 包裝類型代碼詳解:
   Integer a = 1;
        Integer b = 1;
        System.out.println(a==b);

由於 == 是比較地址,而Integer屬於引用類型,分別建立了兩個實例分別存在了堆中,所以直覺來看應該兩個地址應該不同,但是結果是true,原因是cache的存在,再代碼正式運行前,堆中就已經存有緩存,這裏的1屬於-128-+127之間,所以直接返回緩存中的值也就是說,在賦值a的時候,其實它指向了緩存中的1的引用,也就是指向了堆中,而賦值b的時候也是直接指向了緩存,所以他們兩個地址是一致的。
給一個草圖
在這裏插入圖片描述

 Integer a = 128;
      Integer b = 128;
      System.out.println(a==b);

這裏由於128不屬於緩存範圍,所以兩個語句分別建立了兩個不同的對象,所以他們的內存地址也是不一樣的,所以返回false,同樣也給出一個草圖

在這裏插入圖片描述

 public class test2 {
      Integer b=0;
      public static void main(String[] args) {
          test2 test2 = new test2();
          Integer a = 0;
          System.out.println(test2.b==a);
      }
  }

我們來看這個代碼,我們再類中定義了一個Integer對象b,賦值爲0,在main方法中也定義了一個Integer對象,也賦值爲0,比較這兩個地址,會輸出什麼?答案是true,原因很簡單:
在我們在類中定義b時,實際上這個b存於堆中,然後指向常量池中的cache,然後cache又指向在堆中的數組,而數組的每一項均指向了堆中的Integer對象,所以可以直接說是b直接指向了0這個Integer對象,在類中定義的a,其引用存在於局部變量表中,引用指向了Integer的cache,cache指向了堆,所以它們兩個的實際地址引用是一致的,都是cache在堆中的緩存.

所以我們得出了這個結論:在一個程序運行期間,無論是方法內還是方法外,其所定義的Integer,且其值是在-128-+127之間,那麼他們的引用是一致的。

  int a = 1;
  Integer b = 1;
  System.out.println(a==b);

但是如果我們來比較包裝類型和基本類型的地址的時候會輸出什麼?這裏輸出了true,爲什麼呢?
單看代碼看不出來啥,我們來看看字節碼:

 public static void main(java.lang.String[]);
          descriptor: ([Ljava/lang/String;)V
          flags: ACC_PUBLIC, ACC_STATIC
          Code:
            stack=3, locals=3, args_size=1
               0: iconst_1
               1: istore_1
               2: iconst_1
               3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
               6: astore_2
               7: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
              10: iload_1
              11: aload_2
              12: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
              15: if_icmpne     22
              18: iconst_1
              19: goto          23
              22: iconst_0
              23: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
              26: return
           
      }

這裏是main方法的主要字節碼,省略了部分,我們不做細講,主要講跟問題相關的,我們通過行號指示器來看,
首先第3行,invokestatic 表示調用了一個static方法,後面是註釋,表示調用了Integer.valueOf,這裏對應着代碼裏的定義包裝類,因爲定義包裝類就必須進行裝箱,而Java5後直接支持自動拆裝箱,自動不代表不用,所以這裏需要調用靜態方法Integer.valueOF進行裝箱,

看第7行,調用了輸出流的PrintSteam方法,再來看第12行,看後面的註釋,表示調用了Integer.intValue方法,這是拆箱的方法,但是我們代碼中並沒有拆箱,那麼這段代碼是怎麼來的呢?

如果要使用拆箱,就必須有包裝類,看代碼,輸出流裏面只有一個包裝類即b,那就說明了b調用了拆箱的方法,其主要過程是,把b的值取出,判斷局部變量表中是否存在,如果存在,則返回局部變量表中的地址,如果不存在則創建再返回。

所以我們能很好的解釋爲什麼上面的輸出是true,因爲,當我們使用==來比較基本類型和包裝類型時,包裝類型自動進行拆箱,並返回局部變量表中的引用,由於之前局部變量表中已經存在0這個值,並把a的引用執行它,當進行拆箱的時候,b的引用也指向它,如此一比較,他們的地址當然相等。

  1. equals 詳解
 Integer a=128;
        Integer b=128;
        System.out.println(a.equals(b));

爲什麼用equals能比較值呢?來看看equals源碼:

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

非常簡單粗暴,先判斷是否屬於Integer,如果是轉換再進行拆箱直接判斷值是否相等即可。
這個equals方法屬於Object類,如果要分別實現比較不同的值就必須進行重寫,所以上面的equals是Integer類重寫的equals,其實現會根據不同的引用類型給予不同實現。例如 Character-char類型包裝類,其equals是這樣的:

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

和Integer的equals是不一樣的。


幾個問題:

  1. b=b+1;1存在哪?
    當我們執行b=b+1時,b的值和1均存在於局部變量表中,不過b是以變量形式存在,即b的引用可以根據需要指向不同的值,而1是常量,直接存在局部變量表中,當我們執行加法操作時,JVM會先把b這個變量所代表的數(有可能是1、2、3等)壓入操作數棧,然後再把1壓入操作數棧,然後一個個出棧進行加法操作,再把結果入棧,這就完成了一個加法操作,看下面的實例:
public class test6 {
       public static void main(String[] args) {
           int a=1;
           a = a+1;
       }
   }

很簡單一個例子,要分析就要通過字節碼進行分析,字節碼如下:

public static void main(java.lang.String[]);
       descriptor: ([Ljava/lang/String;)V
       flags: ACC_PUBLIC, ACC_STATIC
       Code:
         stack=2, locals=2, args_size=1
            0: iconst_1
            1: istore_1
            2: iload_1
            3: iconst_1
            4: iadd
            5: istore_1
            6: return

省略了部分,我們來分析這個字節碼的內容

第一行代表這個字節碼是main方法的字節碼

第二行是這個方法的描述信息,說明這個方法的參數是String類型的一維數組,其返回值是void

第三行表示控制修飾符:說明這個方法的控制描述符(AccessFlag)是public、static

第五行表示,操作數棧深度爲2,局部變量表數爲2,參數大小是1

剩下的就是這個main方法的具體實現過程

  • 0:iconst_1:把int類型的常量1壓入到操作數棧中,對應着是代碼中的 int a=1;
  • 1:istore_1:把int類型的值從操作數棧中彈出,將其放到位置爲1的局部變量中;
  • 2: iload_1:將位置爲1的int類型的局部變量壓入棧;
  • 3: iconst_1:把int類型的常量1壓入到操作數棧中,對應着是代碼中的 a+1的1;
  • 4:iadd: 從操作數棧棧頂彈出兩個元素然後做加法,把結果壓入棧。對應着代碼中的a=a+1;
  • 5:istore_1:把int類型的值從操作數棧中彈出,將其放到位置爲1的局部變量中;
  • 6:return表示結束。

很容易看出:一個簡單的Java程序,其原理無非就是數據再內存中入棧出棧並進行計算的過程,其常量在編譯期均已經確定並存於局部變量表中,因爲字節碼是根據class文件反編譯而成,

  1. 基本類型的內存模型是什麼
    在這裏插入圖片描述
    方法中的基本類型如int、float等類型是直接存在局部變量表中的,類中的基本類型數據是存在堆中的。

參考資料
java中的基本數據類型和引用類型在JVM中存儲在哪?
包裝類和基本類型
操作數詳解一
操作數詳解二
自動拆箱裝箱
Java基本類型詳解
基本類型和包裝類型的區別

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