В этом вопросе просто спрашивается, почему не используется общий приведение. Вопросы об Erasure, похоже, не объясняют этот крайний случай.
Прежде чем я начну, позвольте мне предварить это, сказав, что меня НЕ интересует вывод типов, как описано здесь:
Особенность вывода типа исключения в Java 8
Это просто для того, чтобы избежать путаницы.
Меня интересует, почему следующий код работает без ClassCastException ?
import java.sql.SQLException;
public class GenericThrows {
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
}
Скомпилируйте код с помощью:
javac -source 1.7 -target 1.7 GenericThrows.java
И производит:
Exception in thread "main" java.sql.SQLException
at GenericTest.main(GenericTest.java:9)
Моя мысленная модель Java Generics и Type Erasure (и почему я думаю, что это бессмысленно):
Когда статический метод компилируется:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
Стирание типов удаляет все универсальные типы и заменяет их верхней границей заданного типа, поэтому метод фактически становится следующим:
static void test(Exception d) throws Exception {
throw (Exception) d;
}
Надеюсь, я прав.
Когда основной метод скомпилирован:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
Параметр типа заменяется конкретным типом: java.lang.RuntimeException.
Таким образом, метод фактически становится:
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
Надеюсь, я прав.
Итак, когда я пытаюсь привести SQLException к RuntimeException, я должен получить ClassCastException, именно это и происходит, если я пишу код без дженериков:
import java.sql.SQLException;
public class NonGenericThrows {
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
public static void main(String[] args) {
NonGenericThrows.test(new SQLException());
}
}
Компиляция и исполнение:
javac -source 1.7 -target 1.7 NonGenericThrows.java
java NonGenericThrows
Полученные результаты:
Exception in thread "main" java.lang.ClassCastException: class java.sql.SQLException cannot be cast to class java.lang.RuntimeException (java.sql.SQLException is in module java.sql of loader 'platform'; java.lang.RuntimeException is in module java.base of loader 'bootstrap')
at NonGenericThrows.test(NonGenericThrows.java:5)
at NonGenericThrows.main(NonGenericThrows.java:9)
Тогда почему универсальная версия не выдает ClassCastException?
Где я ошибаюсь в своей ментальной модели?
Ваша ментальная модель дженериков в основном верна. И если я правильно помню, вы получите ClassCastException, когда попытаетесь получить доступ к выброшенному Exception как RuntimeException`. Все вращается вокруг актерского состава. Я не могу найти точный параграф JLS, но там написано что-то вроде «проверки типов выполняются при необходимости». Это приводит к ситуациям, когда они вставляются только при доступе к значению и в неожиданных местах.
‼Этот вопрос связан.
@ dan1st Я просто понятия не имею, почему люди связывают старые вопросы, если они не дают четкого понимания ЭТОГО КОНКРЕТНОГО СЛУЧАЯ. Нет, это не отвечает на мой вопрос, мой вопрос просто в том, почему нет исключения ClassCastException там, где оно должно быть.
Очень похоже на этот вопрос
@DuncG Это был мой вопрос, я задал новый вопрос и удалил его, потому что он вызывал путаницу. Люди думали, что я говорю о выводе типа.
@ Turing85 Да, вопрос, на который вы ссылаетесь, похоже, связан, однако мой вопрос заключается в передаче конкретного типа и попытке приведения к общему типу. Почему этот каст не происходит? Я передаю SQLException со ссылкой на его суперкласс Exception, он пытается привести его к RuntimeException, это должно вызвать исключение, почему этого не происходит?
@ Turing85 Turing85 Так компилятор игнорирует мой приведение? Но мой код не компилируется без приведения, так что же дает. Нет ли простого ответа, почему это работает?
Я не совсем уверен. Мне нужно будет проанализировать байт-код. Похоже, что приведение вступит в силу только тогда, когда мы заставим, что выброшенный Exceptoin на самом деле является RuntimeException (без другого приведения). catch (RuntimeException e) { ... } не перехватывает исключение.
@Turing85 Turing85 Самое запутанное в том, что он не компилируется без приведения. Так где же и как именно используется актерский состав? catch(RuntimeException), очевидно, не перехватит его, но он также не генерирует исключение ClassCastException.
@Turing85 Здесь есть некоторая информация: javacodegeeks.com/2015/08/…
@Turing85 Turing85 A catch(RuntimeException e) явно не работает, так как RuntimeException не генерируется. catch(SQLException e) не скомпилируется из-за параметра Generic Type, который компилятор ожидает RuntimeException, а не SQLException.
@Turing85 Turing85 Если бы вы могли опубликовать свой анализ байт-кода, это было бы здорово.
@ Turing85 Нет, все, что я сказал тебе выше, — ерунда. Просто замените приведение на RuntimeException, и вы получите ClassCastException. Нет приведения, компилятор жалуется, что проверенные исключения не перехватываются.
@ Turing85 Turing85 Итак, актерский состав используется, но также и не используется. Это какой-то момент.




Вы путаете дженерики с шаблонами (а-ля C++). Вы были на правильном пути, упомянув стирание типа, которое, как следует из названия, стирает любые следы информации о типе для компиляции.
Таким образом, ваш приведение на самом деле не происходит, а если и происходит, то не того типа, которого вы ожидаете.
Вам нужно будет использовать Class#cast, чтобы получить желаемое поведение, но получение экземпляра Class<T> не удобно для вашего варианта использования.
Я понятия не имею, о чем вы говорите. Это не имеет ничего общего с Class<T> или Class#cast.
У вас нет информации о типе дженериков во время выполнения, поэтому приведение фактически игнорируется, поэтому вы не получаете исключение. Вот почему вам нужно использовать Class#cast
Вся общая информация заменяется их границами. Общая информация удалена, а актерский состав - нет. Так почему же приведение не применяется?
По сути, он пытается выполнить приведение к RuntimeException. Я использую каст, но меня беспокоит, почему он не применяется.
@Вивик, у тебя есть какой-то неопровержимый факт, что актерский состав «игнорируется»? Я сомневаюсь в этом утверждении. Есть случаи, когда приведение типов нельзя игнорировать, независимо от того, задействованы дженерики или нет.
Игнорируется или относится к типу, который не является ожидаемым. Насколько я понимаю, приведения с дженериками применяются к «ближайшей» границе. Без ограничений это должно быть Object (бесполезно, поскольку все является Object). При наличии ограничения оно должно соответствовать этому ограничению (избыточно, поскольку мы знаем, что ограничение действительно).
Ваше заблуждение здесь:
Когда основной метод скомпилирован:
static <T extends Exception> void test(Exception d) throws T { throw (T) d; } public static void main(String[] args) { GenericTest.<RuntimeException>test(new SQLException()); }Параметр типа заменяется конкретным типом:
java.lang.RuntimeException.Таким образом, метод фактически выглядит следующим образом: << это ваше заблуждение: метод не меняется!
static void test(Exception d) throws RuntimeException { throw (RuntimeException) d; }
Обобщенные методы в Java работают не так — метод test() не перекомпилируется и не изменяется каким-либо образом. Метод:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
Поскольку T может быть использовано любым Exception, приведение (T) d по сути является пустым, и компилятор не создает никакого кода для этого приведения (он не может выполнить приведение: вызывающая сторона не передает тип T, и код должен работать для всех T extends Exception, но d в любом случае обязательно является Exception, потому что это объявленный тип параметра.)
Компилятор проверяет только место вызова. Является ли переданный параметр (SQLException) Exception или каким-либо подклассом Exception? Да, SQLException является подклассом Exception, поэтому вызов компилируется.
Но код завершится неудачей, если я удалю приведение (T) d и не буду использовать RuntimeException в качестве параметра. Вот что меня смущает.
Вы правы в том, что переменные типа стираются до крайней левой границы. Об этом говорится в §4.6 Стирание типов Спецификации языка Java (JLS).
- Стирание переменной типа (§4.4) — это стирание ее крайней левой границы.
Это означает, что стирание:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
Является:
static void test(Exception d) throws Exception {
throw (Exception) d;
}
Но обратите внимание, что это зависит только от параметра типа. Аргумент типа не имеет значения. В отличие от таких языков, как C++, где каждая уникальная параметризация шаблона приводит к созданию уникального скомпилированного кода, Java компилирует дженерики только один раз. Итак, если вы позвоните выше с помощью:
ClassName.<RuntimeException>test(new SQLException());
В итоге вы не получите байт-код, в котором граница T была изменена на RuntimeException. Крайняя левая граница по-прежнему Exception.
Дженерики не исчезают полностью после компиляции. Если бы они это сделали, то дженерики были бы непригодны для использования при компиляции с предварительно скомпилированными библиотеками.
Что касается приведенного выше метода test, это правда, что сам метод не может знать, чем он был параметризован. Однако тот факт, что метод является универсальным с параметром типа T extends Exception, и что он throws T сохраняется. Другими словами, статическая общая информация записывается в байт-код. Вы даже можете получить эту информацию посредством отражения.
По сути, дженерики — это «иллюзия» компилятора. Это функция языка, а не функция времени выполнения (JVM). С точки зрения компилятора, дженерики существуют всегда (если не используются необработанные типы).
Вот почему вам не нужно обрабатывать исключение при вызове test с RuntimeException (или каким-либо подклассом) в качестве аргумента типа. Компилятор знает о T и, следовательно, в данном случае «знает», что метод не генерирует проверенное исключение. Однако обратите внимание, что строка (T) d метода test выдает предупреждение компилятора о «непроверенном приведении». Это предупреждение появляется именно потому, что приведение может быть успешным, но это не означает, что код не сломан (например, утверждение T является непроверяемым исключением, но затем метод фактически генерирует проверенное исключение).
Из §5.5 Контексты кастинга JLS:
Если в контексте приведения используется сужающее преобразование ссылок, которое отмечено или частично не отмечено (§5.1.6.2 , §5.1.6.3), то для класса значения выражения будет выполнена проверка во время выполнения. , возможно, вызывая
ClassCastException. В противном случае проверка времени выполнения не выполняется. [курсив добавлен]
И из §5.1.6.2 Отмеченные и неотмеченные сужающие ссылочные конверсии:
- Сужающее преобразование ссылки из типа
Sв переменную типаTне отмечено.
И из §5.1.6.3 Сужение ссылочных преобразований во время выполнения:
Неотмеченное преобразование сужающей ссылки из
Sв тип без пересеченияTполностью не отмечено, если|S| <: |T|.В противном случае он частично не отмечен.
Учитывая все это, следующее:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
Это совершенно неконтролируемый актерский состав. Таким образом, нет проверки времени выполнения. Вы можете убедиться в этом, посмотрев на байт-код.
Мне нравится этот ответ. Вы попали прямо в точку, когда сказали, что Java Generics компилируется только один раз. У меня было такое ошибочное мнение.
но есть еще одна вещь, которая меня беспокоит: throws T, если код скомпилирован в throws Exception, то почему использование <RuntimeException>test() не дает ошибки компилятора?
это должно дать мне сообщение об ошибке, что я все еще выдаю исключение без улова, верно? Извините за глупый вопрос, я не знаком с математическим языком JLS.
Я переписал свои комментарии в раздел моего ответа. Помогает ли это ответить на ваши дополнительные вопросы?
Еще одна вещь, которая может помочь: Java — статически типизированный язык. Это позволяет компилятору выполнять множество проверок во время компиляции. Но иногда разработчик знает больше, чем компилятор. По сути, приведение типов — это когда разработчик говорит компилятору: «Поверьте мне, я знаю, что делаю». А дженерики — это, по сути, просто способ предоставить компилятору больше информации для работы, тем самым позволяя компилятору неявно выполнять за вас многие из этих почти гарантированно безопасных приведений («непроверенные» приведения все равно могут привести к ClassCastException). .
да, если вы добавите это в ответ, это очень поможет. Я не уверен, насколько надежна система комментариев.
Отвечает ли это на ваш вопрос? Стирание дженериков Java: когда и что происходит? То, что вы ищете, называется стиранием типа, и оно не относится конкретно к исключениям, а применимо в целом ко всем универсальным типам (и приведению к универсальным типам).