1.包裝類
1.1 基本資料型別對應的包裝類都有哪些
| 基本資料型別 | 包裝類 |
|---|---|
| boolean | Boolean |
| char | Character |
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
1.2自動裝箱 AutoBoxing,自動拆箱 AutoUnboxing
1.2.1 什么是裝箱、拆箱
? 我們以 Integer 為例,
? 首先,什么是裝箱、拆箱,又何來自動一說?讓我們通過下面的例子開始了解,
//裝箱
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
--------------------------
//拆箱
String s = new String("a");
System.out.println(s == "a"); //output:false
Integer i1 = new Integer(200);
double i2 = 200.0;
System.out.println(i1 == i2); //output:true
? 1.裝箱:我們創建了一個泛型為 Integer 的 ArrayList,說明在這個集合中只能存盤 Integer 型別的物件,但我們卻可以通過 list.add(1) 的方式向集合中插入資料,那么我們可以揣測,int 型別的變數一定經歷了某個程序,自動轉換成 Integer 型別的物件后再存入集合,不然為什么不會報錯,
? 2.拆箱:了解過 ‘= =’ 的同學知道,’= =’ 比較的永遠是 ‘值’,當比較物件時,比較的是物件中存盤的 ‘值’ (即記憶體地址);當比較變數時,比較的是變數的值,那么上面的代碼中,我們首先讓 String 物件與 字串常量 進行比較,結果如我們預想的一樣,地址與值相比較,結果不可能為 true,接下來我們讓 Integer 物件與 double 變數相比較,輸出卻是 true ???,那么我們可以根據這個結果進行揣測,不是 Integer 物件自動轉換為變數,就是 double 變數自動轉換為物件,
? 究竟事實是什么呢,我們可以通過編譯、反編譯以上代碼進行了解,
? 編譯為位元組碼檔案,并通過 IDEA 打開:
ArrayList var1 = new ArrayList();
var1.add(1);
String var2 = new String("a");
System.out.println(var2 == "a");
Integer var3 = new Integer(200);
double var4 = 200.0D;
System.out.println((double)var3 == var4); //強轉為 double
? 以下是反編譯結果:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V,創建了 ArrayList 實體
7: astore_1
8: aload_1
9: iconst_1 // 準備好常量 1
10: invokestatic #4 // Method java/lang/Integer.valueOf(I)Ljava/lang/Integer;,
//呼叫了 Integer 的靜態方法 valueOf !!!
13: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)
Z
16: pop
17: new #6 // class java/lang/String
20: dup
21: ldc #7 // String a
23: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String
;)V
26: astore_2
27: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_2
31: ldc #7 // String a
33: if_acmpne 40
36: iconst_1
37: goto 41
40: iconst_0
41: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
44: new #11 // class java/lang/Integer
47: dup
48: sipush 200
51: invokespecial #12 // Method java/lang/Integer."<init>":(I)V
54: astore_3
55: ldc2_w #13 // double 200.0d
58: dstore 4
60: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
63: aload_3
64: invokevirtual #15 // Method java/lang/Integer.intValue:()I,
//呼叫了 Integer 的實體方法 intValue !!!
67: i2d
68: dload 4
70: dcmpl
71: ifne 78
74: iconst_1
75: goto 79
78: iconst_0
79: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
82: return
}
在反編譯結果中序號為 10、36對應行,我們可以看到,Integer "偷偷"呼叫了兩個方法 1. valueOf 2. intValue,接下來讓我們進入 Integer 原始碼查看,這兩個方法到底做了什么,
1.valueOf(int i)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; //快取池
return new Integer(i);
}
通過原始碼我們可以看到,該方法傳入了 int 型別變數,判斷當前變數是否存在于快取池中(先不用了解,本文 1.3 快取池中有詳細講解),然后通過 Integer 的有參構造,創建一個 Integer 物件并回傳,
2.intValue()
public int intValue() {
return value;
}
通過原始碼我們可以看到,該方法將 Integer 物件中的成員變數 value 回傳,
至此,我們明白了,Integer 會通過我們看不見的方式,通過呼叫 valueOf(int i) 自動對變數進行裝箱;通過呼叫 intValue() 自動將物件拆箱為變數,這就是自動裝箱以及自動拆箱的原理,
1.2.2 如何觸發自動裝箱、拆箱
- ? 裝箱:
- 當插入集合當中時,基本資料型別自動裝箱為包裝類物件,例如 list.add(1)
- 以 Integer i = 2;形式初始化時,會自動裝箱,
- 使用 equals 方法比較 Integer 物件與基本資料型別變數時,變數會自動裝箱為包裝類,
- ? 拆箱:
- 當包裝類物件進行 +,-,*,/ 等運算時,會拆箱為基本資料型別
- 當包裝類與基本資料型別進行 == 比較時
1.2.3 自動拆箱的注意事項
我們都知道,自動拆箱是呼叫 Integer 物件的 intValue() 方法,那么如果有以下這種情況:
Integer unbox = null;
int test = unbox;
以上代碼是可以編譯通過的,但因為物件為 null ,所以運行會拋出 NullPointerException,如果沒有對例外進行捕捉,則會導致程式運行終止,所以在使用自動拆箱時要多加注意,不要讓參考指向 null,
1.3 包裝類快取池
1.3.1 快取池詳解
在了解快取池前,先了解一下享元模式,享元模式是 Java 23 種設計模式中最簡單的一個,池化思想就是基于這個設計模式的,享元模式的動機是:避免系統中存在大量相同或相似的物件,在 Java 中,符合這種 “存在大量相同或相似” 的類有很多,比如:1. Connection(資料庫連接) 2. 包裝類 3. String 4. Thread 等等 ,我們經常需要使用這些類的實體,如果頻繁創建及銷毀,會大大降低效率,通過享元模式的核心:享元池,可以避免這種情況,享元池的原理是,定義一個快取區域(即享元池)提前創建并存盤享元物件,需要使用享元物件前,先去快取中尋找物件是否已存在,如果存在則直接將該物件回傳;如果不存在,則在享元池中添加新的享元物件,以供下次使用,
基于享元模式提出的享元池有很多,比如資料庫連接池,執行緒池,包裝類快取池,常量池等等,
下面我們來了解包裝類快取池,以 Integer 為例,
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
上面我們講到 valueOf 方法時,當傳入變數 i 時,需要在快取中先判斷 i 對應的值是否在快取范圍內,如果存在則回傳快取池中的物件,如果不存在則新創建一個 Integer 物件回傳,(這里并沒有將新創建的 Integer 物件存入快取池,原因如下)
字面意思就可以看出來,IntegerCache 就是 Integer 的快取類,下面是原始碼:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static{
...
}
}
IntegerCache 被定義為私有靜態內部類,外部不能訪問(可以暴力反射),IntegerCache 中定義了三個靜態成員,并且都被 static、final 修飾,
首先決議這里 static 關鍵字的含義:當 Integer 類被加載時,類中的靜態成員就已經被確定,有且只有一個,
其次決議這里 final 關鍵字的含義:被 final 修飾的變數只會被初始化一次,當 final 修飾的是成員變數,分為兩種情況:
-
被 staitc 關鍵字修飾的成員變數,需要在類加載時就完成初始化,(宣告時初始化或者在靜態陳述句塊中完成初始化)
(類的加載詳見 https://blog.csdn.net/qq_44707077/article/details/115287769)
-
沒有被 static 關鍵字修飾的成員變數,需要在建構式執行完畢前,完成初始化,(宣告時初始化或者在建構式中完成初始化)
了解了 static及final 的含義后我們知道,此處 high、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;
//創建 Integer 快取池!!!
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;
}
在靜態代碼塊中,大部分代碼是對于 high 值的處理:當 integerCacheHighPropValue 為空時,high 值默認設定為 128;但當 integerCacheHighPropValue 不為空時,high 值會被修改,
那么如何設定 integerCacheHighPropValue 并手動修改 high 值呢?我們可以在 JVM 虛擬機初始化時,傳入啟動引數,根據需求修改快取范圍的最大值,以下為示例代碼:
public class IntegerTest {
//只有 Integer 的快取 max 可修改
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);
Integer i5 = 200;
Integer i6 = 200;
System.out.println(i5 == i6);
Integer i7 = 20000;
Integer i8 = 20000;
System.out.println(i7 == i8);
}
//output 正常運行,不傳引數
true
false
false
//通過命令列運行程式,并傳入引數 AutoBoxCacheMax
cmd 指令分為兩步,1.編譯 IntegerTest.java 為位元組碼檔案 IntegerTest.class 2.通過指令運行位元組碼檔案
javac IntegerTest.java
java -server -XX:AutoBoxCacheMax=200 IntegerTest
//output
true
true
false
//output 通過命令列運行程式,并傳入引數 +AggressiveOpts(設定快取范圍最大值為20000)
javac IntegerTest.java
java -server -XX:+AggressiveOpts IntegerTest
//output
true
true
true
1.3.2 包裝類快取池范圍總結
| 包裝類 | 緩池存范圍 |
|---|---|
| Boolean | true / false |
| Character | 0 ~ 127 |
| Byte | 資料范圍內所有值 |
| Short | -128 ~ 127 |
| Integer | -128 ~ 127(最大值可修改) |
| Long | -128 ~ 127 |
| Float | 沒有快取機制 |
| Double | 沒有快取機制 |
1.4 練習
public static void main(String[] args) {
Integer a = new Integer(200);
int b = 200;
Integer c = 200;
Integer d = 200;
Integer e = 100;
Integer f = 100;
Integer g = new Integer(127);
Integer h = 127;
System.out.println(a.equals(b));
System.out.println(a == b);
System.out.println(e == f);
System.out.println(c == d);
System.out.println(g == h);
}
//output
true
true
true
false
false
1.equals 比較的是兩個物件,當比較物件與基本資料型別型別變數時,變數會自動裝箱為包裝類;并且 Integer 重寫了 equals 方法如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
兩個 Integer 物件如果值相同,那么通過 equals 方法判斷,結果為 true,
2.答錯的同學,回看一下 本文1.2.2,此時 a == b,觸發了 a 自動拆箱,那么兩個 int 型別相比較,200 = 200,顯而易見,true,
3.兩個 Integer 物件相比較,不會觸發自動拆箱,所以比較的是兩個物件存盤的記憶體地址,判斷 e,f是否指向同一個記憶體地址,100 在快取池范圍內,所以兩個物件指向快取池中同一個位置,兩個物件是同一個,true
4.兩個 Integer 物件相比較,不會觸發自動拆箱,所以比較的是兩個物件存盤的記憶體地址,判斷 a,c是否指向同一個記憶體地址,false
5.g 以有參構造的形式創建物件,這樣做會導致 JVM 在堆中開辟一個空間,并且 g 指向堆中對應的地址,此時 g 中存盤的是堆中的記憶體地址,h 通過自動裝箱的方式創建物件,指向快取池中的記憶體地址,結果顯而易見,g,h 中存盤的記憶體地址不同,結果為 false,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/279580.html
標籤:java
下一篇:深入淺出理解類和物件
