Подтвердить что ты не робот

Какие части JLS оправдывают возможность бросить проверенные исключения, как если бы они были сняты?

Недавно обнаружил и сообщил о факте, что можно пропустить проверенное исключение через javac-компилятор и выбросить его там, где его не нужно бросать. Это компилируется и запускается на Java 6 и 7, бросая предложение SQLException без throws или catch:

public class Test {

    // No throws clause here
    public static void main(String[] args) {
        doThrow(new SQLException());
    }

    static void doThrow(Exception e) {
        Test.<RuntimeException> doThrow0(e);
    }

    static <E extends Exception> void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

Сгенерированный байт-код указывает, что JVM действительно не заботится о проверенных/непроверенных исключениях:

// Method descriptor #22 (Ljava/lang/Exception;)V
// Stack: 1, Locals: 1
static void doThrow(java.lang.Exception e);
  0  aload_0 [e]
  1  invokestatic Test.doThrow0(java.lang.Exception) : void [25]
  4  return
    Line numbers:
      [pc: 0, line: 11]
      [pc: 4, line: 12]
    Local variable table:
      [pc: 0, pc: 5] local: e index: 0 type: java.lang.Exception

// Method descriptor #22 (Ljava/lang/Exception;)V
// Signature: <E:Ljava/lang/Exception;>(Ljava/lang/Exception;)V^TE;
// Stack: 1, Locals: 1
static void doThrow0(java.lang.Exception e) throws java.lang.Exception;
  0  aload_0 [e]
  1  athrow
    Line numbers:
      [pc: 0, line: 16]
    Local variable table:
      [pc: 0, pc: 2] local: e index: 0 type: java.lang.Exception

JVM принимает это одно. Но у меня есть некоторые сомнения в том, должен ли Java-язык. Какие части JLS оправдывают это поведение? Это ошибка? Или хорошо скрытая "функция" языка Java?

Мои чувства:

  • doThrow0() <E> привязан к RuntimeException в doThrow(). Следовательно, в doThrow() не требуется предложение throws в строках JLS §11.2.
  • RuntimeException совместим с назначением с Exception, поэтому никакой компилятор (который приведет к созданию ClassCastException) не будет сгенерирован компилятором.
4b9b3361

Ответ 1

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

Ответ от авторов Generics, скорее всего, будет выглядеть следующим образом: "Если вы используете непроверенные броски, это ваша проблема".

Я вижу что-то весьма позитивное в вашем открытии, вырваться из оплота проверенных исключений. К сожалению, это не может превратить существующие проверенные-исключенные-отравленные API в нечто более приятное в использовании.

Как это может помочь

В типичном проектном проекте с многоуровневыми приложениями будет много шаблонов, подобных этому:

try {
  ... business logic stuff ...
} 
catch (RuntimeException e) { throw e; } 
catch (Exception e) { throw new RuntimeException(e); }

Почему я это делаю? Простой: нет исключений для бизнес-значений для улова; любое исключение является симптомом ошибки времени выполнения. Исключение должно распространяться на стек вызовов до глобального барьера исключения. С отличным вкладом Лукаса я могу теперь написать

try {
  ... business logic stuff ... 
} catch (Exception e) { throwUnchecked(e); }

Это может показаться не очень много, но выгоды накапливаются после повторения его 100 раз в течение всего проекта.

Отказ

В моих проектах существует высокая дисциплина в отношении исключений, поэтому это подходит для них. Этот тип обмана не является чем-то, что нужно принять в качестве общего принципа кодирования. Приобретение исключения по-прежнему является единственным безопасным вариантом во многих случаях.

Ответ 2

Ну, это один из нескольких способов повысить исключение проверки, как если бы он был снят. Class.newInstance() является другим, Thread.stop(Trowable) устаревшим.

Единственный способ, чтобы JLS не принимала это поведение, - это то, что среда выполнения (JVM) обеспечит ее выполнение.

Как было указано: Это не так. Проверенные и непроверенные исключения ведут себя одинаково. Для проверенных исключений просто требуется блок catch или предложение throws.

Изменить. Основываясь на обсуждении в комментариях, пример на основе списка, раскрывающий основную причину: Erasure

public class Main {
    public static void main(String[] args) {
        List<Exception> myCheckedExceptions = new ArrayList<Exception>();
        myCheckedExceptions.add(new IOException());

        // here we're tricking the compiler
        @SuppressWarnings("unchecked")
        List<RuntimeException> myUncheckedExceptions = (List<RuntimeException>) (Object) myCheckedExceptions;

        // here we aren't any more, at least not beyond the fact that type arguments are erased
        throw throwAny(myUncheckedExceptions);
    }

