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

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

  • 自動裝箱 / 自動拆箱是發生在什麼階段?編譯階段、運行時?
  • 使用靜態工廠方法 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,數值有限,所以全部都被緩存(-128 到 127 )。
  • Character,緩存範圍 '\u0000' 到 '\u007F'。
  • Long只緩存了 -128 到 127 之間的數值。

自動裝箱 / 自動拆箱似乎很酷,在編程實踐中,有什麼需要注意的嗎? 

原則上,建議避免無意中的裝箱、拆箱行爲,尤其是在性能敏感的場合,創建 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 =
                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;
        }

        private IntegerCache() {}
    }
    ....
}

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

3. 原始類型線程安全

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

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

  • 原始數據類型的變量,顯然要使用併發相關手段,才能保證線程安全,這些我會在專欄後面的併發主題詳細介紹。如果有線程安全的計算需要,建議考慮使用類似 AtomicInteger、AtomicLong 這樣的線程安全類。
  • 特別的是,部分64位的數據類型,long、double,甚至不能保證寫操作的原子性,可能出現程序讀取到只更新了一半數據位的數值! 

“深入java虛擬機”中提到,int等不大於32位的基本類型的操作都是原子操作,但是某些jvm對long和double類型的操作並不是原子操作,這樣就會造成錯誤數據的出現。

錯誤數據出現的原因是: 
在32位的虛擬機中,對於long和double變量,把它們作爲2個原子性的32位值來對待,而不是一個原子性的64位值, 
這樣將一個long型的值保存到內存的時候,可能是2次32位的寫操作, 
2個競爭線程想寫不同的值到內存的時候,可能導致內存中的值是不正確的結果。

1、寫入高位32位值(線程2) 
2、寫入高位32位值(線程1) 
3、寫入低位32位值(線程1) 
4、寫入低位32位值(線程2)


這樣內存中的值變成線程1的高32位值和線程2的低32位值的組合,是個錯誤的值。 
書中還提到,上面出現問題的long和double變量是沒有聲明爲volatile的變量。
volatile本身不保證獲取和設置操作的原子性,僅僅保持修改的可見性。

但是java內存模型保證聲明爲volatile的long和double變量的get和set操作是原子的。

jvm spec引用中的最後一段說到,jvm可以很輕易的將變量操作變成原子性的,但是卻受到了當前硬件的約束,因爲流行的微處理器還是32bit居多,因此64bit的變量需要拆分成兩次,但如果是64bit處理器就能滿足64bit變量的原子性操作了。

針對這個問題,找到了網上的一個總結:

  • 對於64位的long和double,如果沒有被volatile修飾,那麼對其操作可以不是原子的。在操作的時候,可以分成兩步,每次對32位操作;
  • 如果使用volatile修飾long和double,那麼其讀寫都是原子操作;
  • 在實現JVM時,可以自由選擇是否把讀寫long和double作爲原子操作;
  • java中對於long和double類型的寫操作不是原子操作,而是分成了兩個32位的寫操作。讀操作是否也分成了兩個32位的讀呢?在JSR-133之前的規範中,讀也是分成了兩個32位的讀,但是從JSR-133規範開始,即JDK5開始,讀操作也都具有原子性;
  • java中對於其他類型的讀寫操作都是原子操作(除long和double類型以外);
  • 對於引用類型的讀寫操作都是原子操作,無論引用類型的實際類型是32位的值還是64位的值;
  • 對於long類型的不恰當操作可能讀取到從未出現過的值。而對於int的不恰當操作則可能讀取到舊的值;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章