1. 程式人生 > 資訊 >華為鴻蒙 HarmonyOS 應用服務夥伴峰會杭州站 6 月 25 日舉行:一次邏輯程式碼,跨裝置無縫流轉

華為鴻蒙 HarmonyOS 應用服務夥伴峰會杭州站 6 月 25 日舉行:一次邏輯程式碼,跨裝置無縫流轉

包裝類

什麼是包裝類?它們設計的意義是什麼?

Java 是較為純粹的面向物件設計語言,而其中存在八個原始型別,不歸於類的範疇。為了踐行 無物不可引用,萬類皆是物件,Java 為原始型別提供了對應的引用型別(基本型別又可以稱為原始型別),稱其為原始型別的包裝類

在寫法上,除了 int、char,其它六種包裝類均為首字母大寫,例如 byteByte。而 int、char 是縮寫,其包裝類分別是 IntegerCharacter

相較於基本型別,包裝類作為引用型別,可以存在 NULL 值。同時,提供了一些方法,例如

// 求最小求值
int minValue = Integer.MIN_VALUE;
// 求最大取值
int maxValue = Integer.MAX_VALUE;
// 二進位制位數
int size = Integer.SIZE;
// 所佔位元組數
int bytes = Integer.BYTES;

需要注意,Byte、Short、Integer、Long、Float、Double 同屬於 Number 數值類,Character、Boolean 不是

明確一點,所有的包裝類,都是 final 不可變類,這點與 String 是一致的,任何的修改都是新物件的建立

簡單些說,引用型別的賦值,實際是獲得堆記憶體的引用地址,而對於不可變類,任何的修改都會建立新的地址,原有的記憶體空間不再指向

而對於其它的可變引用型別,如陣列(陣列不可變的是型別、長度,而非內容),內容的修改不會導致新的陣列(記憶體空間)建立

int[] ints1 = new int[3];
// ints 1 與 ints 2 使用了同一份記憶體空間
int[] ints2 = ints1;
ints1[2] = 3;
System.out.println(Arrays.toString(ints2));
/* [0, 0, 3] */

這些,需要格外注意,什麼才是真正的 final 不可變,賦值與宣告同步,此後不可修改

IntegerString 這些不可變類,是藉由 final 完成的修飾。所以,若需要修改其中的內容,必須開闢新的記憶體空間,這會造成不必要的浪費,也是各種快取機制存在的必然

// Integer 類已經字首了 final,意為不可變的類
public final class Integer extends Number implements Comparable<Integer> {}

拆箱、裝箱

拆、裝箱,是原始型別與引用型別之間的相互轉換

裝箱:基本型別轉為其包裝類 valueOf()

拆箱:包裝類轉為其基本型別 intValue()

目前,Java 已經支援了自動裝箱、自動拆箱,無須再呼叫特定的方法進行手動拆、裝箱

包裝類的拆箱、拆箱 示例,僅作了解即可

/* 手動裝箱 */
Integer integer1 = Integer.valueOf(12);
/* 手動拆箱 */
int intValue1 = integer1.intValue();

/* 自動裝箱 */
Integer integer3 = 12;
/* 自動拆箱 */
int intValue2 = integer1;

快取池

包裝類中,存在快取池的設定,避免物件的重複建立,以 Integer 為例

在 Integer 類中,存在一個私有靜態內部類 private static class IntegerCache {}

簡單的理解,Integer 類的裝箱操作,會呼叫 valueOf(),並開闢一塊新的堆記憶體

無論是手動裝箱,還是自動裝箱,都會呼叫 valueOf(),只是隱藏了這部分

若裝箱後的 Integer 物件存在於 Integer 的快取池中,則不會建立新物件,而是直接引用自快取池 IntegerCache

Integer a1 = 120;
Integer a2 = 1200;
Integer b1 = 120;
Integer b2 = 1200;
/* == 對於引用型別,比較的是堆記憶體地址,Java 中也不支援運算子過載 */
System.out.println(a1 == b1);
System.out.println(a2 == b2);
/* true、false */

上述的包裝類示例,違背了以往的認知,這就是快取池在發揮作用!對於引用型別,== 是比較二者的堆記憶體地址,同樣數值的 Integer 型別,為何會出現堆記憶體地址相同、相否的情況

