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);
Если вы хотите сделать это параллельно, вы должны использовать параллельную карту.