【问题标题】:Java Lambda Stream Distinct() on arbitrary key? [duplicate]任意键上的Java Lambda Stream Distinct()? [复制]
【发布时间】:2015-03-08 08:08:21
【问题描述】:

我经常遇到 Java lambda 表达式的问题,当我想对对象的任意属性或方法进行 distinct() 流时,但又想保留该对象而不是将其映射到该属性或方法。正如here 所讨论的那样,我开始创建容器,但我开始做得足够让它变得烦人,并制作了很多样板类。

我将这个 Pairing 类放在一起,它包含两种类型的两个对象,并允许您指定对左侧、右侧或两个对象的键控。我的问题是......在某种关键供应商上真的没有内置的 lambda 流函数来 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;
           }

    }

更新:

在下面测试了 Stuart 的功能,它似乎工作得很好。下面的操作区分每个字符串的第一个字母。我想弄清楚的唯一部分是 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

【问题讨论】:

    标签: java lambda java-8


    【解决方案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 类的类型参数。

    这比collectors approach described by Louis Wasserman 需要更多的设置,但这样做的好处是不同的项目会立即通过,而不是在收集完成之前被缓冲。空间应该相同,因为(不可避免地)两种方法最终都会累积从流元素中提取的所有不同键。

    更新

    可以去掉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&lt;T&gt; implements Predicate&lt;T&gt; 并将方法重命名为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);
    

    【讨论】:

    • 静态工厂仍然需要像构造函数一样推断TK。它似乎没有更好的工作。 T 可能可以推断出来,因为过滤器必须是T -&gt; boolean,而你有Stream&lt;T&gt;。但是推断K 可能是造成困难的原因。这样做,编译器必须采用T 的推断类型,将其应用于传递给构造的lamda,它必须是T -&gt; K,然后使用它来推断K。我认为编译器不会这样做,或者即使有可能。不过,也许可以摆脱 K 类型参数,嗯....
    • @ThomasN。它仍然必须是Function,因为它返回了一些东西。但是辅助类不需要类型变量K;它可以只使用Object。看我的更新。我还没有找到让编译器推断T的方法,所以我上面的评论可能不正确。
    • @ThomasN。请参阅更新 2。它使用了完全不同的技术。简单得多。
    • 最后一个解决方案的辅助方法的作用,也可以在没有辅助方法的情况下完成。虽然辅助方法是可重用的。
    • Hongbin Shen 想评论一下:为什么Map&lt;Object,Boolean&gt; seen = new ConcurrentHashMap&lt;&gt;(); 只被调用一次,是因为distinctByKey() 只被调用一次,它返回一个谓词; filter() 所做的只是为每个项目调用 predicate.test()。所以distinctByKey 中的地图只创建一次。
    【解决方案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();
    

    【讨论】:

    • 很有趣,所以我想公平地说,没有原生 lambda 方法可以做到这一点,但有一些解决方法,就像你展示的那样。
    • 这是一种比包装类更清晰的方法。我不必去阅读另一门课来了解它的作用。我能想到的使用包装类的唯一优势是,如果您打算 .limit(...) 流。 Wrap-distinct-unwrap 方法只会处理必要数量的元素,而收集到地图将实现整个流。
    • 我能想到的包装器方法的另一个好处是它不需要中断流的中间收集操作,并且需要构建一个只能再次流式传输的临时集合。但我看到了 Collectors.toMap() 方法的优点。它有很多值得思考的地方,我和我的同事明天可能会对此进行一些有趣的讨论。
    • distinct() 在引擎盖下的工作方式几乎相同,所以我不用担心。
    • distinct 操作会在知道每个元素不同后立即对其进行传递。如果源是无限流,这可能很重要。如果源是有限的,collect(...).stream() 在功能上是相似的。它可能具有性能劣势,因为在收集器完成之前不会运行下游操作。不过,这两种技术最终都需要在中间集合中占用相同数量的空间。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      Stuart Marks 第二次更新的变体。使用集合。

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

      【讨论】:

      • 你也可以使用Collections.synchronizedSet(new HashSet&lt;&gt;())
      • Set&lt;Object&gt; seen = ConcurrentHashMap.newKeySet()
      • 如此处所述:stackoverflow.com/questions/23699371/… 您可以使用Function&lt;? super T, ?&gt; keyExtractor 作为参数以获得更好的兼容性。
      【解决方案5】:

      我们也可以使用RxJava(非常强大的reactive extension库)

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

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

      【讨论】:

      • 哈哈有趣的是你现在提出这个问题,RxJava 是我最近两个月的痴迷。
      • RxExtensions 是一个非常棒的 CS 理念。恕我直言,它很快就会成为一个非常有用且无处不在的工具,就像旧的正则表达式一样,但重要得多
      • 我同意,我猜它们将成为未来几年的标准。
      • 它已经高度标准化。它几乎适用于所有语言。背后有一位聪明的 CS 科学家:Erik Meijer
      【解决方案6】:

      在第二次更新中回答您的问题:

      我想弄清楚的唯一部分是 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 :)

      【讨论】:

      • 如果不想并行,可以用HashMap代替ConcurrentHashMap,对吧?
      【解决方案7】:

      您可以在Eclipse Collections 中使用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 Collections 的提交者。

      【讨论】:

        【解决方案8】:

        Set.add(element) 如果该集合尚未包含 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);
        

        如果你想做这个并行,你必须使用并发映射。

        【讨论】:

        【解决方案9】:

        可以做类似的事情

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

        【讨论】:

          猜你喜欢
          • 2014-08-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-03-26
          • 2018-07-17
          • 1970-01-01
          • 2022-01-12
          • 1970-01-01
          相关资源
          最近更新 更多