Collection of a type that does not have equals and hashCode

Viewed 70

I have to use a type from a 3rd party library that does not have equals and hashCode.

In my example this will be played by the class Box which contains some data. I have create a custom static method which compares to instances according to the specification. In the example if the data are the same.

The problem is that my implementation still does not take into account the cardinality of the unique instances.

import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.Objects;
import java.util.function.BiPredicate;

import static org.assertj.core.util.Lists.newArrayList;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CustomEqualsTest {

    public static final BiPredicate<Box, Box> EQUALS = CustomEqualsTest::equals;

    static boolean equals(Box b1, Box b2) {
        if (b1 == b2) {
            return true;
        }
        if (b1 != null && b2 != null) {
            return Objects.equals(b1.data, b2.data);
        }
        return false;
    }

    static class Box {
        Integer data;

        private Box(Integer data) {
            this.data = data;
        }

        static Box of(Integer data) {
            return new Box(data);
        }
        // intentionally skip equals
        // intentionally skip hashCode
    }

    @Test
    void testCustomEquals() {
        assertTrue(equals(
                newArrayList(Box.of(1)),
                newArrayList(Box.of(1)), EQUALS));
        assertFalse(equals(
                newArrayList(Box.of(1)),
                newArrayList(), EQUALS));
        assertFalse(equals(
                newArrayList(Box.of(1)),
                newArrayList((Box) null), EQUALS));
        assertFalse(equals(
                newArrayList(Box.of(1)),
                newArrayList(Box.of(2)), EQUALS));
        assertFalse(equals(
                newArrayList(Box.of(1), Box.of(1)),
                newArrayList(Box.of(1)), EQUALS));

        // This fails:
        assertFalse(
                equals(
                        newArrayList(Box.of(1), Box.of(1), Box.of(2)),
                        newArrayList(Box.of(1), Box.of(2), Box.of(2)),
                        EQUALS));

    }

    static <T> boolean equals(Collection<T> collection1, Collection<T> collection2, BiPredicate<T, T> equals) {
        if (collection1.size() != collection2.size()) {
            return false;
        }

        for (T t1 : collection1) {
            boolean match = false;
            for (T t2 : collection2) {
                if (equals.test(t1, t2)) {
                    match = true;
                    break;
                }
            }
            if (!match) {
                return false;
            }
        }
        return true;
    }

}
1 Answers

Since you can't provide equals and hashCode, most collections are out of the question. This leaves us only with tree structures(TreeMap to be precise) and custom Comparator to work with. Example null safe comparator:

public static final Comparator<Box> COMPARATOR = Comparator.nullsLast(Comparator.comparing(Box::getData));

Count the elements in each collection using TreeMap

static <T> boolean equals(Collection<T> collection1, Collection<T> collection2, BiPredicate<T, T> equals, Comparator<T> comparator) {
  if (collection1.size() != collection2.size()) {
    return false;
  }
  
  Map<T, Integer> m1 = new TreeMap<>(comparator);
  for (T t : collection1) {
    m1.merge(t, 1, Integer::sum);
  }
  Map<T, Integer> m2 = new TreeMap<>(comparator);
  for (T t : collection2) {
    m2.merge(t, 1, Integer::sum);
  }
  
  if (m1.size() != m2.size()) {
    return false;
  }
  Iterator<Map.Entry<T, Integer>> m2Iterator = m2.entrySet().iterator();
  for (Map.Entry<T, Integer> m1Entry : m1.entrySet()) {
    Map.Entry<T, Integer> m2Entry = m2Iterator.next();
    if (!equals.test(m1Entry.getKey(), m2Entry.getKey())) {
      return false;
    }
    if (!m1Entry.getValue().equals(m2Entry.getValue())) {
      return false;
    }
  }
  return true;
}

Entries now will be in the same order, so we can safely compare current keys(Box) with current counts.

Other option would be to collect collections to sorted lists and compare each element.

static <T> boolean equals(Collection<T> collection1, Collection<T> collection2, BiPredicate<T, T> equals, Comparator<T> comparator) {
  if (collection1.size() != collection2.size()) {
    return false;
  }
  
  List<T> l1 = collection1.stream().sorted(comparator).collect(Collectors.toList());
  List<T> l2 = collection2.stream().sorted(comparator).collect(Collectors.toList());
  for (int i = 0; i < l1.size(); i++) {
    if (!equals.test(l1.get(i), l2.get(i))) {
      return false;
    }
  }
  return true;
}

If collections are guaranteed to be lists and you don't need to keep them in input order, it's possible to sort directly without streaming. I would stick with 2nd option, if possible, it's shorter and more readable i think.

Related