Java Lambda Stream Distinct() on 任意键?

2022-08-31 13:46:15

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

我把这个 Pairing 类放在一起,它包含两种类型的两个对象,并允许您指定对左、右或两个对象的键控。我的问题是...是否真的没有内置的lambda流函数来在某些类型的关键供应商上divall()?这真的会让我感到惊讶。如果不是,这个类会可靠地实现该功能吗?

以下是它的称呼

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

答案 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

这比 Louis Wasserman 描述的收集器方法涉及更多的设置,但这具有一个优点,即不同的项目会立即通过,而不是在收集完成之前进行缓冲。空间应该是相同的,因为(不可避免地)这两种方法最终都会累积从流元素中提取的所有不同键。

更新

可以删除type参数,因为它实际上除了存储在地图中之外,不用于其他任何用途。这样就足够了。KObject

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

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

推荐