Почему @SneakyThrows не генерирует исключение ClassCastException?

В этом вопросе просто спрашивается, почему не используется общий приведение. Вопросы об 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?

Где я ошибаюсь в своей ментальной модели?

Отвечает ли это на ваш вопрос? Стирание дженериков Java: когда и что происходит? То, что вы ищете, называется стиранием типа, и оно не относится конкретно к исключениям, а применимо в целом ко всем универсальным типам (и приведению к универсальным типам).

dan1st 17.09.2023 18:34

Ваша ментальная модель дженериков в основном верна. И если я правильно помню, вы получите ClassCastException, когда попытаетесь получить доступ к выброшенному Exception как RuntimeException`. Все вращается вокруг актерского состава. Я не могу найти точный параграф JLS, но там написано что-то вроде «проверки типов выполняются при необходимости». Это приводит к ситуациям, когда они вставляются только при доступе к значению и в неожиданных местах.

Turing85 17.09.2023 18:36

Этот вопрос связан.

Turing85 17.09.2023 18:39

@ dan1st Я просто понятия не имею, почему люди связывают старые вопросы, если они не дают четкого понимания ЭТОГО КОНКРЕТНОГО СЛУЧАЯ. Нет, это не отвечает на мой вопрос, мой вопрос просто в том, почему нет исключения ClassCastException там, где оно должно быть.

ng.newbie 17.09.2023 19:09

Очень похоже на этот вопрос

DuncG 17.09.2023 19:15

@DuncG Это был мой вопрос, я задал новый вопрос и удалил его, потому что он вызывал путаницу. Люди думали, что я говорю о выводе типа.

ng.newbie 17.09.2023 19:18

@ Turing85 Да, вопрос, на который вы ссылаетесь, похоже, связан, однако мой вопрос заключается в передаче конкретного типа и попытке приведения к общему типу. Почему этот каст не происходит? Я передаю SQLException со ссылкой на его суперкласс Exception, он пытается привести его к RuntimeException, это должно вызвать исключение, почему этого не происходит?

ng.newbie 17.09.2023 19:23

@ Turing85 Turing85 Так компилятор игнорирует мой приведение? Но мой код не компилируется без приведения, так что же дает. Нет ли простого ответа, почему это работает?

ng.newbie 17.09.2023 19:28

Я не совсем уверен. Мне нужно будет проанализировать байт-код. Похоже, что приведение вступит в силу только тогда, когда мы заставим, что выброшенный Exceptoin на самом деле является RuntimeException (без другого приведения). catch (RuntimeException e) { ... } не перехватывает исключение.

Turing85 17.09.2023 19:34

@Turing85 Turing85 Самое запутанное в том, что он не компилируется без приведения. Так где же и как именно используется актерский состав? catch(RuntimeException), очевидно, не перехватит его, но он также не генерирует исключение ClassCastException.

ng.newbie 17.09.2023 19:38

@Turing85 Здесь есть некоторая информация: javacodegeeks.com/2015/08/…

ng.newbie 17.09.2023 19:42

@Turing85 Turing85 A catch(RuntimeException e) явно не работает, так как RuntimeException не генерируется. catch(SQLException e) не скомпилируется из-за параметра Generic Type, который компилятор ожидает RuntimeException, а не SQLException.

ng.newbie 17.09.2023 19:46

@Turing85 Turing85 Если бы вы могли опубликовать свой анализ байт-кода, это было бы здорово.

ng.newbie 17.09.2023 19:47

@ Turing85 Нет, все, что я сказал тебе выше, — ерунда. Просто замените приведение на RuntimeException, и вы получите ClassCastException. Нет приведения, компилятор жалуется, что проверенные исключения не перехватываются.

ng.newbie 17.09.2023 19:51

@ Turing85 Turing85 Итак, актерский состав используется, но также и не используется. Это какой-то момент.

ng.newbie 17.09.2023 19:52
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
15
68
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы путаете дженерики с шаблонами (а-ля C++). Вы были на правильном пути, упомянув стирание типа, которое, как следует из названия, стирает любые следы информации о типе для компиляции.

Таким образом, ваш приведение на самом деле не происходит, а если и происходит, то не того типа, которого вы ожидаете.

Вам нужно будет использовать Class#cast, чтобы получить желаемое поведение, но получение экземпляра Class<T> не удобно для вашего варианта использования.

Я понятия не имею, о чем вы говорите. Это не имеет ничего общего с Class<T> или Class#cast.

ng.newbie 17.09.2023 19:04

У вас нет информации о типе дженериков во время выполнения, поэтому приведение фактически игнорируется, поэтому вы не получаете исключение. Вот почему вам нужно использовать Class#cast

Vivick 17.09.2023 19:11

Вся общая информация заменяется их границами. Общая информация удалена, а актерский состав - нет. Так почему же приведение не применяется?

ng.newbie 17.09.2023 19:20

По сути, он пытается выполнить приведение к RuntimeException. Я использую каст, но меня беспокоит, почему он не применяется.

ng.newbie 17.09.2023 19:21

@Вивик, у тебя есть какой-то неопровержимый факт, что актерский состав «игнорируется»? Я сомневаюсь в этом утверждении. Есть случаи, когда приведение типов нельзя игнорировать, независимо от того, задействованы дженерики или нет.

Turing85 17.09.2023 19:27

Игнорируется или относится к типу, который не является ожидаемым. Насколько я понимаю, приведения с дженериками применяются к «ближайшей» границе. Без ограничений это должно быть Object (бесполезно, поскольку все является Object). При наличии ограничения оно должно соответствовать этому ограничению (избыточно, поскольку мы знаем, что ограничение действительно).

Vivick 17.09.2023 21:56

Ваше заблуждение здесь:

Когда основной метод скомпилирован:

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 в качестве параметра. Вот что меня смущает.

ng.newbie 17.09.2023 21:00
Ответ принят как подходящий

Тип Стирание

Вы правы в том, что переменные типа стираются до крайней левой границы. Об этом говорится в §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 компилируется только один раз. У меня было такое ошибочное мнение.

ng.newbie 17.09.2023 21:39

но есть еще одна вещь, которая меня беспокоит: throws T, если код скомпилирован в throws Exception, то почему использование <RuntimeException>test() не дает ошибки компилятора?

ng.newbie 17.09.2023 21:40

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

ng.newbie 17.09.2023 21:42

Я переписал свои комментарии в раздел моего ответа. Помогает ли это ответить на ваши дополнительные вопросы?

Slaw 18.09.2023 18:58

Еще одна вещь, которая может помочь: Java — статически типизированный язык. Это позволяет компилятору выполнять множество проверок во время компиляции. Но иногда разработчик знает больше, чем компилятор. По сути, приведение типов — это когда разработчик говорит компилятору: «Поверьте мне, я знаю, что делаю». А дженерики — это, по сути, просто способ предоставить компилятору больше информации для работы, тем самым позволяя компилятору неявно выполнять за вас многие из этих почти гарантированно безопасных приведений («непроверенные» приведения все равно могут привести к ClassCastException). .

Slaw 18.09.2023 19:13

да, если вы добавите это в ответ, это очень поможет. Я не уверен, насколько надежна система комментариев.

ng.newbie 18.09.2023 20:39

Другие вопросы по теме