русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

Public Object


Дата добавления: 2015-06-12; просмотров: 630; Нарушение авторских прав


getField(String id) throws NoSuchFieldException {

return fields[getFieldNumber(id)][1];

}

public Object setField(String id, Object value)

throws DynamicFieldsException {

if(value == null) {

// Most exceptions don't have a "cause" constructor.

// In these cases you must use initCause(),

// available in all Throwable subclasses.

DynamicFieldsException dfe =

new DynamicFieldsException();

dfe.initCause(new NullPointerException());

throw dfe;

}

int fieldNumber = hasField(id);

if(fieldNumber == -1)

fieldNumber = makeField(id);

Object result = null;

try {

result = getField(id); // Get old value

} catch(NoSuchFieldException e) {

// Use constructor that takes "cause":

throw new RuntimeException(e);

}

fields[fieldNumber][1] = value;

return result;

}

public static void main(String[] args) {

DynamicFields df = new DynamicFields(3);

print(df);

try {

df.setField("d", "A value for d");

df.setField("number", 47);

df.setField("number2", 48);

print(df);

df.setField("d", "A new value for d");

df.setField("number3", 11);

print("df: " + df);

print("df.getField(\"d\") : " + df.getField("d"));

Object field = df.setField("d", null); // Exception

} catch(NoSuchFieldException e) {

e.printStackTrace(System.out);

} catch(DynamicFieldsException e) {

e.printStackTrace(System.out);

}

}

}

<spoiler text="Output:">

null: null

null: null

null: null

 

d: A value for d

number: 47

number2: 48

 

df: d: A new value for d

number: 47

number2: 48

number3: 11

 

df.getField("d") : A new value for d

DynamicFieldsException



at DynamicFields.setField(DynamicFields.java:64)

at DynamicFields.main(DynamicFields.java:94)

Caused by: java.lang.NullPointerException

at DynamicFields.setField(DynamicFields.java:66)

... 1 more

</spoiler> Каждый объект DynamicFields содержит массив пар Object-Object. Первый объект содержит идентификатор поля (String), а второй объект — значение поля, которое может быть любого типа, кроме неупакованных примитивов. При создании объекта необходимо оценить примерное количество полей. Метод setField() либо находит уже существующее поле с заданным именем, либо создает новое поле и сохраняет значение. Когда пространство для полей заканчивается, метод наращивает его, создавая массив размером на единицу больше и копируя в него старые элементы. При попытке размещения пустой ссылки null метод инициирует исключение DynamicFieldsException, создавая объект нужного типа и передавая методу initCause() в качестве причины исключение NullPointerException. Для возвращаемого значения метод setField() использует старое значение поля, получая его методом getField(), который может возбудить исключение NoSuchFieldException. Если метод getField() вызывает программист-клиент, то он ответственен за обработку возможного исключения NoSuchFieldException, однако, если последнее возникает в методе setField(), это является ошибкой программы; соответственно, полученное исключение преобразуется в исключение RuntimeException с помощью конструктора, принимающего аргумент-причину.

Для создания результата toString() использует объект StringBuilder. Этот класс будет подробно рассмотрен при описании работы со строками.

 

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

Класс Java Throwable описывает все объекты, которые могут возбуждаться как исключения. Существует две основные разновидности объектов Throwable (то есть ветви наследования). Тип Error представляет системные ошибки и ошибки времени компиляции, которые обычно не перехватываются (кроме нескольких особых случаев). Тип Exception может быть возбужден из любого метода стандартной библиотеки классов Java или пользовательского метода в случае неполадок при исполнении программы. Таким образом, для программистов интерес представляет прежде всего тип Exception. Лучший способ получить представление об исключениях — просмотреть документацию JDK. Стоит сделать это хотя бы раз, чтобы получить представление о различных видах исключений, но вскоре вы убедитесь в том, что наиболее принципиальным различием между разными исключениями являются их имена. К тому же количество исключений в Java постоянно растет, и едва ли имеет смысл описывать их в книге. Любая программная библиотека от стороннего про­изводителя, скорее всего, также будет иметь собственный набор исключений. Здесь важнее понять принцип работы и поступать с исключениями сообразно. Основной принцип заключается в том, что имя исключения относительно полно объясняет суть возникшей проблемы. Не все исключения определены в пакете java.lang, некоторые из них созданы для поддержки других библиотек, таких как util, net и io, как можно видеть из полных имен их классов или из базовых классов. Например, все исключения, связанные с вводом/выводом (I/O), унаследованы отjava.io.IOException.

