2
\$\begingroup\$

I'd like to be able to compare two objects with different equality checks. This should work if they're in a Set, List or Map as well.

An example use case is testing of deserialzation of a entity which might contain sensitive information, like passwords, the server side, but not in a REST response.

I'd be interested if you see any design flaws or uncovered issues.

I'd also be grateful for a hint about existing implementations of FLOSS projects.

/**
 * Used by
 * {@link Tools#equals(java.lang.Object, java.lang.Object, EqualsCheck) }.
 *
 * @author richter
 */
@FunctionalInterface
public interface EqualsCheck<T> {

    boolean equals(T a, T b);
}

and the methods:

/**
 * Does what
 * {@link java.util.Objects#equals(java.lang.Object, java.lang.Object) }
 * does, except allows to use methods to check equality as lambda.
 *
 * @param <T> allows to restrict the type of arguments to compare
 * @param a the first item
 * @param b the first item
 * @param equalsCheck the equality check to use
 * @return {@code true} if {@code a} and {@code b} are equal to each other,
 * {@code false} otherwise
 */
public static <T> boolean equals(T a, T b,
        EqualsCheck<T> equalsCheck) {
    return (a == b) || (a != null && equalsCheck.equals(a, b));
}

/**
 * Checks that all elements in {@code a} are contained in {@code b} and vice versa.
 *
 * @param <T> allows to restrict the type of arguments to compare
 * @param a the first set
 * @param b the second set
 * @param equalsCheck the equality check to use
 * @return {@code true} if all elements in {@code a} are contained in
 * {@code b} and {@code b} contains no other items, {@code false} otherwise
 */
public static <T> boolean equalsSet(Set<T> a, Set<T> b,
        EqualsCheck<T> equalsCheck) {
    if(a == b) {
        return true;
    }
    if(a.stream().anyMatch(a0 -> b.stream().noneMatch(b0 -> equalsCheck.equals(a0, b0)))) {
        return false;
    }
    if(b.stream().anyMatch(b0 -> a.stream().noneMatch(a0 -> equalsCheck.equals(b0, a0)))) {
        return false;
    }
    return true;
}

/**
 * Checks that all elements in {@code a} are contained in {@code b} at the
 * same position and vice versa.
 *
 * @param <T> allows to restrict the type of arguments to compare
 * @param a the first list
 * @param b the second list
 * @param equalsCheck the equality check to use
 * @return {@code true} if all elements in {@code a} are contained in
 * {@code b} at the same position and {@code b} contains no other elements,
 * {@code false} otherwise
 */
public static <T> boolean equalsList(List<T> a, List<T> b,
        EqualsCheck<T> equalsCheck) {
    if(a == b) {
        return true;
    }
    if(a.size() != b.size()) {
        return false;
    }
    Iterator<T> aItr = a.iterator();
    Iterator<T> bItr = b.iterator();
    while(aItr.hasNext()) {
        T aNxt = aItr.next();
        T bNxt = bItr.next();
        if(!equalsCheck.equals(aNxt, bNxt)) {
            return false;
        }
    }
    return true;
}

/**
 * Checks that all mappings between key and value in {@code a} are contained
 * in {@code b} and vice versa.
 *
 * @param <K> allows to restrict the type of keys in both maps
 * @param <V> allows to restrict the type of values in both maps
 * @param a the first map
 * @param b the second map
 * @param equalsCheckKey the equality check to use for keys
 * @param equalsCheckValue the equality check to use for values
 * @return {@code true} if the key set of {@code a} and {@code b} is equals
 * as defined by
 * {@link #equalsSet(java.util.Set, java.util.Set, EqualsCheck) }
 * and all keys in {@code a} are mapped to the same values as they are in
 * {@code b}, {@code false} otherwise
 */
public static <K,V> boolean equalsMap(Map<K,V> a, Map<K,V> b,
        EqualsCheck<K> equalsCheckKey,
        EqualsCheck<V> equalsCheckValue) {
    if(a == b) {
        return true;
    }
    if(a.size() != b.size()) {
        return false;
    }
    if(!equalsSet(a.keySet(), b.keySet(), equalsCheckKey)) {
        return false;
    }
    for(K key : a.keySet()) {
        if(!equalsCheckValue.equals(a.get(key), b.get(key))) {
            return false;
        }
    }
    return true;
}
\$\endgroup\$
4
  • \$\begingroup\$ The code is now available at gitlab.com/krichter/equals-lambda and github.com/krichter722/equals-lambda or as Maven artifact de.richtercloud:equals-lambda:1.1 from Sonatype releases. \$\endgroup\$ May 30, 2018 at 16:03
  • \$\begingroup\$ Are you sure equalsSet works as intended? \$\endgroup\$
    – dfhwze
    Aug 23, 2019 at 12:14
  • \$\begingroup\$ Maybe I'm wrong here, but it seems you check not only Checks that all elements in {@code a} are contained in {@code b} but also Checks that all elements in {@code b} are contained in {@code a} \$\endgroup\$
    – dfhwze
    Aug 23, 2019 at 13:18
  • 1
    \$\begingroup\$ @dfhwze Correct. If only the first would be checked I'd check whether a is a equals or a subset of b. I want to check for equality. The Javadoc comment is thus wrong. I'll correct that, thanks. \$\endgroup\$ Aug 23, 2019 at 13:27

2 Answers 2

2
\$\begingroup\$

equals

You have provided this method, but don't reuse it in the other methods. For instance, when you do:

 if(!equalsCheck.equals(aNxt, bNxt)) {
        return false;
 }

you could have done the below to take into account the null checks:

   if(!equals(aNxt, bNxt, equalsCheck)) {
        return false;
   }

equalsSet

This could improved, by:

  • make buffer for set b
  • loop set a, remove any b from buffer that is found that equals; exit when no match
  • if any remaining items in buffer, the sets are not equal
\$\endgroup\$
0
\$\begingroup\$

i think there is no need to use an Iterator for you list compare since you can simply compare the objects inside of the list directly

boolean hasMismatch = IntStream.range(0,a.size()).allMatch(e -> a.get(e).equals(a.get(e)));

or using your functionallity again

boolean hasMismatch = IntStream.range(0,a.size()).allMatch(e -> equalsCheck.equals(a.get(e), b.get(e));

NOTE

richtercloud.project1.jar.controller is an interesting package name - maybe you can find something more suitable

\$\endgroup\$
1
  • \$\begingroup\$ Mhmm... accessing a List with get(int) can cause O(length^2) for linked list or similar implementation whereas an Iterator cost O(1), maybe O(length) for construction and O(length) for the iteration resulting in O(length) vs. O(length^2). The package name is not part of code review. \$\endgroup\$ Mar 16, 2018 at 13:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.