Java 8 объединяет все элементы ListB в ListA, если нет

Мне нужно объединить все элементы listB в другой список listA.

Если элемент уже присутствует (на основе пользовательской проверки равенства) в listA, я не хочу его добавлять.

Я не хочу использовать Set, и я не хочу переопределять equals() и hashCode().

Причины: я не хочу предотвращать дубликаты в списке A как таковой, я только хочу не сливаться с listB, если уже есть элементы в списке A, которые я считаю равными.

Я не хочу переопределять equals() и hashCode(), поскольку это означало бы, что мне нужно убедиться, что моя реализация equals() для элементов имеет место в каждом случае. Однако может быть, что элементы из спискаB не полностью инициализированы, то есть они могут пропустить идентификатор объекта, где это может присутствовать в элементах списка A.

Мой текущий подход включает интерфейс и служебную функцию:

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())
        );
    }
}

И тогда я буду использовать его следующим образом:

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

чтобы получить мой новый список в списке A со всеми элементами, объединенными с B.

Это хороший подход? Есть ли лучший/более простой способ сделать то же самое?

Ответ 1

Вы хотите что-то вроде этого:

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())
    );
}

Вам не нужен интерфейс HasEqualityFunction. Вы можете повторно использовать BiPredicate, чтобы проверить, равны ли эти два объекта относительно вашей логики.

Этот код фильтрует только элементы в listB, которые не содержатся в listA в соответствии с заданным предикатом. Он проходит listA столько раз, сколько есть элементов в listB.


Альтернативной и лучшей реализацией исполнения будет использование класса-оболочки, который обертывает ваши элементы и имеет equals метод вашего предиката:

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())
    );
}

Это сначала обертывает каждый элемент внутри объекта Wrapper и собирает их в Set. Затем он фильтрует элементы listB, которые не содержатся в этом наборе. Проверка равенства выполняется путем делегирования данному предикату. Ограничение состоит в том, что нам также нужно дать hashFunction правильно реализовать hashCode.

Пример кода:

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);

Ответ 2

Вы можете использовать HashingStrategy на основе Set из Eclipse Коллекции

Если вы можете использовать интерфейс MutableList:

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));
}

Если вы не можете изменить тип listA и listB с 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));
}

Примечание. Я участвую в коллекциях Eclipse.