Особый случай: RuntimeException

Вспомним первый пример в этой главе:

if(t == null) throw new NullPointerException();

Только представьте, как ужасно было бы проверять таким образом каждую ссылку, переданную вашему методу. К счастью, делать это не нужно — такая проверка автоматически выполняется во время исполнения Java-программы, и при попытке использования null-ссылок автоматически возбуждается NullPointerException. Таким образом, использованная в примере конструкция избыточна.

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

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

//: exceptions/NeverCaught.java

// Игнорирование RuntimeExceptions.

// {ThrowsException}

 

public class NeverCaught {

static void f() {

throw new RuntimeException("From f()");

}

static void g() {

f();

}

public static void main(String[] args) {

g();

}

}

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

Exception in thread "main" java.lang.RuntimeException- Из f() at NeverCaught.f(NeverCaught.java:7) at NeverCaught.g(NeverCaught.java:10) at NeverCaught.main(NeverCaught.java-13)

Мы приходим к ответу на поставленный вопрос: если RuntimeException добирается до main() без перехвата, то работа программы завершается с вызовом метода printStackTrace().

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

· ошибки, которую невозможно предвидеть (к примеру, получение null- ссылки в вашем методе, переданной снаружи);

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

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

Завершение с помощью finally

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

try {

// Защищенная секция: рискованные операции,

// которые могут породить исключения А, В. или С

} catch(A al) {

// Обработчик для ситуации А

} catch(B bl) {

// Обработчик для ситуации В

} catch(C cl) {

// Обработчик для ситуации С

} finally {

// Действия, производимые в любом случае

}

Чтобы продемонстрировать, что блок finally выполняется всегда, рассмотрим следующую программу:

//: exceptions/FinallyWorks.java

// Блок finally выполняется всегда

class ThreeException extends Exception {}

 

public class FinallyWorks {

static int count = 0;

public static void main(String[] args) {

while(true) {

try {

// Post-increment is zero first time:

if(count++ == 0)

throw new ThreeException();

System.out.println("No exception");

} catch(ThreeException e) {

System.out.println("ThreeException");

} finally {

System.out.println("In finally clause");

if(count == 2) break; // out of "while"

}

}

}

}

<spoiler text="Output:">

ThreeException

In finally clause

No exception

In finally clause

</spoiler> Результат работы программы показывает, что вне зависимости от того, было ли возбуждено исключение, предложение finally выполняется всегда. Данный пример также подсказывает, как справиться с тем фактом, что Java не позволяет вернуться к месту возникновения исключения, о чем говорилось ранее. Если расположить блок try в цикле, можно также определить условие, на основании которого будет решено, должна ли программа продолжаться. Также можно добавить статический счетчик или иной механизм для проверки нескольких разных решений, прежде чем отказаться от попыток восстановления. Это один из способов обеспечения повышенной отказоустойчивости программ.

 

Для чего нужен блок finally?

В языках без сборки мусора и без автоматических вызовов деструкторов блок finally гарантирует освобождение ресурсов и памяти независимо от того, что случилось в блоке try. В Java существует сборщик мусора, поэтому с освобожде­нием памяти проблем не бывает. Также нет необходимости вызывать деструкторы, их просто нет. Когда же нужно использовать finally в Java?

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

//: exceptions/Switch.java

import static net.mindview.util.Print.*;

 

public class Switch {

private boolean state = false;

public boolean read() { return state; }

public void on() { state = true; print(this); }

public void off() { state = false; print(this); }

public String toString() { return state ? "on" : "off"; }

}

 

//: exceptions/OnOffException1.java

public class OnOffException1 extends Exception {}

 

//: exceptions/OnOffException2.java

public class OnOffException2 extends Exception {}

 

 

//: exceptions/OnOffSwitch.java

