it-swarm.com.ru

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

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

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

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

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
56
tmn

Операция 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.

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

ОБНОВЛЕНИЕ

Можно избавиться от параметра типа 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);
98
Stuart Marks

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

 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();
27
Louis Wasserman

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

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

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

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

или же

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

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

Единственная часть, которую я пытаюсь выяснить, - это то, как 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 , например test . Итак, предикат

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

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

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

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

* Помимо того, что хорошо сделано, @ Stuart-отметки :)

3
Jamish

Вы можете использовать метод 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 Collections.

2
Craig P. Motlin

Еще один способ найти отличительные элементы

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

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

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

0
saka1029

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

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