如何在Java序列流中只筛选出与谓词不匹配的第一个元素



我在java流操作中遇到了一个边缘案例。。。

我想对以下行为进行编码:"从任意一篮水果中,收集20个最小的,除了最小的梨,因为我们不想要这样。">

额外的好处是:即将到来的篮子可能根本没有梨。

示例:

  • 从[梨5,苹果1,苹果2,苹果10,梨3,梨7],我们想要[苹果1,Apple 2,梨5,梨7,Apple 10]
  • 从[苹果4,苹果7,梨8,梨2,梨3],我们想要[梨3,苹果4,Apple 7,梨8]

到目前为止,我正处于以下步骤:

output = basket.stream()
.sorted(Comparator.comparing(Fruit::getSize))
//.filter(???)
.limit(20)
.collect(fruitCollector);

这看起来像是有状态lambda过滤器的情况,我不知道如何做到这一点。

我不能在过滤第一个pear后使用局部firstPear布尔值并将其设置为true,因为lambda中的所有局部变量都必须是final。

在最坏的情况下,我可以把篮子分成两部分,梨和非梨,对梨进行分类,如果有的话,可以适当地列出来。这似乎非常低效和丑陋有更好的方法吗


[Edit]答案比较

这里发布的答案多种多样,大多数都是有效的。为了回馈社区,我制作了一个小型测试工具来比较这些算法的性能。

这个比较并没有我想要的那么广泛——已经三周了。它仅适用于简单项目的顺序处理。您可以尝试一下测试工具,添加更多的测试、更多的基准测试或您自己的实现。

我的分析:

算法|作者|性能|评论--------------------------------------------------------------------------------索引删除|Holger|Best|Best整体,有点模糊有状态谓词|pedromss|Best|不用于并行处理直截了当的方法| Misha |最佳|只有少数元素匹配时效果更好自定义收集器|Eugene|Good|all或no元素匹配时效果更好Comprator破解w/dumm|yegodm|Good|-比较器破解|xenteros|*|Perf对输出大小敏感,在边缘情况下失败

我接受了pedromss的答案,因为它是我们在项目中实现的,因为它具有良好的性能和"黑盒"功能(状态管理代码在外部类中,贡献者可以专注于业务逻辑)。

请注意,接受的答案可能不是最适合你的:查看其他人,或者查看我的测试项目

您考虑过直接的方法吗?找到最小的梨,过滤掉(如果存在的话),收集20个最小的:

Optional<Fruit> smallestPear = basket.stream()
.filter(Fruit::isPear)  // or whatever it takes to test if it's a pear
.min(Fruit::getSize);
Stream<Fruit> withoutSmallestPear = smallestPear
.map(p -> basket.stream().filter(f -> f != p))
.orElseGet(basket::stream);
List<Fruit> result = withoutSmallestPear
.sorted(comparing(Fruit::getSize))
.limit(20)
.collect(toList());

据我所知,这上面写满了自定义,所以我在这里尝试了一个自定义收集器:

private static <T> Collector<T, ?, List<T>> exceptCollector(Predicate<T> predicate, int size, Comparator<T> comparator) {
class Acc {
private TreeSet<T> matches = new TreeSet<>(comparator);
private TreeSet<T> doesNot = new TreeSet<>(comparator);
void accumulate(T t) {
if (predicate.test(t)) {
matches.add(t);
} else {
doesNot.add(t);
}
}
Acc combine(Acc other) {
matches.addAll(other.matches);
doesNot.addAll(other.doesNot);
return this;
}
List<T> finisher() {
T smallest = matches.first();
if (smallest != null) {
matches.remove(smallest);
}
matches.addAll(doesNot);
return matches.stream().limit(size).collect(Collectors.toList());
}
}
return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::finisher);
}

使用方法是:

List<Fruit> fruits = basket.getFruits()
.stream()
.collect(exceptCollector(Fruit::isPear, 20, Comparator.comparing(Fruit::getSize)));

为了更容易实现,我附上了一个示例:

class Fruit {
String name;
Long size;
}

以下将起作用:

