Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Исключения Java: Введение в ООП

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

Концепция исключений не связана с ООП, но во многих языках, включая Java, исключения завязаны на классы, поэтому эта тема рассматривается в данном курсе.

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

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

int[] items = {1, 2, 3};
System.out.println(items[5]);

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

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
        at io.hexlet.Application.main (Application.java:24)

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

Другая похожая ситуация – это деление на ноль. Оно тоже приводит к исключению:

var x = 0;
var value = 3 / x;
// Exception in thread "main" java.lang.ArithmeticException: / by zero

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

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

throw new RuntimeException("Сообщение об ошибке");

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


var error = new RuntimeException("Сообщение об ошибке");
throw error;

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

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

Проверяемые исключения – это исключения, которые могут возникнуть в любом случае, даже если в программе нет багов. Чаще всего они возникают при взаимодействии Java с внешним миром. Самое простое – это чтение файла, если файла не существует, то во время его чтения возникнет исключение.

import java.nio.file.Files;
import java.nio.file.Paths;

public class Application {
    public static void main(String[] args) {
        // Создаем объект Paths для описания пути
        var path = Paths.get("path/to/file.txt");
        // Читаем файл и преобразуем в строку
        var content = new String(Files.readAllBytes(path));
        // Выводим содержимое на экран
        System.out.println(content);
    }
}

Если мы напишем такой код, то компилятор выдаст ошибку. Он знает, что метод Files.readAllBytes() выбрасывает исключение IOException, которое является проверяемым. Такое исключение должно быть обработано, так как оно может возникнуть независимо от желания программиста. Обработка исключений делается с помощью конструкции try..catch.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

public class Application {
    public static void main(String[] args) {
        var path = Paths.get("path/to/file.txt");
        try {
            var content = new String(Files.readAllBytes(path));
            System.out.println(content);
        } catch (IOException e) {
            // Обрабатываем ошибку так как нужно в этой ситуации
            System.out.println("Проверьте что файл " + path + " существует и к нему есть доступ");
        }
        // Код который идет тут, будет выполнен
    }
}
# Так выглядит текст ошибки, если ее вывести в консоль
# e.printStackTrace()
Exception in thread "main" java.nio.file.NoSuchFileException: path/to/file.txt
        at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
        at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
        at java.base/java.nio.file.Files.newByteChannel(Files.java:379)
        at java.base/java.nio.file.Files.newByteChannel(Files.java:431)
        at java.base/java.nio.file.Files.readAllBytes(Files.java:3263)
        at io.hexlet.Application.main(Application.java:27)

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

Перехват исключения позволяет программе продолжить работать дальше без остановки. Код после конструкции try..catch продолжит выполнение как ни в чем не бывало.

В примере выше мы обрабатываем исключение в том же месте где оно и произошло, но в реальности, ошибки обычно происходят не там, где они могут быть обработаны. Более того, сам механизм исключений появился именно по этой причине. В коде реальных проектов десятки, сотни и миллионы строк кода. Такой код разбит на множество слоев, в которых один метод вызывает другой, этот вызывает третий и так далее, подобные цепочки могут достигать сотни методов в глубину вызовов. Здесь проявляется то, что код, который вызывается на нижнем уровне, может не знать как конкретно нужно обработать возникшее исключение. Представьте себе библиотеку, которая умеет скачивать файлы по сети. Эта библиотека может использоваться в совершенно разных приложениях, которые по-разному показывают ошибки загрузки.

Похожую ситуацию мы можем имитировать и в нашем примере, если вынесем код чтения файла в отдельный метод:

public class Application {
    public static void main(String[] args) {
        // Переменная задается до try..catch чтобы к ней можно было обратиться из try и из catch
        var path = "/path/to/file.txt";
        try {
            var content = readFile(path);
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("Проверьте что файл " + path + " существует и к нему есть доступ");
        }
        // Код который идет тут, будет выполнен
    }

    public static String readFile(String path) {
        var preparedPath = Paths.get(path);
        var content = new String(Files.readAllBytes(preparedPath));
        return content;
    }
}

Этот код не пройдет компиляцию, так как исключение IOException является проверяемым (checked), но в методе Application.readFile() нет его обработки, как того требуют проверяемые исключения. В этом месте возникает противоречие. Мы не хотим обрабатывать исключение, так как обработка будет где-то дальше, в нашем случае в методе Application.main(). Java разрешает такие ситуации через указание того, что метод выбрасывает проверяемое исключение. Определение Application.readFile() будет выглядеть так:

public static String readFile(String path) throws IOException {
    // Тут содержимое
}

Теперь любой метод, который вызывает внутри себя метод Application.readFile() должен сделать одно из двух:

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

Выбор зависит от того, в каком месте мы хотим обрабатывать исключения.


Дополнительные материалы

  1. Коды возврата & Исключения

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 28 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»