Почему Stream.sorted не безопасен для типов в Java 8?

Это из интерфейса Stream из реализации Oracle JDK 8:

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);
}

?

Очевидно, что у ребят, которые это реализуют (которые на несколько лет впереди меня, если учесть программирование и инжиниринг), должна быть очень веская причина, которую я не вижу, но что это за причина?

Ответ 1

По сути, вы спрашиваете, есть ли способ сообщить компилятору: "эй, этот метод требует, чтобы параметр типа соответствовал более конкретным границам, чем определено на уровне класса". Это невозможно в 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 {}

Теперь вы можете иметь элементы Stream of Bar но попробуйте отсортировать их, как если бы это были элементы Stream of Qux.

Stream<Bar> stream = barCollection.stream().sorted(Qux.class);

Поскольку и Bar и Qux соответствуют Qux Comparable<? super Foo> Comparable<? super Foo> нет ошибки времени компиляции и, следовательно, безопасность типов не добавляется. Кроме того, следствием требования аргумента Class является то, что он будет использоваться для приведения. Во время выполнения это может, как показано выше, по-прежнему приводить к ClassCastException s. Если Class не используется для приведения, то аргумент совершенно бесполезен; Я бы даже посчитал это вредным.

Следующий логический шаг - попытаться потребовать C T а также Comparable<? super T> Comparable<? super T>. Например:

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

Это также невозможно в Java и приводит к ошибке компиляции: "за параметром типа не могут следовать другие границы". Даже если бы это было возможно, я не думаю, что это решило бы все (если вообще что-нибудь).


Некоторые связанные заметки.

Относительно Stream.sorted(Comparator): не Stream делает этот метод безопасным с точки Stream.sorted(Comparator) типов, это Comparator. Comparator обеспечивает сравнение элементов. Для иллюстрации, типобезопасный способ сортировки Stream по естественному порядку элементов:

Stream<String> stream = stringCollection.stream().sorted(Comparator.naturalOrder());

Это безопасно для типов, потому что naturalOrder() требует, чтобы его параметр типа расширялся Comparable. Если универсальный тип Stream не расширяет Comparable то границы не будут совпадать, что приведет к ошибке компиляции. Но опять же, это Comparator который требует, чтобы элементы были Comparable *, в то время как Stream просто не важно.

Таким образом, возникает вопрос: почему разработчики изначально включили метод sorted без аргументов для Stream? Похоже, это связано с историческими причинами и объясняется в ответе Хольгера на другой вопрос.


* Comparator требует, чтобы элементы были Comparable в этом случае.В общем, Comparator, очевидно, способен обрабатывать любой тип, который он определил.

Ответ 2

Документация для Stream#sorted объясняет это прекрасно:

Возвращает поток, состоящий из элементов этого потока, отсортированный в естественном порядке. Если элементы этого потока не являются сопоставимыми, при выполнении операции терминала может быть сгенерировано исключение java.lang.ClassCastException.

Вы используете перегруженный метод, который не принимает аргументов (не тот, который принимает Comparator), а Foo не реализует Comparable.

Если вы спрашиваете, почему метод не генерирует ошибку компилятора, если содержимое Stream не реализует Comparable, это может произойти из-за того, что T не вынужден расширять Comparable, и T нельзя изменить без вызова Stream#map; Кажется, это только удобный метод, поэтому нет необходимости указывать явный Comparator когда элементы уже реализуют Comparable.

Чтобы он был безопасным по типу, T должен был бы расширить Comparable, но это было бы нелепо, так как это предотвратит поток, содержащий любые объекты, которые не являются Comparable.

Ответ 3

Как бы вы это реализовали? sorted - это промежуточная операция (может вызываться в любом месте между другими промежуточными операциями), что означает, что вы можете начать с потока, который не является Comparable, но вызвать sorted по такому, который Comparable:

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.