Comparator<Fruit> fruitComparator = (o1, o2) -> {
if (o1.getName().equals("Peach") && o2.getName().equals("Peach")) {
return o2.getSize().compareTo(o1.getSize()); //reverse order of Peaches
}
if (o1.getName().equals("Peach")) {
return 1;
}
if (o2.getName().equals("Peach")) {
return -1;
}
return o1.getSize().compareTo(o2.getSize());
};

和:

output = basket.stream()
.sorted(Comparator.comparing(Fruit::getSize))
.limit(21)
.sorted(fruitComparator)
.limit(20)
.sorted(Comparator.comparing(Fruit::getSize))
.collect(fruitCollector);

我的比较器会把最小的Peach放在第21个位置,会保持其他Fruit的顺序自然,所以如果没有Peach,它会返回第21个最大的元素。然后我把剩下的按正常顺序排序。

这会奏效的。这是一个黑客攻击,在某些情况下可能是一个糟糕的选择。我想指出的是,对20个元素进行排序不应该是个问题。

您可以使用有状态谓词:

class StatefulPredicate<T> implements Predicate<T> {
private boolean alreadyFiltered;
private Predicate<T> pred;
public StatefulPredicate(Predicate<T> pred) {
this.pred = pred;
this.alreadyFiltered = false;
}
@Override
public boolean test(T t) {
if(alreadyFiltered) {
return true;
}
boolean result = pred.test(t);
alreadyFiltered = !result;
return result;
}
}
Stream.of(1, -1, 3, -4, -5, 6)
.filter(new StatefulPredicate<>(i -> i > 0))
.forEach(System.out::println);

打印:1, 3, -4, -5, 6

如果并发性是一个问题,那么可以使用原子布尔值。

如果您希望跳过一个以上的元素,请将该参数添加到构造函数中,并在StatefulPredicate中构建逻辑

这个谓词过滤第一个负元素,然后让其他所有元素都通过,不管怎样。在您的情况下,您应该测试instanceof Pear

编辑

由于人们对过滤器是无状态的表示担忧,来自文档:

中间操作进一步分为无状态操作和有状态操作。无状态操作,如filter和map,在处理新元素时不会保留以前看到的元素的状态--每个元素都可以独立于对其他元素的操作进行处理。在处理新元素时,有状态的操作(如distinct和sorted)可能会合并以前看到的元素的状态。

该谓词不保留有关以前看到的元素的信息。它保留了有关以前结果的信息。

此外,它还可以使线程安全,以避免并发问题。

关键操作是按类型和大小排序,使最小的梨首先。类似的东西:

// create a dummy pear; size value does not matter as comparing by ref
final Pear dummy = new Pear(-1);
basket
// mix basket with the dummy pear
.concat(basket, Stream.of(dummy))
// sort by type so pears go first, then by size
.sorted(Comparator
.<Fruit>comparingInt(
// arrange the dummy to always be the last 
// among other pears but before other types 
f -> (f == dummy ? 
0 : 
(Pear.class.equals(f.getClass()) ? -1 : 1))
)
.thenComparing(f -> f.size)
)
// skip the smallest pear
.skip(1)
// filter out the dummy
.filter(f -> f != dummy)
// sort again the rest by size
.sorted(Comparator.comparingInt(f -> f.size))
// take 20 at max
.limit(20);

不要尝试预先筛选。考虑

List<Fruit> output = basket.stream()
.sorted(Comparator.comparing(Fruit::getSize))
.limit(21)
.collect(Collectors.toCollection(ArrayList::new));
int index = IntStream.range(0, output.size())
.filter(ix -> output.get(ix).isPear())
.findFirst().orElse(20);
if(index < output.size()) output.remove(index);

只需限制为21元素而不是20即可移除一个元素。通过使用Collectors.toCollection(ArrayList::new),您可以确保接收到一个可变集合。

然后,有三种情况

  1. 该列表包含一个Pear。由于列表是按水果大小排序的,所以第一个Pear也将是最小的Pear,它是必须删除的。随后的… .findFirst()将对元素的索引进行评估。

  2. 该列表不包含Pear,但大小为21。在这种情况下,我们必须删除最后一个元素,即索引20,以获得所需的结果大小。这是由.orElse(20)提供的,它将把空的OptionalInt映射到20

  3. 该列表可能不包含任何Pear并且小于21,因为源列表已经较小。在这种情况下,我们不删除任何元素,通过用if(index < output.size())预处理remove操作进行检查。

