JavaRush /Java блог /Java Developer /Исключения в Java
Roman
33 уровень

Исключения в Java

Статья из группы Java Developer
Когда столкнулся с темой "Исключения", то возникло много вопросов, на которые пришлось искать ответы по различным закоулкам интернета, чтобы понять в деталях, как всё это работает. В результате, я составил собственное объяснение, которое может оказаться более понятным для новичков, только-только столкнувшихся с этим явлением. Исключения в Java - 1В компьютерах Прерывание – это входящий сигнал для процессора, о том что происходит событие, которое требует немедленной реакции. Сигнал прерывания требует от процессора приостановить выполняемую программу с таким условием, чтобы она могла быть продолжена чуть позже, то есть компьютер должен запомнить всю информацию, связанную с выполнением программы. Подобные прерывания временны, если не носят фатальный характер. Такие прерывания могут быть вызваны, как программным кодом, так и некоторой функциональностью «железа» (например, просто нажатием клавиш на клавиатуре; таймеры например, для автоматического отключения компьютера). Количество Прерываний ограничено определённым числом, заложенным при производстве того или иного процессора, то есть под это выделяются специальные «каналы» связи, позволяющие обратиться к процессору в обход всех остальных процессов. Прерывания так же автоматически генерируются при возникновении ошибки в исполняемом программном коде (например, если встречается деление на ноль). Такие прерывания традиционно называются traps или exceptions. В таких случаях принято говорить: «Exception was thrown», то есть сработало Исключениеили Исключение было запущено (брошено), то есть запрос на Прерывание с вопросом «что делать?» послан процессору. В этот момент процессор останавливает работу, запоминая точку, в которой он остановился, точнее кластер следующей ячейки, информация из которой должна быть выполнена. Запоминается вся цепочка выполненных инструкций и НЕ выполненных. После чего, процессор считывает из памяти инструкции для действий при подобной ошибке. В соответствии с этой инструкцией, он может занести новые значения в определённые кластеры, добавить некоторые цепочки действий или новый цикл (например, цикл возврата или кольцевания) и т.д., то есть в зависимости от ошибки выполняются ранее заложенные инструкции. В систему компьютера само по себе встроено много автоматических Прерываний, которые запускаются через определённое количество времени, например, для контроля запущенных на компьютере процессов или выполнения заложенных будильников, сбора входящих внешних сигналов, различных конвертеров данных. Стоит помнить, что большое количество Прерываний по ряду причин, может окончательно «повесить» систему. Ошибка в программном коде вызовет в автоматическом режиме Прерывание в процессоре, которое тот попробует обработать в соответствии с заложенными инструкциями. Но не для всех прерываний заложена их обработка или она может выдавать процедуру, которая нас не устраивает, например, просто свернёт приложение. Поэтому в программировании существует возможность организовать собственное прерывание для определённого участка кода, в котором программист потенциально видит вероятность наличия ошибки. В этом случае ошибка обработается внутри программы и не будет обращаться за инструкциями по обработке к процессору. Задание таких блоков организуется созданием Объекта «Исключение» (Exception). Этот объект автоматически создаётся в блоке try-catch. В блоке >tryпроизводится проверка на наличие ошибки и, если она есть, программа следует в блок catch, где производятся действия предупреждающие или нивелирующие ошибку. Например, если мы вводим с клавиатуры Числа, которые должны в последствии складываться и вычитаться, то введение с клавиатуры Букв приведёт к невозможности их сложения с Числами (обозначим сумму этих двух переменных буквой S). Поэтому в команде try мы должны проверить способно ли число А, содержащее Цифры, быть сложено с числом Б, содержащим Буквы (то есть S = А + Б ), и если это невозможно, а это невозможно, то должны быть приняты определённые меры, чтобы Ошибки НЕ произошло и к процессору не полетело новое Прерывание с вопросом «что делать?». В случае отсутствия в программе Исключения, её выполнение прервётся процессором. При наличии Исключения, когда оно «поймано» командой try, управление переходит команде catch, которая может задать альтернативный вариант решения, например, не будем складывать эти два числа, а зададим S = А.

int a = 4;
String b = “hello”;
int S = 0;
 try {
   S = a + b;
   int r = 1;
 } catch (Exception igogo1) {
   S = a;
 }
 return S;
