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

tmn*_*tmn 60 java lambda java-8

我经常遇到Java lambda表达式的问题,当我想在对象的任意属性或方法上使用distinct()一个流时,但想要保留对象而不是将其映射到该属性或方法.我开始创建这里讨论的容器,但我开始做足够的事情,它变得烦人,并制作了很多样板类.

我将这个Pairing类放在一起,该类包含两个类型的两个对象,并允许您指定左,右或两个对象的键控.我的问题是......对于某些类型的关键供应商,distinct()是否真的没有内置的lambda流功能?那真让我感到惊讶.如果没有,该课程能否可靠地完成该功能?

以下是它的调用方式

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));
Run Code Online (Sandbox Code Playgroud)

这是配对课程

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

    }
Run Code Online (Sandbox Code Playgroud)

更新:

测试斯图尔特的功能如下,它似乎工作得很好.下面的操作区分每个字符串的第一个字母.我想弄清楚的唯一部分是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));
    }
Run Code Online (Sandbox Code Playgroud)

输出是......

ABQ
CHI
PHX
BWI
Run Code Online (Sandbox Code Playgroud)

Stu*_*rks 110

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我不知道你的域类,但我认为,通过这个帮助器类,你可以做你想做的事情:

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

不幸的是,类型推断在表达式中无法得到足够的深度,因此我必须明确指定DistinctByKey类的类型参数.

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

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);
Run Code Online (Sandbox Code Playgroud)

这简化了一些事情,但我仍然必须为构造函数指定类型参数.尝试使用钻石或静态工厂方法似乎并没有改善.我认为困难在于编译器无法推断泛型类型参数 - 对于构造函数或静态方法调用 - 当它们位于方法引用的实例表达式中时.那好吧.

(另一个可能会简化它的变体是将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);
Run Code Online (Sandbox Code Playgroud)

  • [Hongbin Shen](/sf/users/471782601/)想评论:为什么`Map <Object,Boolean> seen = new ConcurrentHashMap <>();`只调用一次,是因为`distinctByKey()`只被调用一次,它返回一个谓词; `filter()`的作用就是为每个项调用`predicate.test()`.所以`distinctByKey`中的地图只创建一次. (4认同)

Lou*_*man 29

你或多或少都要做类似的事情

 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();
Run Code Online (Sandbox Code Playgroud)

  • 这是一个比包装类更清晰的方法.我不必去读另一节课来理解它的作用.我可以想到使用包装器类的唯一优势是你计划`.limit(...)`流.Wrap-distinct-unwrap方法只会根据需要处理尽可能多的元素,而收集到地图将实现整个流. (4认同)
  • 一旦知道不同,`distinct`操作就会通过每个元素.如果源是无限流,则这可能很重要.如果源是有限的,`collect(...).stream()`在功能上是相似的.它可能具有性能劣势,因为下游操作在收集器完成之前不会运行.这两种技术最终都需要在中间集合中需要相同的空间量. (3认同)
  • distinct()在引擎盖下的工作方式几乎相同,所以我不担心. (2认同)

小智 6

Stuart Marks第二次更新的变种.使用Set.

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));
}
Run Code Online (Sandbox Code Playgroud)


小智 6

寻找不同元素的另一种方法

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());
Run Code Online (Sandbox Code Playgroud)


frh*_*ack 5

我们也可以使用RxJava(非常强大的反应式扩展库)

Observable.from(persons).distinct(Person::getName)
Run Code Online (Sandbox Code Playgroud)

要么

Observable.from(persons).distinct(p -> p.getName())
Run Code Online (Sandbox Code Playgroud)


小智 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;
    }
Run Code Online (Sandbox Code Playgroud)

在您的代码示例中,distinctByKey仅被调用一次,因此 ConcurrentHashMap 只创建了一次。这是一个解释:

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

t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null
Run Code Online (Sandbox Code Playgroud)

只是一个声明,实际上并未在 内部进行评估distinctByKey

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

我敢肯定filter比我想象的要复杂,但关键是,谓词在distinctByKey. 没有什么特别*关于distinctByKey; 它只是您调用过一次的函数,因此 ConcurrentHashMap 只创建一次。

*除了制作精良,@stuart-marks :)