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 裡的兩個基礎要素:原始數據類型、包裝類。談到這裡,就可以非常自然地擴展到自動裝箱、自動拆箱機制,進而考察封裝類的一些設計和實踐。坦白說,理解基本原理和用法已經足夠日常工作需求了,但是要落實到具體場景,還是有很多問題需要仔細思考才能確定。

面試官可以結合其他方面,來考察面試者的掌握程度和思考邏輯,比如:

  • 以前介紹的 Java 使用的不同階段:編譯階段、運行時,自動裝箱 / 自動拆箱是發生在什麼階段?
  • 在前面提到使用靜態工廠方法 valueOf 會使用到緩存機制,那麼自動裝箱的時候,緩存機制起作用嗎?
  • 為什麼我們需要原始數據類型,Java 的對象似乎也很高效,應用中具體會產生哪些差異?
  • 閱讀過 Integer 源碼嗎?分析下類或某些方法的設計要點。

知識擴展

1. 理解自動裝箱、拆箱

自動裝箱實際上算是一種語法糖。什麼是語法糖?可以簡單理解為 Java 平臺為我們自動進行了一些轉換,保證不同的寫法在運行時等價,它們發生在編譯階段,也就是生成的字節碼是一致的。

像前面提到的整數,javac 替我們自動把裝箱轉換為 Integer.valueOf(),把拆箱替換為 Integer.intValue(),這似乎這也順道回答了另一個問題,既然調用的是 Integer.valueOf,自然能夠得到緩存的好處啊。

如何程序化的驗證上面的結論呢?

你可以寫一段簡單的程序包含下面兩句代碼,然後反編譯一下。當然,這是一種從表現倒推的方法,大多數情況下,我們還是直接參考規範文檔會更加可靠,畢竟軟件承諾的是遵循規範,而不是保持當前行為。

Integer integer = 1;

int unboxing = integer ++;

  • 1
  • 2

反編譯輸出:

1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

8: invokevirtual #3 // Method java/lang/Integer.intValue:()I

  • 1
  • 2

這種緩存機制並不是只有 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();

}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果利用原始數據類型,可以將其修改為

class CompactCounter {

private volatile long counter;

private static final AtomicLongFieldUpdaterupdater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");

public void increase() {

updater.incrementAndGet(this);

}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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;

}

...

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

第二,我們在分析字符串的設計實現時,提到過字符串是不可變的,保證了基本的信息安全和併發編程中的線程安全。如果你去看包裝類裡存儲數值的成員變量“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語言規範 裡面,不管是 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/。

今天,我梳理了原始數據類型及其包裝類,從源碼級別分析了緩存機制等設計和實現細節,並且針對構建極致性能的場景,分析了一些可以借鑑的實踐。

int 和 Integer 有什麼區別?談談 Integer 的值緩存範圍


分享到:


相關文章: