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

目錄

典型回答

注意事項

知識擴展

1. 理解自動裝箱、拆箱

2. 源碼分析

3. 原始類型線程安全

4.Java 原始數據類型和引用類型侷限性


典型回答

int是 Java 的 8 個原始數據類型(Primitive Types,boolean(1/8字節)、byte(1字節) 、short(2字節)、int(4字節)、long(8字節)、float(4字節)、double(8字節)、char(2字節,一個字符能存儲一箇中文漢字))之一。Java 語言雖然號稱一切都是對象,但原始數據類型是例外。

Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數據,並且提供了基本操作,比如數學運算、int 和字符串之間轉換等。

int的默認值爲0,而Integer的默認值爲null,即Integer可以區分出未賦值和值爲0的區別,int則無法表達出未賦值的情況,例如,要想表達出沒有參加考試和考試成績爲0的區別,則只能使用Integer。因爲int默認值爲0,在JSP開發中,用el表達式在文本框中顯示結果爲0,所以int不適合作爲web層的表單數據的類型。

JDK1.5引入了自動裝箱與自動拆箱功能,Java可根據上下文,實現int/Integer,double/Double,boolean/Boolean等基本類型與相應對象之間的自動轉換,爲開發過程帶來極大便利。

最常用的是通過new方法構建Integer對象。但是,基於大部分數據操作都是集中在有限的、較小的數值範圍,在JDK1.5 中新增了靜態工廠方法 valueOf,將int值爲-128 到 127 之間的Integer對象進行緩存,在調用時候直接從緩存中獲取,進而節省內存提升性能,也就是說使用該方法後,如果兩個對象的int值相同且落在緩存值範圍內,那麼這個兩個對象就是同一個對象;當值較小且頻繁使用時,推薦優先使用整型池方法(時間與空間性能俱佳)。

 

注意事項

  1. 基本類型均具有取值範圍,在大數*大數的時候,有可能會出現越界的情況。
  2. 基本類型轉換時,使用聲明的方式。例:long result= 1234567890 * 24 * 365;結果值一定不會是你所期望的那個值,因爲1234567890 * 24已經超過了int的範圍,如果修改爲:long result= 1234567890L * 24 * 365;就正常了。
  3. 慎用基本類型處理貨幣存儲。如採用double常會帶來差距,常採用BigDecimal、整型(如果要精確表示分,可將值擴大100倍轉化爲整型)解決該問題。
  4. 優先使用基本類型。原則上,建議避免無意中的裝箱、拆箱行爲,尤其是在性能敏感的場合,
  5. 如果有線程安全的計算需要,建議考慮使用類型AtomicInteger、AtomicLong 這樣的線程安全類。部分比較寬的基本數據類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現程序讀取到只更新了一半數據位的數值。

 

知識擴展

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)等可以作爲性能優化的備選項。一些追求極致性能的產品或者類庫,會極力避免創建過多對象。當然,在大多數產品代碼裏,並沒有必要這麼做,還是以開發效率優先。

 

2. 源碼分析

整體看一下 Integer 的職責,它主要包括各種基礎的常量,比如最大值、最小值、位數等;前面提到的各種靜態工廠方法 valueOf();獲取環境變量數值的方法;各種轉換方法,比如轉換爲不同進制的字符串,如 8 進制,或者反過來的解析方法等。

首先,繼續深挖緩存,Integer 的緩存範圍雖然默認是 -128 到 127,但是在特別的應用場景,比如我們明確知道應用會頻繁使用更大的數值,這時候應該怎麼辦呢?

緩存上限值實際是可以根據需要調整的,JVM 提供了參數設置:

-XX:AutoBoxCacheMax=N

第二,我們在分析字符串的設計實現時,提到過字符串是不可變的,保證了基本的信息安全和併發編程中的線程安全。如果你去看包裝類裏存儲數值的成員變量“value”,你會發現,不管是 Integer 還 Boolean 等,都被聲明爲“private final”,所以,它們同樣是不可變類型!

想象一下這個應用場景,比如 Integer 提供了 getInteger() 方法,用於方便地讀取系統屬性,我們可以用屬性來設置服務器某個服務的端口,如果我可以輕易地把獲取到的 Integer 對象改變爲其他數值,這會帶來產品可靠性方面的嚴重問題。

第三,Integer 等包裝類,定義了類似 SIZE(int SIZE = 32) 或者 BYTES(int BYTES = SIZE / Byte.SIZE)這樣的常量,這反映了什麼樣的設計考慮呢?移植對於 Java 來說相對要簡單些,因爲原始數據類型是不存在差異的,這些明確定義在Java 語言規範裏面,不管是 32 位還是 64 位環境,開發者無需擔心數據的位數差異,可以做到宣稱的“一次書寫,到處執行”,應用開發者更多需要考慮的是容量、能力等方面的差異。

 

3. 原始類型線程安全

前面提到了線程安全設計,你有沒有想過,原始數據類型操作是不是線程安全的呢?

這裏可能存在着不同層面的問題:

原始數據類型的變量,顯然要使用併發相關手段,才能保證線程安全,這些我會在專欄後面的併發主題詳細介紹。如果有線程安全的計算需要,建議考慮使用類似 AtomicInteger、AtomicLong 這樣的線程安全類。

特別的是,部分比較寬的數據類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現程序讀取到只更新了一半數據位的數值!

 

4.Java 原始數據類型和引用類型侷限性

前面我談了非常多的技術細節,最後再從 Java 平臺發展的角度來看看,原始數據類型、對象的侷限性和演進。

對於 Java 應用開發者,設計複雜而靈活的類型系統似乎已經習以爲常了。但是坦白說,畢竟這種類型系統的設計是源於很多年前的技術決定,現在已經逐漸暴露出了一些副作用,例如:

原始數據類型和 Java 泛型並不能配合使用

這是因爲 Java 的泛型某種程度上可以算作僞泛型,它完全是一種編譯期的技巧,Java 編譯期會自動將類型轉換爲對應的特定類型,這就決定了使用泛型,必須保證相應類型可以轉換爲 Object。

無法高效地表達數據,也不便於表達複雜的數據結構,比如 vector 和 tuple

我們知道 Java 的對象都是引用類型,如果是一個原始數據類型數組,它在內存裏是一段連續的內存,而對象數組則不然,數據存儲的是引用,對象往往是分散地存儲在堆的不同位置。這種設計雖然帶來了極大靈活性,但是也導致了數據操作的低效,尤其是無法充分利用現代 CPU 緩存機制。

Java 爲對象內建了各種多態、線程安全等方面的支持,但這不是所有場合的需求,尤其是數據處理重要性日益提高,更加高密度的值類型是非常現實的需求。

發佈了96 篇原創文章 · 獲贊 19 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章