/* строка «int r = 1;» не выполняется, так как случилась ошибка и программа перенаправляет работу сразу к обработчику исключения блок catch*/ Таким образом, наличие Исключений – это возможность решить проблему внутри программы, не забрасывая её на уровень процессора. В объект «Исключение», который автоматически создаётся в блоке try при обнаружении ошибки, заносится значение типа ошибки. Назовём его «НашеИсключение» – для нашего конкретного случая с описанием нашей конкретной ошибки. Создатели языка Java, заранее создали некоторый список типовых ошибок и типовые варианты их исправления, то есть в java существует некоторая библиотека Исключений, к которой мы можем обратиться для обработки случившейся ошибки, чтобы самим не писать код обработки и поэтому НашеИсключение скорее всего уже кем-то было описано, поэтому нам надо просто знать название какого из этих исключений вставить в нашу программу для обработки кода, где потенциально может случиться сбой. Если мы ошибёмся и выберем из библиотеки неверное Исключение, то обработчик его не «поймает», ошибка не найдёт решения внутри программы и полетит запрос к процессору. Но существует путь для ленивых. Если мы не знаем название нужного нам исключения из библиотеки, то можем взять общее с названием «Exception», как в вышеописанном примере. Это Исключение способно обработать любой тип ошибки, только не способно выдать конкретную информацию о происшествии, которое мы могли бы занести в логи. Библиотека ранее написанных Исключений состоит из проверяемых (checked) и непроверяемых (unchecked) Исключений. Проверяемые – это те, которые можно поправить, не обрывая работу программы, то есть, если мы пытаемся открыть файл в папке, в которой его нет, то система даст нам об этом знать, мы можем закинуть файл в нужную папку и продолжить работу программы. То есть по факту к процессору послан запрос на Прерывание, но без вопроса: «ищи, что делать с этой проблемой?!?!». Мы послали Прерывание, которое сами обнаружили, с готовой инструкцией, которую процессор обработал и продолжил выполнение программы. Непроверяемые – это те ошибки, которые нельзя поправить и программа будет закрыта до своего завершения, то есть к процессору полетит запрос на Прерывание, которое в любом случае оборвёт выполнение программы. Единственный смысл прописывать такие исключения в программе – это дать пользователю понять, что произошло, так как, поймав это прерывание, мы можем вывести информационное сообщение на экран из-за чего программа свернулась. Второй причиной ловить такие прерывания является возможность занести их в логи для последующего анализа (вас хакнули, но зато вы знаете через какое место). Следствием наличия подобных библиотек является необходимость не забыть их подключить. (Список проверяемых и непроверяемых Исключений с библиотеками можно посмотреть, например, здесь) Если мы не знаем точно какую библиотеку включить или существует несколько вариантов ошибки, то можем в нескольких catch перечислить нужные Исключения. Система сама выберет правильный обработчик, если он есть в списке. Вместо конкретного Исключения можно написать общее «Exception», которое может обработать любой тип Исключения, если оно не обработалось в предыдущих блоках.

int a = 4;
String b = “hello”;
int S = 0;
 try {
   S = a + b;
   int r = 1;
 } 
catch(NullPointerException blabla2) {
   System.out.println("Exception handling code for the NullPointerException.");
 } 
catch (ArithmeticException ex1) {
   S = a;
 }
catch(Exception uups1) {
   System.out.println("Exception occured");
 } 
 return S;
В случае наличия блока try Исключение создаётся автоматически. Если нам надо принудительно в какой-то момент времени вызвать Исключение, то используется команда throw. То есть мы самостоятельно создаём объект new throw… после чего, программа останавливает свою работу, посылает процессору запрос на Прерывание и переносится к разделу программы catch, откуда пытается почерпнуть инструкции по дальнейшим действиям. Создавая вручную Исключение, мы можем указать его конкретный тип из библиотеки:

throw new ArithmeticException("Access denied - You must be at least 18 years old.");
тогда обработчик будет искать блок catch, с именно этим Исключением – искать по всей программе, по всем бокам catch. После команды throw обработки Исключения, весь оставшийся код программы НЕ будет выполнен, за исключением того, который находится в блоке catch. Если обработчик в программе не найден, процессору задаётся вопрос: «решай сам, что делать» и тот прерывает программу. Вызов new throw… может производиться как в блоке >try, так и вне его (в любом месте программы)

