13

I need to merge all elements of a listB into another list listA.

If an element is already present (based on a custom equality-check) in listA I don't want to add it.

I don't want to use Set, and I don't want to override equals() and hashCode().

Reasons are, I don't want to prevent duplicates in listA per se, I only want to not merge from listB if there are already elements in listA which I consider being equal.

I don't want to override equals() and hashCode() since that would mean I need to make sure, my implementation of equals() for the elements hold in every circumstance. It might however be, that elements from listB are not fully initialized, i.e. they might miss an object id, where that might be present in elements of listA.

My current approach involves an interface and a Utility-Function:

public interface HasEqualityFunction<T> {

    public boolean hasEqualData(T other);
}

public class AppleVariety implements HasEqualityFunction<AppleVariety> {
    private String manufacturerName;
    private String varietyName;

    @Override
    public boolean hasEqualData(AppleVariety other) {
        return (this.manufacturerName.equals(other.getManufacturerName())
            && this.varietyName.equals(other.getVarietyName()));
    }

    // ... getter-Methods here
}


public class CollectionUtils {
    public static <T extends HasEqualityFunction> void merge(
        List<T> listA,
        List<T> listB) {
        if (listB.isEmpty()) {
            return;
        }
        Predicate<T> exists
            = (T x) -> {
                return listA.stream().noneMatch(
                        x::hasEqualData);
            };
        listA.addAll(listB.stream()
            .filter(exists)
            .collect(Collectors.toList())
        );
    }
}

And then I'd use it like this:

...
List<AppleVariety> appleVarietiesFromOnePlace = ... init here with some elements
List<AppleVariety> appleVarietiesFromAnotherPlace = ... init here with some elements
CollectionUtils.merge(appleVarietiesFromOnePlace, appleVarietiesFromAnotherPlace);
...

to get my new list in listA with all elements merged from B.

Is this a good approach? Is there a better/easier way to accomplish the same?

SebastianRiemer
  • 1,495
  • 2
  • 20
  • 33

2 Answers2

9

You want something like this:

public static <T> void merge(List<T> listA, List<T> listB, BiPredicate<T, T> areEqual) {
    listA.addAll(listB.stream()
                      .filter(t -> listA.stream().noneMatch(u -> areEqual.test(t, u)))
                      .collect(Collectors.toList())
    );
}

You don't need a HasEqualityFunction interface. You can reuse BiPredicate to test whether the two objects are equal with regard to your logic.

This code filters only the elements in listB which are not contained in listA as per the predicate given. It does traverse listA as many times as there are elements in listB.


An alternative and better performant implementation would be to use a wrapper class that wraps your elements and has as equals method your predicate:

public static <T> void merge(List<T> listA, List<T> listB, BiPredicate<T, T> areEqual, ToIntFunction<T> hashFunction) {

    class Wrapper {
        final T wrapped;
        Wrapper(T wrapped) {
            this.wrapped = wrapped;
        }
        @Override
        public boolean equals(Object obj) {
            return areEqual.test(wrapped, ((Wrapper) obj).wrapped);
        }
        @Override
        public int hashCode() {
            return hashFunction.applyAsInt(wrapped);
        }
    }

    Set<Wrapper> wrapSet = listA.stream().map(Wrapper::new).collect(Collectors.toSet());

    listA.addAll(listB.stream()
                      .filter(t -> !wrapSet.contains(new Wrapper(t)))
                      .collect(Collectors.toList())
    );
}

This first wraps every element inside a Wrapper object and collects them into a Set. Then, it filters the elements of listB that are not contained in this set. The equality test is done by delegating to the given predicate. The constraint is that we also need to give a hashFunction to properly implement hashCode.

Sample code would be:

List<String> listA = new ArrayList<>(Arrays.asList("foo", "bar", "test"));
List<String> listB = new ArrayList<>(Arrays.asList("toto", "foobar"));
CollectionUtils.merge(listA, listB, (s1, s2) -> s1.length() == s2.length(), String::length);
System.out.println(listA);
Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • Thanks, I already applied your suggestion with the BiPredicate and eliminated the Interface. I'll also look into your 2nd suggestion later - using a wrapper class to be able to override equals and hashCode in a specific context is a great idea. – SebastianRiemer Mar 24 '16 at 11:13
2

You can use a HashingStrategy based Set from Eclipse Collections

If you can use MutableList interface:

public static void merge(MutableList<AppleVariety> listA, MutableList<AppleVariety> listB)
{
    MutableSet<AppleVariety> hashingStrategySet = HashingStrategySets.mutable.withAll(
        HashingStrategies.fromFunctions(AppleVariety::getManufacturerName,
            AppleVariety::getVarietyName), 
        listA);   
    listA.addAllIterable(listB.asLazy().reject(hashingStrategySet::contains));
}

If you can't change the type of listA and listB from List:

public static void merge(List<AppleVariety> listA, List<AppleVariety> listB)
{
    MutableSet<AppleVariety> hashingStrategySet = HashingStrategySets.mutable.withAll(
        HashingStrategies.fromFunctions(AppleVariety::getManufacturerName,
            AppleVariety::getVarietyName), 
        listA);
    listA.addAll(ListAdapter.adapt(listB).reject(hashingStrategySet::contains));
}

Note: I am a contributor to Eclipse Collections.

Nikhil Nanivadekar
  • 1,152
  • 11
  • 10