JavaRush /Java блог /Random UA /Жуём тему исключений для исключительных
Stan
16 уровень

Жуём тему исключений для исключительных

Статья из группы Random UA
В далёкой стране Пргораммии жил волшебник, выполняющий определённые поручения от ещё более старшего волшебника (как часто и бывает). Заклинания, необходимые для решения определённых задач - это команды. Волшебнику каждый день присылают свиток задач (после волшебной планёрки), в котором написано, что ему необходимо делать. Обычно всё идёт по плану, однако иногда что-то идет не так — волшебник, например, пытается налить воду в чашку, но чашка оказывается разбитой, или он хочет открыть дверь, а ключ не подходит. Вот такие "нежданчики" называются исключениями. Как это работает: 1. Заклинание (код) работает как надо — волшебник доволен и все идет по плану. 2. Что-то пошло не так — например, волшебник пытается считать число из заклинания, а там вместо цифры вдруг буква. Это и есть "исключение" — нечто неожиданное и неправильное. 3. Волшебник пугается и не знает, что делать. Если ему не объяснить, как поступать в такой ситуации, он остановится и перестанет колдовать. Как вообще можно помочь незадачливому колле... волшебнику, чтобы работа опять заладилась? Чтобы волшебник не останавливался из-за таких ошибок, ему можно дать запасной план действий (план В), который называется "обработчиком исключений". Это как подушка безопасности. Волшебнику говорят: "Если что-то пойдет не так, вот что нужно сделать." Например, если он не может налить воду в разбитую чашку, пусть найдет новую чашку. Если дверь не открывается, пусть попробует другой ключ или просто обойдет это препятствие. В Java это выглядит так: 1. try — волшебник пытается выполнить заклинание. 2. catch — если случается исключение, волшебник ловит его, как мышку, и делает то, что написано в обработчике. 3. finally — волшебник делает то, что нужно сделать в любом случае, даже если все пошло не по плану. Ну а как же примеры кода, спросите вы? Конечно, давайте разбираться) Представь, что наш волшебник-программа пытается разделить одно число на другое, но иногда ему может встретиться злой ноль, который не разрешает делить на себя. Если волшебник не будет готов к такой ситуации, его заклинание остановится с ошибкой.

public static void main(String[] args) {
        // Волшебник пробует разделить два числа
        int a = 10;
        int b = 0; // Коварный ноль, который вызовет исключение
        
        try {
            // Здесь волшебник пытается выполнить деление
            int result = a / b; // Попытка разделить на ноль
            System.out.println("Результат: " + result); // Если всё получится, покажем результат
        } catch (ArithmeticException e) {
            // Если случается исключение, ловим его здесь
            System.out.println("Ошибка: нельзя делить на ноль!"); // Скажем волшебнику, что делать
        } finally {
            // Это выполняется всегда, независимо от того, случилось исключение или нет
            System.out.println("Магия завершена."); // Подведение итогов
        }
    }

Давайте детально разберём, что происходит в этом коде: 1. try — волшебник пытается разделить `a` на `b`. Но `b` равно нулю, поэтому обычное деление здесь невозможно, и произойдет ошибка. 2. catch (ArithmeticException e) — волшебник ловит ошибку (исключение), которая называется `ArithmeticException`. Это как если бы волшебник нашел инструкцию: "Что делать, если встречаешь злого нуля? Просто скажи об этом и продолжай." 3. finally — выполняется всегда, даже если не было исключений. Это часть, где волшебник завершает свое колдовство. Этот код защищает волшебника от того, чтобы он не испугался несчастного нуля и не остановил свое колдовство. Вместо этого он понимает, что делить на ноль нельзя, и продолжает работать дальше. Ну а как же ситуации, когда на пути волшебника может встретиться несколько исключений или даже разных? Давай представим, что волшебник выполняет заклинания, но на его пути может встретиться не только злой ноль, но и другие препятствия — например, он может попытаться достать что-то из пустой коробки (массива), или найти нужную книгу по неверному адресу (неправильному индексу). Посмотрим, как волшебник справляется с разными исключениями:

