Почему javac допускает некоторые невозможные приведения, а другие нет?

52

Если я пытаюсь привести Stringк a java.util.Date, компилятор Java ловит ошибку. Так почему же компилятор не помечает следующее как ошибку?

List<String> strList = new ArrayList<>();                                                                      
Date d = (Date) strList;

Конечно, JVM генерирует a ClassCastExceptionво время выполнения, но компилятор не помечает его.

Поведение то же самое с javac 1.8.0_212 и 11.0.2.

Майк Войноски
источник
2
Здесь нет ничего особенного List. Date d = (Date) new Object();
Эллиот Фриш
1
Я играл с Arduino в последнее время. Мне бы понравился компилятор, который с радостью не принимал никаких приведений, а потом просто делал их с совершенно непредсказуемыми результатами. Строка в целое число? Конечно, вещь! Удвоить до целого числа? Да сэр! Строка в логическое значение? По крайней мере, этот человек в основном становится ложным ...
Стиан Иттервик
@ElliottFrisch: Существует очевидная связь наследования между датой и объектом, но нет никакой связи между датой и списком. Поэтому я ожидал, что компилятор пометит это приведение так же, как это будет помечать приведение типа String к Date. Но, как объясняет Забуза в своем превосходном ответе, List - это интерфейс, поэтому приведение было бы законным, если бы он strListбыл экземпляром класса, который реализует List.
Майк Войноски
Это часто повторяющийся вопрос, и я уверен, что видел несколько его дубликатов. Это в основном обратная версия сильно связанной: stackoverflow.com/questions/21812289/…
Халк
1
@StianYttervik -fpermissive это то, что делает это. Включите предупреждения компилятора.
Бобсбернер

Ответы:

86

Актерское это технически возможно. Javac не может легко доказать, что это не так в вашем случае, и JLS фактически определяет это как допустимую Java-программу, поэтому пометка ошибки будет неправильной.

Это потому, что Listэто интерфейс. Таким образом, у вас может быть подкласс класса a, Dateкоторый на самом деле реализует Listзамаскированный как Listздесь - и затем приведение к нему Dateбудет совершенно нормально. Например:

public class SneakyListDate extends Date implements List<Foo> {
    ...
}

А потом:

List<Foo> list = new SneakyListDate();
Date date = (Date) list; // This one is valid, compiles and runs just fine

Обнаружение такого сценария не всегда возможно, так как для этого потребуется информация времени выполнения, если экземпляр поступает, например, из метода. И даже если это потребует гораздо больше усилий для компилятора. Компилятор только предотвращает приведения, которые абсолютно невозможны из-за невозможности сопоставления дерева классов. Что не так здесь, как видно.

Обратите внимание, что JLS требует, чтобы ваш код был допустимой программой Java. В 5.1.6.1. Разрешено сужающее ссылочное преобразование :

Существует сужающее ссылочное преобразование из ссылочного типа Sв ссылочный тип, Tесли выполняются все следующие условия :

  • [...]
  • Применяется один из следующих случаев :
    • [...]
    • Sявляется типом интерфейса, Tявляется типом класса и Tне называет finalкласс.

Таким образом, даже если компилятор может выяснить, что ваш случай действительно доказуемо невозможен, он не может пометить ошибку, потому что JLS определяет ее как допустимую программу Java.

Было бы разрешено только показать предупреждение.

Zabuzard
источник
16
И стоит заметить, что причина, по которой он улавливает случай с String, заключается в том, что String является окончательным, поэтому компилятор знает, что ни один класс не может его расширить.
MTilsted
5
На самом деле, я не думаю, что это «окончательность» String, которая делает myDate = (Date) myStringнеудачу. Используя терминологию JLS, оператор пытается преобразовать из S(the String) в T(the Date). Здесь Sне тип интерфейса, поэтому приведенное выше условие JLS не применяется. В качестве примера попробуйте привести Календарь к дате, и вы получите ошибку компилятора, даже если ни один из классов не является окончательным.
Майк Войноски
1
Я не знаю, разочарован или нет компилятор не может сделать достаточно статического анализа, чтобы доказать, что strList может быть только когда-либо типа ArrayList.
Джошуа
3
Компилятору не запрещено проверять. Но запрещено называть это ошибкой. Это сделало бы компилятор несовместимым. (Смотрите мой ответ ...)
Стивен C
3
Чтобы добавить немного языка, компилятор должен доказать, что тип Date & Listне пригоден для жизни , недостаточно просто доказать, что он необитаем в настоящее время (это может произойти в будущем).
Полигном
15

