任何一個(gè)學(xué)過 Java 的人茸歧,肯定知道 int 是原始數(shù)據(jù)類型,Integer 是一個(gè)對(duì)象显沈,他們之間可以自動(dòng)地拆箱裝箱举娩。但,如果繼續(xù)挖掘构罗,仍然大有分析地余地铜涉。你,真的懂 int 和 Integer 了嗎遂唧?
關(guān)于自動(dòng)拆箱和裝箱
我們都知道 int 和 Integer 可以自動(dòng)相互轉(zhuǎn)換芙代,這是 Java 給我們提供的一種語法糖,但是盖彭,它是“怎么”轉(zhuǎn)換的纹烹?在什么時(shí)候轉(zhuǎn)換?是編譯期還是運(yùn)行期召边?
眼見為實(shí)铺呵,讓我們編寫一段代碼來實(shí)際看看過程
public class TestClass {
public void inc () {
Integer integer = 1;
int unboxing = integer ++;
}
}
這是一段普通的代碼,但它完整包含了三個(gè)動(dòng)作:將一個(gè)原始數(shù)據(jù)類型轉(zhuǎn)換成包裝類型隧熙、將一個(gè)包裝類型轉(zhuǎn)換成一個(gè)原始數(shù)據(jù)類型片挂、對(duì)一個(gè)包裝類型進(jìn)行直接的運(yùn)算。
接下來通過 javap -verbose
指令解析編譯后的 class 文件,以下是在我的電腦上的 JDK 1.8 版本解析的結(jié)果音念,看看編譯過程發(fā)生了什么事
public void inc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: aload_1
6: astore_3
7: aload_1
8: invokevirtual #3 // Method java/lang/Integer.intValue:()I
11: iconst_1
12: iadd
13: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
16: dup
17: astore_1
18: astore 4
20: aload_3
21: invokevirtual #3 // Method java/lang/Integer.intValue:()I
24: istore_2
25: return
這是節(jié)選自 inc() 方法的一段字節(jié)碼沪饺,查看它的操作指令及注釋,可以得出結(jié)論
- 自動(dòng)拆箱裝箱是在編譯期完成的
- javac 將裝箱操作用
Integer.valueOf()
代替了闷愤,將拆箱用Integer.intValue()
代替了 - 包裝對(duì)象的運(yùn)算整葡,要先拆箱成原始數(shù)據(jù)類型,進(jìn)行運(yùn)算完畢后再裝箱
通過對(duì)這個(gè)反編譯例子的研究讥脐,我們就明白遭居,在以后編程中,需要進(jìn)行大量計(jì)算的地方旬渠,應(yīng)該使用原始數(shù)據(jù)類型魏滚,在性能敏感的場(chǎng)合,優(yōu)先使用原始數(shù)據(jù)類型坟漱。
關(guān)于 Integer 源碼
整體看一下 Integer 類的源碼鼠次,可以發(fā)現(xiàn)這個(gè)類主要是由幾個(gè)常量,兩個(gè)裝箱拆箱方法芋齿,一些進(jìn)制轉(zhuǎn)換方法腥寇,一些位操作方法組成。
其中有幾個(gè)比較有意思的點(diǎn)觅捆,
- 類和常量都被 final 修飾了起來赦役。
很容易由此聯(lián)想到這是一個(gè)不可變類 (immutable),由此想到不可變類的設(shè)計(jì)原則:
- 類標(biāo)識(shí)打上 final 標(biāo)志
- 類變量使用 final 修飾
- 提供構(gòu)造或者工廠方法設(shè)置類變量的初始值栅炒,不提供 set 方法
- 構(gòu)造時(shí)掂摔,對(duì)引用類型使用深拷貝,避免外部不確定因素的影響
- 將集合改為不可變集合赢赊,比如使用 Java 9
List.of()
方法
- 特別的常量
Integer 類除了提供最大乙漓、最小的常數(shù)外,還提供了位數(shù)和字節(jié)數(shù)的兩個(gè)常量释移。后面這兩個(gè)常量叭披,值得提一下。
如果我們寫過 c 或者 c++ 語言玩讳,就會(huì)知道涩蜘,在這兩種語言中, int 類型在 32 位和 64 位系統(tǒng)中是不確定的熏纯。而在 Java 中同诫,我們無需擔(dān)憂這種不同,因?yàn)樵?Java 語言規(guī)范中明確規(guī)定了各種基本類型的長(zhǎng)度樟澜。
https://docs.oracle.com/javase/specs/jls/se10/html/jls-4.html#jls-4.2
這也是 Java 實(shí)現(xiàn)它的承諾——一次書寫误窖,到處運(yùn)行叮盘,的一個(gè)細(xì)節(jié)。
- 關(guān)于緩存
我們都知道 Java 自動(dòng)緩存了一個(gè)小范圍的整數(shù)值贩猎。并且都在無數(shù)的地方中看到了別人對(duì)這個(gè)范圍的描述—— -128~127熊户。但萍膛,如果讀過了 Integer 的源碼吭服,你就會(huì)發(fā)現(xiàn)這是錯(cuò)誤的說法。
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;
}
在生成緩存的這段代碼中蝗罗,我們注意到了一個(gè)細(xì)節(jié)艇棕,范圍的下限確實(shí)是 -128,但上限是可以調(diào)節(jié)的串塑。
如果我們的程序需要更大的緩存范圍沼琉,我們可以通過在啟動(dòng) JVM 時(shí)增加參數(shù)-XX:AutoBoxCacheMax=N
,來設(shè)置這個(gè)緩存范圍的上限桩匪。
對(duì)于緩存打瘪,還可以同樣觀察下其他基本類型的包裝類,都同樣做了一個(gè)小范圍的緩存傻昙。
關(guān)于占用空間
如果我們要?jiǎng)?chuàng)建 10 萬個(gè)整數(shù)闺骚,那么光是對(duì)象頭的占用空間,Integer 就要比 int 多出一個(gè)數(shù)量級(jí)妆档。這是對(duì)象機(jī)制不可避免帶來的問題僻爽,我們?cè)诰帉懖僮鞔罅繑?shù)據(jù)的代碼時(shí),也應(yīng)該考慮占用空間的問題贾惦。
關(guān)于線程安全
我們都知道胸梆,不可變類是實(shí)現(xiàn)線程安全共享對(duì)象的一種方式。但是原始元素類型的運(yùn)算就不是線程安全的须板。如果多個(gè)線程同時(shí)對(duì)一個(gè) int 對(duì)象做運(yùn)算碰镜,就可能引發(fā)并發(fā)問題。
如果有線程安全計(jì)算的需要习瑰,可以使用 AtomicInteger 這樣的線程安全類進(jìn)行計(jì)算洋措。
但,如果不想涉及到類杰刽,想直接在原始數(shù)據(jù)類型上做并發(fā)操作菠发,也是有辦法的。Java 提供了 AtomicIntegerFieldUpdater 這樣的類進(jìn)行 cas 安全操作贺嫂。下面是使用原始數(shù)據(jù)類型實(shí)現(xiàn)一個(gè)計(jì)數(shù)器的方式滓鸠。
class CompactCounter {
private volatile long counter;
private static final AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");
public void increase() {
updater.incrementAndGet(this);
}
}
關(guān)于局限性
Java 走過了這么多年的歷程,這種類型系統(tǒng)的設(shè)計(jì)已經(jīng)是很久前的了第喳,現(xiàn)在也逐漸暴露了一些缺點(diǎn)糜俗。
- 原始數(shù)據(jù)類型不能和泛型完美配合。
Java 的泛型機(jī)制,是一種偽泛型悠抹,它在編譯期將類型轉(zhuǎn)換為特定的類型珠月。這就要求相應(yīng)類型可以轉(zhuǎn)換為 Object。
- 無法高效地表達(dá)數(shù)據(jù)
如果我們寫過 python楔敌、scala啤挎。就會(huì)知道,這些語言中有 vector卵凑、tuple庆聘。極大地方便了我們編程,簡(jiǎn)潔了代碼勺卢。
但是由于 Java 語言的實(shí)現(xiàn)機(jī)制伙判,在對(duì)象數(shù)組中存放的是對(duì)象的引用,實(shí)際對(duì)象分散在堆內(nèi)存的各個(gè)地方黑忱。這種方式雖然帶來了極大的靈活性宴抚,但是卻帶來了數(shù)據(jù)操作的低效。