try {
   /* функция или действие, в котором есть сомнения. То есть: «попробуй выполнить это, а если не получится, а, если не получится, запускай режим исключения» */
   throw new CallForException(); /* Назначаем исключение, которое будет работать в случае наличия ошибки в функции, описанной выше. Здесь исключение «CallForException» - берется из библиотеки существующих исключений */
} catch (CallForException ee1) {
   /* Корректируем ошибку, чтобы программа не «отвалилась» или выводим сообщение об ошибке или что-то ещё */
} finally {
   /* этот блок работает всегда независимо от того была ошибка или нет. А если была, то сработало ли решение в catch или нет */
   /* часто используется для подчистки хвостов, например, для закрытия запущенного файла или базы данных */
   /* в ряде случаев блок catch вообще может быть опущен и оставлен только блок finally и наоборот finally может быть опущен и оставлен только catch */
   /* Не допускается использование этого блока в ряде случаев, например, когда функция System.exit() запущена или другие системные Исключения, типа «отключение электроэнергии» и т.п. */
}

Оповещение о наличии Исключения

Ранее написанные кем-то методы могут включать в себя вызов Исключений. Просто на всякий случай программист, который писал код, предупредил последующих программистов, что в написанном им методе может случится ошибка. Так, например, метод создания файла, описанный ниже, предусматривает, что при создании файла может произойти ошибка (нет файла по заданному пути), а значит нужен будет её обработчик:

public void createFile(String path, String text) throws IOException {
    FileWriter writer = new FileWriter(path, true);
    writer.write(text);
    writer.close();
}
Но при этом самого обработчика нет, а значит теперь просто вызвать написанный метод в своей программе в обычном режиме мы не сможем. Теперь мы обязаны написать обработчик ошибки и вызывать этот метод в блоке try:

String filePath = "hello.txt";
String text = "Hello World";
 
try {
    createFile(filePath, text);
} catch (IOException ex) {
    System.err.println("Error creating file: " + ex);
}

Собственные исключения

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

public class StudentNotFoundException extends Exception {
 
    public StudentNotFoundException (String message) {
        super(message);
    }
}
При создании собственных исключений следует учитывать два правила:
  1. Название нашего класса должно оканчиваться на «Exception»
  2. Класс должен содержать конструктор со строковой переменной, описывающей детали проблемы Исключения. В конструкторе вызывается супер-конструктор с передачей сообщения.
Пример использования созданного исключения:

public class StudentManager { 
    public Student find(String studentID) throws StudentNotFoundException {
        if (studentID.equals("123456")) {
            return new Student();
        } else {
            throw new StudentNotFoundException(
                "Could not find student with ID " + studentID);
        }
    }
}
Отлавливаем это Исключение кодом:

public class StudentTest {
    public static void main(String[] args) {
        StudentManager manager = new StudentManager();
         try { 
            Student student = manager.find("0000001");
        } catch (StudentNotFoundException ex) {
            System.err.print(ex);
        }
    }
}
Результатом выполнения программы будет: StudentNotFoundException: Could not find student with ID 0000001

Зачем нужно писать Исключения

В 1996 году разбилась ракета Ariane 5 из-за неверной конвертации переменной float в переменную integer. Исключений и обработчиков этой ситуации предусмотрено не было. Если при загрузке файла произошел обрыв связи с интернетом, то наличие Исключения позволит продолжить закачку, после того как соединение восстановится. При отсутствии Исключения закачку придётся начинать заново.

Использованная литература:

Комментарии (5)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Sergei Polushin Уровень 15
3 июня 2020
Хорошая статья начального уровня :)
Viacheslav Уровень 3
17 декабря 2019
Прочитал и понял, что запутался немного в повествовании. Если я не прав, то поправьте. Как я понимаю, по терминологии получается, что исключения - это один из видов прерываний. Исключения в Java - это просто механизм обработки ошибок, которым управляет в конечном итоге сама JVM. Поэтому, почти любая ошибка в ходе выполнения Java программы, не приведёт к прерываниям, отправленным к процессору. Если я правильно понимаю, если в сторону процессора будет прерывание, то это будет в случае падения самой JVM. Можно сказать, что исключения в Java - это своего рода "прерывания в мире JVM". Например, OutOfMemoryError не приведёт к падению JVM, но приведёт к завершению потока. Но может не хватить памяти на столько, что памяти не хватит не только процессу java приложения, но и всей JVM. И тогда JVM упадёт. Тогда, возможно, будет и прерывание в сторону процессора из-за нехватки памяти. Кроме того, любое исключение, если оно не поймано, не останавливает "программу", а останавливает поток (thread), в котором исключение возникло. Любая Java программа - это совокупность потоков выполнения (threads). Таким образом, если у нас несколько не демон потоков и остались "живые" не демон потоки, то программа продолжит своё выполнение.