Java 8流的.min()和.max():为什么这个能编译?

218

注意:这个问题起源于一个死链接,该链接是之前的SO问题,但问题如下...

请看这段代码(注意:我知道这段代码不会“运行”,应该使用Integer::compare -- 我只是从链接的问题中提取出来的):

final ArrayList <Integer> list 
    = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

System.out.println(list.stream().max(Integer::max).get());
System.out.println(list.stream().min(Integer::min).get());
根据.min().max()的javadoc文档,它们的参数都应该是一个Comparator。但是这里使用的是Integer类的静态方法引用,所以为什么这个代码还可以编译通过呢?

6
请注意,它无法正常工作,应该使用"Integer::compare"而不是"Integer::max"和"Integer::min"。 - Christoffer Hammarström
@ChristofferHammarström 我知道;请注意我在代码片段之前说的“我知道,这很荒谬”。 - fge
3
我并不是要纠正你,我是在向大众传达信息。你的表述听起来好像你认为Integer类的方法不能成为Comparator类的方法是荒谬的,但事实并非如此。 - Christoffer Hammarström
5个回答

244
让我解释一下这里发生了什么,因为它并不明显!首先,Stream.max()接受一个Comparator实例,以便可以将流中的项目相互比较,以找到最小或最大值,在一些您不需要过多担心的最佳顺序中。所以问题是,当然,为什么会接受Integer::max?毕竟它不是一个比较器!
答案在Java 8中新的lambda功能的工作方式中。它依赖于一个概念,非正式地称为“单个抽象方法”接口或“SAM”接口。其思想是任何具有一个抽象方法的接口都可以被任何方法签名与接口上的一个方法匹配的lambda或方法引用自动实现。因此,检查Comparator接口(简化版本):
public Comparator<T> {
    T compare(T o1, T o2);
}

如果一个方法正在寻找一个Comparator<Integer>,那么它实际上是在寻找这个签名:
int xxx(Integer o1, Integer o2);

我使用 "xxx",因为方法名称不用于匹配目的。
因此,Integer.min(int a, int b)Integer.max(int a, int b)都足够接近,自动装箱将允许它在方法上下文中显示为Comparator<Integer>

28
或者使用另一种方式:list.stream().mapToInt(i -> i).max().getAsInt() - assylias
14
@assylias,你应该使用.getAsInt()而不是get(),因为你正在处理一个OptionalInt - skiwi
我们只是想为max()函数提供一个自定义比较器,却不得不这样做! - Manu343726
值得注意的是,这个“SAM接口”实际上被称为“函数式接口”,通过查看Comparator文档,我们可以看到它被装饰了注释@FunctionalInterface。这个装饰器是允许将Integer::maxInteger::min转换为Comparator的魔法。 - Chris Kerekes
2
@ChrisKerekes 装饰器 @FunctionalInterface 主要用于文档目的,因为编译器可以轻松地使用任何只有一个抽象方法的接口来完成此操作。 - errantlinguist

119

Comparator是一个函数式接口Integer::max符合该接口(考虑到自动装箱/拆箱)。它接收两个int值并返回一个int - 就像您期望一个Comparator<Integer>一样(再次忽略Integer/int的区别)。

然而,我不会指望它按照Comparator.compare语义执行正确的操作。事实上,它通常不能正常工作。例如,请做一个小小的更改:

for (int i = 1; i <= 20; i++)
    list.add(-i);

...现在max的值为-20,而min的值为-1。

相反,两个调用都应该使用Integer::compare

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());

1
我知道函数式接口;也知道为什么会得到错误的结果。我只是想知道编译器怎么就没警告我呢? - fge
6
你是说你不确定的是拆箱过程吗?(我没有详细研究这个部分。)一个 Comparator<Integer> 将会有一个 int compare(Integer, Integer) 方法...Java允许一个 int max(int, int) 的方法引用转换成它并不是什么令人费解的事情。 - Jon Skeet
7
编译器是否应该知道Integer::max的语义?从它的角度来看,你传递了一个符合其规范的函数,这就是它所能理解的。 - Mark Peters
6
特别是,如果你已经理解了一部分内容但对其中的某个特定方面感到好奇,最好在问题中明确提出,以避免人们浪费时间解释你已经知道的内容。 - Jon Skeet
20
我认为潜在的问题是Comparator.compare的类型签名。它应该返回一个枚举{LessThan, GreaterThan, Equal},而不是一个整数。这样,函数接口就不会实际匹配,你将会得到编译错误。换句话说,Comparator.compare的类型签名并没有充分捕捉比较两个对象的语义,因此其他与比较对象完全无关的接口意外地具有相同的类型签名。 - Jörg W Mittag
显示剩余2条评论

19
这是因为Integer :: min解析为Comparator<Integer> 接口的实现。 Integer::min 的方法引用解析为Integer.min(int a, int b),解析为IntBinaryOperator,并且可能在某个地方发生自动装箱,使其成为BinaryOperator<Integer>Stream<Integer>min()max() 方法要求实现 Comparator<Integer>接口。现在这被解析为单个方法 Integer compareTo(Integer o1, Integer o2)。它是类型为BinaryOperator<Integer>的函数。
因此,神奇的事情发生了,因为两种方法都是BinaryOperator<Integer>

Integer::min 实现了 Comparable 并不完全正确。它不是一个可以实现任何东西的类型。但它会被计算成一个实现了 Comparable 的对象。 - Lii
1
@Lii 谢谢,我刚刚已经修复了它。 - skiwi
1
Comparator<Integer> 是一种单一抽象方法(也称为“函数式”)接口,而 Integer::min 符合其协定,因此可以将 lambda 解释为这个接口。我不知道您如何在这里使用 BinaryOperator(或 IntBinaryOperator),因为它与 Comparator 之间没有子类型关系。 - Paŭlo Ebermann

2
除了David M. Lloyd提供的信息,还可以补充一点,即使这种机制被称为“目标类型推断”(target typing)。

这个想法是,编译器为lambda表达式或方法引用分配的类型不仅取决于表达式本身,还取决于它在哪里使用。

表达式的目标是其结果分配给的变量或传递给的参数。

如果可以找到与其目标类型匹配的类型,则会为Lambda表达式和方法引用分配一个该类型。

有关更多信息,请参阅Java教程中的类型推断部分


1

我在使用数组获取最大值和最小值时遇到了错误,所以我的解决方案是:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接