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

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

В языке Java предусмотрены специальные средства для обработки подобных ситуаций. Одним из таких средств является конструкция try…​catch…​finally. При возникновении исключения в блоке try управление переходит в блок catch, который может обработать данное исключение. Если такого блока не найдено, то пользователю отображается сообщение о необработанном исключении, а дальнейшее выполнение программы останавливается. И чтобы подобной остановки не произошло, и надо использовать блок try…​catch. Например:

int[] numbers = new int[3];
numbers[4] = 45;
System.out.println(numbers[4]);

Так как у нас массив numbers может содержать только 3 элемента, то при выполнении инструкции numbers[4] = 45 консоль отобразит исключение, и выполнение программы будет завершено. Теперь попробуем обработать это исключение:

try {
    int[] numbers = new int[3];
    numbers[4] = 45;
    System.out.println(numbers[4]);
} catch (Exception ex) {
    ex.printStackTrace();
}
System.out.println("Программа завершена");

При использовании блока try…​catch вначале выполняются все инструкции между операторами try и catch. Если в блоке try вдруг возникает исключение, то обычный порядок выполнения останавливается и переходит к инструкции сatch. Поэтому когда выполнение программы дойдет до строки numbers[4] = 45;, программа остановится и перейдет к блоку catch.

Выражение catch имеет следующий синтаксис: catch (тип_исключения имя_переменной). В данном случае объявляется переменная ex, которая имеет тип Exception. Но если возникшее исключение не является исключением типа, указанного в инструкции сatch, то оно не обрабатывается, а программа просто зависает или выбрасывает сообщение об ошибке.

Но так как тип Exception является базовым классом для всех исключений, то выражение catch (Exception ex) будет обрабатывать практически все исключения. Обработка же исключения в данном случае сводится к выводу на консоль стека трассировки ошибки с помощью метода printStackTrace(), определенного в классе Exception.

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

Конструкция try…​catch также может иметь блок finally. Однако этот блок необязательный, и его можно при обработке исключений опускать. Блок finally выполняется в любом случае, возникло ли исключение в блоке try или нет:

try {
    int[] numbers = new int[3];
    numbers[4] = 45;
    System.out.println(numbers[4]);
} catch (Exception ex) {
    ex.printStackTrace();
} finally {
    System.out.println("Блок finally");
}
System.out.println("Программа завершена");

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

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

int[] numbers = new int[3];
try {
    numbers[6] = 45;
    numbers[6] = Integer.parseInt("gfd");
} catch (ArrayIndexOutOfBoundsException ex) {
    System.out.println("Выход за пределы массива");
} catch (NumberFormatException ex) {
    System.out.println("Ошибка преобразования из строки в число");
}

Если у нас возникает исключение определенного типа, то оно переходит к соответствующему блоку catch.

3. Оператор throw

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

package firstapp;

import java.util.Scanner;
public class FirstApp {
    public static void main(String[] args) {
        try {
            Scanner in = new Scanner(System.in);
            int x = in.nextInt();
            if (x >= 30) {
                throw new Exception("Число х должно быть меньше 30");
            }
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
        System.out.println("Программа завершена");
    }
}

Здесь для создания объекта исключения используется конструктор класса Exception, в который передается сообщение об исключении. И если число х окажется больше 29, то будет выброшено исключение и управление перейдет к блоку catch.

В блоке catch мы можем получить сообщение об исключении с помощью метода getMessage().

4. Оператор throws

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

Например, у нас имеется метод вычисления факториала, и нам надо обработать ситуацию, если в метод передается число меньше 1:

public static int getFactorial(int num) throws Exception {
    if (num < 1) {
        throw new Exception("The number is less than 1");
    }
    int result = 1;
    for (int i = 1; i <= num; i++) {
        result *= i;
    }
    return result;
}

С помощью оператора throw по условию выбрасывается исключение. В то же время метод сам это исключение не обрабатывает с помощью try…​catch, поэтому в определении метода используется выражение throws Exception.

Теперь при вызове этого метода нам обязательно надо обработать выбрасываемое исключение:

public static void main(String[] args) {
    try {
        int result = getFactorial(-6);
        System.out.println(result);
    } catch (Exception ex) {
        System.out.println(ex.getMessage());
    }
}

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

В качестве альтернативы мы могли бы и не использовать оператор throws, а обработать исключение прямо в методе:

public static int getFactorial(int num) {
    int result = 1;
    try {
        if (num < 1) {
            throw new Exception("The number is less than 1");
        }
        for (int i = 1; i <= num; i++) {
            result *= i;
        }
    } catch (Exception ex) {
        System.out.println(ex.getMessage());
        result = num;
    }
    return result;
}

5. Классы исключений

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

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

Собственно исключения наследуются от класса Exception. Среди этих исключений следует выделить класс RuntimeException. RuntimeException является базовым классом для так называемой группы непроверяемых исключений (unchecked exceptions) - компилятор не проверяет факт обработки таких исключений и их можно не указывать вместе с оператором throws в объявлении метода. Такие исключения являются следствием ошибок разработчика, например, неверное преобразование типов или выход за пределы массива.

Некоторые из классов непроверяемых исключений:

