Урок 15. Исключения Java


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

История обработки исключений

Механизм исключений существует не во всех языках. Он был введен в язык программирования Lisp в 1972 году, то есть примерно через 30 лет после того, как был изобретен первый язык программирования. Первоначально были только непроверяемые исключения, но в 1995 году язык программирования Java ввел понятие “проверяемые исключения”, которое заставляет вас обрабатывать исключение, прежде чем вам разрешат запустить программу.

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

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

Исключительными случаями обычно являются ошибки программирования или (другие) системные сбои. Например, если ваш код содержит перечисление, которое имеет такие состояния, как зеленый желтый красный и переключатель, повторяющий его, текущая версия кода, вероятно, имеет дело с перечислением, думая, что оно имеет только эти три состояния. Если позже код был изменен, и теперь перечисление имело больше состояний, это была бы ситуация, которую вы не могли ожидать. Вы никогда не знаете, что случится с вашим кодом в будущем. Если бы Вы бросили RuntimeException в случае неизвестного значения enum, вы бы следовали подходу “fail early”. Выбрасывание исключения позволяет быстро найти и устранить то, что вызывает исключение. Это гораздо лучше, чем "скрывать" ошибку, пока она не станет большой проблемой для системы.

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

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

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

Проверка входных данных

Итак, как мы проверяем наши аргументы, прежде чем фактически использовать их? Ниже я покажу вам, как написать метод, который делает это. Я собираюсь вызвать этот метод isValid(). Он принимает один аргумент и возвращает логическое значение, которое говорит нам, является ли аргумент допустимым.

public class CarSelector {
    public static void main(String[] arguments) throws Exception {
        CarService carService = new CarService();
        for (String argument : arguments) {
            if(isValid(argument)){
                carService.process(argument);
            } else {
                System.out.println(“invalid argument:” + argument);
            }
        }
    }
}
Теперь давайте напишем метод isValid(). Поскольку наш основной main() статичен, этот метод тоже должен быть статичным. Мы хотим, чтобы метод возвращал true, если аргумент является допустимым, и false, если он не является таковым:

private static boolean isValid(String argument) {
    try {
        CarState.valueOf(argument);
    } catch (IllegalStateException e) {
        return false;
    }
    return true;
}
В методе isValid у нас будет блок try/catch. Сначала мы попробуем использовать метод valueOf() класса enum. Этот метод является частью перечисления Java. Он непосредственно преобразует строку в соответствующее значение enum. Этот метод вызовет исключение IllegalStateException в случае незаконного значения, и если это произойдет, мы можем поймать его и вернуть false. В остальном наш аргумент был справедлив. Если вам интересно узнать больше о перечислениях, вы можете прочитать мою статью о перечислениях.

Сила исключений

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

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

Создание исключения

Давайте погрузимся в пример кода.

public enum CarState {
    DRIVING, WAITING, PARKING;
    public static CarState from(String state) {
        switch (state) {
            case "DRIVING":
                return DRIVING;
            case "WAITING":
                return WAITING;
            case "PARKING":
                return PARKING;
            default:
                throw new Exception();
        }
    }
}
Тут нас есть enum CarState с различными значениями состояния и метод public static from(), который преобразует строку с именем состояния в значение enum (начиная с Java 7 было возможно использовать строку внутри оператора switch, но это обычно не рекомендуется. Однако в данном случае мы будем использовать его, так как пользовательский ввод выполнен в виде строки).

Мы добавим обработку исключений, если имя состояния не является допустимым. Мы сделаем это, вызвав исключение из случая по умолчанию оператора switch. Чтобы создать исключение, вы пишете слово throw, а затем создаете новый объект Exception ().

В этом случае исключение, которое мы выбрасываем, возникает всякий раз, когда в метод from()отправляется недопустимое состояние.

Исключение, которое мы бросили в примере 1, является проверяемым исключением, поэтому мы должны обрабатывать его всякий раз, когда мы вызываем метод from(). Как правило, вы не хотите обрабатывать исключение в методе, где оно создается, так как это не является целью исключения. Наша цель состоит в том, чтобы это исключение обрабатывалось, когда другой класс использует этот метод, чтобы оно было защищено в случае возникновения исключения. Чтобы передать это исключение, мы добавляем исключение throws к объявлению метода:

public enum CarState {
    DRIVING, WAITING, PARKING;
    public static CarState from(String state) throws Exception {
    [...]
    }
}
Теперь давайте напишем класс, который вызывает метод from(). Назовем этот класс CarService. Опять же, я просто передам это исключение по цепочке, так сказать.

