Java Lambda Stream Distinct()上的任意键?

我经常遇到一个Java lambdaexpression式的问题,当我想在一个对象的任意属性或方法上使用distinct()stream时,但是想保留该对象而不是将其映射到该属性或方法。 我开始创build这里讨论的容器,但是我已经开始做足够多的工作,变得烦人,并且做了很多样板类。

我把这个Pairing类放在一起,这个类包含两个types的两个对象,并允许你指定closures左边,右边或两个对象。 我的问题是…真的没有内置的lambdastream函数distinct()在某种关键供应商? 那真让我感到吃惊。 如果不是,这个class级能否可靠地履行这个职能?

这是如何被调用的

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

更新:

testingStuart的function在下面,它似乎很好。 下面的操作区分每个string的第一个字母。 我试图找出的唯一部分是如何ConcurrentHashMap只维护整个stream的一个实例

 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 

distinct操作是有状态的pipe道操作; 在这种情况下,它是一个有状态的filter。 自己创build这些东西有点不方便,因为没有任何内置的东西,但是一个小的帮手类应该可以做到这一点:

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

不幸的是,types推断无法在expression式中得到足够的信息,所以我必须明确指定DistinctByKey类的types参数。

这涉及比Louis Wasserman描述的收集器方法更多的设置,但是这具有不同的项目直接通过而不是被缓冲直到收集完成的优点。 空间应该是相同的,因为(不可避免地)两种方法最终都会累积从stream元素中提取的所有不同的密钥。

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

这简化了一些事情,但我仍然必须指定构造函数的types参数。 试图使用钻石或静态工厂方法似乎并没有改善的事情。 我认为困难在于编译器无法推断genericstypes参数 – 对于构造函数或静态方法调用 – 当在方法引用的实例expression式中时。 好吧。

(另一个可能会简化它的变体是使DistinctByKey<T> implements Predicate<T> ,并将方法重命名为eval 。这将消除使用方法引用的需要,并可能改进types推断。像下面的解决scheme一样好。)

更新2

不能停止想这个。 而不是辅助类,使用高阶函数。 我们可以使用捕获的本地人来维护状态,所以我们甚至不需要单独的课程! 奖金,事情简化,所以types推理工作!

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

你或多或less不得不做类似的事情

  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如何只维护整个stream的一个实例:

 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只创build一次。 这是一个解释:

distinctByKey函数只是一个返回对象的普通旧函数,而该对象恰好是一个谓词。 请记住,谓词基本上是一段可以稍后评估的代码。 要手动评估谓词,您必须调用Predicate接口中的方法(如test 。 所以,谓词

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

仅仅是一个在distinctByKey实际上没有被评估的声明。

谓词像任何其他对象一样传递。 它被返回并传递给filter操作,它基本上通过调用test对stream的每个元素重复评估谓词。

我相信filter比我想象的要复杂得多,但重点是谓词在distinctByKey之外被多次评估。 关于distinctByKey没有什么特别的* 这只是你一次调用的函数,所以ConcurrentHashMap只创build一次。

*除了做得好,@ stuart-marks 🙂

您可以在Eclipse集合中使用distinct(HashingStrategy)方法。

 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集合的提交者。

如果set不包含elementSet.add(element)返回true,否则返回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()); 

find不同元素的另一种方法

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