《Java架構築基》從Java基礎講起——Int和Integer深入分析

1.關於int和Integer的問題區別分析

  1. 編譯階段、運行時,自動裝箱 / 自動拆箱是發生在什麼階段?
  2. 使用靜態工廠方法 valueOf 會使用到緩存機制,那麼自動裝箱的時候,緩存機制起作用嗎?
  3. 爲什麼我們需要原始數據類型,Java 的對象似乎也很高效,應用中具體會產生哪些差異?
  4. 閱讀過 Integer 源碼嗎?分析下類或某些方法的設計要點?
  5. int和Integer的區別
1、Integer是int的包裝類,int則是java的一種基本數據類型 
2、Integer變量必須實例化後才能使用,而int變量不需要 
3、Integer實際是對象的引用,當new一個Integer時,實際上是生成一個指針指向此對象;而int則是直接存儲數據值 
4、Integer的默認值是null,int的默認值是0

延伸: 
關於Integer和int的比較 
1、由於Integer變量實際上是對一個Integer對象的引用,所以兩個通過new生成的Integer變量永遠是不相等的(因爲new生成的是兩個對象,其內存地址不同)。

Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer變量和int變量比較時,只要兩個變量的值是向等的,則結果爲true(因爲包裝類Integer和基本數據類型int比較時,java會自動拆包裝爲int,然後進行比較,實際上就變爲兩個int變量的比較)

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3、非new生成的Integer變量和new Integer()生成的變量比較時,結果爲false。(因爲非new生成的Integer變量指向的是java常量池中的對象,而new Integer()生成的變量指向堆中新建的對象,兩者在內存中的地址不同)

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、對於兩個非new生成的Integer對象,進行比較時,如果兩個變量的值在區間-128到127之間,則比較結果爲true,如果兩個變量的值不在此區間,則比較結果爲false

Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true

Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

對於第4條的原因: 
java在編譯Integer i = 100 ;時,會翻譯成爲Integer i = Integer.valueOf(100);,而java API中對Integer類型的valueOf的定義如下:

public static Integer valueOf(int i){
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high){
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}

java對於-128到127之間的數,會進行緩存,Integer i = 127時,會將127進行緩存,下次再寫Integer j = 127時,就會直接從緩存中取,就不會new了

2.Integer的值緩存的原理

2.1 Java 5 中引入緩存特性

在 Java 5 中,爲 Integer 的操作引入了一個新的特性,用來節省內存和提高性能。整型對象在內部實現中通過使用相同的對象引用實現了緩存和重用。

這種 Integer 緩存策略僅在自動裝箱(autoboxing)的時候有用,使用構造器創建的 Integer 對象不能被緩存。

2.2 Integer類中的IntegerCache類

在創建新的 Integer 對象之前會先在 IntegerCache.cache (是個Integer類型的數組)中查找。有一個專門的 Java 類來負責 Integer 的緩存。

這個類是用來實現緩存支持,並支持 -128 到 127 之間的自動裝箱過程。最大值 127 可以通過 JVM 的啓動參數 -XX:AutoBoxCacheMax=size 修改。 緩存通過一個 for 循環實現。從小到大的創建儘可能多的整數並存儲在一個名爲 cache 的整數數組中。這個緩存會在 Integer 類第一次被使用的時候被初始化出來。以後,就可以使用緩存中包含的實例對象,而不是創建一個新的實例(在自動裝箱的情況下)。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

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() {}
}

2.3 其他整型類型的緩存機制

這種緩存行爲不僅適用於Integer對象。我們針對所有整數類型的類都有類似的緩存機制。

  • 有 ByteCache 用於緩存 Byte 對象
  • 有 ShortCache 用於緩存 Short 對象
  • 有 LongCache 用於緩存 Long 對象
  • 有 CharacterCache 用於緩存 Character 對象
  • Byte,Short,Long 有固定範圍: -128 到 127。對於 Character, 範圍是 0 到 127。除了 Integer 可以通過參數改變範圍外,其它的都不行。

3. 理解自動裝箱和拆箱

3.1 什麼是裝箱?什麼是拆箱?

裝箱就是 自動將基本數據類型轉換爲包裝器類型;拆箱就是 自動將包裝器類型轉換爲基本數據類型。

