Главная > Java сниппеты > Использование исключительных ситуаций

Тема Зацепин
268

Java-разработчик 🧩
323
2 минуты

Использование исключительных ситуаций

1 Введение 2 Классы исключительных ситуаций 3 Сохранение объекта при генерировании исключительной ситуации 4 Избавление от "мусора" 5 Ссылки

Добавлено : 29 Mar 2009, 10:24

Содержание

Введение

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

import java.io.*;

class BadArgumentException extends RuntimeException {
public BadArgumentException() {
super();
}

public BadArgumentException(String s) {
super(s);
}
}

public class ExDemo1 {
static void processFile(String fname) throws IOException {
if (fname == null || fname.length() == 0) {
throw new BadArgumentException();
}
FileInputStream fis = new FileInputStream(fname);
// ... обработать файл ...
fis.close();
}

public static void main(String args[]) {
try {
processFile("badfile");
} catch (IOException e1) {
System.out.println("I/O error");
}

try {
processFile("");
} catch (IOException e1) {
System.out.println("I/O error");
}
}
}

Пример ExDemo1 работает так, как написан. Результат выполнения программы следующий:

I/O error
Exception in thread
"main" BadArgumentException
at ExDemo1.processFile
(ExDemo1.java:18)
at ExDemo1.main(ExDemo1.java:35)

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

Первый момент касается использования стандартных исключительных ситуаций в противоположность применению своих собственных. Обычно предпочтительнее использовать стандартные исключительные ситуации. То есть, вместо применения BadArgumentException, лучше использовать IllegalArgumentException.

Определение собственных исключительных ситуаций в примере ExDemo1 не дает никаких преимуществ.

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

import java.io.*;

public class ExDemo2 {
static void processFile(String fname) throws IOException {
if (fname == null || fname.length() == 0) {
throw new IllegalArgumentException("null or empty filename");
}
FileInputStream fis = new FileInputStream(fname);
// ... обработать файл ...
fis.close();
}

public static void main(String args[]) {
try {
processFile("badfile");
} catch (IOException e1) {
System.out.println("I/O error");
}

try {
processFile("");
} catch (IOException e1) {
System.out.println("I/O error");
}
}
}

Результат выполнения программы следующий:

I/O error
Exception in thread
"main"
java.lang.IllegalArgumentException: null or empty filename
at ExDemo2.processFile
(ExDemo2.java:7)
at ExDemo2.main(ExDemo2.java:25)

В этом примере есть еще третий момент, который необходимо отметить. Метод processFile вызывается дважды, первый раз с несуществующим именем файла, второй - с пустой строкой. В первом случае генерируется IOException, а во втором - IllegalArgumentException. Первая исключительная ситуация перехватывается, вторая нет.

Классы исключительных ситуаций

IOException является так называемой контролируемой исключительной ситуацией, в то время как IllegalArgumentException является исключительной ситуацией времени исполнения. Иерархия классов исключительных ситуаций в java.lang package выглядит следующим образом:

Throwable
Exception
RuntimeException
IllegalArgumentException
IOException
Error

Основное правило состоит в следующем: программа, вызывающая метод, который генерирует контролируемую исключительную ситуацию, должна обработать ее в операторе catch, либо передать ее далее. Другими словами, processFile вызывает конструктор FileInputStream, а затем метод FileInputStream.close. И конструктор и метод close генерируют контролируемую исключительную ситуацию IOException или ее подкласс FileNotFoundException. Таким образом, processFile должен перехватить эту исключительную ситуацию или определить, что он сам генерирует исключительную ситуацию. Поскольку он делает последнее, вызывающий его метод main должен перехватить исключительную ситуацию.

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

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

Третий тип исключительных ситуаций - подкласс Error, который зарезервирован, по соглашению, для виртуальной машин Java. OutOfMemoryError - это пример такого типа исключительной ситуации.

Сохранение объекта при генерировании исключительной ситуации

Еще один аспект использования исключительных ситуаций касается так называемой "неудачной элементарности", то есть, сохранение объекта в непротиворечивом состоянии при генерировании исключительной ситуации. Приведем пример:

class MyList {
private static final int MAXSIZE = 3;
private final int vec[] = new int[MAXSIZE];
private int ptr = 0;

public void addNum(int i) {
vec[ptr++] = i;
/*
if (ptr == MAXSIZE) {
throw new ArrayIndexOutOfBoundsException(
"ptr == MAXSIZE");
}
vec[ptr++] = i;
*/
}

public int getSize() {
return ptr;
}
}

