Статьи

Как и когда использовать исключения

Эта статья является частью нашего Академического курса под названием Advanced Java .

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

1. Введение

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

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

2. Исключения и когда их использовать

В двух словах, исключения — это какие-то события (или сигналы), которые происходят во время выполнения программы и прерывают обычный поток выполнения. Идея, которая привела к введению исключений, родилась как замена кодов ошибок и методов проверки статуса, использовавшихся в те времена. С тех пор исключения получили широкое признание в качестве стандартного способа устранения условий ошибок во многих языках программирования, включая Java.

Существует только одно важное правило, относящееся к обработке исключений (не только в Java): никогда не игнорируйте их! Каждое исключение должно быть как минимум зарегистрировано (см. « Исключения и ведение журнала» ), но не игнорироваться никогда Тем не менее, существуют те редкие обстоятельства, когда исключение можно безопасно игнорировать, потому что на самом деле с этим ничего не поделаешь (см. Пример в разделе « Использование попыток с ресурсами »).

И еще одна вещь, в шестой части урока, « Как эффективно писать методы , мы обсудили проверку аргументов и проверку работоспособности» . Исключения являются важной частью этих практик: каждый публичный метод должен проверять все необходимые предварительные условия перед выполнением любой реальной работы и выдвигать соответствующее исключение, если некоторые из них не были выполнены.

3. Проверенные и непроверенные исключения

Управление исключениями в языке Java отличается от других языков программирования. Это в первую очередь потому, что в Java существует два класса исключений: проверенные и непроверенные исключения. Интересно, что эти два класса являются несколько искусственными и налагаются правилами языка Java и его компилятором (но JVM не делает различий между ними).

Как правило, непроверенные исключения используются для оповещения об ошибочных условиях, связанных с логикой программы и сделанными предположениями (недопустимые аргументы, нулевые указатели, неподдерживаемые операции и т. Д.). Любое непроверенное исключение является подклассом RuntimeException и именно так Java-компилятор понимает, что конкретное исключение принадлежит классу непроверенных.

Непроверенные исключения не обязательно должны быть перехвачены вызывающей стороной или перечислены как часть сигнатуры метода (с использованием ключевого слова throws). NullPointerException является наиболее известным членом непроверенных исключений, и вот его объявление из стандартной библиотеки Java:

1
2
3
4
5
6
7
8
9
public class NullPointerException extends RuntimeException {
    public NullPointerException() {
        super();
    }
 
    public NullPointerException(String s) {
        super(s);
    }
}

Следовательно, проверенные исключения представляют недопустимые условия в областях, которые находятся вне непосредственного контроля над программой (например, память, сеть, файловая система и т. Д.). Любое проверенное исключение является подклассом исключения. В отличие от непроверенных исключений, проверяемые исключения должны быть либо перехвачены вызывающей стороной, либо перечислены как часть сигнатуры метода (с использованием ключевого слова throws ). IOException , пожалуй, самый известный среди проверенных исключений:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class IOException extends Exception {
    public IOException() {
        super();
    }
 
    public IOException(String message) {
        super(message);
    }
 
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public IOException(Throwable cause) {
        super(cause);
    }
}

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

1
2
3
4
5
try {
    // Some I/O operation here
} catch( final IOException ex ) {
    throw new RuntimeException( "I/O operation failed", ex );
}

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

Стоит упомянуть, что в Java существует другой класс исключений, который расширяет класс Error (например, OutOfMemoryError или StackOverflowError ). Эти исключения обычно указывают на фатальный сбой выполнения, который приводит к немедленному завершению программы, поскольку восстановление после таких состояний ошибки невозможно.

4. Использование try-with-resources

Любое выброшенное исключение вызывает некоторое, так называемое, раскручивание стека и изменения в потоке выполнения программы. Результатом этого являются возможные утечки ресурсов, связанные с незакрытыми собственными ресурсами (такими как файловые дескрипторы и сетевые сокеты). Типичная операция ввода-вывода с хорошим поведением в Java (до версии 7) требовала использования обязательного блока finally для выполнения очистки и обычно выглядела так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public void readFile( final File file ) {
    InputStream in = null;
 
    try {
        in = new FileInputStream( file );
        // Some implementation here
    } catch( IOException ex ) {
        // Some implementation here
    } finally {
        if( in != null ) {
            try {
                in.close();
            } catch( final IOException ex ) {
                /* do nothing */
            }
        }
    }
}

Тем не менее, блок finally выглядит очень некрасиво (к сожалению, здесь не так много можно сделать, поскольку вызов метода close для входного потока также может привести к исключению IOException ), независимо от того, что происходит при попытке закрыть входной поток (и освободить ресурсы операционной системы, стоящие за это) будет выполнено. В разделе « Исключения» и о том, когда их использовать, мы подчеркивали тот факт, что исключения никогда не следует игнорировать, однако те, которые вызываются методом close, возможно, являются единственным исключением из этого правила.

К счастью, в Java 7 появилась новая конструкция, которая называется try-with-resources, что значительно упростило общее управление ресурсами. Вот фрагмент кода выше, переписанный с использованием try-with-resources :

1
2
3
4
5
6
7
public void readFile( final File file ) {
    try( InputStream in = new FileInputStream( file ) ) {
        // Some implementation here
    } catch( final IOException ex ) {
        // Some implementation here
    }
}

Единственное, что ресурс должен иметь для использования в блоках try-with-resources, — это реализация интерфейса AutoCloseable . За кулисами Java-компилятор расширяет эту конструкцию до чего-то более сложного, но для разработчиков код выглядит очень читабельным и лаконичным. Пожалуйста, используйте эту очень удобную технику, где это уместно.

