Java Lambda Stream Distinct() на произвольном ключе?

Я часто сталкивался с проблемой ямбда-выражений Java, где, когда я хотел различать() поток по произвольному свойству или методу объекта, но хотел сохранить объект, а не сопоставить его с этим свойством или методом. Я начал создавать контейнеры, как обсуждалось здесь, но я начал делать это достаточно, чтобы он стал раздражать и сделал много классов шаблонов.

Я объединил этот класс Pairing, который содержит два объекта двух типов и позволяет вам указывать отключение левого, правого или обоих объектов. Мой вопрос: нет ли встроенной функции лямбда-потока в отдельном() ключевом поставщике каких-то родов? Это меня действительно удивило бы. Если нет, будет ли этот класс надежно выполнять эту функцию?

Вот как это можно было бы назвать

BigDecimal totalShare = orders.stream().map(c -> Pairing.keyLeft(c.getCompany().getId(), c.getShare())).distinct().map(Pairing::getRightItem).reduce(BigDecimal.ZERO, (x,y) -> x.add(y));

Вот класс Pairing

    public final class Pairing<X,Y>  {
           private final X item1;
           private final Y item2;
           private final KeySetup keySetup;

           private static enum KeySetup {LEFT,RIGHT,BOTH};

           private Pairing(X item1, Y item2, KeySetup keySetup) {
                  this.item1 = item1;
                  this.item2 = item2;
                  this.keySetup = keySetup;
           }
           public X getLeftItem() { 
                  return item1;
           }
           public Y getRightItem() { 
                  return item2;
           }

           public static <X,Y> Pairing<X,Y> keyLeft(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.LEFT);
           }

           public static <X,Y> Pairing<X,Y> keyRight(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.RIGHT);
           }
           public static <X,Y> Pairing<X,Y> keyBoth(X item1, Y item2) { 
                  return new Pairing<X,Y>(item1, item2, KeySetup.BOTH);
           }
           public static <X,Y> Pairing<X,Y> forItems(X item1, Y item2) { 
                  return keyBoth(item1, item2);
           }

           @Override
           public int hashCode() {
                  final int prime = 31;
                  int result = 1;
                  if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) {
                  result = prime * result + ((item1 == null) ? 0 : item1.hashCode());
                  }
                  if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) {
                  result = prime * result + ((item2 == null) ? 0 : item2.hashCode());
                  }
                  return result;
           }

           @Override
           public boolean equals(Object obj) {
                  if (this == obj)
                         return true;
                  if (obj == null)
                         return false;
                  if (getClass() != obj.getClass())
                         return false;
                  Pairing<?,?> other = (Pairing<?,?>) obj;
                  if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) {
                         if (item1 == null) {
                               if (other.item1 != null)
                                      return false;
                         } else if (!item1.equals(other.item1))
                               return false;
                  }
                  if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) {
                         if (item2 == null) {
                               if (other.item2 != null)
                                      return false;
                         } else if (!item2.equals(other.item2))
                               return false;
                  }
                  return true;
           }

    }

UPDATE:

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

public class DistinctByKey {

    public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

    public static void main(String[] args) { 

        final ImmutableList<String> arpts = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI");

        arpts.stream().filter(distinctByKey(f -> f.substring(0,1))).forEach(s -> System.out.println(s));
    }

Выход...

ABQ
CHI
PHX
BWI

Ответ 1

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

/**
 * Stateful filter. T is type of stream element, K is type of extracted key.
 */
static class DistinctByKey<T,K> {
    Map<K,Boolean> seen = new ConcurrentHashMap<>();
    Function<T,K> keyExtractor;
    public DistinctByKey(Function<T,K> ke) {
        this.keyExtractor = ke;
    }
    public boolean filter(T t) {
        return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
}

Я не знаю ваших классов домена, но я думаю, что с этим вспомогательным классом вы могли бы сделать то, что хотите:

BigDecimal totalShare = orders.stream()
    .filter(new DistinctByKey<Order,CompanyId>(o -> o.getCompany().getId())::filter)
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

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

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

UPDATE

Можно избавиться от параметра типа K, поскольку он фактически не используется для чего-либо, кроме сохранения на карте. Таким образом, Object достаточно.

/**
 * Stateful filter. T is type of stream element.
 */
static class DistinctByKey<T> {
    Map<Object,Boolean> seen = new ConcurrentHashMap<>();
    Function<T,Object> keyExtractor;
    public DistinctByKey(Function<T,Object> ke) {
        this.keyExtractor = ke;
    }
    public boolean filter(T t) {
        return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
}

BigDecimal totalShare = orders.stream()
    .filter(new DistinctByKey<Order>(o -> o.getCompany().getId())::filter)
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

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

(Другой вариант этого, который, вероятно, упростит это, состоит в том, чтобы сделать DistinctByKey<T> implements Predicate<T> и переименовать метод в eval. Это устранит необходимость использования ссылки на метод и, вероятно, улучшит вывод типа. так же хорошо, как и решение ниже.)

ОБНОВЛЕНИЕ 2

Не могу перестать думать об этом. Вместо класса-помощника используйте функцию более высокого порядка. Мы можем использовать захваченных локальных жителей для поддержания состояния, поэтому нам даже не нужен отдельный класс! Бонус, все упрощается, поэтому вы можете сделать вывод о типе!

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
    Map<Object,Boolean> seen = new ConcurrentHashMap<>();
    return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

BigDecimal totalShare = orders.stream()
    .filter(distinctByKey(o -> o.getCompany().getId()))
    .map(Order::getShare)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Ответ 2

Вы более или менее должны сделать что-то вроде