// Зачем использовать finally?

public class OnOffSwitch {

private static Switch sw = new Switch();

public static void f()

throws OnOffException1,OnOffException2 {}

public static void main(String[] args) {

try {

sw.on();

// Code that can throw exceptions...

f();

sw.off();

} catch(OnOffException1 e) {

System.out.println("OnOffException1");

sw.off();

} catch(OnOffException2 e) {

System.out.println("OnOffException2");

sw.off();

}

}

}

<spoiler text="Output:">

on

off

</spoiler> Наша цель — убедиться в том, что переключатель был выключен по завершении метода main(), поэтому в конце блока try и в конце каждого обработчика исключения помещается вызов sw.off(). Однако в программе может возникнуть неперехватываемое исключение, и тогда вызов sw.off() будет пропущен. Однако благодаря finally завершающий код можно поместить в одном определенном месте:

//: exceptions/WithFinally.java

// Finally гарантирует выполнение завершающего кода.

 

public class WithFinally {

static Switch sw = new Switch();

public static void main(String[] args) {

try {

sw.on();

// Code that can throw exceptions...

OnOffSwitch.f();

} catch(OnOffException1 e) {

System.out.println("OnOffException1");

} catch(OnOffException2 e) {

System.out.println("OnOffException2");

} finally {

sw.off();

}

}

}

<spoiler text="Output:">

on

off

</spoiler> Здесь вызов метода sw.off() просто перемещен в то место, где он гарантированно будет выполнен.

Даже если исключение не перехватывается в текущем наборе условий catch, блок finally отработает перед тем, как механизм обработки исключений продолжит поиск обработчика на более высоком уровне:

//: exceptions/AlwaysFinally.java

// Finally выполняется всегда

import static net.mindview.util.Print.*;

 

class FourException extends Exception {}

 

public class AlwaysFinally {

public static void main(String[] args) {

print("Entering first try block");

try {

print("Entering second try block");

try {

throw new FourException();

} finally {

print("finally in 2nd try block");

}

} catch(FourException e) {

System.out.println(

"Caught FourException in 1st try block");

} finally {

System.out.println("finally in 1st try block");

}

}

}

<spoiler text="Output:">

Entering first try block

Entering second try block

finally in 2nd try block

Caught FourException in 1st try block

finally in 1st try block

</spoiler> Блок finally также исполняется при использовании команд break и continue. Заметьте, что комбинация finally в сочетании с break и continue с метками снимает в Java всякую необходимость в операторе goto.

 

Использование finally с return

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

//: exceptions/MultipleReturns.java

import static net.mindview.util.Print.*;

 

public class MultipleReturns {

public static void f(int i) {

print("Initialization that requires cleanup");

try {

print("Point 1");

if(i == 1) return;

print("Point 2");

if(i == 2) return;

print("Point 3");

if(i == 3) return;

print("End");

return;

} finally {

print("Performing cleanup");

}

}

public static void main(String[] args) {

for(int i = 1; i <= 4; i++)

f(i);

}

}

<spoiler text="Output:">

Initialization that requires cleanup

Point 1

Performing cleanup

Initialization that requires cleanup

Point 1

Point 2

Performing cleanup

Initialization that requires cleanup

Point 1

Point 2

Point 3

Performing cleanup

Initialization that requires cleanup

Point 1

Point 2

Point 3

End

Performing cleanup

</spoiler> Из выходных данных видно, что выполнение finally не зависит от того, в какой точке защищенной секции была выполнена команда return.

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

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

//: exceptions/LostMessage.java

// Как теряются исключения.

 

class VeryImportantException extends Exception {

public String toString() {

return "A very important exception!";

}

}

 

class HoHumException extends Exception {

public String toString() {

return "A trivial exception";

}

}

 

public class LostMessage {

void f() throws VeryImportantException {

throw new VeryImportantException();

}

void dispose() throws HoHumException {

throw new HoHumException();

}

public static void main(String[] args) {

try {

LostMessage lm = new LostMessage();

try {

lm.f();

} finally {

lm.dispose();

}

} catch(Exception e) {

System.out.println(e);

}

}

}

<spoiler text="Output:">

