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?
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.
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With