我经常遇到Java lambda表达式的问题,当我想在对象的任意属性或方法上使用distinct()时,想要保留对象而不是将其映射到该属性或方法。我开始创建容器,如here所述,但我开始做足够的事情,它变得烦人,并制作了很多样板类。
我将这个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));
这是Pairing类
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
答案 0 :(得分:101)
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);
这简化了一些事情,但我仍然必须为构造函数指定type参数。尝试使用钻石或静态工厂方法似乎并没有改善。我认为困难在于编译器无法推断泛型类型参数 - 对于构造函数或静态方法调用 - 当它们位于方法引用的实例表达式中时。哦,好吧。
(可能会简化它的另一个变体是使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);
答案 1 :(得分: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();
答案 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));
}
答案 3 :(得分:5)
答案 4 :(得分:3)
在第二次更新中回答您的问题:
我想弄清楚的唯一部分是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:)
答案 5 :(得分:2)
您可以使用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的提交者。
答案 6 :(得分:2)
寻找不同元素的另一种方式
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());
答案 7 :(得分:0)
可以这样做
Set<String> distinctCompany = orders.stream()
.map(Order::getCompany)
.collect(Collectors.toSet());
答案 8 :(得分:0)
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);
如果要并行执行此操作,则必须使用并发映射。