public static void main(String[] args) {
        int[] numbers = {10, 20}; // Массив с числами, волшебная коробка с двумя предметами
        int a = 10;
        int b = 0; // Опять этот ноль, который вызовет исключение

        try {
            // Попытка деления, где b равно нулю
            int result = a / b; 
            System.out.println("Результат деления: " + result);

            // Попытка достать элемент из массива, который находится за его пределами
            int number = numbers[5]; 
            System.out.println("Достали из массива: " + number);

        } catch (ArithmeticException e) {
            // Ловим исключение при делении на ноль
            System.out.println("Ошибка: нельзя делить на ноль!");

        } catch (ArrayIndexOutOfBoundsException e) {
            // Ловим исключение при попытке достать элемент за пределами массива
            System.out.println("Ошибка: такого элемента в массиве нет!");

        } finally {
            // Действия, которые выполняются в любом случае
            System.out.println("Магия завершена, не важно, были ошибки или нет.");
        }
    }

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

public static void main(String[] args) {
        int a = 10;
        int b = 0; // Проклятый ноль
        int[] array = {1, 2, 3}; // Массив с тремя элементами
        String cup = null; // Разбитая чашка (null символизирует отсутствие)

        try {
            // Попытка деления на ноль
            int result = a / b; 
            System.out.println("Результат деления: " + result);

            // Попытка достать элемент за пределами массива
            int element = array[5]; 
            System.out.println("Элемент массива: " + element);

            // Попытка налить воду в разбитую чашку
            if (cup == null) {
                throw new NullPointerException(); // Имитируем разбитую чашку
            }

            // Попытка открыть уже открытую дверь
            boolean doorOpen = true; // Дверь уже открыта
            if (doorOpen) {
                throw new IllegalStateException(); // Имитируем попытку открыть открытую дверь
            }

        } catch (ArithmeticException e) {
            // Ловим деление на ноль
            System.out.println("Ошибка: на ноль делить нельзя!");

        } catch (ArrayIndexOutOfBoundsException e) {
            // Ловим ошибку выхода за пределы массива
            System.out.println("Ошибка: нет элемента с таким индексом!");

        } catch (NullPointerException e) {
            // Ловим попытку работы с разбитой (не существующей) чашкой
            System.out.println("Ошибка: возьми целую чашку!");

        } catch (IllegalStateException e) {
            // Ловим попытку открыть уже открытую дверь
            System.out.println("Ошибка: выбери закрытую дверь!");

        } finally {
            // Действие, которое выполняется в любом случае
            System.out.println("Все магические действия завершены.");
        }
    }

Когда в коде возникает ошибка, и для неё предусмотрен catch (обработчик исключения), компилятор переходит к запасному сценарию, который описан в этом catch. Немного сухого языка в нашей дивной истории (ложка дёгтя) Вот как это происходит этот процесс: 1. Ошибка возникает в коде внутри `try` блока. Это может быть деление на ноль, обращение к неверному элементу массива, работа с несуществующим объектом и так далее. 2. Программа ищет подходящий `catch` блок. Если ошибка соответствует типу исключения, указанному в `catch`, программа переходит к этому блоку и выполняет запасной сценарий. 3. Программа продолжает работу. Благодаря обработчику, программа не "паникует" и не завершает свою работу неожиданно. Вместо этого она выполняет действия, описанные в `catch`, и продолжает выполнять оставшийся код. Пример на пальцах: Представь, что волшебник нашёл камень на своём пути. Вместо того чтобы остановиться и больше ничего не делать, он использует один из своих запасных планов: перепрыгнуть камень, обойти его или убрать с дороги. Благодаря этому он может продолжить своё путешествие, а не закончить его преждевременно. Если исключения не обработаны: Если же на ошибку "нет соответствующего `catch`", программа действительно остановится и завершит работу с сообщением об ошибке. Это напоминает ситуацию, когда волшебник не знает, как справиться с препятствием и просто останавливается. Именно поэтому в программировании обработка исключений помогает делать программы более устойчивыми и предсказуемыми — они продолжают работу, даже если что-то пошло не так! Вот такая презабавная история, с помощью которой я попытался вам объяснить тему исключений. Надеюсь, вам было интересно и, что главное, понятно.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Денис Уровень 37
30 августа 2024
Любопытный формат :) Стоит еще отметить, что не всегда обработка исключений делается не с целью устойчивости. - Иногда это делается для логирования и/ли оборачивания в нужный тип исключения (после чего код вполне может упасть кстати, ведь так задумано программистом - иногда приложению лучше упасть чем продолжить работу); - Иногда отлавливать исключение и вовсе не нужно (разные рантайм исключения) и, например, у тебя есть кастомный ExceptionHandler который знает что ему нужно делать в каком случае и уже он разруливает все что приложение навыбрасывало; Вообще тема исключений достаточно интересная. Единственное, я бы на них старался логику не выстраивать, но иногда и это тоже делают.