Философия Java

         

Конструкторы


Когда пишете код с исключениями, обычно важно, чтобы вы всегда спрашивали: “Если случится исключение, будет ли оно правильно очищено?” Большую часть времени вы этим сохраните, но в конструкторе есть проблемы. Конструктор переводит объект в безопасное начальное состояние, но он может выполнить некоторые операции — такие как открытие файла — которые не будут очищены, пока пользователь не закончит работать с объектом и не вызовет специальный очищающий метод. Если вы выбросили исключение из конструктора, это очищающее поведение может не сработать правильно. Это означает, что вы должны быть особенно осторожными при написании конструктора.

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

В приведенном ниже примере класс, называемый InputFile, при создании открывает файл и позволяет вам читать его по одной строке (конвертируя в String). Он использует классы FileReader и BufferedReader из стандартной библиотеки Java I/O, которая будет обсуждаться в Главе 11, но которая достаточно проста, что вы, вероятно, не будете иметь трудностей в понимании основ ее использования:

//: c10:Cleanup.java

// Уделение внимание на исключение

// в конструкторе.

import java.io.*;

class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Другой код, который может выбросить исключение


} catch(FileNotFoundException e) { System.err.println( "Could not open " + fname); // Что не открыто, то не закроется

throw e; } catch(Exception e) { // Все другие исключения должны быть перекрыты

try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } throw e; // Повторное выбрасывание

} finally { // Не закрывайте их здесь!!!

} } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.err.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } } }

public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); } } } ///:~

Конструктор для InputFile получает аргумент String, который является именем файла, который вы открываете. Внутри блока try создается FileReader с использование имени файла. FileReader не очень полезен до тех пор, пока вы не используете его для создания BufferedReader, с которым вы фактически можете общаться — обратите внимание, что в этом одна из выгод InputFile, который комбинирует эти два действия.

Если конструктор FileReader завершится неудачно, он выбросит FileNotFoundException, которое должно быть поймано отдельно, потому что это тот случай, когда вам не надо закрывать файл, так как его открытие закончилось неудачно. Любое другое предложение catch должно закрыть файл, потому что он был открыт до того, как произошел вход в предложение catch. (Конечно это ненадежно, если более одного метода могут выбросить FileNotFoundException. В этом случае вы можете захотеть разбить это на несколько блоков try.) Метод close( ) может выбросить исключение, так что он проверяется и ловится, хотя он в блоке другого предложения catch — это просто другая пара фигурных скобок для компилятора Java. После выполнения локальных операций исключение выбрасывается дальше, потому что конструктор завершился неудачей, и вы не захотите объявить, что объект правильно создан и имеет силу.



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

Метод getLine( ) возвращает String, содержащую следующую строку файла. Он вызывает readLine( ), который может выбросить исключение, но это исключение ловится, так что getLine( ) не выбрасывает никаких исключений. Одна из проблем разработки исключений заключается в том, обрабатывать ли исключение полностью на этом уровне, обрабатывать ли его частично и передавать то же исключение (или какое-то другое) или просто передавать его дальше. Дальнейшая передача его, в подходящих случаях, может сильно упростить код. Метод getLine( ) превратится в:

String getLine() throws IOException { return in.readLine(); }

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

Метод cleanup( ) должен быть вызван пользователем, когда закончится использование объекта InputFile. Это освободит ресурсы системы (такие как указатель файла), которые используются объектами BufferedReader и/или FileReader [56]. Вам не нужно делать этого до тех пор, пока вы не закончите работать с объектом InputFile. Вы можете подумать о перенесении такой функциональности в метод finalize( ), но как показано в Главе 4, вы не можете всегда быть уверены, что будет вызвана finalize( ) (даже если вы можете быть уверены, что она будет вызвана, вы не будете знать когда). Это обратная сторона Java: вся очистка — отличающаяся от очистки памяти — не происходит автоматически, так что вы должны информировать клиентского программиста, что он отвечает за это и, возможно, гарантировать возникновение такой очистки с помощью finalize( ).

В Cleanup.java InputFile создается для открытия того же исходного файла, который создает программа, файл читается по строкам, а строки нумеруются. Все исключения ловятся в основном в main( ), хотя вы можете выбрать лучшее решение.

Польза от этого примера в том, что он показывает вам, почему исключения введены именно в этом месте книги — вы не можете работать с основами ввода/вывода, не используя исключения. Исключения настолько интегрированы в программирование на Java, особенно потому, что компилятор навязывает их, что вы можете выполнить ровно столько, не зная их, сколько может сделать, работая с ними.


Содержание раздела