 elements.stream()
    .collect(Collectors.toMap(
        obj -> extractKey(obj), 
        obj -> obj, 
       (first, second) -> first
           // pick the first if multiple values have the same key
       )).values().stream();

Ответ 3

Вариант второго обновления Стюарта Маркса. Использование набора.

public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
    Set<Object> seen = Collections.newSetFromMap(new ConcurrentHashMap<>());
    return t -> seen.add(keyExtractor.apply(t));
}

Ответ 4

Мы также можем использовать RxJava (очень мощную реактивную библиотеку расширений)

Observable.from(persons).distinct(Person::getName)

или же

Observable.from(persons).distinct(p -> p.getName())

Ответ 5

Чтобы ответить на ваш вопрос во втором обновлении:

Единственная часть, которую я пытаюсь выяснить, заключается в том, как ConcurrentHashMap поддерживает только один экземпляр для всего потока:

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

В вашем примере кода distinctByKey вызывается только один раз, поэтому ConcurrentHashMap создается только один раз. Вот объяснение:

Функция distinctByKey - это просто старая функция, возвращающая объект, и этот объект оказывается предикатом. Имейте в виду, что предикат - это в основном фрагмент кода, который можно оценить позже. Чтобы вручную оценить предикат, вы должны вызвать метод в Predicate interface, например test. Итак, предикат

t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null

- это просто объявление, которое фактически не оценивается внутри distinctByKey.

Предикат передается как любой другой объект. Он возвращается и передается в операцию filter, которая в основном оценивает предикат несколько раз по отношению к каждому элементу потока, вызывая test.

Я уверен, что filter сложнее, чем я это делал, но дело в том, что предикат оценивается много раз за пределами distinctByKey. Нет ничего особенного * о distinctByKey; это просто функция, которую вы вызывали один раз, поэтому ConcurrentHashMap создается только один раз.

* Помимо хорошо сделанных, @stuart-marks:)

Ответ 6

Вы можете использовать метод distinct(HashingStrategy) в Eclipse Collections.

List<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI");
ListIterate.distinct(list, HashingStrategies.fromFunction(s -> s.substring(0, 1)))
    .each(System.out::println);

Если вы можете реорганизовать list для реализации интерфейса Eclipse Collections, вы можете вызвать метод непосредственно в списке.

MutableList<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI");
list.distinct(HashingStrategies.fromFunction(s -> s.substring(0, 1)))
    .each(System.out::println);

HashingStrategy - это просто интерфейс стратегии, который позволяет вам определять пользовательские реализации equals и hashcode.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

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

Ответ 7

Другой способ поиска отдельных элементов

List<String> uniqueObjects = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI")
            .stream()
            .collect(Collectors.groupingBy((p)->p.substring(0,1))) //expression 
            .values()
            .stream()
            .flatMap(e->e.stream().limit(1))
            .collect(Collectors.toList());

Ответ 8

Можно сделать что-то вроде

Set<String> distinctCompany = orders.stream()
        .map(Order::getCompany)
        .collect(Collectors.toSet());

Ответ 9

Set.add(element) возвращает true, если в наборе еще не было element, иначе false. Таким образом, вы можете сделать это.

Set<String> set = new HashSet<>();
BigDecimal totalShare = orders.stream()
    .filter(c -> set.add(c.getCompany().getId()))
    .map(c -> c.getShare())
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Если вы хотите сделать это параллельно, вы должны использовать параллельную карту.