當呼叫 valueOf() 時,會先判斷當前建立的物件是否存在於快取池中

public static Integer valueOf(int i) {
    // 判斷當前原始型別數值,是否存在於快取池中
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

下述是 Integer 快取池的具體實現原始碼

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    static {
        int h = 127;
        String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        assert IntegerCache.high >= 127;
    }
    private IntegerCache() {}
}

通過一系列的示例、原始碼,可以很清楚的認識到快取池。根據包裝的原始型別大小,決定是否新建物件,還是從快取池中取出相同的物件

在 Integer 中,快取池是 -128~127,一個 byte 的取值範圍。可以手動的擴充快取池的大小,但並不推薦這樣做

當然,若是直接使用 new Integer(),可以建立一個新的 Integer 物件,但已經廢棄。關鍵字 new,為強制建立,無視快取池機制

值得注意的是,並非所有的包裝類都存在快取池,浮點型的 Float、Double 與 Boolean 不存在快取池的概念。Byte、Short、Integer、Long、Character 存在快取池,預設範圍都是 -128~127

重寫 equals()

同樣的以 Integer 為例,探討其中的方法重寫。在 Integer 類、String 類中,對於 Object 類的 equals() 方法已經被重寫

預設的 equals() 方法,對於引用型別,比較的是 變數是否引用自同一物件,判斷的依據是堆記憶體地址。在重寫之後,根據實際的數值判斷

Integer a = 12;
Integer b = 12;
Integer c = new Integer(12);
/* == 不可重寫,依舊是根據物件的引用地址判斷 */
System.out.println(a == b);
System.out.println(a == c);
/* equals() 已被重寫,根據物件的值進行判斷 */
System.out.println(a.equals(b));
System.out.println(a.equals(c));
/*
true、false、true、true
*/

上述的示例可以看出

  • a、b 由於 Integer 的快取池機制,引用的是同一個物件,堆記憶體地址與實際數值皆一致
  • c 通過關鍵字 new,沒有引用快取池,而是全新的堆記憶體地址,但實際數值依舊保持一致
  • == 的判斷中,不存在問題,a 與 b 相等,與 c 不相等

可是,equals() 的預設實現,是根據物件的引用地址進行的。而經過 Integer 的重寫,根據實際的數值判斷,以至於堆記憶體不同的變數在 equals() 的判斷中為 true

所以,請看 Integer 類中的重寫實現,與 Object 類中的預設實現,二者的區別

/* Object 類的預設 equals() 方法,其中 this 指代當前物件 */
public boolean equals(Object obj) {
    return (this == obj);
}
/* Integer 重寫後的 equals() 方法 */
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        /* Integer 類的 equals() 根據值判斷二者的相等 */
        return value == ((Integer)obj).intValue();
    }
    return false;
}

重寫 hashCode()

Object 類中,也存在一個方法,hashCode(),負責返回物件的雜湊值

共同的認知是,重寫 equals() 的同時,必須重寫 hashCode()

雜湊值是根據物件的屬性,在通過雜湊演算法生成的,在 Integer 重寫之後,規則發生改變,Integer 的雜湊值等於它的數值本身

@Override
public int hashCode() {
    return Integer.hashCode(value);
}

值得注意的是

  • 相同的物件,雜湊值一定相同
  • 而雜湊值相同的物件,也可能不是同一個物件
// Arrays.hashCode() 也重寫 hashCode(),暫時不用
int[] ints = new int[2];
System.out.println(ints.hashCode());
System.out.println(new int[2].hashCode());
/*
雜湊值:2083562754、1239731077
*/

hashCode() 的預設實現,是將物件的引用地址轉換為整數值。引用地址右虛擬機器生成,可覆蓋

可以參考 HashMap 中的鍵值儲存形式,它允許存在重複的 hash 值,以單向連結串列的形式儲存

對於 equals() 與 hashCode(),優先是通過物件的雜湊值判斷相等性。若雜湊值不同,則並非同一個物件;若雜湊值相同,則根據 equals() 二次判斷。這樣,可以達到效能與安全的平衡