public class CarService {
    private final Logger log = LoggerFactory.getLogger(CarService.class);
    public void process(String input) throws Exception {
        log.debug("processing car:" + input);
        CarState carState = CarState.from(input);
    }
}
Хорошо, теперь мы получим следующий метод в нашем коде, но опять же, я просто напишу исключение throws.

public class CarSelector {
    public static void main(String[] arguments) throws Exception {
        CarService carService = new CarService();
        for (String argument : arguments) {
            carService.process(argument);
        }
 
Как вы видите, мы закончили тем, что добавили исключение в каждый вызывающий метод, вплоть до нашего метода main(). Исключение не обрабатывается, и если оно возникнет, программа остановится с ошибкой. Это типичная ситуация, в которой вы в конечном итоге добавляете оператор throws к каждой функции в коде. Вот почему проверяемое исключение, откровенно говоря, не очень полезно; мы даже не утруждаем себя его обработкой, и в конечном итоге получаем ту же трассировку стека, что и наши выходные данные.

Вместо этого мы могли бы написать непроверяемое исключение. Если мы пишем такое исключение, мы можем удалить каждое отдельное исключение throws, которое мы только что написали, и заменить его на throw new RuntimeException(); с этим исключением мы также можем предоставить некоторую информацию о том, почему оно было вызвано. В этом случае я собираюсь сказать пользователю, что они отправили нам неизвестное состояние, а затем сказать им, какое состояние было вызвано этим исключением.

public enum CarState {
    DRIVING, WAITING, PARKING;
    public static CarState from(String state) {
        switch (state) {
            case "DRIVING":
                return DRIVING;
            case "WAITING":
                return WAITING;
            case "PARKING":
                return PARKING;
            default:
                throw new RuntimeException(“unknown state: “ + state);
        }
    }
}
Обработка исключения

Допустим, мы не хотим, чтобы наш код перестал работать, когда пользователь отправляет нам неизвестное состояние. Мы могли бы обработать исключение, окружив наш вызов метода from () блоком try/catch. Обычно вы хотите иметь этот блок try/catch как можно раньше в коде, поэтому везде, где потенциально может возникнуть проблема, вы должны окружить этот вызов в try/catch. В данном случае это наш метод main().

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

В блоке try примера ниже

public class CarSelector {
    public static void main(String[] arguments) {
        CarService carService = new CarService();
        for (String argument : arguments) {
            try {
                carService.process(argument);
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
        }
    }
}
Если происходит исключение, ни одна из строк после строки, вызвавшей исключение, не будет выполнена. Если исключения нет, то можно смело предположить, что вызов был успешно выполнен в следующих строках. Из-за этого мы можем написать наш блок try кода, как будто все работает, обрабатывая исключения отдельно.

finally

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

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

По этой причине Java определяет ключевое слово finally, чтобы добавить необязательный блок finally в конструкцию try / catch.

try {
    carService.process(argument);
} catch (RuntimeException e) {
    LOG.error(e.getMessage(), e);
} finally {
    System.out.println(“i print no matter what”);
}
При наличии допустимого аргумента код в блоке try будет выполнен. С недопустимым аргументом нормальный поток программы будет прерван, и код в блоке try не будет выполнен, но вместо этого мы зарегистрируем нашу ошибку. Несмотря ни на что, строка “я печатаю, несмотря ни на что” все равно будет печататься.

Способы обработки исключений

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

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

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

Как правило, требуется иметь такую информацию, как время возникновения ошибки, имя системы, полное имя класса и метода, а также точный номер строки, в которой произошла ошибка в коде. Кроме того, необходимо регистрировать состояние соответствующих объектов, таких как параметры, используемые неисправным методом. С помощью структуры ведения журнала, такой как Logback, это может быть достигнуто без особых усилий. Он может быть настроен для входа в файл или базу данных, которая будет постоянно сохранять ошибку. Чтобы узнать больше о логировании, прочитайте мою статью о логировании с помощью SLF4J и LOGBack.

Использование структуры ведения журнала для уведомления об ошибках обычно предпочтительнее, чем прямая печать ошибки на консоль, поскольку это более гибкий и мощный способ получения причины исключения и сохранения ее на носителе данных, таком как файл или база данных.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, уроки по java, java, исключения




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




Урок 9. Свойства computed и watch в Vue
Кто находится за прокси от оперы?
Управляем позицией элемента на разных экранах при помощи @media screen