A trivial exception

</spoiler> В выводе нет никаких признаков VeryImportantException, оно было просто замещено исключением HoHumException в предложении finally. Это очень серьезный недочет, так как потеря исключения может произойти в гораздо более скрытой и трудно диагностируемой ситуации, в отличие от той, что показана в примере. Например, в C++ подобная ситуация (возбуждение второго исключения без обработки первого) рассматривается как грубая ошибка программиста. Возможно, в новых версиях Java эта проблема будет решена (впрочем, любой метод, способный возбуждать исключения — такой, как dispose() в приведенном примере — обычно заключается в конструкцию try-catch).

Еще проще потерять исключение простым возвратом из finally:

//: exceptions/ExceptionSilencer.java

 

public class ExceptionSilencer {

public static void main(String[] args) {

try {

throw new RuntimeException();

} finally {

// Команда 'return' в блоке finally

// прерывает обработку исключения

return;

}

}

}

Запустив эту программу, вы увидите, что она ничего не выводит — несмотря на исключение.

 

Ограничения при использовании исключений

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

//: exceptions/StormyInning.java

// Переопределенные методы могут возбуждать только

// исключения, описанные в версии базового класса,

// или исключения, унаследованные от исключений

// базового класса.

 

class BaseballException extends Exception {}

class Foul extends BaseballException {}

class Strike extends BaseballException {}

 

abstract class Inning {

public Inning() throws BaseballException {}

public void event() throws BaseballException {

// Doesn't actually have to throw anything

}

public abstract void atBat() throws Strike, Foul;

public void walk() {} // Throws no checked exceptions

}

 

class StormException extends Exception {}

class RainedOut extends StormException {}

class PopFoul extends Foul {}

 

interface Storm {

public void event() throws RainedOut;

public void rainHard() throws RainedOut;

}

 

public class StormyInning extends Inning implements Storm {

// OK to add new exceptions for constructors, but you

// must deal with the base constructor exceptions:

public StormyInning()

throws RainedOut, BaseballException {}

public StormyInning(String s)

throws Foul, BaseballException {}

// Regular methods must conform to base class:

//! void walk() throws PopFoul {} //Compile error

// Interface CANNOT add exceptions to existing

// methods from the base class:

//! public void event() throws RainedOut {}

// If the method doesn't already exist in the

// base class, the exception is OK:

public void rainHard() throws RainedOut {}

// You can choose to not throw any exceptions,

// even if the base version does:

public void event() {}

// Overridden methods can throw inherited exceptions:

public void atBat() throws PopFoul {}

public static void main(String[] args) {

try {

StormyInning si = new StormyInning();

si.atBat();

} catch(PopFoul e) {

System.out.println("Pop foul");

} catch(RainedOut e) {

System.out.println("Rained out");

} catch(BaseballException e) {

System.out.println("Generic baseball exception");

}

// Strike not thrown in derived version.

try {

// What happens if you upcast?

Inning i = new StormyInning();

i.atBat();

// You must catch the exceptions from the

// base-class version of the method:

} catch(Strike e) {

System.out.println("Strike");

} catch(Foul e) {

System.out.println("Foul");

} catch(RainedOut e) {

System.out.println("Rained out");

} catch(BaseballException e) {

System.out.println("Generic baseball exception");

}

}

}

