任意键上的 Java Lambda 流独特() ?

我经常遇到 Java lambda 表达式的问题,当我想在对象的任意属性或方法上区分()一个流时,却想保留对象而不是将其映射到该属性或方法。我开始像讨论的 给你那样创建容器,但是我已经开始做得够多了,因为它已经变得很烦人了,并且创建了很多样板类。

我将这个 Pairing 类放在一起,它包含两种类型的两个对象,并允许您指定左键、右键或两个对象的键。我的问题是... 是否真的没有内置的 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));

这是结对类

    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
84851 次浏览

你或多或少要做一些像

 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操作是一个 有声有色管道操作; 在本例中,它是一个有状态过滤器。自己创建它们有点不方便,因为它们没有内置的东西,但是一个小的 helper 类应该可以做到这一点:

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

我不知道您的域类,但是我认为,使用这个 helper 类,您可以像下面这样做:

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

遗憾的是,类型推断在表达式内部的位置不够远,因此我必须显式地指定 DistinctByKey类的类型参数。

这涉及比 路易斯 · 沃瑟曼描述的收藏家方法更多的设置,但是这样做的优点是不同的项目可以立即通过,而不是在集合完成之前被缓冲。空间应该是相同的,因为(不可避免地)两种方法最终都会累积从流元素中提取的所有不同的键。

更新

可以去掉 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<T> implements Predicate<T>并将方法重命名为 eval。这将消除使用方法引用的需要,并可能改进类型推断。然而,它不太可能像下面的解决方案那么好。)

更新2

我一直在想这件事。使用高阶函数代替 helper 类。我们可以使用捕获的局部变量来维护状态,所以我们甚至不需要单独的类!额外的好处是,事情被简化了,所以类型推理工作!

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(非常强大的 反应性延伸反应性延伸库)

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

或者

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

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

我唯一想弄清楚的是 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函数只是一个返回对象的普通函数,而该对象恰好是一个谓词。请记住,谓词基本上是一段可以在以后计算的代码。要手动计算谓词,必须调用 预测接口中的方法,如 test。那么,谓词

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

只是一个实际上在 distinctByKey中没有计算的声明。

谓词像其他对象一样传递。它被返回并传递给 filter操作,filter操作通过调用 test重复对流中的每个元素计算谓词。

我确信 filter比我想象的要复杂得多,但关键是,谓词在 distinctByKey之外被计算了很多次。distinctByKey没有什么特别之处; 它只是一个调用过一次的函数,所以 ConcurrentHashMap 只创建了一次。

* 除了制作精良,@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 集合中使用 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 Collectionsinterface,则可以直接在列表中调用该方法。

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<String> distinctCompany = orders.stream()
.map(Order::getCompany)
.collect(Collectors.toSet());

如果集合中尚未包含 element,则 Set.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);

如果要进行此并行操作,则必须使用并发映射。

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

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