正如我们之前已经知道的,整个后处理可以被认为与性能无关,它将应用于一个非常小的列表,在本例中最多有21元素。这与源basket列表的大小无关。

[Update],在阅读了更新后的OP后,我对需求有了更好的了解:以下是StreamEx:更新的代码

Optional<Integer> smallestPear = StreamEx.of(basket).filter(Fruit::isPear)
.mapToInt(Fruit::getSize).min();
StreamEx.of(basket)
.chain(s -> smallestPear.map(v -> s.remove(f -> f.isPear() && f.getSize() == v).orElse(s))
.sortedBy(Fruit::getSize).limit(20).toList();

[再次更新]上述解决方案与Misha提供的解决方案非常相似。如果你不想两次通过流,如果篮子里的一对(水果类型、大小)是唯一的,这里有另一个通过有限谓词的解决方案:

// Save this method in your toolkit.
public class Fn {
public static <T> Predicate<T> limited(final Predicate<T> predicate, final int limit) {
Objects.requireNonNull(predicate);    
return new Predicate<T>() {
private final AtomicInteger counter = new AtomicInteger(limit);
@Override
public boolean test(T t) {
return predicate.test(t) && counter.decrementAndGet() >= 0;
}
};
}
}
StreamEx.of(basket).sortedBy(Fruit::getSize)
.remove(f -> Fn.limited(Fruit::isPear, 1))
.limit(20).toList();

我认为Predicate是您操作的原子运算符。因此,最简单的方法是编写自己的Predicate来包装原始的Predicate。假设包装命名为once,那么您的代码可以简化为以下内容:

output = basket.stream().sorted(comparing(Fruit::getSize))
.filter(once(Fruit::isPear))
.limit(20).collect(fruitCollector);

static <T> Predicate<T> once(Predicate<T> predicate){
boolean[] seen = {true};
return it -> !seen[0] || (seen[0]=predicate.test(it));
}

如果您想支持并发,可以使用AtomicInteger,例如:

static <T> Predicate<T> once(Predicate<T> predicate){
AtomicInteger seen = new AtomicInteger(0);
return it -> {
//if seen==0 then test predicate, otherwise increment only 
IntBinaryOperator accumulator = (x,y)-> x==0 && predicate.test(it) ? x : x+y;
return seen.accumulateAndGet(1, accumulator) != 1; 
};
}

我也有同样的问题,但我自己使用Map and ignore列表解决了。这是样品供您参考。希望能有所帮助。

@Test
public void testGetStckTraceElements() {
StackTraceElement[] stElements = Thread.currentThread().getStackTrace();
// define a list for filter out
List<String> ignoreClasses = Arrays.asList(
Thread.class.getName(),
this.getClass().getName()
);
// Map is using for check found before or not
Map<String,Boolean> findFrist = new HashMap<String,Boolean>();
Arrays.asList(stElements).stream()
.filter(s -> {
Platform.print("check: {}", s.getClassName());
if (Optional.ofNullable(findFrist.get(s.getClassName())).orElse(false)) {
return true;
}
findFrist.put(s.getClassName(), true);
for (String className:ignoreClasses) {
if (s.getClassName().equals(className)) return false;
}
return true;
})
.forEach(s->{
Platform.print("Result: {} {} {} {}", s.getClassName(), s.getMethodName(), s.getFileName(), s.getLineNumber());
});
}

类似的东西可能会起作用(但如您所述,将其分组为两个篮子)

Function<Fruit, Boolean> isPear = f -> f.getType().equals("Pear");
Comparator<Fruit> fruitSize = Comparator.comparing(Fruit::getSize);
Map<Boolean, List<Fruit>> pearsAndOthers = basket.sorted(fruitSize).limit(21).collect(Collectors.groupingBy(isPear));
List<Fruit> pears = pearsAndOthers.get(true);
List<Fruit> others = pearsAndOthers.get(false);
Stream<Fruit> result;
if (pears.size() == 0) {
result = others.stream().limit(20);
} else if (pears.size() == 1) {
result = others.stream();
} else {
// You can probably merge in a nicer fashion since they should be sorted
result = Stream.concat(pears.stream().skip(1), others.stream()).sorted(fruitSize);
}

最新更新