Обработка исключений в Java

Обработка исключений в Java

1. обзор

В этом руководстве мы рассмотрим основы обработки исключений в Java, а также некоторые из ее ошибок.

2. Первые принципы

2.1. Что это?

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

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

Аналогично, в Java код может испытывать ошибки при выполнении наших инструкций. Хорошийexception handling может обрабатывать ошибки и изящно перенаправлять программу, чтобы пользователь оставался положительным.

2.2. Зачем это использовать?

Обычно мы пишем код в идеализированной среде: файловая система всегда содержит наши файлы, сеть исправна, а JVM всегда имеет достаточно памяти. Иногда мы называем это «счастливым путем».

Однако в процессе работы файловые системы могут повреждаться, сети выходят из строя, а JVM не хватает памяти. Благополучие нашего кода зависит от того, как он работает с «несчастными путями».

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

public static List getPlayers() throws IOException {
    Path path = Paths.get("players.dat");
    List players = Files.readAllLines(path);

    return players.stream()
      .map(Player::new)
      .collect(Collectors.toList());
}

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

Но что может произойти в производственной среде, еслиplayers.dat  отсутствует?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist
    at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
    at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
    // ... more stack trace
    at java.nio.file.Files.readAllLines(Unknown Source)
    at java.nio.file.Files.readAllLines(Unknown Source)
    at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
    at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Without handling this exception, an otherwise healthy program may stop running altogether! Нам нужно убедиться, что в нашем коде есть план на случай, если что-то пойдет не так.

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

3. Иерархия исключений

В конечном итогеexceptions  - это просто объекты Java, все они расширяются отThrowable:

              ---> Throwable <---
              |    (checked)     |
              |                  |
              |                  |
      ---> Exception           Error
      |    (checked)        (unchecked)
      |
RuntimeException
  (unchecked)

Существует три основных категории исключительных условий:

  • Проверенные исключения

  • Непроверенные исключения / исключения во время выполнения

  • ошибки

Время выполнения и непроверенные исключения относятся к одному и тому же. Мы часто можем использовать их взаимозаменяемо.

3.1. Checked Exceptionsс

Проверенные исключения - это исключения, которые компилятор Java требует от нас обработки. Мы должны либо декларативно выбросить исключение в стек вызовов, либо мы должны обработать его сами. Подробнее об этих моментах.

Oracle’s documentation говорит нам использовать проверенные исключения, когда мы можем разумно ожидать, что вызывающий наш метод сможет восстановить.

Несколько примеров проверенных исключений:IOException иServletException..

3.2. Непроверенные исключения

Непроверенные исключения - это исключения, которые компилятор Java требует от нас обрабатыватьnot.

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

И хотя это звучит удобно,Oracle’s documentation говорит нам, что есть веские причины для обеих концепций, например, различие между ситуационной ошибкой (отмечено) и ошибкой использования (не отмечено).

Некоторые примеры непроверенных исключений:NullPointerException, IllegalArgumentException, andSecurityException.

3.3. ошибки

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

И хотя они не расширяютRuntimeException, они также не отмечены.

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

Вот несколько примеров ошибок:StackOverflowError иOutOfMemoryError.

4. Обработка исключений

В Java API есть много мест, где что-то может пойти не так, и некоторые из этих мест помечены исключениями, либо в сигнатуре, либо в Javadoc:

/**
 * @exception FileNotFoundException ...
 */
public Scanner(String fileName) throws FileNotFoundException {
   // ...
}

Как было сказано немного ранее, когда мы вызываем эти «рискованные» методы, мыmust обрабатываем отмеченные исключения, аmay - непроверенные. Java дает нам несколько способов сделать это:

4.1. throwsс

Самый простой способ «обработать» исключение - это сбросить его:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {

    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

ПосколькуFileNotFoundException  - проверенное исключение, это простейший способ удовлетворить компилятор, ноit does mean that anyone that calls our method now needs to handle it too!

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

4.2. try -catch

Если мы хотим попытаться обработать исключение самостоятельно, мы можем использовать блокtry-catch. Мы можем справиться с этим, отбросив наше исключение:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        throw new IllegalArgumentException("File not found");
    }
}

Или выполнив шаги восстановления:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch ( FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    }
}

4.3. finallyс

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

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

Конечно, можем ли мы прочитать файл или нет, мы хотим убедиться, что мы делаем соответствующую очистку!

Давайте сначала попробуем "ленивый" способ:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
    Scanner contents = null;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } finally {
        if (contents != null) {
            contents.close();
        }
    }
}

Здесь блокfinally указывает, какой код мы хотим, чтобы Java запускала, независимо от того, что происходит при попытке прочитать файл.

Даже еслиFileNotFoundException выбрасывается в стек вызовов, Java перед этим вызовет содержимоеfinally.

Мы также можем обработать исключениеand, чтобы убедиться, что наши ресурсы закрыты:

