为什么Java 8中的Stream.sorted方法不是类型安全的?

39

这是来自Oracle JDK 8实现的Stream接口:

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    Stream<T> sorted();
} 

很容易在运行时出现问题,而在编译时不会生成警告信息。以下是一个例子:

class Foo {
    public static void main(String[] args) {
        Arrays.asList(new Foo(), new Foo()).stream().sorted().forEach(f -> {});
    }
}

这段代码可以编译通过,但在运行时会抛出异常:

Exception in thread "main" java.lang.ClassCastException: Foo cannot be cast to java.lang.Comparable

可能的原因是,sorted方法未定义在编译器能够捕获此类问题的位置。也许我错了,但难道不是这么简单吗?
interface Stream<T> {
    <C extends Comparable<T>> void sorted(C c);
}

显然,实施这个功能的人(就编程和工程而言,他们比我超前了好几年)一定有我无法理解的很好的原因,但是这个原因是什么呢?


4
你是指Comparator吗?已经有 重载 的方法了。不过我不确定你为什么需要C - shmosel
4
不可能做到那个。 - shmosel
5
由于泛型的特性,其整体设计重点在于实现通用功能,这意味着不允许特定类型的逻辑。您提供的示例代码没有任何意义。我认为您试图重新定义T(这是不可能的),但实际上只是在为一个无用的参数定义了一个类型。 - shmosel
4
但是 sorted 函数不接受任何参数作为输入 - 你从哪里获取这个参数? - Eugene
6
顺便说一下,这个问题早在Java 8之前就存在了。 - Bohemian
显示剩余9条评论
3个回答

32
基本上,你正在询问是否有一种方法可以告诉编译器:“嘿,这个方法需要类型参数匹配比类级别定义的更具体的边界。”在Java中不可能实现这一点。这样的功能可能很有用,但我也预计会产生混淆和/或复杂性。
目前的泛型实现方式无法使Stream.sorted()类型安全,除非你想要避免使用Comparator。例如,你提出了以下内容:
public interface Stream<T> {

    <C extends Comparable<? super T>> Stream<T> sorted(Class<C> clazz);

} // other Stream methods omitted for brevity

很遗憾,Class<C> 不能保证可以从 Class<T> 赋值。考虑以下层次结构:
public class Foo implements Comparable<Foo> { /* implementation */ }

public class Bar extends Foo {}

public class Qux extends Foo {}

现在您可以拥有一个Bar元素的Stream,但尝试按照它是Qux元素的Stream进行排序。
Stream<Bar> stream = barCollection.stream().sorted(Qux.class);

由于 Bar 和 Qux 都与 Comparable<? super Foo> 匹配,因此没有编译时错误,因此不会添加类型安全性。此外,要求 Class 参数的含义是它将用于强制转换。在运行时,这仍然可能导致 ClassCastException 。如果未使用 Class 进行转换,则该参数完全无用; 我甚至认为它有害。
下一个逻辑步骤是尝试要求 C 扩展 T 以及 Comparable<? super T>。例如:
<C extends T & Comparable<? super T>> Stream<T> sorted(Class<C> clazz);

这在Java中也是不可能的,会导致编译错误:"type parameter cannot be followed by other bounds"。即使这样做是可能的,我认为它也解决不了所有问题(如果有的话)。
一些相关的注释。 关于Stream.sorted(Comparator):它的类型安全并不是由Stream实现的,而是由Comparator实现的。 Comparator确保元素可以进行比较。举例来说,按照元素的自然顺序对Stream进行排序的类型安全方式是:
Stream<String> stream = stringCollection.stream().sorted(Comparator.naturalOrder());

这是类型安全的,因为naturalOrder()要求其类型参数扩展Comparable。如果Stream的泛型类型不扩展Comparable,那么上限将不匹配,导致编译错误。但是,再次强调,是Comparator要求元素是Comparable*,而Stream根本不关心。
所以问题变成了,为什么开发人员首先在Stream中包含一个无参的sorted方法?这似乎是出于历史原因,并在Holger的另一个问题的答案中得到解释。

* 在这种情况下,Comparator要求元素是Comparable。一般来说,Comparator显然可以处理其定义的任何类型。


19
“Stream#sorted”的文档已经很好地解释了它的作用:
返回一个流,其中包含按自然顺序排序的此流的元素。如果此流的元素不可比较,则在执行终端操作时可能抛出java.lang.ClassCastException。
你正在使用不带参数的重载方法(而不是接受Comparator的方法),并且Foo没有实现Comparable
如果你想知道为什么如果Stream的内容没有实现Comparable,该方法为什么不会抛出编译器错误,那是因为T没有强制扩展Comparable,而且不能更改T而不调用Stream#map;它似乎只是一个方便的方法,因此当元素已经实现Comparable时,无需提供显式的Comparator
为了保证类型安全,T必须扩展Comparable,但这是荒谬的,因为它将防止流包含任何不是Comparable的对象。

1
我知道,但为什么sorted接受T而不是像我在问题末尾建议的那样的东西呢?可能是因为T没有被强制扩展Comparable。 - Koray Tugay
已经有一个重载的Stream#sorted方法可以接受一个Comparator。它接受一个Comparable没有太多意义。 - Jacob G.
@user7 没错 :) 这就是为什么文档中会说明“按自然顺序排序”。没有实现 Comparable 接口的对象就没有自然顺序。 - Jacob G.
@user7 是的,显然那将是一场灾难... 但我们不能只是定义一个方法参数类型为接口的通用类型,并限制它也是其他东西吗?(这就是我在问题中最后一个代码片段中尝试过的,至少我试过了..) - Koray Tugay
10
@JacobG. 实际上,通过方法参数限制接收器类型是可能的,而且这样的方法甚至已经存在,即 sorted(Comparator.naturalOrder()) 只能在流上调用,如果其元素是可比较的。添加另一个没有参数的 sorted() 方法是一个有意的决定。 - Holger
显示剩余2条评论

16

你会如何实现这个功能? sorted 是一项中间操作(可以在其他中间操作之间随时调用),这意味着你可以从一个不可比较的流开始,但在可比较的流上调用 sorted

Arrays.asList(new Foo(), new Foo())
      .stream()
      .map(Foo::getName) // name is a String for example
      .sorted()
      .forEach(f -> {});
你提出的方案需要输入参数,但是Stream::sorted不接受参数,所以你不能这样做。重载版本可以接受一个Comparator——这意味着你可以按属性对某些东西进行排序,但仍然返回Stream<T>。如果你尝试编写最小化的Stream接口/实现,我认为这很容易理解。

2
在这个例子中,“sorted”知道类型是Stream<String>,不是吗? - Koray Tugay
14
好的,如果不提供零参数的 sorted() 方法,则需要使用 sorted(Comparator.naturalOrder()) 代替,这样做将更加类型安全。这是一种权衡。可以参考这个答案,其中讨论了排序操作的特殊处理。 - Holger
@Holger,我希望你能提供一个答案,你在评论中分享的内容非常有价值,但可能会被忽视。 - Koray Tugay
@Holger 很好的观点,既然你已经明确说出来,我实在不明白为什么一开始就提供了简单的 sorted - Eugene

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