5. Исключения и лямбды

В третьей части учебного пособия « Как проектировать классы и интерфейсы» мы уже говорили о последних и лучших функциях Java 8, в частности о лямбда-функциях. Однако мы не изучили многие случаи практического использования, и исключения являются одним из них.

Неудивительно, что непроверенные исключения работают должным образом, однако синтаксис лямбда-функций Java не позволяет указывать проверенные исключения (если они не определены самим @FunctionalInterface ), которые могут быть выброшены. Следующий фрагмент кода не будет компилироваться с ошибкой компиляции «Необработанное исключение типа IOException» (которое может быть выдано в строке 03 ):

1
2
3
4
5
6
7
8
9
public void readFile() {
     run( () -> {
         Files.readAllBytes( new File( "some.txt" ).toPath() );
     } );
 }
 
 public void run( final Runnable runnable ) {
     runnable.run();
 }

Единственное решение сейчас — перехватить исключение IOException внутри тела лямбда-функции и повторно RuntimeException соответствующее исключение RuntimeException (не забывая передать исходное исключение в качестве причины), например:

1
2
3
4
5
6
7
8
9
public void readFile() {
    run( () -> {
        try {
            Files.readAllBytes( new File( "some.txt" ).toPath() );
        } catch( final IOException ex ) {
            throw new RuntimeException( "Error reading file", ex );
        }
    } );
}

Многие функциональные интерфейсы объявлены с возможностью выбросить любое Исключение из его реализации, но если нет (например, Runnable), оборачивать (или перехватывать) проверенные исключения в непроверенные — единственный путь.

6. Стандартные исключения Java

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

Исключительный класс Цель
NullPointerException Попытки использовать null в случае, когда требуется объект.
IllegalArgumentException Метод был передан незаконный или неуместный аргумент.
IllegalStateException Метод был вызван в незаконное или неподходящее время.
IndexOutOfBoundsException Какой-то индекс (например, массив, строка или вектор) находится вне диапазона.
UnsupportedOperationException Запрошенная операция не поддерживается.
ArrayIndexOutOfBoundsException Доступ к массиву с недопустимым индексом.
ClassCastException Код попытался привести объект к подклассу, экземпляром которого он не является.
EnumConstantNotPresentException Попытка получить доступ к константе enum по имени, а тип enum не содержит константы с указанным именем ( enums были рассмотрены в части 5 руководства « Как и когда использовать перечисления и аннотации» ).
NumberFormatException Попытки преобразовать строку в один из числовых типов, но строка не имеет соответствующего формата.
StringIndexOutOfBoundsException Индекс либо отрицательный, либо превышает размер строки.
IOException Возникла какая-то исключительная ситуация ввода / вывода. Этот класс является общим классом исключений, создаваемых неудачными или прерванными операциями ввода-вывода.

Таблица 1 — Стандартные исключения Java

7. Определение ваших собственных исключений

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

Настоятельно рекомендуется, чтобы все пользовательские исключения были унаследованы от класса RuntimeException и попадали в класс непроверенных исключений (однако из правила всегда есть исключения). Например, давайте определим исключение для набора номера с аутентификацией:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class NotAuthenticatedException extends RuntimeException {
    private static final long serialVersionUID = 2079235381336055509L;
 
    public NotAuthenticatedException() {
        super();
    }
 
    public NotAuthenticatedException( final String message ) {
        super( message );
    }
 
    public NotAuthenticatedException( final String message, final Throwable cause ) {
        super( message, cause );
    }
}

Цель этого исключения — сигнализировать о несуществующих или недействительных учетных данных пользователя во время процесса регистрации, например:

1
2
3
4
5
6
public void signin( final String username, final String password ) {
    if( !exists( username, password ) ) {
        throw new NotAuthenticatedException(
            "User / Password combination is not recognized" );
    }
}

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

8. Документирование исключений

В шестой части руководства « Как эффективно писать методы» мы рассмотрели надлежащую документацию по методам в Java. В этом разделе мы собираемся потратить немного больше времени на обсуждение того, как сделать исключения, которые также будут частью документации.

Если метод как часть его реализации может генерировать проверенное исключение, он должен стать частью сигнатуры метода (используя объявление throws ). Соответственно, инструмент документации Java имеет тег @throws для описания этих исключений. Например:

1
2
3
4
5
6
7
/**
 * Reads file from the file system.
 * @throws IOException if an I/O error occurs.
 */
public void readFile() throws IOException {
    // Some implementation here
}

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

1
2
3
4
5
6
7
8
9
/**
 * Parses the string representation of some concept.
 * @param str String to parse
 * @throws IllegalArgumentException if the specified string cannot be parsed properly
 * @throws NullPointerException if the specified string is null
 */
public void parse( final String str ) {
    // Some implementation here
}

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

9. Исключения и регистрация

Ведение журнала ( http://en.wikipedia.org/wiki/Logfile ) является неотъемлемой частью любого более или менее сложного приложения, библиотеки или инфраструктуры Java. Это журнал о важных событиях, происходящих в приложении, и исключения являются важной частью этого потока. Позже в этом руководстве мы можем немного рассмотреть подсистему журналирования, предоставляемую стандартной библиотекой Java, однако, пожалуйста, помните, что исключения должны быть должным образом зарегистрированы и проанализированы позже, чтобы обнаружить проблемы в приложениях и устранить критические проблемы.

10. Что дальше

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

11. Загрузите исходный код

Это был урок о том, как и когда использовать исключения. Вы можете скачать исходный код здесь: advanced-java-part-8