Java 的類型系統是語言的基石。理解基本類型與包裝類的區別、String 的設計原理,以及物件相等性判斷,是寫出高品質 Java 程式碼的前提。
基本類型與包裝類#
八種基本類型#
Java 提供 8 種原始資料類型(Primitive Types):
| 類型 | 位數 | 預設值 | 包裝類 |
|---|---|---|---|
| boolean | 1 | false | Boolean |
| byte | 8 | 0 | Byte |
| short | 16 | 0 | Short |
| char | 16 | ‘\u0000’ | Character |
| int | 32 | 0 | Integer |
| long | 64 | 0L | Long |
| float | 32 | 0.0f | Float |
| double | 64 | 0.0d | Double |
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:僅快取
TRUE和FALSE兩個實體 - 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+
}不可變性帶來的好處:
- 安全性:作為參數傳遞時不會被意外修改
- 線程安全:天然支援並行訪問
- 支援字串池:相同內容可以共享同一物件
- 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#
| 特性 | StringBuffer | StringBuilder |
|---|---|---|
| 線程安全 | 是(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() 實現必須滿足:
- 自反性:
x.equals(x)必須回傳 true - 對稱性:
x.equals(y)和y.equals(x)結果一致 - 傳遞性:如果
x.equals(y)且y.equals(z),則x.equals(z) - 一致性:多次呼叫結果一致
- 非空性:
x.equals(null)必須回傳 false
實踐建議#
- 優先使用基本類型:在效能敏感場合,避免不必要的裝箱拆箱
- 警惕 Integer 比較:使用
equals()而非==比較包裝類 - 字串拼接用 StringBuilder:循環中避免使用
+拼接字串 - 理解不可變性:String 的不可變性是設計特性,不是限制
- 正確實現 equals/hashCode:使用 IDE 生成或
Objects.hash()輔助