equals() 重寫,而 hashCode() 不重寫,根據物件的值進行判斷,則會出現,equals() 判斷為 true,而 hashCode() 不相同,以至於操作失誤

簡單的理解為:equals() 判斷為 true,則 hashCode() 必須為 true;而 hashCode() 為 true時,equals() 存在為 false 的可能

String 儲存位置

String 類經常使用,所以 Java 也對它做了 “快取” 處理

String 類是不可以變的字元陣列,任何的修改都會生成新的 String 物件

在 JVM 虛擬機器中,存在一個特殊的記憶體空間,常量池

對於普通方式建立的字串,則放入常量池中,可以參考 Integer 類的快取池

String a = "1";
String b = "1";
// == 用於比較物件的引用地址
System.out.println(a == b);
/* true */

當直接賦值為字串字面量時,相同內容的新物件,也會被認為是同一個物件

String a = "1";
String c = new String("1");
System.out.println(a == c);
/* false */

關鍵字 new,強制在堆記憶體中,建立一個新的 String 類物件,不存入常量池

值得注意的是,字串的實際儲存位置,或者說常量池位置,隨著 JDK 版本迭代而變更

例如,在 JDK8.0 中,常量池已經由方法區移動至堆記憶體中

StringBuffer、StringBuilder

String 類是不可變的,每次修改都會建立新的物件,這樣的效率並不是很高

Java 中提供了兩個可變長的字串類,StringBufferStringBuilder

StringBuffer:JDK1.0 提供,執行緒安全、執行效率慢

StringBuilder:JDK5.0 提供,執行緒不安全、執行效率快

這兩個方法中,常用的是 StringBuild,並且二者的實現是差不多的

核心在於執行緒的安全與否,而保證執行緒的同時,不可避免的降低執行效率

簡單的說,StringBuffer、StringBuild 的本質也是字元陣列

不同的是,這二者的字元陣列,其每一個元素為一個字串,而不是純粹的單個字元

二者對於 String 類的改造,在於字元陣列的可擴容

在對字串做修改時,String 類是直接建立新的物件,原記憶體直接廢棄

而 StringBuffer、StringBuilder 則是將該記憶體 擴容

以下是 StringBuilder 的 API

  • appeand(String str):新增 String 物件至字元陣列中
  • appeand(StringBuilder sb):新增 StringBuilder 物件值字元陣列中
  • delete(int start, int end):刪除字元陣列中的部分元素

可以看出,StringBuilder 使用方法 appeand() 替代了 String 類拼接字元的 +

現在,簡單介紹一些,StringBuilder 對於字元陣列擴容的操作

初始大小:StringBuilder 繼承抽象父類,預設字元陣列長度為 16

public StringBuilder() {
    super(16);
}

新增 String 物件:新增 String 物件之前,先判斷字元陣列是否需要擴容

// StringBuilder 的物件新增,已經交由抽象父類完成
public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);
    putStringAt(count, str);
    count += len;
    return this;
}

字元陣列擴容:是否需要擴容,是根據添加當前 String 物件後的字元陣列長度確定的

// 判斷新增 String 物件是否超出長度,若超出,則擴容為之前的兩倍
private void ensureCapacityInternal(int minimumCapacity) {
    int oldCapacity = value.length >> coder;
    if (minimumCapacity - oldCapacity > 0) {
        value = Arrays.copyOf(value, newCapacity(minimumCapacity) << coder);
    }
}

StringBuild 的擴容,使得無需再開闢新的記憶體空間

簡單的理解,StringBuilder 相當於 String 類的容器

接下來,簡單介紹 StringBuffer 的執行緒安全問題,當然,這不常用

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

上述為 StringBuffer 的字串物件新增方法,這直接解釋了執行緒安全的緣由

StringBuffer 在修飾符、返回值型別之間,加入了 關鍵字 synchronized

synchronized:執行緒安全,該方法同一時間點,只允許一條執行緒進行操作

String 類的不可變,使得程式安全、簡單且易於理解,但頻繁的建立新物件,則使得效率較低

StringBuffer、StringBuilder 在則通過 擴容,避免了頻繁的記憶體操縱