对Java 8比较器类型推断非常困惑

eyh26e7m  于 2022-12-21  发布在  Java
关注(0)|答案(4)|浏览(144)

我一直在研究Collections.sortlist.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())
        );

现在是CONFUSING部分。对于p laylist1.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的最后一次调用显示错误。
有谁能解释一下为什么会发生这种情况,以及为什么在简单地调用比较方法(没有进一步的thenComparing调用)时根本不需要使用(Song p1)
关于同一主题的另一个查询是当我对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.sortCollections.sort当我删除第一个Song参数类型时,所有调用都显示语法错误。
编辑以包括我在Eclipse Kepler SR 2中收到的错误的屏幕截图,我现在发现这些错误是Eclipse特有的,因为当在命令行上使用JDK 8 java编译器编译时,它编译正常。

lsmd5eda

lsmd5eda1#

首先,您所说的所有导致错误的示例在参考实现(来自JDK 8的javac)中编译良好。它们在IntelliJ中也工作良好,因此您看到的错误很可能是Eclipse特有的。
你潜在的问题似乎是:“为什么当我开始链接时它停止工作?”原因是,当lambda表达式和泛型方法调用作为方法参数出现时,它们是 *poly表达式 *(它们的类型是上下文相关的),而当它们作为方法接收器表达式出现时,它们不是。
当你说

Collections.sort(playlist1, comparing(p1 -> p1.getTitle()));

存在足够的类型信息来求解comparing()的类型自变量和自变量类型p1comparing()调用从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>,为显式目标类型提供强制转换。
ia2d9nvy

ia2d9nvy2#

问题出在类型推理上,如果没有在第一个比较中添加(Song s)comparator.comparing就不知道输入的类型,所以默认为Object。
您可以通过以下3种方法之一解决此问题:
1.使用新的Java 8方法引用语法

Collections.sort(playlist,
            Comparator.comparing(Song::getTitle)
            .thenComparing(Song::getDuration)
            .thenComparing(Song::getArtist)
            );

1.将每个比较步骤提取到本地引用中

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)
            );
    • 编辑**

1.强制使用Comparator返回的类型(注意,输入类型和比较键类型都需要)

sort(
  Comparator.<Song, String>comparing((s) -> s.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

我认为"last" thenComparing语法错误误导了你,它实际上是整个链的类型问题,只是编译器只将链的末尾标记为语法错误,因为我猜这是最终返回类型不匹配的时候。
我不知道为什么ListCollection做得更好,因为它应该做相同的捕获类型,但显然不是。

8dtrkrch

8dtrkrch3#

处理此编译时错误的另一种方法:
显式地转换你的第一个比较函数的变量,然后就可以开始了。我已经对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());
tv6aics1

tv6aics14#

playlist1.sort(...)根据playlist1的声明为类型变量E创建Song的边界,其“波及”到比较器。
Collections.sort(...)中,没有这样的界限,从第一个比较器的类型推断不足以让编译器推断其余的比较器。
我想你会从Collections.<Song>sort(...)中得到“正确”的行为,但是没有安装java8来测试它。

相关问题