//拆箱
int yc = 5;
//裝箱
Integer yc = 5;
3.2 裝箱和拆箱是如何實現的
以Interger類爲例,下面看一段代碼來了解裝箱和拆箱的實現
public class Main {
    public static void main(String[] args) {
        Integer y = 10;
        int c = i;
    }
}

然後來編譯一下,看下所示:

  • 從反編譯得到的字節碼內容可以看出,在裝箱的時候自動調用的是Integer的valueOf(int)方法。而在拆箱的時候自動調用的是Integer的intValue方法。
  • 因此可以用一句話總結裝箱和拆箱的實現過程:裝箱過程是通過調用包裝器的valueOf方法實現的,而拆箱過程是通過調用包裝器的 xxxValue方法實現的。

3.3 裝箱和拆箱在編程實際中注意點

建議避免無意中的裝箱、拆箱行爲,尤其是在性能敏感的場合,創建 10 萬個 Java 對象和 10 萬個整數的開銷可不是一個數量級的,不管是內存使用還是處理速度,光是對象頭的空間佔用就已經是數量級的差距了。

4. 原始類型線程安全問題

4.1 那些類型是線程安全的

Java自帶的線程安全的基本類型包括: AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等

4.2 如何驗證int類型是否線程安全

200個線程,每個線程對共享變量 count 進行 50 次 ++ 操作

int 作爲基本類型,直接存儲在內存棧,且對其進行+,-操作以及++,–操作都不是原子操作,都有可能被其他線程搶斷,所以不是線程安全。int 用於單線程變量存取,開銷小,速度快

int count = 0;
private void startThread() {
    for (int i = 0;i < 200; i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int k = 0; k < 50; k++){
                    count++;
                }
            }
        }).start();
    }
    // 休眠10秒,以確保線程都已啓動
    try {
        Thread.sleep(1000*10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        Log.e("打印日誌----",count+"");
    }
}

//期望輸出10000,最後輸出的是9818
//注意:打印日誌----: 9818

4.3 AtomicInteger線程安全版

AtomicInteger類中有有一個變量valueOffset,用來描述AtomicInteger類中value的內存位置 。

當需要變量的值改變的時候,先通過get()得到valueOffset位置的值,也即當前value的值.給該值進行增加,並賦給next

compareAndSet()比較之前取到的value的值當前有沒有改變,若沒有改變的話,就將next的值賦給value,倘若和之前的值相比的話發生變化的話,則重新一次循環,直到存取成功,通過這樣的方式能夠保證該變量是線程安全的

value使用了volatile關鍵字,使得多個線程可以共享變量,使用volatile將使得VM優化失去作用,在線程數特別大時,效率會較低。

private static AtomicInteger atomicInteger = new AtomicInteger(1);
static Integer count1 = Integer.valueOf(0);
private void startThread1() {
    for (int i = 0;i < 200; i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int k = 0; k < 50; k++){
                    // getAndIncrement: 先獲得值,再自增1,返回值爲自增前的值
                    count1 = atomicInteger.getAndIncrement();
                }
            }
        }).start();
    }
    // 休眠10秒,以確保線程都已啓動
    try {
        Thread.sleep(1000*10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        Log.e("打印日誌----",count1+"");
    }
}

//期望輸出10000,最後輸出的是10000
//注意:打印日誌----: 10000

//AtomicInteger使用了volatile關鍵字進行修飾,使得該類可以滿足線程安全。
private volatile int value;
/**
 * Creates a new AtomicInteger with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

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

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

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

5.2 無法高效地表達數據,也不便於表達複雜的數據結構

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

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

6. 關於其他知識延伸

6.1 對象的內存結構

對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

6.2 對象頭的結構

HotSpot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,官方稱它爲"Mark Word"。

對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不一定要經過對象本身,這點將在2.3.3節討論。另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

6.3 如何計算或者獲取某個Java對象的大小

獲取一個JAVA對象的大小,可以將一個對象進行序列化爲二進制的Byte,便可以查看大小

//獲取一個JAVA對象的大小,可以將一個對象進行序列化爲二進制的Byte,便可以查看大小
Integer value = 10;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos ;
try {
    oos = new ObjectOutputStream(bos);
    oos.writeObject(value);
    oos.close();
} catch (IOException e) {
    e.printStackTrace();
}
// 讀出當前對象的二進制流信息
Log.e("打印日誌----",bos.size()+"");
//打印日誌----: 81
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章