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


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

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

вот как это будет называться

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

вот класс сопряжения

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

    }

обновление:

протестирована функция Стюарта ниже, и она, похоже, отлично работает. Этот операция ниже различает по первой букве каждой строки. Единственная часть, которую я пытаюсь выяснить, - это то, как 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
9 52

9 ответов:

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

/**
 * 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 класса.

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

обновление

можно избавиться от 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);

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

(еще один вариант этого, который, вероятно, упростит это сделать 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);

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

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

вариация на Стюарт отмечает второе обновление. С помощью набора.

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

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

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

или

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

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

единственная часть, которую я пытаюсь выяснить, - это то, как 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 создается только один раз. Вот объяснение:

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

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

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

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

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

*помимо того, что хорошо сделано, @stuart-marks :)

можно использовать distinct(HashingStrategy) метод Коллекции Eclipse.

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 можно вызвать метод непосредственно из списка.

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.

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

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

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

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

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

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