Давайте рассмотрим обобщение вашего примера:

List<String> strList = someMethod();       
Date d = (Date) strList;

Это основные причины, почему Date d = (Date) strList;не ошибка компиляции.

  • Интуитивная причина в том , что компилятор не (в целом) знать точный тип объекта , возвращаемого этим методом вызова. Возможно, что в дополнение к классу, который реализует List, это также подкласс Date.

  • Техническая причина в том , что Спецификация языка Java «позволяет» Сужение ссылочное преобразование , которое соответствует этому типу актеров. Согласно JLS 5.1.6.1 :

    «Существует сужающее ссылочное преобразование из ссылочного типа Sв ссылочный тип, Tесли выполняются все следующие условия:»

    ...

    5) « Sявляется типом интерфейса, Tявляется типом класса и Tне называет finalкласс».

    ...

    В другом месте JLS также говорит, что исключение может быть выдано во время выполнения ...

    Обратите внимание, что определение JLS 5.1.6.1 основано исключительно на объявленных типах задействованных переменных, а не на фактических типах времени выполнения. В общем случае компилятор не знает и не может знать фактические типы времени выполнения.


Итак, почему Java-компилятор не может решить, что приведение не будет работать?

  • В моем примере someMethodвызов может возвращать объекты различных типов. Даже если компилятор смог проанализировать тело метода и определить точный набор типов, которые могут быть возвращены, ничто не помешает кому-то изменить его, чтобы он возвращал разные типы ... после компиляции кода, который его вызывает. Это основная причина, почему JLS 5.1.6.1 говорит то, что говорит.

  • В вашем примере умный компилятор может выяснить, что приведение никогда не может быть успешным. И разрешено выдавать предупреждение во время компиляции, чтобы указать на проблему.

Так почему же умному компилятору не разрешается говорить, что это ошибка?

  • Потому что JLS говорит, что это действительная программа. Период. Любой компилятор, который назвал это ошибкой , не будет Java-совместимым.

  • Кроме того, любой компилятор, который отклоняет программы Java, которые JLS и другие компиляторы считают допустимыми, является препятствием для переносимости исходного кода Java.

Стивен С
источник
4
Подтверждение того факта, что после компиляции вызывающего класса реализация вызываемой функции может измениться , поэтому, даже если это доказуемо во время компиляции при текущей реализации вызываемого, что приведение невозможно, это может быть не так в более поздние времена выполнения когда вызываемый объект изменился или был заменен.
Питер - Восстановить Монику
2
Upvote для выделения проблемы переносимости, которая возникнет, если компилятор попытается быть слишком умным.
Майк Войноски
2

5.5.1. Тип литья:

Заданный тип ссылки времени компиляции S(источник) и тип ссылки времени компиляцииT (цель), преобразование литья существует с , Sчтобы , Tесли никаких ошибок времени компиляции не происходят из - за следующие правила.

[...]

Если Sэто тип интерфейса:

  • [...]

  • Если Tкласс или тип интерфейса , который не является окончательным, то , если существует супертип Xиз Tи надтипа Yиз S, например , что оба Xи Yявляются доказуемо различными параметризованными типами, и что подчистки из XиY тех же, ошибки времени компиляции происходит.

    В противном случае приведение всегда допустимо во время компиляции (потому что даже если Tне реализуется S, подкласс Tможет).

List<String>есть Sи Dateесть Tв вашем случае.

Александр Пирохов
источник