public class ExDemo3 {
public static void main(String args[]) {
MyList list = new MyList();

try {
list.addNum(1);
list.addNum
(2);
list.addNum
(3);
list.addNum
(4);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e);
}

System.out.println("size = " + list.getSize());
}
}

Результат выполнения этой программы следующий:

java.lang.ArrayIndexOutOfBoundsException
size = 4

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

Проблема в выражении "ptr++". Берется значение указателя списка, указатель инкрементируется и затем оригинальное значение используется для индексирования массива. Это индексирование вызывает исключительную ситуацию, а указатель уже установлен в новое, неправильное значение.

Решение этой проблемы приведено в закомментированном участке кода в классе MyList:

if (ptr == MAXSIZE) {
throw new ArrayIndexOutOfBoundsException("ptr == MAXSIZE");
}
vec[ptr++] = i;

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

Избавление от "мусора"

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

Вот пример, демонстрирующий этот момент:

import java.io.*;
import java.util.*;

public class ExDemo4 {
static final int NUMFILES = 2048;
static final String FILENAME = "testfile";
static final String BADFILE = "";
static final List stream_list = new ArrayList();

// скопировать один файл в другой

public static void copyFile(String infile, String outfile)
throws IOException {

// открыть файлы

FileInputStream fis = new FileInputStream(infile);
stream_list.add
(fis);
FileOutputStream fos =
new FileOutputStream(outfile);

// если исключительная ситуация, дальше не выполнять

// ... копировать файл ...

// закрыть файлы

fis.close();
fos.close
();

/*
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(infile);
stream_list.add(fis);
fos = new FileOutputStream(outfile);
// ... копировать файл ...
}

// блок finally, выполняемый даже при
// возникновении исключительной ситуации

finally {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
}
*/
}

public static void main(String args[]) throws IOException {

// создать файл

new File(FILENAME).createNewFile();

// периодически пытаться скопировать его в bad-файл

for (int i = 1; i <= NUMFILES; i++) {
try {
copyFile(FILENAME, BADFILE);
} catch (IOException e) {
}
}

// отобразить количество успешных
// вызовов конструктора FileInputStream

System.out.println("open count = " + stream_list.size());

// попытаться открыть другой файл

FileInputStream fis = new FileInputStream(FILENAME);
}
}

Программа драйвера в методе main создает файл и периодически пытается скопировать его в другой файл, в файл с неверным именем.

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

В большинстве случаев такой подход работает, но существует одна проблема. Поток первого файла не закрыт. Это создает утечку ресурсов, поскольку открытый поток имеет дескриптор файла. Обычно вы можете положиться на сборщик мусора, устраняющий утечку ресурсов. Если инициируется сборщик мусора, он вызывает метод finalize для FileInputStream. Это закрывает поток и освобождает дескриптор файла. Однако в примере ExDemo4 действие сборщика мусора блокируется добавлением ссылок FileInputStream в список ссылок, существующих вне метода. Даже если бы в примере этого не делалось, сборщик мусора не может гарантировать свою работу в течение конкретного времени для решения проблемы. Этот пример является надуманным, но он демонстрирует проблему корректной очистки при генерировании исключительной ситуации в методе.

После выполнения программы ExDemo4 вы должны увидеть примерно следующий результат:

open count = 1019
Exception in thread "main"
java.io.FileNotFoundException:
testfile
(Too many open files)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.
(FileInputStream.java:64)
at ExDemo4.main(ExDemo4.java:83)

Решение этой проблемы дается в закомментированном коде в copyFile:

FileInputStream fis = null;
FileOutputStream fos =
null;
try {
fis = new FileInputStream(infile);
stream_list.add
(fis);
fos =
new FileOutputStream(outfile);
// ... копировать файл ...
}

// блок finally, выполняемый даже при
// возникновении исключительной ситуации

finally {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
}

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

open count = 2048

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

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

Ссылки

