lombok踩坑與思考
雖然接觸到lombok已經有很長時間,但是大量使用lombok以減少程式碼編寫還是在新團隊編寫新程式碼維護老程式碼中遇到的。
我個人並不主張使用lombok,其帶來的代價足以抵消其便利,但是由於團隊編碼風格需要一致,用還是要繼續使用下去。使用期間遇到了一些問題並進行了一番研究和思考,記錄一下。
1. 一些雜七雜八的問題
這些是最初我不喜歡lombok的原因。
1.1 額外的環境配置
作為IDE外掛+jar包,需要對IDE進行一系列的配置。目前在idea中配置還算簡單,幾年前在eclipse下也配置過,會複雜不少。
1.2 傳染性
一般來說,對外打的jar包最好儘可能地減少三方包依賴,這樣可以加快編譯速度,也能減少版本衝突。一旦在resource包裡用了lombok,別人想看原始碼也不得不裝外掛。
而這種不在對外jar包中使用lombok僅僅是約定俗成,當某一天lombok第一次被引入這個jar包時,新的感染者無法避免。
1.3 降低程式碼可讀性
定位方法呼叫時,對於自動生成的程式碼,getter/setter還好說,找到成員變數後find usages,再根據上下文區分是哪種;equals()這種,想找就只能寫段測試程式碼再去find usages了。
目前主流ide基本都支援自動生成getter/setter程式碼,和lombok註解相比不過一次鍵入還是一次快捷鍵的區別,實際減輕的工作量十分微小。
2. @EqualsAndHashCode和equals()
2.1 原理
當這個註解設定callSuper=true
時,會呼叫父類的equlas()方法,對應編譯後class檔案程式碼片段如下:
public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof BaseVO)) { return false; } else { BaseVO other = (BaseVO)o; if (!other.canEqual(this)) { return false; } else if (!super.equals(o)) { return false; } else { // 各項屬性比較 } } }
如果一個類的父類是Object(java中預設沒有繼承關係的類父類都是Object),那麼這裡會呼叫Object的equals()方法,如下
public boolean equals(Object obj) { return (this == obj); }
2.2 問題
對於父類是Object且使用了@EqualsAndHashCode(callSuper = true)
註解的類,這個類由lombok生成的equals()方法只有在兩個物件是同一個物件時,才會返回true,否則總為false,無論它們的屬性是否相同。這個行為在大部分時間是不符合預期的,equals()失去了其意義。即使我們期望equals()是這樣工作的,那麼其餘的屬性比較程式碼便是累贅,會大幅度降低程式碼的分支覆蓋率。以一個近6000行程式碼的業務系統舉例,是否修復該問題並編寫對應測試用例,可以使整體的jacoco分支覆蓋率提高10%~15%。
相反地,由於這個註解在jacoco下只算一行程式碼,未覆蓋行數倒不會太多。
2.3 解決
有幾種解決方法可以參考:
callSuper = true
2.4 其他
@data
註解包含@EqualsAndHashCode
註解,由於不呼叫父類equals(),避免了Object.equals()的坑,但可能帶來另一個坑。詳見@data章節
。
3. @data
3.1 從一個坑出來掉到另一個大坑
上文提到@EqualsAndHashCode(callSuper = true) 註解的坑,那麼@data
是否可以避免呢?很不幸的是,這裡也有個坑。
由於@data
實際上就是用的@EqualsAndHashCode
,沒有呼叫父類的equals(),當我們需要比較父類屬性時,是無法比較的。示例如下:
@Data public class ABO { private int a; } @Data public class BBO extends ABO { private int b; public static void main(String[] args) { BBO bbo1 = new BBO(); BBO bbo2 = new BBO(); bbo1.setA(1); bbo2.setA(2); bbo1.setB(1); bbo2.setB(1); System.out.print(bbo1.equals(bbo2)); // true } }
很顯然,兩個子類忽略了父類屬性比較。這並不是因為父類的屬性對於子類是不可見——即使把父類private屬性改成protected,結果也是一樣——而是因為lombok自動生成的equals()只比較子類特有的屬性。
3.2 解決方法
@data @EqualsAndHashCode(callSuper = true)
3.3 關於@data和data
在瞭解了@data
的行為後,會發現它和kotlin語言中的data修飾符有點像:都會自動生成一些方法,並且在繼承上也有問題——前者一旦有繼承關係就會踩坑,而後者修飾的類是final的,不允許繼承。kotlin為什麼要這樣做,二者有沒有什麼聯絡呢?在一篇流傳較廣的文章(ofollow,noindex" target="_blank">拋棄 Java 改用 Kotlin 的六個月後,我後悔了(譯文)
)中,對於data修飾符,提到:
Kotlin 對 equals()、hashCode()、toString() 以及 copy() 有很好的實現。在實現簡單的DTO 時它非常有用。但請記住,資料類帶有嚴重的侷限性。你無法擴充套件資料類或者將其抽象化,所以你可能不會在核心模型中使用它們。
這個限制不是 Kotlin 的錯。在 equals() 沒有違反 Liskov 原則的情況下,沒有辦法產生正確的基於值的資料。
對於Liskov(里氏替換)原則,可以簡單概括為:
一個物件在其出現的任何地方,都可以用子類例項做替換,並且不會導致程式的錯誤。換句話說,當子類可以在任意地方替換基類且軟體功能不受影響時,這種繼承關係的建模才是合理的。
根據上一章的討論,equals()的實現實際上是受業務場景影響的,無論是否使用父類的屬性做比較都是有可能的。但是kotlin無法決定equals()預設的行為,不使用父類屬性就會違反了這個原則,使用父類屬性有可能落入呼叫Object.equals()的陷阱,進入了兩難的境地。
kotlin的開發者迴避了這個問題,不使用父類屬性並且禁止繼承即可。只是kotlin的使用者就會發現自己定義的data物件沒法繼承,不得不刪掉這個關鍵字手寫其對應的方法。
回過頭來再看@data
,它並沒有避免這些坑,只是把更多的選擇權交給開發者決定,是另一種做法。
4. 後記
其他lombok註解實際使用較少,整體閱讀了官方文件 暫時沒有發現其他問題,遇到以後繼續更新。
實際上官方文件中也提到了equals()的坑。