Есть ли в Java простой способ получить разницу между двумя коллекциями с помощью настраиваемой функции равенства без переопределения равенства?

Я открыт для использования библиотеки lib. Я просто хочу что-то простое, чтобы различать две коллекции по другим критериям, чем обычная функция равенства.

Сейчас я использую что-то вроде:

collection1.stream()
           .filter(element -> !collection2.stream()
                                          .anyMatch(element2 -> element2.equalsWithoutSomeField(element)))
           .collect(Collectors.toSet());

и я бы хотел что-то вроде:

Collections.diff(collection1, collection2, Foo::equalsWithoutSomeField);

(править) Больше контекста:

Следует упомянуть, что я ищу то, что уже существует, а не кодирую это сам. Я мог бы написать небольшую утилиту из ваших идей, если ничего не существует.

Кроме того, в моем случае настоящие дубликаты невозможны: коллекции - это наборы. Однако дубликаты в соответствии с обычным равенством возможны и не должны удаляться этой операцией. Кажется, это ограничение во многих возможных решениях.

Я думаю, что ваш подход хорош и максимально эффективен. Вы можете просто обернуть его в метод (заменив вызов equals... соответствующим BiPredicate). Для удобочитаемости я бы заменил вложенную потоковую передачу в критерии фильтрации вызовом такого метода, как boolean contains(Collection<T>, T, BiPredicate<T, T>), но в остальном здесь вроде бы все в порядке.

Marco13 17.03.2018 03:11

Я думаю, что ключевым моментом здесь является пользовательская функция равенства без переопределения равенства, а не сама разница.

Jose Da Silva 17.03.2018 03:41

@JoseDaSilva Если это было так, в вопросе следует сделать много более ясным. Хотя тогда ответ будет BiPredicate<T, T> p = (t0,t1) -> t0.equalsWithoutSomeField(t1);, что является довольно "тривиальной" синтаксической деталью ...

Marco13 17.03.2018 20:07

Я согласен, если у него уже есть метод Foo::equalsWithoutSomeField, правильным ответом будет простая статическая функция, использующая этот BiPredicate. Решение может использовать тот же метод сравнения, который он уже предоставил, потому что он работает нормально. В противном случае и даже для того, чтобы помочь людям с аналогичной проблемой, у которых нет этого equalsWithoutSomeField, ключ будет здесь, в этом методе.

Jose Da Silva 17.03.2018 21:11

@ Marco13, конечно, я могу это сделать. Я искал некоторые скрытые утилиты, о которых я не знал, которые будут делать этот OOTB, передавая только коллекции и предикат. Как я уже упоминал в вопросе.

FredBoutin 19.03.2018 16:01
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
5
5
502
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Мы используем аналогичные методы в нашем проекте, чтобы сократить повторяющуюся фильтрацию коллекции. Мы начали с нескольких основных строительных блоков:

static <T> boolean anyMatch(Collection<T> set, Predicate<T> match) {
    for (T object : set)
        if (match.test(object))
            return true;
    return false;
}

Исходя из этого, мы можем легко реализовать такие методы, как noneMatch, и более сложные, такие как isSubset или ваш diff:

static <E> Collection<E> disjunctiveUnion(Collection<E> c1, Collection<E> c2, BiPredicate<E, E> match)
{
    ArrayList<E> diff = new ArrayList<>();
    diff.addAll(c1);
    diff.addAll(c2);
    diff.removeIf(e -> anyMatch(c1, e1 -> match.test(e, e1)) 
                       && anyMatch(c2, e2 -> match.test(e, e2)));
    return diff;
}

Обратите внимание, что некоторые возможности для настройки производительности наверняка есть. Но разделение его на небольшие методы помогает с легкостью понять и использовать их. В коде они читаются довольно хорошо.

Затем вы использовали бы его, как вы уже сказали:

CollectionUtils.disjunctiveUnion(collection1, collection2, Foo::equalsWithoutSomeField);

Принимая во внимание предложение Хосе да Силвы, вы даже можете использовать Comparator для построения ваших критериев на лету:

Comparator<E> special = Comparator.comparing(Foo::thisField)
                                  .thenComparing(Foo::thatField);
BiPredicate specialMatch = (e1, e2) -> special.compare(e1, e2) == 0;

Есть ли причина сначала добавить элементы все, а затем удалить ненужные, вместо того, чтобы удалять только те из c1, которые уже содержатся в c2?

Marco13 17.03.2018 03:15

Создаю новую коллекцию, чтобы не менять входные параметры. Если, например, в настоящее время выполняется итерация одного из входных параметров, мы не хотим изменять его и запускать ConcurrentModificationExceptions. Вы, конечно, можете добавить только соответствующие элементы в новую коллекцию, но removeIf сохраняет ее очень читаемой (imho) для целей SO. Попав в свою кодовую базу, вы можете улучшить ее для повышения производительности.

Malte Hartwig 19.03.2018 08:01

Конечно, нет изменение входных коллекций важно как часть контракта, но я думаю, что независимо от этого реализацию можно было бы сделать более эффективной. Однако вы уже упомянули о возможных улучшениях, так что все в порядке.

