Vladislav Lezhnin
Vladislav Lezhnin

Reputation: 837

Compare two sets of different types

I'm looking for a way to tell if two sets of different element types are identical if I can state one-to-one relation between those element types. Is there a standard way for doing this in java or maybe guava or apache commons?

Here is my own implementation of this task. For example, I have two element classes which I know how to compare. For simplicity, I compare them by id field:

class ValueObject {
    public int id;
    public ValueObject(int id) { this.id=id; }
    public static ValueObject of(int id) { return new ValueObject(id); }
}

class DTO {
    public int id;
    public DTO(int id) { this.id=id; }
    public static DTO of(int id) { return new DTO(id); }
}

Then I define an interface which does the comparison

interface TwoTypesComparator<L,R> {
    boolean areIdentical(L left, R right);
}

And the actual method for comparing sets looks like this

public static <L,R> boolean areIdentical(Set<L> left, Set<R> right, TwoTypesComparator<L,R> comparator) {
    if (left.size() != right.size()) return false;
    boolean found;
    for (L l : left) {
        found = false;
        for (R r : right) {
            if (comparator.areIdentical(l, r)) {
                found = true; break;
            }
        }
        if (!found) return false;
    }
    return true;
}

Example of a client code

HashSet<ValueObject> valueObjects = new HashSet<ValueObject>();
valueObjects.add(ValueObject.of(1));
valueObjects.add(ValueObject.of(2));
valueObjects.add(ValueObject.of(3));

HashSet<DTO> dtos = new HashSet<DTO>();
dtos.add(DTO.of(1));
dtos.add(DTO.of(2));
dtos.add(DTO.of(34));

System.out.println(areIdentical(valueObjects, dtos, new TwoTypesComparator<ValueObject, DTO>() {
    @Override
    public boolean areIdentical(ValueObject left, DTO right) {
        return left.id == right.id;
    }
}));

I'm looking for the standard solution to to this task. Or any suggestions how to improve this code are welcome.

Upvotes: 2

Views: 1887

Answers (4)

Olivier Gr&#233;goire
Olivier Gr&#233;goire

Reputation: 35457

This is what I would do in your case. You have sets. Sets are hard to compare, but on top of that, you want to compare on their id.

I see only one proper solution where you have to normalize the wanted values (extract their id) then sort those ids, then compare them in order, because if you don't sort and compare you can possibly skip pass over duplicates and/or values.

Think about the fact that Java 8 allows you to play lazy with streams. So don't rush over and think that extracting, then sorting then copying is long. Lazyness allows it to be rather fast compared to iterative solutions.

HashSet<ValueObject> valueObjects = new HashSet<>();
valueObjects.add(ValueObject.of(1));
valueObjects.add(ValueObject.of(2));
valueObjects.add(ValueObject.of(3));

HashSet<DTO> dtos = new HashSet<>();
dtos.add(DTO.of(1));
dtos.add(DTO.of(2));
dtos.add(DTO.of(34));

boolean areIdentical = Arrays.equals(
    valueObjects.stream()
        .mapToInt((v) -> v.id)
        .sorted()
        .toArray(),
    dtos.stream()
        .mapToInt((d) -> d.id)
        .sorted()
        .toArray()
);

You want to generalize the solution? No problem.

public static <T extends Comparable<?>> boolean areIdentical(Collection<ValueObject> vos, Function<ValueObject, T> voKeyExtractor, Collection<DTO> dtos, Function<DTO, T> dtoKeyExtractor) {
  return Arrays.equals(
    vos.stream()
      .map(voKeyExtractor)
      .sorted()
      .toArray(),
    dtos.stream()
      .map(dtoKeyExtractor)
      .sorted()
      .toArray()
  );
}

And for a T that is not comparable:

public static <T> boolean areIdentical(Collection<ValueObject> vos, Function<ValueObject, T> voKeyExtractor, Collection<DTO> dtos, Function<DTO, T> dtoKeyExtractor, Comparator<T> comparator) {
  return Arrays.equals(
    vos.stream()
      .map(voKeyExtractor)
      .sorted(comparator)
      .toArray(),
    dtos.stream()
      .map(dtoKeyExtractor)
      .sorted(comparator)
      .toArray()
  );
}

You mention Guava and if you don't have Java 8, you can do the following, using the same algorithm:

List<Integer> voIds = FluentIterables.from(valueObjects)
  .transform(valueObjectIdGetter())
  .toSortedList(intComparator());
List<Integer> dtoIds = FluentIterables.from(dtos)
  .transform(dtoIdGetter())
  .toSortedList(intComparator());
return voIds.equals(dtoIds);

Upvotes: 1

NimChimpsky
NimChimpsky

Reputation: 47290

You could override equals and hashcode on the dto/value object and then do : leftSet.containsAll(rightSet) && leftSet.size().equals(rightSet.size())

If you can't alter the element classes, make a decorator and have the sets be of the decorator type.

Upvotes: 0

Frank Andres
Frank Andres

Reputation: 146

Your current method is incorrect or at least inconsistent for general sets.

Imagine the following:

L contains the Pairs (1,1), (1,2), (2,1).

R contains the Pairs (1,1), (2,1), (2,2).

Now if your id is the first value your compare would return true but are those sets really equal? The problem is that you have no guarantee that there is at most one Element with the same id in the set because you don't know how L and R implement equals so my advise would be to not compare sets of different types.

If you really need to compare two Sets the way you described I would go for copying all Elements from L to a List and then go through R and every time you find the Element in L remove it from the List. Just make sure you use LinkedList instead of ArrayList .

Upvotes: 0

user4655581
user4655581

Reputation:

Another solution would be to use List instead of Set (if you are allowed to do so). List has a method called get(int index) that retrieves the element at the specified index and you can compare them one by one when both your lists have the same size. More on lists: http://docs.oracle.com/javase/7/docs/api/java/util/List.html

Also, avoid using public variables in your classes. A good practice is to make your variables private and use getter and setter methods.

Instantiate lists and add values

    List<ValueObject> list = new ArrayList<>();
    List<DTO> list2 = new ArrayList<>();

    list.add(ValueObject.of(1));
    list.add(ValueObject.of(2));
    list.add(ValueObject.of(3));

    list2.add(DTO.of(1));
    list2.add(DTO.of(2));
    list2.add(DTO.of(34));

Method that compares lists

public boolean compareLists(List<ValueObject> list, List<DTO> list2) {
    if(list.size() != list2.size()) {
        return false;
    }
    for(int i = 0; i < list.size(); i++) {
        if(list.get(i).id == list2.get(i).id) {
            continue;
        } else {
            return false;
        }
    }
    return true;
}

Upvotes: 0

Related Questions