  • ArithmeticException: исключение, возникающее при делении на ноль

  • IndexOutOfBoundException: индекс вне границ массива

  • IllegalArgumentException: использование неверного аргумента при вызове метода

  • NullPointerException: использование пустой ссылки

  • NumberFormatException: ошибка преобразования строки в число

Все остальные классы, образованные от класса Exception, называются проверяемыми исключениями (checked exceptions).

Некоторые из классов проверяемых исключений:

  • CloneNotSupportedException: класс, для объекта которого вызывается клонирование, не реализует интерфейс Clonable

  • InterruptedException: поток прерван другим потоком

  • ClassNotFoundException: невозможно найти класс

Подобные исключения обрабатываются с помощью конструкции try..catch. Либо можно передать обработку методу, который будет вызывать данный метод, указав исключения после оператора throws:

public Person clone() throws CloneNotSupportedException {
    Person p = (Person) super.clone();
    return p;
}

В итоге получается следующая иерархия исключений:

Exceptions

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

  • getMessage() возвращает сообщение об исключении

  • getStackTrace() возвращает массив, содержащий трассировку стека исключения

  • printStackTrace() отображает трассировку стека

Например:

try {
    int x = 6 / 0;
} catch (Exception ex) {
    ex.printStackTrace();
}

6. Создание своих классов исключений

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

Чтобы создать свой класс исключений, надо унаследовать его от класса Exception. Например, у нас есть класс, вычисляющий факториал, и нам надо выбрасывать специальное исключение, если число, передаваемое в метод, меньше 1:

class Factorial {
    public static int getFactorial(int num) throws FactorialException {
        int result = 1;
        if (num < 1) {
            throw new FactorialException("The number is less than 1", num);
        }
        for (int i = 1; i <= num; i++) {
            result *= i;
        }
        return result;
    }
}

class FactorialException extends Exception {
    private int number;

    public int getNumber() {
        return number;
    }

    public FactorialException(String message, int num) {
        super(message);
        number = num;
    }
}

Здесь для определения ошибки, связанной с вычислением факториала, определен класс FactorialException, который наследуется от Exception и который содержит всю информацию о вычислении. В конструкторе FactorialException в конструктор базового класса Exception передается сообщение об ошибке: super(message). Кроме того, отдельное поле предназначено для хранения числа, факториал которого вычисляется.

Для генерации исключения в методе вычисления факториала выбрасывается исключение с помощью оператора throw: throw new FactorialException("Число не может быть меньше 1", num). Кроме того, так как это исключение не обрабатывается с помощью try…​catch, то мы передаем обработку вызывающему методу, используя оператор throws: public static int getFactorial(int num) throws FactorialException

Теперь используем класс в методе main:

public static void main(String[] args) {
    try {
        int result = Factorial.getFactorial(6);
        System.out.println(result);
    } catch (FactorialException ex) {
        System.out.println(ex.getMessage());
        System.out.println(ex.getNumber());
    }
}