public int getPlayerScore(String playerFile) {
    Scanner contents;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    } finally {
        try {
            if (contents != null) {
                contents.close();
            }
        } catch (IOException io) {
            logger.error("Couldn't close the reader!", io);
        }
    }
}

Посколькуclose также является «рискованным» методом, нам также необходимо перехватить его исключение!

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

4.4. try-с-ресурсами

К счастью, начиная с Java 7, мы можем упростить приведенный выше синтаксис при работе с вещами, расширяющимиAutoCloseable:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
      return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e ) {
      logger.warn("File not found, resetting score.");
      return 0;
    }
}

Когда мы размещаем ссылки, которые являютсяAutoClosable с объявлениемtry , нам не нужно закрывать ресурс самостоятельно.

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

Прочтите нашу статью, посвященнуюtry-with-resources, чтобы узнать больше.

4.5. Несколько блоковcatch

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

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Многократные уловы дают нам возможность по-разному обрабатывать каждое исключение в случае необходимости.

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

Допустим, однако, что нам нужно рассматриватьFileNotFoundException  иначе, чем более общийIOException:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile)) ) {
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e) {
        logger.warn("Player file not found!", e);
        return 0;
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Java позволяет обрабатывать исключения подкласса отдельно,remember to place them higher in the list of catches.

4.6. Объединение блоковcatch

Когда мы знаем, что способ обработки ошибок будет одинаковым, Java 7 представила возможность перехватывать несколько исключений в одном блоке:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException | NumberFormatException e) {
        logger.warn("Failed to load score!", e);
        return 0;
    }
}

5. Бросать исключения

Если мы не хотим обрабатывать исключение самостоятельно или хотим сгенерировать наши исключения, чтобы другие могли их обрабатывать, то нам необходимо ознакомиться с ключевым словомthrow.

Допустим, у нас есть следующее проверенное исключение, которое мы создали сами:

public class TimeoutException extends Exception {
    public TimeoutException(String message) {
        super(message);
    }
}

и у нас есть метод, который потенциально может занять много времени:

public List loadAllPlayers(String playersFile) {
    // ... potentially long operation
}

5.1. Выброс проверенного исключения

Как и при возврате из метода, мы можемthrow сесть в любую точку.

Конечно, мы должны бросить, когда пытаемся указать, что что-то пошло не так:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    while ( !tooLong ) {
        // ... potentially long operation
    }
    throw new TimeoutException("This operation took too long");
}

ПосколькуTimeoutException проверяется, мы также должны использовать ключевое словоthrows в подписи, чтобы вызывающие нашего метода знали, как его обработать.

5.2. Throwпоет непроверенное исключение

Если мы хотим сделать что-то вроде, скажем, проверки ввода, мы можем использовать вместо этого непроверенное исключение:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    if(!isFilenameValid(playersFile)) {
        throw new IllegalArgumentException("Filename isn't valid!");
    }

    // ...
}

ПосколькуIllegalArgumentException не отмечен, нам не нужно отмечать метод, хотя мы можем это сделать.

Некоторые в любом случае помечают метод как форму документации.

5.3. Обертывание и перетягивание

Мы также можем повторно вызвать обнаруженное исключение:

public List loadAllPlayers(String playersFile)
  throws IOException {
    try {
        // ...
    } catch (IOException io) {
        throw io;
    }
}

Или сделать обертку и отбросить

public List loadAllPlayers(String playersFile)
  throws PlayerLoadException {
    try {
        // ...
    } catch (IOException io) {
        throw new PlayerLoadException(io);
    }
}

Это может быть полезно для объединения множества различных исключений в одно.

5.4. ПеретяжкаThrowable илиException

Теперь для особого случая.

Если единственными возможными исключениями, которые может вызвать данный блок кода, являются исключенияunchecked, то мы можем поймать и повторно выброситьThrowable илиException w, не добавляя их в нашу подпись метода:

public List loadAllPlayers(String playersFile) {
    try {
        throw new NullPointerException();
    } catch (Throwable t) {
        throw t;
    }
}

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

This is handy with proxy classes and methods. Подробнее об этом можно прочитатьhere.

5.5. Inheritanceс

Когда мы отмечаем методы ключевым словомthrows, это влияет на то, как подклассы могут переопределять наш метод.

В случае, когда наш метод выдает проверенное исключение:

public class Exceptions {
    public List loadAllPlayers(String playersFile)
      throws TimeoutException {
        // ...
    }
}

Подкласс может иметь «менее рискованную» подпись:

public class FewerExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) {
        // overridden
    }
}

Но не подпись «more riskier»:

public class MoreExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) throws MyCheckedException {
        // overridden
    }
}

Это связано с тем, что во время компиляции контракты определяются ссылочным типом. Если я создам экземплярMoreExceptions and, сохраните его вExceptions:

Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");

Тогда JVM сообщит мне толькоcatchTimeoutException, что неверно, поскольку я сказал, чтоMoreExceptions#loadAllPlayers вызывает другое исключение.

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