В классе Inning и конструктор, и метод event() объявляют, что будут возбуждать исключения, но в действительности этого не делают. Это допустимо, поскольку подобный подход заставляет пользователя перехватывать все виды исключений, которые потом могут быть добавлены в переопределенные версии метода event(). Данный принцип распространяется и на абстрактные методы, что и показано для метода atBat(). Интерфейс Storm интересен тем, что содержит один метод (event()), уже определенный в классе Inning, и один уникальный. Оба метода возбуждают новый тип исключения RainedOut. Когда класс StormyInning расширяет Inning и реализует интерфейс Storm, выясняется, что метод event() из Storm не способен изменить тип исключения для метода event() класса Inning. Опять-таки это вполне разумно, так как иначе вы бы никогда не знали, перехватываете ли нужное исключение в случае работы с базовым классом. Конечно, когда метод, описанный в интерфейсе, отсутствует в базовом классе (как rainHard()), никаких проблем с возбуждением исключений нет. Метод StormyInning.walk() не компилируется из-за того, что он возбуждает исключение, тогда как Inning.walk() такого не делает. Если бы это позволялось, вы могли бы написать код, вызывающий метод Inning.walk() и не перехватывающий никаких исключений, а потом при подстановке объекта класса, производного от Inning, возникли бы исключения, нарушающие работу программы. Таким образом, принудительно обеспечивая соответствие спецификаций исключений в производных и базовых версиях методов, Javaдобивается взаимозаменяемости объектов. Переопределенный метод event() показывает, что метод производного класса может вообще не возбуждать исключений, даже если это делается в базовой версии. Опять-таки это нормально, так как не влияет на уже написанный код — подразумевается, что метод базового класса возбуждает исключения. Аналогичная логика применима для метода atBat(), возбуждающего исключение PopFoul, производное от Foul, которое возбуждается базовой версией atBat(). Итак, если вы пишете код, работающий сInning и вызывающий atBat(), то он должен перехватывать исключение Foul. Так как PopFoul наследует от Foul, обработчик исключения для Foul перехватит и PopFoul. Последний интересный момент встречается в методе main(). Мы видим, что при работе именно с объектом StormyInning компилятор заставляет перехватывать только те исключения, которые характерны для этого класса, но при восходящем преобразовании к базовому типу компилятор заставляет перехватывать исключения из базового класса. Все эти ограничения значительно повышают ясность и надежность кода обработки исключений. Хотя компилятор заставляет описывать исключения при наследовании, спецификация исключений не является частью объявления (сигнатуры) метода, которое состоит только из имени метода и типов аргументов. Соответственно, нельзя переопределять методы только по спецификациям исключений. Вдобавок, даже если спецификация исключения присутствует в методе базового класса, это вовсе не гарантирует его существования в методе производного класса. Данная практика сильно отличается от правил наследования, по которым метод базового класса обязательно присутствует и в производном классе. Другими словами, «интерфейс спецификации исключений» для определенного метода может сузиться в процессе наследования и переопределения, но никак не расшириться — и это прямая противоположность интерфейсу класса во время наследования.

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

При программировании обработки исключений всегда спрашивайте себя: «Если произойдет исключение, будет ли все корректно завершено?» Чаще все идет более или менее безопасно, но с конструкторами возникает проблема. Конструктор приводит объект в определенное начальное состояние, но может начать выполнять какое-либо действие — такое как открытие файла — которое не будет правильно завершено, пока пользователь не освободит объект, вызвав специальный завершающий метод. Если исключение произойдет в конструкторе, эти финальные действия могут быть исполнены ошибочно. А это означает, что при написании конструкторов необходимо быть особенно внимательным.

Казалось бы, блок finally решает все проблемы. Но в действительности все сложнее — ведь finally выполняется всегда, и даже тогда, когда завершающий код не должен активизироваться до вызова какого-то метода. Если сбой в конструкторе произойдет где-то на середине, может оказаться, что часть объекта, освобождаемая в finally, еще не была создана.

В следующем примере создается класс, названный InputFile, который открывает файл и позволяет читать из него по одной строке. Он использует классы FileReader и BufferedReader из стандартной библиотеки ввода/вывода Java, которая будет изучена далее, но эти классы достаточно просты, и у вас не возникнет особых сложностей при работе с ними:

//: exceptions/InputFile.java

// Специфика исключений в конструкторах

import java.io.*;

 

public class InputFile {

private BufferedReader in;

public InputFile(String fname) throws Exception {

try {

in = new BufferedReader(new FileReader(fname));

// Other code that might throw exceptions

} catch(FileNotFoundException e) {

System.out.println("Could not open " + fname);

// Wasn't open, so don't close it

throw e;

} catch(Exception e) {

// All other exceptions must close it

try {

in.close();

} catch(IOException e2) {

System.out.println("in.close() unsuccessful");

}

throw e; // Rethrow

} finally {

// Don't close it here!!!

}

}

public String getLine() {

String s;

try {

s = in.readLine();

} catch(IOException e) {

throw new RuntimeException("readLine() failed");

}

return s;

}

public void dispose() {

try {

in.close();

System.out.println("dispose() successful");

} catch(IOException e2) {

throw new RuntimeException("in.close() failed");

}

}

}

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

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

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