Дополнительная информация об использовании исключительных ситуаций находится в разделах 40, 42, 45 и 46 книги "Эффективное руководство по языку программирования Java" Joshua Bloch (http://java.sun.com/docs/books/effective/).

Обратитесь также к разделу 12.3 "Финализация" книги "Язык программирования Java(tm), 3-е издание" Arnold, Gosling и Holmes

http://java.sun.com/docs/books/javaprog/thirdedition/.

Теги: exceptionsjava

Еще от автора

Применение WeakHashmap для списков слушателей

В статье от 11мая 1999 года Reference Objects были описаны основные идеи применения ссылочных объектов, но не приводилось детального описания. Данная статья позволит вам получить больше сведений, касающихся данной темы. В основном ссылочные объекты применяются для косвенных ссылок на память необходимую объектам. Ссылочные объекты хранятся в очереди (класс ReferenceQueue), в которой отслеживается доступность ссылочных объектов. Исходя из типа ссылочного объекта, сборщик мусора может освобождать память даже тогда, когда обычные ссылки не могут быть освобождены.

Заставки в Mustang

Согласно определению, данному в Wikipedia, заставка - это компьютерный термин, обозначающий рисунок, появляющийся во время загрузки программы или операционной системы. Заставка для пользователя является визуальным отображением инициализации программы. До выхода версии Java SE 6 (кодовое название Mustang) единственной возможностью применения заставки было создание окна, во время запуска метода main, и размещение в нем картинки. Хотя данный способ и работал, но он требовал полной инициализации исполняемой Java среды до появления окна заставки. При инициализации загружались библиотеки AWT и Swing, таким образом, появление заставки задерживалось. В Mustang появился новый аргумент командной строки, значительно облегчающий использование заставок. Этот способ позволяет выводить заставку значительно быстрее до запуска исполняемой Java среды. Окончательное добавление данной функциональности находится на рассмотрении в JCP.

Совмещение изображений

1 Введение 2 Правила визуализации и пример 3 Совмещение изображений в оперативной памяти 4 Постепенное исчезновение изображения 5 Ссылки и дополнительная информация

Еще по теме

Применение WeakHashmap для списков слушателей

В статье от 11мая 1999 года Reference Objects были описаны основные идеи применения ссылочных объектов, но не приводилось детального описания. Данная статья позволит вам получить больше сведений, касающихся данной темы. В основном ссылочные объекты применяются для косвенных ссылок на память необходимую объектам. Ссылочные объекты хранятся в очереди (класс ReferenceQueue), в которой отслеживается доступность ссылочных объектов. Исходя из типа ссылочного объекта, сборщик мусора может освобождать память даже тогда, когда обычные ссылки не могут быть освобождены.

Заставки в Mustang

Согласно определению, данному в Wikipedia, заставка - это компьютерный термин, обозначающий рисунок, появляющийся во время загрузки программы или операционной системы. Заставка для пользователя является визуальным отображением инициализации программы. До выхода версии Java SE 6 (кодовое название Mustang) единственной возможностью применения заставки было создание окна, во время запуска метода main, и размещение в нем картинки. Хотя данный способ и работал, но он требовал полной инициализации исполняемой Java среды до появления окна заставки. При инициализации загружались библиотеки AWT и Swing, таким образом, появление заставки задерживалось. В Mustang появился новый аргумент командной строки, значительно облегчающий использование заставок. Этот способ позволяет выводить заставку значительно быстрее до запуска исполняемой Java среды. Окончательное добавление данной функциональности находится на рассмотрении в JCP.

Использование потоков

1 Введение 2 Работа с выражениями типа Boolean 3 Класс JoptionPane 4 Приложение-счетчик 5 Ссылки

Перехват необрабатываемых исключений

В статье от 16 марта 2004 года Best Practices in Exception Handling были описаны приемы обработки исключений. В данной статье вы изучите новый способ обработки исключений при помощи класса UncaughtExceptionHandler добавленного в J2SE 5.0.

Использование класса LinkedHashMap

1 Введение 2 Сортировка хэш-таблицы 3 Копирование таблицы 4 Сохранение порядка доступа к элементам 5 Ссылки

Сказ про кодировки и java

С кодировками в java плохо. Т.е., наоборот, все идеально хорошо: внутреннее представление строк – Utf16-BE (и поддержка Unicode была с самых первых дней). Все возможные функции умеют преобразовывать строку из маленького регистра в большой, проверять является ли данный символ буквой или цифрой, выполнять поиск в строке (в том числе с регулярными выражениями) и прочее и прочее. Для этих операций не нужно использовать какие-то посторонние библиотеки вроде привычных для php mbstring или iconv. Как говорится, поддержка многоязычных тестов “есть в коробке”. Так откуда берутся проблемы? Проблемы возникают, как только строки текста пытаются “выбраться” из jvm (операции вывода текста различным потребителям) или наоборот пытаются в эту самую jvm “залезть” (операция чтения данных от некоторого поставщика).