Java 的類型系統是語言的基石。理解基本類型與包裝類的區別、String 的設計原理,以及物件相等性判斷,是寫出高品質 Java 程式碼的前提。

基本類型與包裝類#

八種基本類型#

Java 提供 8 種原始資料類型(Primitive Types):

類型位數預設值包裝類
boolean1falseBoolean
byte80Byte
short160Short
char16‘\u0000’Character
int320Integer
long640LLong
float320.0fFloat
double640.0dDouble

Java 語言雖然號稱「一切都是物件」,但原始資料類型是例外。原始類型直接存儲在堆疊上,而包裝類是物件,存儲在堆上。

自動裝箱與拆箱#

Java 5 引入了自動裝箱(boxing)和拆箱(unboxing)機制:

// 自動裝箱:編譯器自動呼叫 Integer.valueOf()
Integer boxed = 100;

// 自動拆箱:編譯器自動呼叫 intValue()
int unboxed = boxed;

反編譯後可以看到實際呼叫:

// 裝箱
invokestatic Integer.valueOf:(I)Ljava/lang/Integer;
// 拆箱
invokevirtual Integer.intValue:()I

自動裝箱拆箱的效能陷阱

在效能敏感的場合,創建 10 萬個 Java 物件和 10 萬個整數的開銷不是一個數量級。光是物件頭的空間佔用就已經是數量級的差距。

效能最佳化案例:線程安全計數器

傳統實現使用 AtomicLong 物件:

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);
    }
}

Integer 快取機制#

Integer.valueOf() 使用了快取機制,預設快取範圍是 -128 到 127

Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true(快取命中)

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false超出快取範圍

可以通過 JVM 參數調整快取上限:

-XX:AutoBoxCacheMax=N

其他包裝類的快取策略:

  • Boolean:僅快取 TRUEFALSE 兩個實體
  • Byte:全部 256 個值都被快取
  • Short:快取 -128 到 127
  • Character:快取 ‘\u0000’ 到 ‘\u007F’

String 的不可變性#

為什麼 String 是不可變的?#

String 被聲明為 final class,內部的字元陣列也是 final

public final class String {
    private final char value[];  // Java 8
    // private final byte[] value; // Java 9+
}

不可變性帶來的好處:

  1. 安全性:作為參數傳遞時不會被意外修改
  2. 線程安全:天然支援並行訪問
  3. 支援字串池:相同內容可以共享同一物件
  4. hashCode 可快取:計算一次後可重複使用

字串池與 intern()#

String s1 = "Hello";           // 字面量,存入字串池
String s2 = "Hello";           // 複用池中物件
String s3 = new String("Hello"); // 新建物件,不在池中

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // false
System.out.println(s1 == s3.intern()); // true

intern() 方法會將字串放入常數池。如果池中已存在相同內容的字串,則回傳池中的引用。

intern() 的版本差異

Java 6 及之前:字串池在永久代(PermGen),容量有限,大量使用可能導致 OOM。

Java 7+:字串池移到堆中,可以享受堆的動態擴展,降低了 OOM 風險。

使用場景:當有大量重複字串時,使用 intern() 可以節省記憶體:

// 適合:讀取大量重複的城市名稱
String city = reader.readLine().intern();

但要注意:字串池使用 HashMap 實現,大量 intern 會增加查詢開銷。

StringBuffer vs StringBuilder#

特性StringBufferStringBuilder
線程安全是(synchronized)
效能較低較高
適用場景多線程環境單線程環境
// 單線程場景首選 StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");

// 多線程場景使用 StringBuffer
StringBuffer sbf = new StringBuffer();
sbf.append("Thread-safe");

在現代 Java 開發中,99% 的場景應該使用 StringBuilder。如果需要線程安全的字串拼接,更好的做法是使用局部變數或同步機制,而非 StringBuffer。


物件相等性判斷#

== 與 equals 的區別#

// == 比較引用(記憶體地址)
String s1 = new String("test");
String s2 = new String("test");
System.out.println(s1 == s2);      // false

// equals 比較內容
System.out.println(s1.equals(s2)); // true

正確覆寫 equals 和 hashCode#

覆寫 equals() 時必須同時覆寫 hashCode()

public class Person {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

如果覆寫了 equals() 但沒有覆寫 hashCode(),在使用 HashMap、HashSet 時會出現問題:

Set<Person> set = new HashSet<>();
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
set.add(p1);
set.contains(p2); // 可能回傳 false

equals 契約#

正確的 equals() 實現必須滿足:

  1. 自反性x.equals(x) 必須回傳 true
  2. 對稱性x.equals(y)y.equals(x) 結果一致
  3. 傳遞性:如果 x.equals(y)y.equals(z),則 x.equals(z)
  4. 一致性:多次呼叫結果一致
  5. 非空性x.equals(null) 必須回傳 false

實踐建議#

  1. 優先使用基本類型:在效能敏感場合,避免不必要的裝箱拆箱
  2. 警惕 Integer 比較:使用 equals() 而非 == 比較包裝類
  3. 字串拼接用 StringBuilder:循環中避免使用 + 拼接字串
  4. 理解不可變性:String 的不可變性是設計特性,不是限制
  5. 正確實現 equals/hashCode:使用 IDE 生成或 Objects.hash() 輔助