Метод getLine() возвращает объект String со следующей строкой из файла. Он вызывает метод readLine(), способный возбуждать исключения, но они перехватываются; таким образом, сам getLine() исключений не возбуждает. При про­ектировании обработки исключений вы выбираете между полной обработкой исключения на определенном уровне, его частичной обработкой и передачей далее того же (или другого) исключения и, наконец, простой передачей далее. Там, где это возможно, передача исключения значительно упрощает программирование. В данной ситуации метод getLine() преобразует исключение в RuntimeException, чтобы указать на ошибку в программе.

Метод dispose() должен вызываться пользователем при завершении работы с объектом InputFile. Он освобождает системные ресурсы (такие, как открытые файлы), закрепленные за объектами BufferedReader и (или) FileReader. Делать это следует только тогда, когда работа с объектом InputFile действительно будет завершена. Казалось бы, подобные действия удобно разместить в методе finalize(), но, как упоминалось в главе 5, вызов этого метода не гарантирован (и даже если вы знаете, что он будет вызван, то неизвестно, когда). Это один из недостатков Java: все завершающие действия, кроме освобождения памяти, не производятся автоматически, так что вам придется информировать пользователя о том, что он ответственен за их выполнение.

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

//: exceptions/Cleanup.java

// Гарантированное освобождение ресурсов.

 

public class Cleanup {

public static void main(String[] args) {

try {

InputFile in = new InputFile("Cleanup.java");

try {

String s;

int i = 1;

while((s = in.getLine()) != null)

; // Perform line-by-line processing here...

} catch(Exception e) {

System.out.println("Caught Exception in main");

e.printStackTrace(System.out);

} finally {

in.dispose();

}

} catch(Exception e) {

System.out.println("InputFile construction failed");

}

}

}

<spoiler text="Output:">

dispose() successful

</spoiler> Присмотритесь к логике происходящего: конструирование объекта InputFile фактически заключено в собственный блок try. Если попытка завершается неудачей, мы входим во внешнюю секцию catch и метод dispose() не вызывается. Но, если конструирование прошло успешно, мы хотим обеспечить гарантированное завершение, поэтому сразу же после конструирования создается новый блок try. Блок finally, выполняющий завершение, связывается с внутренним блоком try; таким образом, блок finally не выполняется при неудачном конструировании и всегда выполняется, если конструирование прошло удачно. Эта универсальная идиома применяется и в тех ситуациях, когда конструктор не выдает исключений. Основной принцип: сразу же после создания объекта, требующего завершения, начинается конструкция try-finally:

//: exceptions/CleanupIdiom.java

// За каждым освобождаемым объектом следует try-finally

 

class NeedsCleanup { // Construction can't fail

private static long counter = 1;

private final long id = counter++;

public void dispose() {

System.out.println("NeedsCleanup " + id + " disposed");

}

}

 

class ConstructionException extends Exception {}

 

class NeedsCleanup2 extends NeedsCleanup {

// Construction can fail:

public NeedsCleanup2() throws ConstructionException {}

}

 

