Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

java 8 merge all elements of ListB into ListA if not present

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?

like image 386
SebastianRiemer Avatar asked Mar 24 '16 10:03

SebastianRiemer


People also ask

How do you concatenate an ArrayList element in Java?

Approach: ArrayLists can be joined in Java with the help of Collection. addAll() method. This method is called by the destination ArrayList and the other ArrayList is passed as the parameter to this method. This method appends the second ArrayList to the end of the first ArrayList.

How do you flatten a list in Java?

flatMap() method. The standard solution is to use the Stream. flatMap() method to flatten a List of Lists. The flatMap() method applies the specified mapping function to each element of the stream and flattens it.


1 Answers

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);
like image 159
Tunaki Avatar answered Oct 17 '22 08:10

Tunaki