2

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.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Vladislav Lezhnin
  • 837
  • 1
  • 7
  • 17

4 Answers4

1

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);
Olivier Grégoire
  • 33,839
  • 23
  • 96
  • 137
  • Sets are actually very easy and fast to compare, but only when they're of the same type. – Kayaman Apr 02 '15 at 10:58
  • If they really were, there wouldn't be [so many helper methods in Guava](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Sets.html). Not that I'm against your word, but `Set` clearly suffers the most from the lack of thoughts when first designed in Java 1.2. – Olivier Grégoire Apr 02 '15 at 11:49
  • Well, most of those methods are of the form `newXXXSet` which I would count out. Then count out the ones that return immutable views, and you don't actually have that many methods that would provide non-existing functionality. – Kayaman Apr 02 '15 at 11:54
  • Of course, you discount the `newXxxSet` and the two immutable methods... Those are completely auxiliary to the argument. – Olivier Grégoire Apr 02 '15 at 12:01
  • I think when you call toArray() - you lose the advantage of streams' laziness. Arrays.equals works with copied and sorted arrays already. – Vladislav Lezhnin Apr 02 '15 at 14:41
  • The laziness of Stream happens in the streams, not in the final method. Of course you need to build at some point. From the [javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html) : "Laziness-seeking. Many stream operations, such as filtering, mapping, or duplicate removal, can be implemented lazily, exposing opportunities for optimization." – Olivier Grégoire Apr 02 '15 at 14:44
  • I agree. I'm just saying that there is no laziness used in your particular solution. No offense, just want to avoid misunderstanding. – Vladislav Lezhnin Apr 02 '15 at 14:52
  • Yes, there is. This solution doesn't use temporary collections to 1. transform, 2. sort, 3. move in array. The laziness happens in the way that each element is transformed put in array and then sorted. That's the laziness. Doing it the extensive way would create several intermediary collections and then only copying the last collection into an array. Using an array instead of another structure makes use of a structure that is invisbly taken in (nearly) all other structures, so performance-wise, it's better. – Olivier Grégoire Apr 02 '15 at 15:00
  • @OlivierGrégoire I have similar basic question related to guava [here](http://stackoverflow.com/questions/29530071/how-to-call-other-machine-from-the-linkedlist-if-one-is-down). Actually I am using ListenableFuture call back so it is kind of related to that. If you can help me out, then it will be of great help. – john Apr 09 '15 at 20:32
0

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.

NimChimpsky
  • 46,453
  • 60
  • 198
  • 311
0

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;
}
  • What would be the difference if I iterate through a set elements one by one, what I actually do. – Vladislav Lezhnin Apr 02 '15 at 10:22
  • Lists maintain order of insertion. And from what I gather, you want your result to be true only if each collection has the exact same elements in the exact same positions. If I misinterpreted your question, please let me know. –  Apr 02 '15 at 10:26
  • no, same positions is not a requirement here. Actually I use Set interface which does not require preserving positions – Vladislav Lezhnin Apr 02 '15 at 10:29
  • Oh, ok. Then disregard the solution. If I come up with something for sets, I'll post a comment. –  Apr 02 '15 at 10:45
0

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 .

Frank Andres
  • 146
  • 5