深入理解Java包裝類:自動裝箱拆箱與緩存池機制
對象包裝器
Java中的數據類型可以分為兩類:基本類型和引用類型。作為一門面向對象編程語言, 一切皆對象是Java語言的設計理念之一。但基本類型不是對象,無法直接參與面向對象操作,為了解決這個問題,Java讓每個基本類型都有一個與之對應的包裝器類型。
Java中有8種不可變的基本類型,分別為:
- 整型:
byte
、short
、int
、long
- 浮點類型:
float
、double
- 字符類型:
char
- 布爾類型:
boolean
類型 | 大小 | 默認值 | 示例 |
---|---|---|---|
byte | 1字節 | 0 | byte b = 10 |
short | 2字節 | 0 | short s = 200 |
int | 4字節 | 0 | int i = 1000 |
long | 8字節 | 0L | long l = 5000L |
float | 4字節 | 0.0f | float f = 3.14f |
double | 8字節 | 0.0d | double d = 2.718 |
char | 2字節 | ‘\u0000’ | char c = 'A' |
boolean | 未明確定義 | false | boolean flag = true |
這八種基本類型都有對應的包裝類分別為:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
(前6個派生于公共的超類Number
)。包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。同時,包裝器類還是final
,因此不能派生它們的子類。
Java不是“一切皆對象”嗎,為什么還要保留基本數據類型?
包裝類是引用類型,對象的引用存儲在棧中,對象本身存儲在堆中;而對于基本數據類型,變量對應的內存塊直接存儲數據本身(棧中)。因此,基本數據類型讀寫效率更高效。在64位JVM上,在開啟引用壓縮的情況下,一個Integer對象占用16個字節的內存空間,而一個int類型數據只占用4字節的內存空間,前者對空間的占用是后者的4倍。也就是說,不管是讀寫效率還是存儲效率,基本類型都更高效。盡管Java強調面向對象,但為了性能做了妥協。
自動裝箱與拆箱
裝箱和拆箱是實現基本數據類型與包裝類之間相互轉換的特性。Java 5引入自動裝箱/拆箱功能,進一步簡化了包裝類的使用。
- 裝箱:將基本數據類型轉化為對應的包裝類對象。
- 拆箱:將包裝類對象轉化為對應的基本數據類型值。
示例:
Integer a = 100; // 自動裝箱 -> Integer.valueOf(100)int b = a; // 自動拆箱 -> a.intValue()// 自動裝箱和拆箱也適用于算術表達式
Integer n = 3;
n++; // 編譯器將自動插入一條對象拆箱的指令,然后進行自增運算,最后再將結果裝箱
裝箱其實就是調用了包裝類的valueOf()
方法,拆箱其實就是調用了 xxxValue()
方法。
API
java.lang.Integer
int intValue()
將這個
Integer
對象的值作為一個int
返回(覆蓋Number
類中的intValue
方法)。
static Integer valueOf(String s)
返回一個新的
Integer
對象,用字符串s
表示的整數初始化。指定字符串必須表示一個十進制整數。
關于自動裝箱還有幾點需要注意:
高頻裝箱拆箱(如循環)會產生大量臨時對象,消耗內存和GC資源:
// 錯誤示例:每次循環觸發裝箱 Long sum = 0L; for (long i = 0; i < 1e6; i++) {sum += i; // sum = Long.valueOf(sum.longValue() + i) }// 正確優化:使用基本類型 long sum = 0L; for (long i = 0; i < 1e6; i++) {sum += i; }
由于包裝器類引用可以為
null
,所以自動裝箱有可能會拋出一個NullPointerException
異常:Integer n = null; System.out.println(2 * n) // throws NullPointerException
如果在一個表達式中混合使用
Integer
和Double
類型,Integer
值就會拆箱,提升為double
,再裝箱為Double
:Integer n = 1; Double x = 2.0; System.out.println(true ? n : x); // 1.0
裝箱和拆箱是編譯器要做的工作,而不是虛擬機。編譯器在生成類的字節碼時會插入必要的方法調用。虛擬機只是執行這些字節碼。
緩存池機制
緩存池是 Java 為優化包裝類對象創建和內存消耗而設計的核心機制,通過預創建和復用常用數值的包裝類對象,減少重復對象創建的開銷。Java 基本數據類型的包裝類型的大部分都用到了緩存機制來提升性能。
Byte
,Short
,Integer
,Long
這 4 種包裝類默認創建了數值 [-128,127] 的相應類型的緩存數據,Character
創建了數值在 [0,127] 范圍的緩存數據,Boolean
直接返回 TRUE
or FALSE
。
示例:
Integer a = 100; // Integer.valueOf(100)
Integer b = 100; // Integer.valueOf(100)
Integer c = 200; // Integer.valueOf(200)
Integer d = 200; // Integer.valueOf(200)System.out.println(a == b); // true
System.out.println(c == d); // false
System.out.println(c.equals(d)); //true
Integer.valueOf()
的緩存邏輯:
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i); // 超出緩存范圍時創建新對象
}
緩存池機制:Java對
-128 ~ 127
范圍內的Integer
對象預先生成并緩存,a
和b
指向同一個緩存對象,a == b
比較對象地址,返回true
。200
超出默認緩存范圍(-128 ~ 127
),Integer.valueOf(200)
每次會創建新對象,c
和d
指向不同對象,c == d
比較對象地址,返回false
。
對于 Integer
,可以通過 JVM 參數 -XX:AutoBoxCacheMax=<size>
修改緩存上限,但不能修改下限 -128。實際使用時,并不建議設置過大的值,避免浪費內存,甚至是 OOM(全稱Out Of Memory, 即內存溢出)。
- 內存溢出:申請的內存超出了JVM能提供的內存大小,此時稱之為溢出。
- 內存泄漏:申請使用完的內存沒有釋放,導致虛擬機不能再次使用該內存,此時這段內存就泄露了,因為申請者不用了,而又不能被虛擬機分配給別人用。
對于Byte
,Short
,Long
,Character
沒有類似 -XX:AutoBoxCacheMax
參數可以修改,因此緩存范圍是固定的,無法通過 JVM 參數調整。Boolean
則直接返回預定義的 TRUE
和 FALSE
實例,沒有緩存范圍的概念。
Character
的緩存邏輯:
public static Character valueOf(char c) {if (c <= 127) { // must cachereturn CharacterCache.cache[(int)c];}return new Character(c);
}
Boolean
的緩存邏輯:
public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE);
}
兩種浮點數類型的包裝類 Float
,Double
并沒有實現緩存機制:
Float a = 3f;
Float b = 3f;
System.out.println(a == b);// 輸出 falseDouble c = 1.2;
Double d = 1.2;
System.out.println(c == d);// 輸出 false
下面這段代碼的輸出結果是什么?
Integer a = 40;
Integer b = new Integer(40);
System.out.println(a == b);
Integer a = 40
自動裝箱等價于Integer a = Integer.valueOf(40)
,40
在默認緩存范圍內,所以a
直接使用的是緩存中的對象,而Integer b = new Integer(40)
會直接創建新的對象。因此,答案是false
。