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

Java Lambda Stream Distinct() on arbitrary key?

我经常 运行 遇到 Java lambda 表达式的问题,当我想在任意 属性 或对象的方法上 distinct() 流时,我想保留对象而不是将其映射到 属性 或方法。我开始按照 here 的讨论创建容器,但我开始做的足够多以至于它变得烦人并制作了很多样板 classes。

我将这个 Pairing class 放在一起,它包含两种类型的两个对象,并允许您指定对左侧、右侧或两个对象进行键控。我的问题是......在某种关键供应商上真的没有内置的 distinct() lambda 流函数吗?那真的会让我感到惊讶。如果不是,这个 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));

这是配对class

    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

你或多或少需要做一些类似的事情

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

distinct操作是一个有状态管道操作;在这种情况下,它是一个有状态的过滤器。自己创建这些有点不方便,因为没有内置的东西,但是一个小帮手 class 应该可以解决问题:

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

我不知道你的域 classes,但我认为,有了这个助手 class,你可以像这样做你想做的事:

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

不幸的是,类型推断无法深入表达式内部,因此我必须明确指定 DistinctByKey class.

的类型参数

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

更新

可以去掉 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);

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

(另一种可能会简化它的变体是制作 DistinctByKey<T> implements Predicate<T> 并将方法重命名为 eval。这将消除使用方法引用的需要,并且可能会改进类型推断. 但是,它不太可能像下面的解决方案那样好。)

更新 2

无法停止思考这个问题。使用高阶函数代替助手 class。我们可以使用捕获的局部变量来维护状态,所以我们甚至不需要单独的 class!奖励,事情被简化了,所以类型推断有效!

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

我们也可以使用RxJava (very powerful reactive extension库)

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

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

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

The only part I'm trying to figure out is how the ConcurrentHashMap maintains only one instance for the entire 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 仅创建一次。这里有一个解释:

distinctByKey函数只是一个普通的函数,returns一个对象,而这个对象恰好是一个Predicate。请记住,谓词基本上是一段可以稍后评估的代码。要手动评估谓词,您必须调用 Predicate interface such as test 中的方法。所以,谓词

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

只是一个声明,实际上并未在 distinctByKey.

中求值

谓词像任何其他对象一样传递。它被返回并传递给 filter 操作,该操作基本上通过调用 test.

对流的每个元素重复评估谓词

我敢肯定 filter 比我想象的要复杂,但重点是,谓词在 distinctByKey 之外计算了很多次。 distinctByKey; 没有什么特别的*;它只是您调用过一次的函数,因此 ConcurrentHashMap 仅创建一次。

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

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

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

可以像

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

Set.add(element) returns 如果集合尚未包含 element,则为真,否则为假。 所以你可以这样做。

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

如果要并行,必须使用concurrent map。

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

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