Рубрики
Без рубрики

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

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

Автор оригинала: baeldung.

1. Обзор

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

2. Основные принципы

2.1. Что Это Такое?

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

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

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

2.2. Зачем Его Использовать?

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

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

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

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

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

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

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

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

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

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

  • Проверенные исключения
  • Непроверенные исключения/Исключения во время выполнения
  • Ошибки

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

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

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

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

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

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

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

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

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

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

3.3. Ошибки

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

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

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

Несколько примеров ошибок-это StackOverflowError и OutOfMemoryError .

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

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

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

Как было сказано чуть ранее, когда мы называем эти “рискованные” методы, мы должны обрабатывать проверенные исключения, и мы можем обрабатывать непроверенные. Java дает нам несколько способов сделать это:

4.1. бросает

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

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
 
    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

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

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

4.2. попробуй-поймай

Если мы хотим попытаться обработать исключение самостоятельно, мы можем использовать блок 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 .

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

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

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);
        }
    }
}

Потому что закрывать это также “рискованный” метод, нам также нужно поймать его исключение!

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

4.4. попробуйте с помощью ресурсов

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

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;
    }
}

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

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

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

4.5. Несколько блоков улавливания

Иногда код может выдавать более одного исключения, и у нас может быть более одного 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 , и это потому, что он расширяет 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 позволяет нам обрабатывать исключения подклассов отдельно, не забудьте поместить их выше в списке уловов.

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

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

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

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

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

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

5.2. Создание непроверенного исключения

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

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 или Исключение без добавления их в сигнатуру нашего метода:

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

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

Это удобно с прокси-классами и методами. Подробнее об этом можно узнать здесь .

5.5. Наследование

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

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

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

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

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

Но не ” более более рискованная” подпись:

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

Это связано с тем, что контракты определяются во время компиляции ссылочным типом. Если я создам экземпляр Больше исключений и сохраню его в Исключения :

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

Тогда JVM скажет мне только поймать исключение TimeoutException , что неверно, так как я сказал, что Больше исключений#loadAllPlayers выдает другое исключение.

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

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

6.1. Глотательные исключения

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

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

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

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

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();
    }
}

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

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

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

Обратите внимание на тонкую разницу между включением IOException как причина |//CoreException .

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

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

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

В соответствии со спецификацией языка Java :

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

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

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

6.3. Использование броска в блоке 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 заявления:

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. Исключения во время выполнения

  • ArrayIndexOutOfBoundsException – это исключение означает, что мы пытались получить доступ к несуществующему индексу массива, например, при попытке получить индекс 5 из массива длиной 3.
  • ClassCastException – это исключение означает, что мы пытались выполнить незаконное приведение, например, пытались преобразовать Строку в Список . Обычно мы можем избежать этого, выполнив защитные проверки instanceof перед приведением.
  • IllegalArgumentException – это исключение является общим способом для нас сказать, что один из предоставленных параметров метода или конструктора является недопустимым.
  • IllegalStateException – Это исключение является общим способом для нас сказать, что наше внутреннее состояние, как и состояние нашего объекта, недопустимо.
  • NullPointerException – Это исключение означает, что мы пытались ссылаться на объект null . Обычно мы можем избежать этого, либо выполнив защитные проверки null , либо используя Необязательно.
  • NumberFormatException – Это исключение означает, что мы пытались преобразовать строку | в число, но строка содержала недопустимые символы, например, при попытке преобразовать “5f3” в число.

7.3. Ошибки

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

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

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

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