Marco13 19.03.2018 23:11
static <T> Collection<T> diff(Collection<T> minuend, Collection<T> subtrahend, BiPredicate<T, T> equals) {
    Set<Wrapper<T>> w1 = minuend.stream().map(item -> new Wrapper<>(item, equals)).collect(Collectors.toSet());
    Set<Wrapper<T>> w2 = subtrahend.stream().map(item -> new Wrapper<>(item, equals)).collect(Collectors.toSet());
    w1.removeAll(w2);
    return w1.stream().map(w -> w.item).collect(Collectors.toList());
}

static class Wrapper<T> {
    T item;
    BiPredicate<T, T> equals;

    Wrapper(T item, BiPredicate<T, T> equals) {
        this.item = item;
        this.equals = equals;
    }

    @Override
    public int hashCode() {
        // all items have same hash code, check equals
        return 1;
    }

    @Override
    public boolean equals(Object that) {
        return equals.test(this.item, ((Wrapper<T>) that).item);
    }
}

Вы можете привести пример, показывающий, как это предполагается использовать?

Marco13 17.03.2018 03:12

Прежде чем сравнивать мыслительные элементы, необходимо преобразовать их. Отредактировано для использования настраиваемого предиката.

Bax 17.03.2018 17:15
Ответ принят как подходящий

Вы можете использовать UnifiedSetWithHashingStrategy из Коллекции Eclipse. UnifiedSetWithHashingStrategy позволяет создавать набор с помощью собственного HashingStrategy. HashingStrategy позволяет пользователю использовать собственные hashCode() и equals(). hashCode() и equals() Объекта не используются.

Изменить в соответствии с требованиями OP через комментарий:

Вы можете использовать reject() или removeIf() в зависимости от ваших требований.

Пример кода:

// Common code
Person person1 = new Person("A", "A");
Person person2 = new Person("B", "B");
Person person3 = new Person("C", "A");
Person person4 = new Person("A", "D");
Person person5 = new Person("E", "E");

MutableSet<Person> personSet1 = Sets.mutable.with(person1, person2, person3);
MutableSet<Person> personSet2 = Sets.mutable.with(person2, person4, person5);

HashingStrategy<Person> hashingStrategy =
    HashingStrategies.fromFunction(Person::getLastName);

1) Использование reject(): Создает новый Set, содержащий все элементы, которые удовлетворяют нетPredicate.

@Test
public void reject()
{
    MutableSet<Person> personHashingStrategySet = HashingStrategySets.mutable.withAll(
        hashingStrategy, personSet2);

    // reject creates a new copy
    MutableSet<Person> rejectSet = personSet1.reject(personHashingStrategySet::contains);
    Assert.assertEquals(Sets.mutable.with(person1, person3), rejectSet);
}

2) Использование removeIf(): мутирует исходный Set с помощью удаление элементов, которые удовлетворяют требованиям Predicate.

@Test
public void removeIfTest()
{
    MutableSet<Person> personHashingStrategySet = HashingStrategySets.mutable.withAll(
        hashingStrategy, personSet2);

    // removeIf mutates the personSet1
    personSet1.removeIf(personHashingStrategySet::contains);
    Assert.assertEquals(Sets.mutable.with(person1, person3), personSet1);
}

Ответ перед требованием от OP через комментарий: Сохранено для справки, если другие сочтут это полезным.

3) Использование Sets.differenceInto() API, доступного в коллекциях Eclipse:

В приведенном ниже коде set1 и set2 - это два набора, в которых используются Person и equals()hashCode(). differenceSet - это UnifiedSetWithHashingStrategy, поэтому он использует lastNameHashingStrategy для определения уникальности. Следовательно, хотя set2 не содержит person3, но имеет то же последнее имя, что и person1, differenceSet содержит только person1.

@Test
public void differenceTest()
{
    MutableSet<Person> differenceSet = Sets.differenceInto(
        HashingStrategySets.mutable.with(hashingStrategy), 
        set1, 
        set2);

    Assert.assertEquals(Sets.mutable.with(person1), differenceSet);
}

Класс человека, общий для обоих блоков кода:

public class Person
{
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName)
    {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName()
    {
        return firstName;
    }

    public String getLastName()
    {
        return lastName;
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
        {
            return true;
        }
        if (o == null || getClass() != o.getClass())
        {
            return false;
        }
        Person person = (Person) o;
        return Objects.equals(firstName, person.firstName) &&
                Objects.equals(lastName, person.lastName);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(firstName, lastName);
    }
}

Документы Javadoc:MutableSet, UnifiedSet, UnifiedSetWithHashingStrategy, ХешированиеСтратегии, Наборы, отклонять, removeIf

Примечание: Я участник коллекций Eclipse

Я думал, что это все ... но я не хочу удалять «дубликаты» в соответствии с пользовательской функцией хеширования. Set должен быть построен с использованием реальной функции хеширования, а затем функция diff, которая мне нужна, будет использовать пользовательскую. Итак, в вашем тестовом примере я хотел бы, чтобы в окончательном наборе были person1 и person3. Но закройте одно, может быть полезно для других.

FredBoutin 19.03.2018 16:21

Я обновил ответ, который удовлетворяет вашим требованиям. Пожалуйста, дайте мне знать, если это сработает. Спасибо!

Nikhil Nanivadekar 20.03.2018 04:50

Не так просто, как то, что я искал, но действительно интересно!

FredBoutin 20.03.2018 16:51

Другие вопросы по теме