    public static <T extends Throwable> T throwAny(Collection<T> throwables) throws T {
        // here the compiler will insert an implicit cast to T (just like your explicit one)
        // however, since T is a type variable, it gets erased to the erasure of its bound
        // and that happens to be Throwable, so a useless cast...
        throw throwables.iterator().next();
    }
}

Ответ 3

Этот пример описан в Стандарт безопасного кодирования Oracle CERT для Java, который документирует несколько несовместимых примеров кода.

Несоответствующий пример кода (общее исключение)

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

interface Thr<EXC extends Exception> {
  void fn() throws EXC;
}

public class UndeclaredGen {
static void undeclaredThrow() throws RuntimeException {
@SuppressWarnings("unchecked")  // Suppresses warnings
Thr<RuntimeException> thr = (Thr<RuntimeException>)(Thr)
  new Thr<IOException>() {
    public void fn() throws IOException {
      throw new IOException();
}
  };
thr.fn();
}

public static void main(String[] args) {
  undeclaredThrow();
 }
}

Это работает, потому что RuntimeException является дочерним классом Exception, и вы не можете преобразовать какой-либо класс, простирающийся от Exception до RuntimeException, но если вы сделаете как показано ниже, он будет работать

 Exception e = new IOException();
 throw (RuntimeException) (e);

Случай, который вы делаете, аналогичен этому. Поскольку это явный тип приведения, этот вызов приведет к ClassCastException, но компилятор разрешает его.

Из-за Erasure, однако, в вашем случае нет приведения в действие, и, следовательно, в вашем сценарии он не бросает ClassCastException

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

Однако, если вы замените подпись подписи ниже, она начнет жаловаться на нее.

static <E extends Exception> void doThrow0(E e) throws E {

Ответ 4

JLS 11.2:

Для каждого проверенного исключения, которое является возможным результатом, броски условие для метода (§8.4.6) или конструктор (§8.8.5) должно указывать класс этого исключения или один из суперклассов класса это исключение (§11.2.3).

Это ясно указывает, что doThrow должно иметь исключение в этом вопросе. Или, поскольку задействовано downcasting (Exception to RuntimeException), необходимо проверить, что Exception IS RuntimeException, которое должно завершиться неудачей в примере, потому что исключаемое исключение - SQLException. Таким образом, ClassCastException должно быть запущено во время выполнения.

С практической стороны эта ошибка java позволяет создать хак небезопасно для любого стандартного кода обработки исключений, например next:

try {
 doCall();
} catch (RuntimeException e) {
 handle();
}

Исключение будет расти без обработки.

Ответ 5

Я не думаю, что можно оспорить, что JLS допускает этот код. Вопрос заключается не в том, должен ли этот кодекс быть законным. Компилятор просто не располагает достаточной информацией для выдачи ошибки.

Проблема здесь с кодом, испускаемым при вызове метода генерирования метаданных. Компилятор в настоящее время вставляет типы приведения после вызова методов возвращаемого типа, где возвращаемое значение будет немедленно присвоено ссылке.

Если компилятор захотел, это могло бы предотвратить проблему, молча окружая все вызовы метода обобщенного металирования в фрагменте try-catch-rethrow. Поскольку исключение будет поймано и присвоено локальной переменной, тип cast будет обязательным. Таким образом, вы получите ClassCastException, если это не экземпляр RuntimeException.

Но спецификация не требует ничего особенного в методах генерирования метаданных. Я считаю, что ошибка спецификации. Почему бы не написать об этом ошибку, чтобы ее можно было зафиксировать или хотя бы задокументировать.

Кстати, ClassCastException подавляет исходное исключение, что может привести к труднодоступным ошибкам. Но эта проблема имеет простое решение с JDK 7.