6. Антипаттерны

6.1. Проглатывание исключений

Теперь есть еще один способ, которым мы могли бы удовлетворить компилятор:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {} // <== catch and swallow
    return 0;
}

The above is calledswallowing an exception. В большинстве случаев это будет для нас немного подлым, потому что это не решает проблемуand, а также мешает другому коду решить эту проблему.

Бывают случаи, когда есть проверенное исключение, которое, как мы уверены, никогда не произойдет. In those cases, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        // this will never happen
    }
}

Другой способ, которым мы можем «проглотить» исключение, - просто распечатать исключение для потока ошибок:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

Мы немного улучшили нашу ситуацию, по крайней мере, выписав ошибку где-нибудь для последующей диагностики.

Но лучше было бы использовать логгер:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        logger.error("Couldn't load the score", e);
        return 0;
    }
}

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

Наконец, мы можем непреднамеренно проглотить исключение, не включив его в качестве причины при создании нового исключения:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException();
    }
}

Здесь мы похвалим себя за то, что предупредили вызывающего абонента об ошибке, ноwe fail to include the IOException as the cause. Из-за этого мы потеряли важную информацию, которую вызывающие абоненты или операторы могли использовать для диагностики проблемы.

Нам лучше сделать:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException(e);
    }
}

Обратите внимание на небольшую разницу во включенииIOException какcause вPlayerScoreException.

6.2. Использованиеreturn в блокеfinally

Другой способ перехвата исключений - этоreturn из блокаfinally. Это плохо, потому что при внезапном возврате JVM отбросит исключение, даже если оно было сгенерировано нашим кодом:

public int getPlayerScore(String playerFile) {
    int score = 0;
    try {
        throw new IOException();
    } finally {
        return score; // <== the IOException is dropped
    }
}

СогласноJava Language Specification:

Если выполнение блока try завершается внезапно по какой-либо другой причинеR, то выполняется блок finally, и тогда есть выбор.

Если блокfinally завершается нормально, то оператор try завершается внезапно по причине R.

Если блокfinally завершается внезапно по причине S, то оператор try завершается внезапно по причине S (и причина R отбрасывается).

6.3. Использованиеthrow в блокеfinally

Подобно использованиюreturn в блокеfinally, исключение, созданное в блокеfinally, будет иметь приоритет над исключением, которое возникает в блоке catch.

Это «сотрет» исходное исключение из блокаtry, и мы потеряем всю эту ценную информацию:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch ( IOException io ) {
        throw new IllegalStateException(io); // <== eaten by the finally
    } finally {
        throw new OtherException();
    }
}

6.4. Используяthrow какgoto

Некоторые люди также поддались искушению использоватьthrow как выражениеgoto:

public void doSomething() {
    try {
        // bunch of code
        throw new MyException();
        // second bunch of code
    } catch (MyException e) {
        // third bunch of code
    }
}

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

7. Общие исключения и ошибки

Вот некоторые распространенные исключения и ошибки, с которыми мы все время от времени сталкиваемся:

7.1. Проверенные исключения

  • IOException - это исключение обычно является способом сказать, что что-то в сети, файловой системе или базе данных не удалось.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException - это исключение означает, что мы пытались получить доступ к несуществующему индексу массива, как при попытке получить индекс 5 из массива длиной 3.

  • ClassCastException – это исключение означает, что мы пытались выполнить недопустимое приведение, например, пытались преобразоватьString вList. Обычно мы можем избежать этого, выполняя защитные схемыinstanceof перед приведением.

  • IllegalArgumentException - это исключение является для нас общим способом сказать, что один из предоставленных параметров метода или конструктора недействителен.

  • IllegalStateException - это исключение является общим способом сказать, что наше внутреннее состояние, как и состояние нашего объекта, недействительно.

  • NullPointerException - это исключение означает, что мы пытались сослаться на объектnull. Обычно мы можем избежать этого, выполняя защитные проверкиnull или используяOptional.

  • NumberFormatException - Это исключение означает, что мы пытались преобразоватьString в число, но строка содержала недопустимые символы, например, попытка преобразовать «5f3» в число.

7.3. ошибки

  • StackOverflowError – это исключение означает, что трассировка стека слишком велика. Это может иногда случаться в больших приложениях; однако обычно это означает, что в нашем коде происходит бесконечная рекурсия.

  • NoClassDefFoundError - это исключение означает, что класс не удалось загрузить либо из-за отсутствия в пути к классам, либо из-за сбоя при статической инициализации.

  • OutOfMemoryError - это исключение означает, что у JVM больше нет доступной памяти для выделения дополнительных объектов. Иногда это связано с утечкой памяти.

8. Заключение

В этой статье мы рассмотрели основы обработки исключений, а также несколько примеров хорошей и плохой практики.

Как всегда, весь код в этой статье можно найтиover on GitHub!