FindBugs引出的Lombok @Data註解使用的問題

原地址:https://my.oschina.net/u/3049601/blog/2961654

 

先看代碼:

package com.leyou.item.pojo;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Data
@Table(name = "tb_spu")
public class Spu {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long brandId;
    private Long cid1;// 1級類目
    private Long cid2;// 2級類目
    private Long cid3;// 3級類目
    private String title;// 標題
    private String subTitle;// 子標題
    private Boolean saleable;// 是否上架
    private Boolean valid;// 是否有效,邏輯刪除用
    private Date createTime;// 創建時間
    private Date lastUpdateTime;// 最後修改時間

}
package com.leyou.item.bo;

import com.leyou.item.pojo.Spu;
import lombok.Data;


@Data
public class SpuBo extends Spu {

    private String cname;

    private String bname;


}

在SpuBo中使用@data註解時會報錯

大概意思是(神奇的有道。。。。):

這個類定義了一個equals方法,它覆蓋了超類中的equals方法。兩個equals方法都使用instanceof來確定兩個對象是否相等。這是充滿危險的,因爲equals方法是對稱的(換句話說,a = (b) == b = (a))是很重要的。如果B是a的一個子類型,a的equals方法檢查參數是否是a的instanceof, B的equals方法檢查參數是否是B的instanceof,那麼這些方法定義的等價關係很可能是不對稱的。

初級程序員只會百度:

原文如下:

@Data註解包含了getter  settter equals hashCode方法

上面的英文是:重寫equals方法可能會導致equals方法失去它的一致性原則,這個問題會出現a.equals(b)==true,b.equals(a)==false的情況。

補充重寫equals方法的要點:

  1 自反性:對於任意的引用值x,x.equals(x)一定爲true
  2  對稱性:對於任意的引用值x 和 y,當x.equals(y)返回true,y.equals(x)也一定返回true
  3 傳遞性:對於任意的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)也一定返   回 true
  4 一致性:對於任意的引用值x 和 y,如果用於equals比較的對象信息沒有被修改, 多次調用x.equals(y)要麼一致地返回true,要麼一致地返回false
  5 非空性:對於任意的非空引用值x,x.equals(null)一定返回false
  請注意:重寫equals方法後最好重寫hashCode方法,否則兩個等價對象可能得到不同的hashCode,這在集合框架中使用可能產生嚴重後果

下面關於使用Lombok的可能踩坑詳細描述是轉載的http://www.cnblogs.com/wuyuegb2312/p/9750462.html

雖然接觸到lombok已經有很長時間,但是大量使用lombok以減少代碼編寫還是在新團隊編寫新代碼維護老代碼中遇到的。

1. 使用Lombok的問題

這些是最初我不喜歡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 解決

有幾種解決方法可以參考:

  • 不使用該註解。大部分pojo我們是不會調用equals進行比較的,實際用到時再重寫即可。
  • 去掉callSuper = true。如果父類是Object,推薦使用。
  • 重寫父類的equals()方法,確保父類不會調用或使用類似實現的Ojbect的equals()。

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 就不要有繼承關係,類似kotlin的做法,具體探討見下一節
  • 自己重寫equals(),lombok不會對顯式重寫的方法進行生成  (我實際選擇的方案,有了繼承就不用 @data)
  • 顯式使用@EqualsAndHashCode(callSuper = true)。lombok會以顯式指定的爲準。

3.3 關於@data和data

在瞭解了 @data 的行爲後,會發現它和kotlin語言中的data修飾符有點像:都會自動生成一些方法,並且在繼承上也有問題——前者一旦有繼承關係就會踩坑,而後者修飾的類是final的,不允許繼承。kotlin爲什麼要這樣做,二者有沒有什麼聯繫呢?在一篇流傳較廣的文章(拋棄 Java 改用 Kotlin 的六個月後,我後悔了(譯文))中,對於data修飾符,提到:

Kotlin 對 equals()、hashCode()、toString() 以及 copy() 有很好的實現。在實現簡單的DTO 時它非常有用。但請記住,數據類帶有嚴重的侷限性。你無法擴展數據類或者將其抽象化,所以你可能不會在覈心模型中使用它們。

這個限制不是 Kotlin 的錯。在 equals() 沒有違反 Liskov 原則的情況下,沒有辦法產生正確的基於值的數據。

對於Liskov(里氏替換)原則,可以簡單概括爲:

一個對象在其出現的任何地方,都可以用子類實例做替換,並且不會導致程序的錯誤。換句話說,當子類可以在任意地方替換基類且軟件功能不受影響時,這種繼承關係的建模纔是合理的。

根據上一章的討論,equals()的實現實際上是受業務場景影響的,無論是否使用父類的屬性做比較都是有可能的。但是kotlin無法決定equals()默認的行爲,不使用父類屬性就會違反了這個原則,使用父類屬性有可能落入調用Object.equals()的陷阱,進入了兩難的境地。

kotlin的開發者迴避了這個問題,不使用父類屬性並且禁止繼承即可。只是kotlin的使用者就會發現自己定義的data對象沒法繼承,不得不刪掉這個關鍵字手寫其對應的方法。

回過頭來再看 @data ,它並沒有避免這些坑,只是把更多的選擇權交給開發者決定,是另一種做法。

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章