effective Java 學習筆記(第一彈)
整理自《effective Java 中文第3版》
本篇筆記整理第3,4章的內容。
重寫equals方法需要注意的地方
- 自反性:對于任何非空引用 x,x.equals(x) 必須返回 true。
- 對稱性:對于任何非空引用 x 和 y,如果且僅當 y.equals(x) 返回 true 時 x.equals(y) 必須返回 true。
- 傳遞性:對于任何非空引用 x、y、z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,則 x.equals(z) 必須返回 true。
- 一致性:對于任何非空引用 x 和 y,如果在 equals 比較中使用的信息沒有修改,則 x.equals(y) 的多次調用必須始終返回 true 或始終返回 false。
- 對于任何非空引用 x,x.equals(null) 必須返回 false。
在equals方法聲明中,不要將參數Object替換成其他類型!可以用instanceof來判斷類型是否一致。
重寫equals方法時同時也要重寫hashcode方法。相等的對象必須要具有相等的哈希碼(hash code)。
@EqualsAndHashCode(callSuper = false) 是 Lombok 庫中的一個注解,用于自動生成 equals 和 hashCode 方法。這個注解可以幫助開發者減少樣板代碼的編寫。
callSuper = false 參數表示在生成的 equals 和 hashCode 方法中不調用父類的 equals 和 hashCode 方法。這意味著生成的方法將僅基于當前類的字段來實現相等性和哈希值的計算。這樣可以確保子類在繼承父類時,不會因為父類的 equals 和 hashCode 方法而影響子類的行為。
始終重寫toString方法
雖然Object類提供了toString方法的實現,但它返回的字符串是它由類名后跟一個「at」符號(@)和哈希碼的無符號十六進制表示組成。若想輸出需要的易懂的內容,需要重寫。
@Data 注解是 Lombok 庫提供的一個注解,用于簡化 Java 類的編寫。使用 @Data 注解后,Lombok 會自動生成以下內容:
- 生成 getter 和 setter 方法:為類中的所有字段自動生成 getter 和 setter 方法。
- 生成 toString 方法:為類生成 toString 方法,包含所有字段的值。
- 生成 equals 和 hashCode 方法:為類生成 equals 和 hashCode 方法,基于所有字段。
- 生成 toString 方法:為類生成 toString 方法,包含所有字段的值。
- 生成無參構造函數:為類生成一個無參構造函數。
- 生成全參構造函數:為類生成一個包含所有字段的全參構造函數。
import lombok.Data;@Data
public class User {private String name;private int age;private String email;
}
使用 @Data 注解后,Lombok 會為 User 類生成以下內容:
public String getName()
public void setName(String name)
public int getAge()
public void setAge(int age)
public String getEmail()
public void setEmail(String email)
public String toString()
public boolean equals(Object obj)
public int hashCode()
無參構造函數 public User()
全參構造函數 public User(String name, int age, String email)
考慮實現Comparable接口
無論何時實現具有合理排序的值類,你都應該讓該類實現Comparable接口,以便在基于比較的集合中輕松對其實例進行排序,搜索和使用。 比較 compareTo 方法的實現中的字段值時,請避免使用「<」和「>」運算符。 相反,使用包裝類中的靜態compare方法或Comparator接口中的構建方法。
如下是反例,可能導致整形最大長度移除和IEEE754浮點運算失真危險。
static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return o1.hashCode() - o2.hashCode();}
};
可以使用如下兩種方式替代
static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return Integer.compare(o1.hashCode(), o2.hashCode());}
};static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
額外話題:IEEE754浮點運算失真
IEEE754浮點運算失真(或精度丟失)是指在使用IEEE754標準表示和計算浮點數時,由于二進制存儲和有限位數限制,導致無法精確表示某些十進制小數或運算結果出現微小誤差的現象。這是計算機科學中浮點數處理的固有挑戰。
核心原因:
- 二進制與十進制的進制差異
- 許多十進制小數(如0.1)無法用有限位二進制精確表示(類似1/3無法用有限十進制表示)。
- 例如,0.1的二進制表示是無限循環小數:0.0001100110011…,存儲時會被截斷。
- IEEE754的存儲結構限制
- 浮點數按三部分存儲:符號位、指數位(控制范圍)、尾數位(控制精度)。
- 單精度(32位):1符號位 + 8指數位 + 23尾數位 → 約6-9位有效十進制數字。
- 雙精度(64位):1符號位 + 11指數位 + 52尾數位 → 約15-17位有效十進制數字。
- 舍入規則的影響
- IEEE754默認使用“向最近偶數舍入”(Round to Nearest, Ties to Even),可能導致累積誤差。
典型表現:
console.log(0.1 + 0.2); // 輸出:0.30000000000000004(非精確0.3)
console.log(0.3 - 0.2 === 0.1); // 輸出:false
>>> 1.0000000000000001 == 1.0
True # 雙精度無法區分過小的差異
實際影響場景:
- 科學計算:迭代計算中誤差累積可能影響結果可靠性。
- 金融系統:貨幣計算要求精確到分,浮點誤差可能導致賬務錯誤。
- 游戲物理引擎:微小誤差可能引發碰撞檢測異常。
解決方案:
- 整數替代法
用整數表示最小單位(如分而不是元):
price_cents = 1000 # 表示10.00元,避免浮點運算
- 高精度計算庫
Python:decimal 模塊(基于十進制的精確計算):
from decimal import Decimal
print(Decimal(‘0.1’) + Decimal(‘0.2’)) # 輸出精確0.3 - 誤差容忍比較
使用極小值(epsilon)判斷近似相等:
function areEqual(a, b, epsilon = 1e-10) {return Math.abs(a - b) < epsilon;
}
- 特殊場景處理
避免超大數與超小數直接相加(會丟失小數部分):
double big = 1e20;
double small = 1.0;
printf("%f\n", big + small - big); // 輸出0.0(small被吞沒)
雖然IEEE754的精度問題無法徹底消除,但通過合理的設計(如定點數、符號處理、誤差控制)可將其影響降至最低。理解這一機制是開發可靠數值計算程序的關鍵基礎。
使類的成員的可訪問性最小化
非零長度的數組總是可變的,所以類具有公共靜態 final 數組屬性,或返回這樣一個屬性的訪問器是錯誤的。
如果一個類有這樣的屬性或訪問方法,客戶端將能夠修改數組的內容。 這是安全漏洞的常見來源:
public static final Thing[] VALUES = { ... };
有兩種方法可以解決這個問題。
- 可以使公共數組私有并添加一個公共的不可變列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
- 可以將數組設置為 private,并添加一個返回私有數組拷貝的公共方法:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {return PRIVATE_VALUES.clone();
}
應該盡可能地減少程序元素的可訪問性(在合理范圍內)。 在仔細設計一個最小化的公共 API 之后,你應該防止任何散亂的類,接口或成員成為 API 的一部分。 除了作為常量的公共靜態 final 屬性之外,公共類不應該有公共屬性。 確保 public static final 屬性引用的對象是不可變的。
我自己也犯過這樣的錯誤,在某個枚舉中創建private static final Set的集合,枚舉中加了靜態的方法返回這個數組,在外層刪除這個集合的某個值,這個集合的值就徹底變了。大致代碼如下:
@Getter
public enum VIPTypeEnum {BRONZE(0,"bronze"),SILVER(1,"silver"),GOLD(2,"gold"),SUPPER(999,"supper"),;private int code;private String desc;VIPTypeEnum(int code, String desc) {this.code = code;this.desc = desc;}private static final Set<VIPTypeEnum> showSet = Sets.newHashSet(BRONZE,SILVER,GOLD);public static Set<VIPTypeEnum> getShowSet(){return showSet;}
}
public static void main(String[] args) {Set<VIPTypeEnum> showSet = VIPTypeEnum.getShowSet();System.out.println(showSet);showSet.remove(VIPTypeEnum.SILVER);System.out.println(VIPTypeEnum.getShowSet());
}
輸出如下:
[BRONZE, GOLD, SILVER]
[BRONZE, GOLD]
使用static final修飾的成員變量值改變了。上面說的書中的方法也是可以的:
修改為:
public static Set<VIPTypeEnum> getShowSet(){return Collections.unmodifiableSet(showSet);}
那么運行main方法,會拋出異常:
[BRONZE, GOLD, SILVER]
Exception in thread "main" 與目標 VM 斷開連接, 地址為: ''127.0.0.1:52720',傳輸: '套接字''
java.lang.UnsupportedOperationExceptionat java.util.Collections$UnmodifiableCollection.remove(Collections.java:1058)at com.example.demo.DemoApplication.main(DemoApplication.java:121)
書中的第二種方法改后會報錯:‘clone()’ 在 ‘java.lang.Object’ 中具有 protected 訪問權限,因為Set接口并沒有定義clone()方法。這通常會導致編譯錯誤,提示無法找到符號或類似的問題。為了解決這個問題,我們可以使用其他方式來復制集合,例如通過構造函數創建一個新的HashSet實例。
public static Set<VIPTypeEnum> getShowSet(){return new HashSet<>(showSet);}public static void main(String[] args) {Set<VIPTypeEnum> showSet = VIPTypeEnum.getShowSet();System.out.println(showSet);showSet.remove(VIPTypeEnum.GOLD);System.out.println(showSet);System.out.println(VIPTypeEnum.getShowSet());}
輸出結果:
[BRONZE, GOLD, SILVER]
[BRONZE, SILVER]
[BRONZE, GOLD, SILVER]
關于深、淺拷貝的操作,見:Java 對實例進行深拷貝操作
最小化可變性
“最小化可變性”強調設計不可變類(Immutable Class)的重要性。不可變類的實例一旦創建,狀態就不可修改,這能顯著提升代碼的線程安全性、可維護性和可靠性。以下是關鍵原則及示例:
核心原則
- 不提供修改狀態的方法(Mutators)
- 如 setXxx() 方法,禁止直接修改對象屬性。
- 確保類不可被繼承
- 避免子類破壞不可變性,通常用 final 修飾類或私有化構造函數。
- 保護對可變組件的訪問
- 如果類持有可變對象(如數組、集合),需防御性拷貝(Defensive Copy),避免外部修改影響內部狀態。
示例1:簡單的不可變類
public final class ImmutablePoint {private final int x;private final int y;public ImmutablePoint(int x, int y) {this.x = x;this.y = y;}// 只有getter,沒有setterpublic int getX() { return x; }public int getY() { return y; }
}
不可變性體現:
- x 和 y 被聲明為 final,只能在構造函數中初始化。
- 沒有提供修改字段的方法(如 setX())。
- 類為 final,不允許子類覆蓋行為。
示例2:處理深層次可變對象
若類中包含可變對象(如 Date、數組),需確保外部無法修改其內部狀態:
public final class ImmutableEvent {private final Date eventDate; // Date本身是可變的!public ImmutableEvent(Date date) {this.eventDate = new Date(date.getTime()); // 防御性拷貝,避免外部修改原Date}public Date getEventDate() {return (Date) eventDate.clone(); // 返回拷貝,避免外部修改內部Date}
}
構造函數中創建 Date 的副本存儲,而非直接引用外界傳入的 Date。getEventDate() 返回克隆對象,防止外部通過獲取引用修改內部狀態。
示例3:Java標準庫中的不可變類
String 類:
String s = "Hello";
s = s.concat(" World"); // 返回新對象,原s未被修改
所有看似修改的操作(如 concat()、substring())都返回新對象,原始字符串不變。
BigInteger、BigDecimal:
數值運算(如 add())均返回新實例,確保原有對象不變。
為何不可變類更安全?
- 線程安全:無需同步,多線程共享時不會出現競態條件。
- 緩存友好:可安全復用對象(如 String 常量池)。
- 防御性拷貝不必要:不可變對象本身無法被修改,傳遞時無需復制。
- 可靠的哈希鍵:作為 HashMap 的鍵時,哈希值不會改變,避免定位錯誤。
何時使用可變類?
不可變類的缺點是頻繁創建對象可能影響性能,此時可選擇可變配套類:String(不可變) ? StringBuilder(可變,用于高效拼接字符串)。復雜計算中,若需頻繁修改狀態,可使用可變對象臨時操作,最終生成不可變結果。