包裝類型和基本類型
-
Java中有八種基本數據類型,分別對應着八大包裝類型,因爲包裝類型的實例都存在於堆中,所以包裝類型也稱爲引用類型。
-
基本類型屬於原始數據類型,變量中存儲的就是原始值。包裝類型屬於引用數據類型,變量中存儲的是存儲原始值的地址的引用。
- 基本類型中,局部變量存在方法虛擬機棧的局部變量表中,而類中聲明的的變量存在堆裏。
- 包裝類型中,無論局部變量還是類中聲明的變量均存在堆中,而方法內的局部包裝類型變量,其也存在局部變量表中,不過期值爲該變量在堆中的地址。
-
手動裝箱 / 手動拆箱
Integer b = valueOf(1);
int a = b.intValue();
- 自動裝箱 / 自動拆箱
Integer b = 1;
int a = b;
Java SE5 爲了減少開發人員的工作,提供了自動裝箱與自動拆箱的功能,3等價4。
5. == and equals
:
- == 比較的是內存地址
- equals 比較的是值
- 代碼實例
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;
- 裝箱拆箱詳解
- 裝箱:裝箱是通過 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會先判斷
在局部變量表中是否有這個值,如果有,直接放回在局部變量表中的引用,如果沒有則新建再返回。
- 包裝類型代碼詳解:
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的引用也指向它,如此一比較,他們的地址當然相等。
- 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是不一樣的。
幾個問題:
- 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文件反編譯而成,
- 基本類型的內存模型是什麼
方法中的基本類型如int、float等類型是直接存在局部變量表中的,類中的基本類型數據是存在堆中的。
參考資料
java中的基本數據類型和引用類型在JVM中存儲在哪?
包裝類和基本類型
操作數詳解一
操作數詳解二
自動拆箱裝箱
Java基本類型詳解
基本類型和包裝類型的區別