Java 8 Comparator 类型推断非常困惑
Posted
技术标签:
【中文标题】Java 8 Comparator 类型推断非常困惑【英文标题】:Very confused by Java 8 Comparator type inference 【发布时间】:2014-08-17 16:20:50 【问题描述】:我一直在研究Collections.sort
和list.sort
之间的区别,特别是关于使用Comparator
静态方法以及lambda 表达式中是否需要参数类型。在我们开始之前,我知道我可以使用方法引用,例如Song::getTitle
来克服我的问题,但是我在这里的查询并不是我想要修复的东西,而是我想要回答的东西,即为什么 Java 编译器以这种方式处理它。
这些是我的发现。假设我们有一个Song
类型的ArrayList
,加上一些歌曲,有3 个标准的get 方法:
ArrayList<Song> playlist1 = new ArrayList<Song>();
//add some new Song objects
playlist.addSong( new Song("Only Girl (In The World)", 235, "Rhianna") );
playlist.addSong( new Song("Thinking of Me", 206, "Olly Murs") );
playlist.addSong( new Song("Raise Your Glass", 202,"P!nk") );
这是对两种类型的排序方法的调用,没问题:
Collections.sort(playlist1,
Comparator.comparing(p1 -> p1.getTitle()));
playlist1.sort(
Comparator.comparing(p1 -> p1.getTitle()));
一旦我开始链接thenComparing
,就会发生以下情况:
Collections.sort(playlist1,
Comparator.comparing(p1 -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
playlist1.sort(
Comparator.comparing(p1 -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
即语法错误,因为它不再知道 p1
的类型。所以为了解决这个问题,我将类型 Song
添加到第一个参数(比较):
Collections.sort(playlist1,
Comparator.comparing((Song p1) -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
playlist1.sort(
Comparator.comparing((Song p1) -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
现在是令人困惑的部分。对于 playlist1.sort
,即 List,这解决了以下两个 thenComparing
调用的所有编译错误。但是,对于Collections.sort
,它解决了第一个问题,而不是最后一个问题。我测试了向thenComparing
添加了几个额外的调用,它总是显示最后一个错误,除非我将(Song p1)
作为参数。
现在我继续通过创建TreeSet
和使用Objects.compare
来进一步测试:
int x = Objects.compare(t1, t2,
Comparator.comparing((Song p1) -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
Set<Song> set = new TreeSet<Song>(
Comparator.comparing((Song p1) -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
同样的事情发生在TreeSet
,没有编译错误,但Objects.compare
最后一次调用thenComparing
显示错误。
谁能解释一下为什么会发生这种情况,以及为什么在简单地调用比较方法时根本不需要使用(Song p1)
(无需进一步的thenComparing
调用)。
当我对TreeSet
执行此操作时,另一个关于同一主题的查询是:
Set<Song> set = new TreeSet<Song>(
Comparator.comparing(p1 -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
即从比较方法调用的第一个 lambda 参数中删除类型 Song
,它在比较调用和对 thenComparing
的第一次调用下显示语法错误,但对 thenComparing
的最终调用不显示语法错误 - 几乎与什么相反上面发生了!而对于所有其他 3 个示例,即 Objects.compare
、List.sort
和 Collections.sort
,当我删除第一个 Song
参数类型时,它会显示所有调用的语法错误。
非常感谢。
已编辑以包含我在 Eclipse Kepler SR2 中收到的错误截图,我现在发现它是 Eclipse 特定的,因为在命令行上使用 JDK8 java 编译器编译时,它可以编译。
【问题讨论】:
如果您在问题中包含您在所有测试中收到的所有编译错误消息,将会很有帮助。 老实说,我认为通过自己运行源代码来了解问题所在最容易。t1
和t2
示例中的t2
的类型是什么?我试图推断它们,但是将我的类型推断分层到编译器的类型推断上是难以处理的。 :-)
另外你用的是什么编译器?
这里有两个不同的问题。其中一位回答者指出您可以使用方法引用,但您对此略过不屑一顾。正如 lambda 有“显式类型”和“隐式类型”两种风格一样,方法引用也有“精确”(一个重载)和“不精确”(多个重载)两种风格。如果不存在,则可以使用精确方法 ref 或显式 lambda 来提供额外的类型信息。 (也可以使用显式类型的证人和演员表,但通常是更大的锤子。)
【参考方案1】:
首先,您所说的所有导致错误的示例都可以使用参考实现(JDK 8 中的 javac)正常编译。它们在 IntelliJ 中也可以正常工作,因此您看到的错误很可能是 Eclipse 特定的。
您的基本问题似乎是:“为什么当我开始链接时它停止工作。”原因是,当 lambda 表达式和泛型方法调用作为方法参数出现时,它们是 poly 表达式(它们的类型是上下文相关的),而当它们作为方法接收器表达式出现时,它们不是。
当你说
Collections.sort(playlist1, comparing(p1 -> p1.getTitle()));
有足够的类型信息来解决comparing()
的类型参数和p1
的参数类型。 comparing()
调用从Collections.sort
的签名中获取其目标类型,因此已知comparing()
必须返回Comparator<Song>
,因此p1
必须是Song
。
但是当你开始链接时:
Collections.sort(playlist1,
comparing(p1 -> p1.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist()));
现在我们遇到了问题。我们知道复合表达式comparing(...).thenComparing(...)
的目标类型是Comparator<Song>
,但是因为链的接收者表达式comparing(p -> p.getTitle())
是一个泛型方法调用,我们不能从它的其他参数推断它的类型参数,我们有点不走运。由于我们不知道这个表达式的类型,所以也不知道它有thenComparing
方法等。
有几种方法可以解决这个问题,所有这些方法都涉及注入更多类型信息,以便可以正确键入链中的初始对象。在这里,它们的粗略顺序是降低可取性和增加侵入性:
使用精确的方法引用(没有重载的方法),例如Song::getTitle
。然后,这提供了足够的类型信息来推断 comparing()
调用的类型变量,因此给它一个类型,从而继续沿着链。
使用显式 lambda(如您在示例中所做的那样)。
为comparing()
调用提供类型见证:Comparator.<Song, String>comparing(...)
。
通过将接收器表达式强制转换为Comparator<Song>
,提供具有强制类型转换的显式目标类型。
【讨论】:
+1 用于实际回答 OP“为什么编译器不能推断这一点”,而不仅仅是给出解决方法/解决方案。 谢谢你的回答布赖恩。但是,我仍然发现一些未解决的问题,为什么 List.sort 的行为与 Collections.sort 不同,因为前者只需要第一个 lambda 来包含参数类型,但后者还需要最后一个,例如如果我有一个比较,然后是 5 个 thenComparing 调用,我必须将 (Song p1) 放在比较中,最后放在 thenComparing 中。同样在我的原始帖子中,您将看到 TreeSet 的底部示例,在该示例中我删除了所有参数类型,但最后一次调用 thenComparing 是可以的,但其他的不是 - 所以它的行为不同。 @user3780370 你还在使用 Eclipse 编译器吗?如果我正确理解您的问题,我还没有看到这种行为。您能否(a)使用 JDK 8 中的 javac 进行尝试,(b)如果仍然失败,请发布代码? @BrianGoetz 感谢您的建议。我刚刚使用 javac 在命令行窗口中编译了它,它按照您所说的进行编译。这似乎是一个 Eclipse 问题。我还没有更新到 Eclipse Luna,它是专门为 JDK8 构建的,所以希望它可以修复。实际上,我有一个屏幕截图可以向您展示 Eclipse 中发生的事情,但不知道如何在此处发布。 我想你的意思是Comparator.<Song, String>comparing(...)
。【参考方案2】:
问题在于类型推断。如果没有在第一个比较中添加 (Song s)
,comparator.comparing
不知道输入的类型,因此它默认为 Object。
您可以通过 3 种方法中的 1 种方法解决此问题:
使用新的 Java 8 方法参考语法
Collections.sort(playlist,
Comparator.comparing(Song::getTitle)
.thenComparing(Song::getDuration)
.thenComparing(Song::getArtist)
);
将每个比较步骤提取到本地参考
Comparator<Song> byName = (s1, s2) -> s1.getArtist().compareTo(s2.getArtist());
Comparator<Song> byDuration = (s1, s2) -> Integer.compare(s1.getDuration(), s2.getDuration());
Collections.sort(playlist,
byName
.thenComparing(byDuration)
);
编辑
强制比较器返回的类型(注意你需要输入类型和比较键类型)
sort(
Comparator.<Song, String>comparing((s) -> s.getTitle())
.thenComparing(p1 -> p1.getDuration())
.thenComparing(p1 -> p1.getArtist())
);
我认为“最后一个”thenComparing
语法错误会误导您。这实际上是整个链的类型问题,只是编译器仅将链的末尾标记为语法错误,因为我猜那是最终返回类型不匹配的时候。
我不确定为什么 List
的推理工作比 Collection
做得更好,因为它应该执行相同的捕获类型,但显然不是。
【讨论】:
为什么它知道ArrayList
而不是Collections
解决方案(假设链中的第一个调用有Song
参数)?
感谢您的回复,但是,如果您阅读我的帖子,您会看到我说:“在我们开始之前,我知道我可以使用方法引用,例如 Song::getTitle 来解决我的问题,但我在这里的查询并不是我想要修复的问题,而是我想要回答的问题,即为什么 Java 编译器会以这种方式处理它。”
我想知道为什么当我使用 lambda 表达式时编译器会这样。它接受比较(s -> s.getArtist()) 但是当我链接 .thenComparing(s -> s.getDuration()) 例如它给我两个调用的语法错误,如果我然后添加一个显式类型比较调用,例如compare((Song s) -> s.getArtist()) 那么这解决了这个问题,对于 List.sort 和 TreeSet 它还解决了所有进一步的编译错误,而无需添加额外的参数类型,但是对于 Collections.sort & Objects.compare 示例最后 thenComparing 仍然失败【参考方案3】:
处理这个编译时错误的另一种方法:
显式地转换你的第一个比较函数的变量,然后就可以开始了。我已经对 org.bson.Documents 对象的列表进行了排序。请看示例代码
Comparator<Document> comparator = Comparator.comparing((Document hist) -> (String) hist.get("orderLineStatus"), reverseOrder())
.thenComparing(hist -> (Date) hist.get("promisedShipDate"))
.thenComparing(hist -> (Date) hist.get("lastShipDate"));
list = list.stream().sorted(comparator).collect(Collectors.toList());
【讨论】:
【参考方案4】:playlist1.sort(...)
从 playlist1 的声明中为类型变量 E 创建一个 Song 的边界,它“涟漪”到比较器。
在Collections.sort(...)
中,没有这样的界限,从第一个比较器的类型推断,编译器还不足以推断其余部分。
我认为您会从 Collections.<Song>sort(...)
获得“正确”的行为,但没有安装 java 8 来为您测试。
【讨论】:
嗨,是的,您在添加收藏方面是正确的。以上是关于Java 8 Comparator 类型推断非常困惑的主要内容,如果未能解决你的问题,请参考以下文章
java图中代码改用Lambda表达式实现Comparator接口?
基于实现 Comparator 或 ToIntFunction、Java 8 的流排序、反转绝对值