【Java】通過 getter 方法引用,來比較兩個物件是否相等
阿新 • • 發佈:2022-04-09
背景
編寫程式碼時,會經常需要編寫兩個物件是否相等的邏輯,一般會有如下做法
- 直接寫在業務程式碼中;
- 單獨寫個方法,業務程式碼中呼叫;
- 重寫 equals 方法;
上面這些做法,都比較複雜,如果屬性太多或複雜點(如果是 list 和 map 就更復雜了),就需要編寫更多的判斷邏輯程式碼了。
想法(需求)
如果能只需要提供比較的方法引用列表,有個地方能自動方法引用取值,並比較就好了。
思路
- 在 java8 中可以使用方法引用,如:People::getName;
- 可以將所有要比較的 Getter 儲存到列表中;
- 在 比較的時候,根據 方法引用獲取具體的值進行比較;
- 全部比較都相等了,就認為是相等的。
舉個例子 1(改造前)
@Getter @Setter public class EqualDemo implements Equable { private Integer id; private Integer age; private String username; private Date createTime; private Date updateTime; private List<EqualDemo> list; private Map<String, EqualDemo> map; @Override public boolean equals(Object obj) { if (obj instanceof EqualDemo) { if (!Objects.equals(((EqualDemo) obj).getId(), this.getId())) { return false; } if (!Objects.equals(((EqualDemo) obj).getAge(), this.getAge())) { return false; } if (!Objects.equals(((EqualDemo) obj).getUsername(), this.getUsername())) { return false; } final List<EqualDemo> list1 = ((EqualDemo) obj).getList(); final List<EqualDemo> list2 = this.getList(); // todo 比較 list1 和 list2 final Map<String, EqualDemo> map1 = ((EqualDemo) obj).getMap(); final Map<String, EqualDemo> map2 = this.getMap(); // todo 比較 map1 和 map2 return true; } return Objects.equals(this, obj); } } // 使用 public class EqualDemoTest { @Test public void testSimpleTrue() { final EqualDemo s1 = new EqualDemo(); s1.setId(1); s1.setUsername("bob"); final EqualDemo s2 = new EqualDemo(); s2.setId(1); s2.setUsername("bob"); // // s1 和 s2 相等 assert s1.equals(s2); } }
舉個例子 2(改造後)
@Getter @Setter public class EqualDemo implements Equable { private Integer id; private Integer age; private String username; private Date createTime; private Date updateTime; private List<EqualDemo> list; private Map<String, EqualDemo> map; @SuppressWarnings("unchecked") @Override public List<EqualGetter<EqualDemo>> listEqualToGetter() { return Arrays.asList( EqualDemo::getId, EqualDemo::getAge, EqualDemo::getUsername, EqualDemo::getList, EqualDemo::getMap ); } } // 使用 public class EqualDemoTest { @Test public void testSimpleTrue() { final EqualDemo s1 = new EqualDemo(); s1.setId(1); s1.setUsername("bob"); final EqualDemo s2 = new EqualDemo(); s2.setId(1); s2.setUsername("bob"); // // s1 和 s2 相等 assert s1.equalTo(s2); } }
這樣就簡潔很多了,由於 equalTo 方法是 Equable 介面中的預設方法,具體邏輯全封裝起來了(包括 object, collection 和 map 的處理),不需要重寫就可以直接使用,具體實現看下面的原始碼。
原始碼
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
/**
* 通過 getter 方法引用,來比較兩個物件是否相等
*
* @author lilou
* @date 2022/4/9 9:30
*/
public interface Equable extends Serializable {
long serialVersionUID = 1L;
/**
* 預設使用 Objects.equals 方法比較
*
* @param other 另一個 model
* @return 結果
*/
default boolean equalTo(Equable other) {
final List<EqualGetter<Equable>> getterList = listEquableGetter();
// getter方法引用列表中沒有需要比較的 getter,直接比較物件
if (getterList == null || getterList.isEmpty()) {
return Objects.equals(other, this);
}
// 列表有值,計算兩個物件在列表中 getter 值,並比較是否相等
for (EqualGetter<Equable> equalGetter : getterList) {
Object o1 = equalGetter.apply(this);
Object o2 = equalGetter.apply(other);
// 對於屬性中也有 實現 Equable 介面的,遞迴呼叫
if (o1 instanceof Equable && o2 instanceof Equable) {
return ((Equable) o1).equalTo((Equable) o2);
}
// equalTo collection
if (o1 instanceof Collection && o2 instanceof Collection) {
if (((Collection<?>) o1).size() != ((Collection<?>) o2).size()) {
return false;
}
if (((Collection<?>) o1).isEmpty() && ((Collection<?>) o2).isEmpty()) {
return true;
}
// 判斷不相等
if (notEqualCollection((Collection<?>) o1, (Collection<?>) o2)) return false;
// collection 已經判斷完了,開始下一個屬性
continue;
}
// equalTo map
if (o1 instanceof Map && o2 instanceof Map) {
// 比較 key
final Collection<?> set1 = ((Map<?, ?>) o1).keySet();
final Collection<?> set2 = ((Map<?, ?>) o2).keySet();
if (notEqualCollection(set1, set2)) return false;
// 比較 map
final Collection<?> values1 = ((Map<?, ?>) o1).values();
final Collection<?> values2 = ((Map<?, ?>) o2).values();
if (notEqualCollection(values1, values2)) return false;
// map 已經判斷完了,開始下一個屬性
continue;
}
if (!Objects.equals(o1, o2)) {
return false;
}
}
return true;
}
/**
* 判斷 collection 是否不相等
*
* @param o1 第一個 collection
* @param o2 第二個 collection
* @return 是否不相等
*/
default boolean notEqualCollection(Collection<?> o1, Collection<?> o2) {
// 根據 hashCode 來排序
o1 = o1.stream().sorted(Comparator.comparingInt(Object::hashCode)).collect(Collectors.toList());
o2 = o2.stream().sorted(Comparator.comparingInt(Object::hashCode)).collect(Collectors.toList());
// 逐個比較
final Iterator<?> iterator1 = o1.iterator();
final Iterator<?> iterator2 = o2.iterator();
while (iterator1.hasNext() && iterator2.hasNext()) {
final Object next1 = iterator1.next();
final Object next2 = iterator2.next();
if (next1 instanceof Equable && next2 instanceof Equable) {
if (!((Equable) next1).equalTo((Equable) next2)) {
return true;
}
} else {
return !Objects.equals(next1, next2);
}
}
return false;
}
/**
* 需要比較的 Getter 方法列表(原理是通過Getter 的 apply 方法得到實際的值,再進行比較)
*
* @param <T> 泛型
* @return getter 方法列表
*/
default <T extends Equable> List<EqualGetter<T>> listEquableGetter() {
return Collections.emptyList();
}
/**
* getter方法介面定義
*/
@FunctionalInterface
interface EqualGetter<T extends Equable> extends Serializable {
Object apply(T source);
}
}
單元測試
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
/**
* @author lilou
* @date 2022/4/9 9:24
*/
public class EqualDemoTest {
@Test
public void testSimpleTrue() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");
// // s1 和 s2 相等
assert s1.equalTo(s2);
}
@Test
public void testSimpleFalse() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s3 = new EqualDemo();
s3.setId(1);
s3.setUsername("bob");
s3.setAge(18);
// // s1 和 s2 相等
assert !s1.equalTo(s3);
}
@Test
public void testCollectionTrue() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");
final EqualDemo s3 = new EqualDemo();
s3.setId(1);
s3.setUsername("bob");
s3.setAge(18);
final EqualDemo s11 = new EqualDemo();
s11.setId(1);
s11.setUsername("bob");
final ArrayList<EqualDemo> list11 = new ArrayList<>();
list11.add(s2);
list11.add(s3);
s11.setList(list11);
final EqualDemo s12 = new EqualDemo();
s12.setId(1);
s12.setUsername("bob");
final ArrayList<EqualDemo> list12 = new ArrayList<>();
list12.add(s2);
list12.add(s3);
s12.setList(list12);
assert s12.equalTo(s11);
}
@Test
public void testCollectionFalse() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");
final EqualDemo s3 = new EqualDemo();
s3.setId(1);
s3.setUsername("bob");
s3.setAge(18);
final EqualDemo s11 = new EqualDemo();
s11.setId(1);
s11.setUsername("bob");
final ArrayList<EqualDemo> list11 = new ArrayList<>();
list11.add(s2);
list11.add(s3);
s11.setList(list11);
// s11 和 s13 數量不相等
final EqualDemo s13 = new EqualDemo();
s13.setId(1);
s13.setUsername("bob");
final ArrayList<EqualDemo> list13 = new ArrayList<>();
list13.add(s2);
s13.setList(list13);
// s11 和 s14 資料不相等
final EqualDemo s14 = new EqualDemo();
s14.setId(1);
s14.setUsername("bob");
final ArrayList<EqualDemo> list14 = new ArrayList<>();
list14.add(s2);
list14.add(new EqualDemo());
s14.setList(list14);
assert !s13.equalTo(s11);
assert !s14.equalTo(s11);
}
@Test
public void testMapTrue() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");
final EqualDemo s3 = new EqualDemo();
s3.setId(1);
s3.setUsername("bob");
s3.setAge(18);
s3.setMap(new HashMap<String, EqualDemo>() {{
put("key", s1);
}});
final EqualDemo s4 = new EqualDemo();
s4.setId(1);
s4.setUsername("bob");
s4.setAge(18);
s4.setMap(new HashMap<String, EqualDemo>() {{
put("key", s2);
}});
assert s3.equalTo(s4);
}
@Test
public void testMapFalse() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob1");
final EqualDemo s3 = new EqualDemo();
s3.setId(1);
s3.setUsername("bob");
s3.setAge(18);
s3.setMap(new HashMap<String, EqualDemo>() {{
put("key", s1);
}});
final EqualDemo s4 = new EqualDemo();
s4.setId(1);
s4.setUsername("bob");
s4.setAge(18);
s4.setMap(new HashMap<String, EqualDemo>() {{
put("key", s2);
}});
assert !s3.equalTo(s4);
}
}
彩蛋
如果不想繼承 Equable 介面,或者想相容之前的資料,可以通工具類中的靜態方法來比較,原始碼如下
public class FieldUtil{
/**
* 判斷兩個物件是否相等,根據 getter 方法引用來比較
*
* @param t1 物件1
* @param t2 物件2
* @param getters getter 方法引用列表
* @param <T> 輸入型別
* @param <U> 輸出型別
* @return 是否相等
*/
public static <T, U> boolean equal(T t1, T t2, List<IGetter<T, U>> getters) {
for (IGetter<T, U> getter : getters) {
if (!Objects.equals(getter.apply(t1), getter.apply(t2))) {
return false;
}
}
return true;
}
/**
* getter方法介面定義
*/
@FunctionalInterface
public interface IGetter<T, U> extends Serializable {
U apply(T source);
}
}
// 使用
public class Test {
public static void main(String[] args) {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");
final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");
// true
System.out.println(FieldUtil.equal(s1, s2, Arrays.asList(
EqualDemo::getId,
EqualDemo::getUsername,
EqualDemo::getList,
EqualDemo::getMap
)));
}
}