public class CleanupIdiom {

public static void main(String[] args) {

// Section 1:

NeedsCleanup nc1 = new NeedsCleanup();

try {

// ...

} finally {

nc1.dispose();

}

 

// Section 2:

// If construction cannot fail you can group objects:

NeedsCleanup nc2 = new NeedsCleanup();

NeedsCleanup nc3 = new NeedsCleanup();

try {

// ...

} finally {

nc3.dispose(); // Reverse order of construction

nc2.dispose();

}

 

// Section 3:

// If construction can fail you must guard each one:

try {

NeedsCleanup2 nc4 = new NeedsCleanup2();

try {

NeedsCleanup2 nc5 = new NeedsCleanup2();

try {

// ...

} finally {

nc5.dispose();

}

} catch(ConstructionException e) { // nc5 constructor

System.out.println(e);

} finally {

nc4.dispose();

}

} catch(ConstructionException e) { // nc4 constructor

System.out.println(e);

}

}

<spoiler text="Output:">

NeedsCleanup 1 disposed

NeedsCleanup 3 disposed

NeedsCleanup 2 disposed

NeedsCleanup 5 disposed

NeedsCleanup 4 disposed

</spoiler> Секция 1 метода main() весьма прямолинейна: за созданием завершаемого объекта следует try-finally. Если конструирование не может завершиться неудачей, наличие catch не требуется. В секции 2 мы видим, что конструкторы, которые не могут завершиться неудачей, могут группироваться как для конструирования, так и для завершения.

Секция 3 показывает, как поступать с объектами, при конструировании которых возможны сбои и которые нуждаются в завершении. Здесь программа усложняется, потому что каждое конструирование должно заключаться в отдельную копию try-catch и за ним должна следовать конструкция try-finally, обеспечивающая завершение.

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

Идентификация исключений

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

//: exceptions/Human.java

// Перехват иерархии исключений.

 

class Annoyance extends Exception {}

class Sneeze extends Annoyance {}

 

public class Human {

public static void main(String[] args) {

// Catch the exact type:

try {

throw new Sneeze();

} catch(Sneeze s) {

System.out.println("Caught Sneeze");

} catch(Annoyance a) {

System.out.println("Caught Annoyance");

}

// Catch the base type:

try {

throw new Sneeze();

} catch(Annoyance a) {

System.out.println("Caught Annoyance");

}

}

}

<spoiler text="Output:">

Caught Sneeze

Caught Annoyance

</spoiler> Исключение Sneeze будет перехвачено в первом блоке catch, который ему соответствует — конечно, это будет первый блок. Но, если удалить первый блок catch, оставив только проверку Annoyance, программа все равно работает, потому что она перехватывает базовый класс Sneeze. Другими словами, блок catch (Annoyance а) поймает Annoyance или любой другой класс, унаследованный от него. Если вы добавите новые производные исключения в свой метод, программа пользователя этого метода не потребует изменений, так как клиент перехватывает исключения базового класса. Если вы попытаетесь «замаскировать» исключения производного класса, поместив сначала блок catch базового класса:

try { throw new Sneeze();

} catch(Annoyance a) {

// ..

} catch(Sneeze s) {

//...

}

компилятор выдаст сообщение об ошибке, так как он видит, что блок catch для исключения Sneeze никогда не выполнится.

Альтернативные решения

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

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

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

try {

// делает что-то полезное

} саtch (ОбязывающееИсключение е) {} // Проглотили!

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

Ужаснувшись тем, что я так поступил, во втором издании книги я «исправил» проблему, распечатыв в обработчике трассировку стека исключения (и сейчас это можно видеть — в подходящих местах — в некоторых примерах данной главы). Хотя это и полезно при отслеживании поведения исключений, трассировка фактически означает, что вы так и не знаете, что же делать с исключением в данном фрагменте кода. В этом разделе мы рассмотрим некоторые тонкости и осложнения, порождаемые контролируемыми исключениями, и варианты работы с последними.

Несмотря на кажущуюся простоту, проблема не только очень сложна, но и к тому же неоднозначна. Существуют твердые приверженцы обеих точек зрения, которые считают, что верный ответ (их) очевиден и просто бросается в глаза. Вероятно, одна из точек зрения основана на несомненных преимуществах перехода от слабо типизированного языка (например, C до выхода стандарта ANSI) к языку с строгой статической проверкой типов (то есть с проверкой во время компиляции), подобному C++ или Java. Преимущества такого перехода настолько очевидны, что строгая статическая проверка типов кажется панацеей от всех бед. Я надеюсь поставить под вопрос ту небольшую часть моей эволюции, отличающуюся абсолютной верой в строгую статическую проверку типов: без сомнения, большую часть времени она приносит пользу, но существует неформальная граница, за которой такая проверка становится препятствием на вашем пути (одна из моих любимых цитат такова: «Все модели неверны, но некоторые полезны»).



<== предыдущая лекция | следующая лекция ==>
Блок try | Предыстория


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.079 сек.