Java Stream:查找具有最小/最大值属性的元素
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/36775518/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java Stream: find an element with a min/max value of an attribute
提问by Jan Pomikálek
I have a stream of objects and I would like to find the one with a maximal value of some attribute that's expensive to calculate.
我有一个对象流,我想找到某个属性的最大值,而这个属性的计算成本很高。
As a specific simple example, say that we have a list of strings and we want to find the coolest one, given a coolnessIndex
function.
作为一个具体的简单例子,假设我们有一个字符串列表,我们想找到最酷的一个,给定一个coolnessIndex
函数。
The following should work:
以下应该工作:
String coolestString = stringList
.stream()
.max((s1, s2) -> Integer.compare(coolnessIndex(s1), coolnessIndex(s2)))
.orElse(null);
Now, there are two problems with this. First, assuming the coolnessIndex
is expensive to calculate, this probably won't be very efficient. I suppose the max
method will need to use the comparator repeatedly, which in turn will call the coolnessIndex
repeatedly and at the end it will be called more than once for each string.
现在,这有两个问题。首先,假设coolnessIndex
计算成本很高,这可能不会很有效。我想该max
方法需要重复使用比较器,而比较器又会coolnessIndex
重复调用,最后会为每个字符串多次调用。
Second, having to provide the comparator leads to some redundancy in the code. I would much prefer syntax like this:
其次,必须提供比较器会导致代码中出现一些冗余。我更喜欢这样的语法:
String coolestString = stringList
.stream()
.maxByAttribute(s -> coolnessIndex(s))
.orElse(null);
However, I haven't been able to find a matching method in the Stream
API. This surprises me, since finding min/max by an attribute seems like a common pattern. I wonder if there's a better way than using the comparator (other than a for loop).
但是,我一直无法在Stream
API 中找到匹配的方法。这让我感到惊讶,因为通过属性查找最小值/最大值似乎是一种常见模式。我想知道是否有比使用比较器更好的方法(除了 for 循环)。
采纳答案by Jan Pomikálek
Thanks everyone for suggestions. At last I found the solution I like the most at Efficiency of the way comparator works-- the answer from bayou.io:
谢谢大家的建议。最后,我在比较器工作方式的效率中找到了我最喜欢的解决方案——来自 bayou.io 的答案:
Have a general purpose cache
method:
有一个通用的cache
方法:
public static <K,V> Function<K,V> cache(Function<K,V> f, Map<K,V> cache)
{
return k -> cache.computeIfAbsent(k, f);
}
public static <K,V> Function<K,V> cache(Function<K,V> f)
{
return cache(f, new IdentityHashMap<>());
}
This could then be used as follows:
这可以按如下方式使用:
String coolestString = stringList
.stream()
.max(Comparator.comparing(cache(CoolUtil::coolnessIndex)))
.orElse(null);
回答by kensei62
How about using two streams, one to create a map with the pre-calculated values and a second using the map's entry set to find the max value:
如何使用两个流,一个使用预先计算的值创建地图,第二个使用地图的条目集来查找最大值:
String coolestString = stringList
.stream()
.collect(Collectors.toMap(Function.identity(), Test::coolnessIndex))
.entrySet()
.stream()
.max((s1, s2) -> Integer.compare(s1.getValue(), s2.getValue()))
.orElse(null)
.getKey();
回答by GregA100k
This is a reduction problem. Reducing a list down to a specific value. In general reduce works down the list operating on a partial solution and an item in the list. In this case, that would mean comparing the previous 'winning' value to the new value from the list which will calculate the expensive operation twice on each comparison.
这是一个减少问题。将列表减少到特定值。通常,reduce 会在列表中对部分解决方案和列表中的项目进行操作。在这种情况下,这意味着将先前的“获胜”值与列表中的新值进行比较,这将在每次比较时计算两次昂贵的操作。
According to https://docs.oracle.com/javase/tutorial/collections/streams/reduction.htmlan alternative is to use collect instead of reduce.
根据https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html,另一种方法是使用 collect 而不是 reduce。
A custom consumerclass will allow keeping track of the expensive operations as it reduces the list. Consumer can get around the multiple calls to the expensive calculation by working with mutable state.
自定义使用者类将允许在减少列表时跟踪昂贵的操作。消费者可以通过使用可变状态来绕过对昂贵计算的多次调用。
class Cooler implements Consumer<String>{
String coolestString = "";
int coolestValue = 0;
public String coolest(){
return coolestString;
}
@Override
public void accept(String arg0) {
combine(arg0, expensive(arg0));
}
private void combine (String other, int exp){
if (coolestValue < exp){
coolestString = other;
coolestValue = exp;
}
}
public void combine(Cooler other){
combine(other.coolestString, other.coolestValue);
}
}
This class accepts a string and if it is cooler than the previous winner, it replaces it and saves the expensive calculated value.
这个类接受一个字符串,如果它比之前的获胜者更酷,它会替换它并保存昂贵的计算值。
Cooler cooler = Stream.of("java", "php", "clojure", "c", "lisp")
.collect(Cooler::new, Cooler::accept, Cooler::combine);
System.out.println(cooler.coolest());
回答by VGR
I would create a local class (a class defined inside a method—rare, but perfectly legal), and map your objects to that, so the expensive attribute is computed exactly once for each:
我会创建一个本地类(在方法中定义的类——很少见,但完全合法),并将您的对象映射到该类,因此每个对象都只计算一次昂贵的属性:
class IndexedString {
final String string;
final int index;
IndexedString(String s) {
this.string = Objects.requireNonNull(s);
this.index = coolnessIndex(s);
}
String getString() {
return string;
}
int getIndex() {
return index;
}
}
String coolestString = stringList
.stream()
.map(IndexedString::new)
.max(Comparator.comparingInt(IndexedString::getIndex))
.map(IndexedString::getString)
.orElse(null);
回答by Kedar Mhaswade
You can utilize the idea of collectingthe results from the stream appropriately. The constraint of expensive coolness calculation function makes you consider calling that function exactly once for each element of the stream.
您可以适当地利用从流中收集结果的想法。昂贵的酷度计算函数的约束使您考虑为流的每个元素只调用一次该函数。
Java 8 provides the collect
method on the Stream
and a variety of ways in which you can use collectors. It appears that if you used the TreeMap
to collect your results, you can retain the expressiveness and at the same time remain considerate of efficiency:
Java 8 提供了使用收集器的collect
方法Stream
和多种方式。看来,如果你用TreeMap
来收集你的结果,你可以在保留表现力的同时兼顾效率:
public class Expensive {
static final Random r = new Random();
public static void main(String[] args) {
Map.Entry<Integer, String> e =
Stream.of("larry", "moe", "curly", "iggy")
.collect(Collectors.toMap(Expensive::coolness,
Function.identity(),
(a, b) -> a,
() -> new TreeMap<>
((x, y) -> Integer.compare(y, x))
))
.firstEntry();
System.out.println("coolest stooge name: " + e.getKey() + ", coolness: " + e.getValue());
}
public static int coolness(String s) {
// simulation of a call that takes time.
int x = r.nextInt(100);
System.out.println(x);
return x;
}
}
This code prints the stooge
with maximum coolness and the coolness
method is called exactly once for each stooge
. The BinaryOperator
that works as the mergeFunction
((a, b) ->a
) can be further improved.
此代码stooge
以最酷的coolness
方式打印 ,并且该方法对每个stooge
. 在BinaryOperator
该工作作为mergeFunction
((a, b) ->a
)可以进一步改善。
回答by gustf
Here's a variant using an Object[]
as a tuple, not the prettiest code but concise
这是一个使用 anObject[]
作为元组的变体,不是最漂亮的代码,而是简洁的
String coolestString = stringList
.stream()
.map(s -> new Object[] {s, coolnessIndex(s)})
.max(Comparator.comparingInt(a -> (int)a[1]))
.map(a -> (String)a[0])
.orElse(null);
回答by frhack
Stream<String> stringStream = stringList.stream();
String coolest = stringStream.reduce((a,b)->
coolnessIndex(a) > coolnessIndex(b) ? a:b;
).get()
回答by Paul Janssens
just create your (object,metric) pairs first:
只需先创建您的 (object,metric) 对:
public static <T> Optional<T> maximizeOver(List<T> ts, Function<T,Integer> f) {
return ts.stream().map(t -> Pair.pair(t, f.apply(t)))
.max((p1,p2) -> Integer.compare(p1.second(), p2.second()))
.map(Pair::first);
}
(those are com.googlecode.totallylazy.Pair's)
(那些是 com.googlecode.totallylazy.Pair 的)