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

Содержание

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

Введение

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

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.<init>
                            
(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/.

Теги: exceptions java