1. 程式人生 > 其它 >【Java】通過 getter 方法引用,來比較兩個物件是否相等

【Java】通過 getter 方法引用,來比較兩個物件是否相等

背景

編寫程式碼時,會經常需要編寫兩個物件是否相等的邏輯,一般會有如下做法

  1. 直接寫在業務程式碼中;
  2. 單獨寫個方法,業務程式碼中呼叫;
  3. 重寫 equals 方法;

上面這些做法,都比較複雜,如果屬性太多或複雜點(如果是 list 和 map 就更復雜了),就需要編寫更多的判斷邏輯程式碼了。

想法(需求)

如果能只需要提供比較的方法引用列表,有個地方能自動方法引用取值,並比較就好了。

思路

  1. 在 java8 中可以使用方法引用,如:People::getName;
  2. 可以將所有要比較的 Getter 儲存到列表中;
  3. 在 比較的時候,根據 方法引用獲取具體的值進行比較;
  4. 全部比較都相等了,就認為是相等的。

舉個例子 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
        )));
    }
}

參考資源