Java 雖然號稱是面向對象的語言,但是原始數據類型仍然是重要的組成元素,所以在面試中,經常考察原始數據類型和包裝類等 Java 語言特性。
今天我要問你的問題是,int 和 Integer 有什么區別?談談 Integer 的值緩存范圍。
## 典型回答
int 是我們常說的整形數字,是 Java 的 8 個原始數據類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數據類型是例外。
Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數據,并且提供了基本操作,比如數學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據上下文,自動進行轉換,極大地簡化了相關編程。
關于 Integer 的值緩存,這涉及 Java 5 中另一個改進。構建 Integer 對象的傳統方式是直接調用構造器,直接 new 一個對象。但是根據實踐,我們發現大部分數據操作都是集中在有限的、較小的數值范圍,因而,在 Java 5 中新增了靜態工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。
## 考點分析
今天這個問題涵蓋了 Java 里的兩個基礎要素:原始數據類型、包裝類。談到這里,就可以非常自然地擴展到自動裝箱、自動拆箱機制,進而考察封裝類的一些設計和實踐。坦白說,理解基本原理和用法已經足夠日常工作需求了,但是要落實到具體場景,還是有很多問題需要仔細思考才能確定。
面試官可以結合其他方面,來考察面試者的掌握程度和思考邏輯,比如:
* 我在專欄第 1 講中介紹的 Java 使用的不同階段:編譯階段、運行時,自動裝箱 / 自動拆箱是發生在什么階段?
* 我在前面提到使用靜態工廠方法 valueOf 會使用到緩存機制,那么自動裝箱的時候,緩存機制起作用嗎?
* 為什么我們需要原始數據類型,Java 的對象似乎也很高效,應用中具體會產生哪些差異?
* 閱讀過 Integer 源碼嗎?分析下類或某些方法的設計要點。
似乎有太多內容可以探討,我們一起來分析一下。
## 知識擴展
1. 理解自動裝箱、拆箱
自動裝箱實際上算是一種語法糖。什么是語法糖?可以簡單理解為 Java 平臺為我們自動進行了一些轉換,保證不同的寫法在運行時等價,它們發生在編譯階段,也就是生成的字節碼是一致的。
像前面提到的整數,javac 替我們自動把裝箱轉換為 Integer.valueOf(),把拆箱替換為 Integer.intValue(),這似乎這也順道回答了另一個問題,既然調用的是 Integer.valueOf,自然能夠得到緩存的好處啊。
如何程序化的驗證上面的結論呢?
你可以寫一段簡單的程序包含下面兩句代碼,然后反編譯一下。當然,這是一種從表現倒推的方法,大多數情況下,我們還是直接參考規范文檔會更加可靠,畢竟軟件承諾的是遵循規范,而不是保持當前行為。
```
Integer integer = 1;
int unboxing = integer ++;
```
反編譯輸出:
```
1: invokestatic #2 // Method
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: invokevirtual #3 // Method
java/lang/Integer.intValue:()I
```
這種緩存機制并不是只有 Integer 才有,同樣存在于其他的一些包裝類,比如:
* Boolean,緩存了 true/false 對應實例,確切說,只會返回兩個常量實例 Boolean.TRUE/FALSE。
* Short,同樣是緩存了 -128 到 127 之間的數值。
* Byte,數值有限,所以全部都被緩存。
* Character,緩存范圍’\u0000’ 到 ‘\u007F’。
自動裝箱 / 自動拆箱似乎很酷,在編程實踐中,有什么需要注意的嗎?
原則上,建議避免無意中的裝箱、拆箱行為,尤其是在性能敏感的場合,創建 10 萬個 Java 對象和 10 萬個整數的開銷可不是一個數量級的,不管是內存使用還是處理速度,光是對象頭的空間占用就已經是數量級的差距了。
我們其實可以把這個觀點擴展開,使用原始數據類型、數組甚至本地代碼實現等,在性能極度敏感的場景往往具有比較大的優勢,用其替換掉包裝類、動態數組(如 ArrayList)等可以作為性能優化的備選項。一些追求極致性能的產品或者類庫,會極力避免創建過多對象。當然,在大多數產品代碼里,并沒有必要這么做,還是以開發效率優先。以我們經常會使用到的計數器實現為例,下面是一個常見的線程安全計數器實現。
```
class Counter {
private final AtomicLong counter = new AtomicLong();
public void increase() {
counter.incrementAndGet();
}
}
```
如果利用原始數據類型,可以將其修改為
```
class CompactCounter {
private volatile long counter;
private static final AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");
public void increase() {
updater.incrementAndGet(this);
}
}
```
2. 源碼分析
考察是否閱讀過、是否理解 JDK 源代碼可能是部分面試官的關注點,這并不完全是一種苛刻要求,閱讀并實踐高質量代碼也是程序員成長的必經之路,下面我來分析下 Integer 的源碼。
整體看一下 Integer 的職責,它主要包括各種基礎的常量,比如最大值、最小值、位數等;前面提到的各種靜態工廠方法 valueOf();獲取環境變量數值的方法;各種轉換方法,比如轉換為不同進制的字符串,如 8 進制,或者反過來的解析方法等。我們進一步來看一些有意思的地方。
首先,繼續深挖緩存,Integer 的緩存范圍雖然默認是 -128 到 127,但是在特別的應用場景,比如我們明確知道應用會頻繁使用更大的數值,這時候應該怎么辦呢?
緩存上限值實際是可以根據需要調整的,JVM 提供了參數設置:
```
-XX:AutoBoxCacheMax=N
```
這些實現,都體現在java.lang.Integer源碼之中,并實現在 IntegerCache 的靜態初始化塊里。
```
private static class IntegerCache {
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 = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
...
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
...
}
```
第二,我們在分析字符串的設計實現時,提到過字符串是不可變的,保證了基本的信息安全和并發編程中的線程安全。如果你去看包裝類里存儲數值的成員變量“value”,你會發現,不管是 Integer 還 Boolean 等,都被聲明為“private final”,所以,它們同樣是不可變類型!
這種設計是可以理解的,或者說是必須的選擇。想象一下這個應用場景,比如 Integer 提供了 getInteger() 方法,用于方便地讀取系統屬性,我們可以用屬性來設置服務器某個服務的端口,如果我可以輕易地把獲取到的 Integer 對象改變為其他數值,這會帶來產品可靠性方面的嚴重問題。
第三,Integer 等包裝類,定義了類似 SIZE 或者 BYTES 這樣的常量,這反映了什么樣的設計考慮呢?如果你使用過其他語言,比如 C、C++,類似整數的位數,其實是不確定的,可能在不同的平臺,比如 32 位或者 64 位平臺,存在非常大的不同。那么,在 32 位 JDK 或者 64 位 JDK 里,數據位數會有不同嗎?或者說,這個問題可以擴展為,我使用 32 位 JDK 開發編譯的程序,運行在 64 位 JDK 上,需要做什么特別的移植工作嗎?
其實,這種移植對于 Java 來說相對要簡單些,因為原始數據類型是不存在差異的,這些明確定義在[Java 語言規范](https://docs.oracle.com/javase/specs/jls/se10/html/jls-4.html#jls-4.2)里面,不管是 32 位還是 64 位環境,開發者無需擔心數據的位數差異。
對于應用移植,雖然存在一些底層實現的差異,比如 64 位 HotSpot JVM 里的對象要比 32 位 HotSpot JVM 大(具體區別取決于不同 JVM 實現的選擇),但是總體來說,并沒有行為差異,應用移植還是可以做到宣稱的“一次書寫,到處執行”,應用開發者更多需要考慮的是容量、能力等方面的差異。
3. 原始類型線程安全
前面提到了線程安全設計,你有沒有想過,原始數據類型操作是不是線程安全的呢?
這里可能存在著不同層面的問題:
* 原始數據類型的變量,顯然要使用并發相關手段,才能保證線程安全,這些我會在專欄后面的并發主題詳細介紹。如果有線程安全的計算需要,建議考慮使用類似 AtomicInteger、AtomicLong 這樣的線程安全類。
* 特別的是,部分比較寬的數據類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現程序讀取到只更新了一半數據位的數值!
4.Java 原始數據類型和引用類型局限性
前面我談了非常多的技術細節,最后再從 Java 平臺發展的角度來看看,原始數據類型、對象的局限性和演進。
對于 Java 應用開發者,設計復雜而靈活的類型系統似乎已經習以為常了。但是坦白說,畢竟這種類型系統的設計是源于很多年前的技術決定,現在已經逐漸暴露出了一些副作用,例如:
* 原始數據類型和 Java 泛型并不能配合使用
這是因為 Java 的泛型某種程度上可以算作偽泛型,它完全是一種編譯期的技巧,Java 編譯期會自動將類型轉換為對應的特定類型,這就決定了使用泛型,必須保證相應類型可以轉換為 Object。
* 無法高效地表達數據,也不便于表達復雜的數據結構,比如 vector 和 tuple
我們知道 Java 的對象都是引用類型,如果是一個原始數據類型數組,它在內存里是一段連續的內存,而對象數組則不然,數據存儲的是引用,對象往往是分散地存儲在堆的不同位置。這種設計雖然帶來了極大靈活性,但是也導致了數據操作的低效,尤其是無法充分利用現代 CPU 緩存機制。
Java 為對象內建了各種多態、線程安全等方面的支持,但這不是所有場合的需求,尤其是數據處理重要性日益提高,更加高密度的值類型是非常現實的需求。
針對這些方面的增強,目前正在 OpenJDK 領域緊鑼密鼓地進行開發,有興趣的話你可以關注相關工程:http://openjdk.java.net/projects/valhalla/ 。
今天,我梳理了原始數據類型及其包裝類,從源碼級別分析了緩存機制等設計和實現細節,并且針對構建極致性能的場景,分析了一些可以借鑒的實踐。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?留一道思考題給你,前面提到了從空間角度,Java 對象要比原始數據類型開銷大的多。你知道對象的內存結構是什么樣的嗎?比如,對象頭的結構。如何計算或者獲取某個 Java 對象的大小?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?