Философия Java

Ограничения исключений


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

Этот пример демонстрирует виды налагаемых ограничений (времени компиляции) на исключения:

//: c10:StormyInning.java

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

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

// базового класса, или унаследованное от

// исключения базового класса.

class BaseballException extends Exception {} class Foul extends BaseballException {} class Strike extends BaseballException {}

abstract class Inning { Inning() throws BaseballException {} void event () throws BaseballException { // На самом деле ничего не выбрасывает

} abstract void atBat() throws Strike, Foul; void walk() {} // Ничего не выбрасывает

}

class StormException extends Exception {} class RainedOut extends StormException {} class PopFoul extends Foul {}

interface Storm { void event() throws RainedOut; void rainHard() throws RainedOut; }

public class StormyInning extends Inning implements Storm { // можно добавить новое исключение для



// конструкторов, но вы должны работать

// с базовым исключеним конструктора:

StormyInning() throws RainedOut, BaseballException {} StormyInning(String s) throws Foul, BaseballException {} // Обычный метод должен соответствовать базовому классу:

//! void walk() throws PopFoul {} //Ошибка компиляции

// Интерфейс НЕ МОДЕТ добавлять исключения к существующим

// методам базового класса:

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

// Если метод еще не существует в базовом классе

// исключение допустимо:

public void rainHard() throws RainedOut {} // Вы можете решить не выбрасывать исключений вообще,

// даже если версия базового класса делает это:

public void event() {} // Перегруженные методы могут выбрасывать


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

void atBat() throws PopFoul {} public static void main(String[] args) { try { StormyInning si = new StormyInning(); si.atBat(); } catch(PopFoul e) { System.err.println("Pop foul"); } catch(RainedOut e) { System.err.println("Rained out"); } catch(BaseballException e) { System.err.println("Generic error"); } // Strike не выбрасывается в унаследованной версии.

try { // Что случится при обратном приведении?

Inning i = new StormyInning(); i.atBat(); // Вы должны ловить исключения от метода

// версии базового класса:

} catch(Strike e) { System.err.println("Strike"); } catch(Foul e) { System.err.println("Foul"); } catch(RainedOut e) { System.err.println("Rained out"); } catch(BaseballException e) { System.err.println( "Generic baseball exception"); } } } ///:~

В Inning вы можете увидеть, что и конструктор, и метод event( ) говорят о том, что они будут выбрасывать исключение, но они не делают этого. Это допустимо, потому что это позволяет вам заставить пользователя ловить любое исключение, которое может быть добавлено и перегруженной версии метода event( ). Эта же идея применена к абстрактным методам, как видно в atBat( ).

Интересен interface Storm, потому что он содержит один метод (event( )), который определен в Inning, и один метод, которого там нет. Оба метода выбрасывают новый тип исключения: RainedOut. Когда StormyInning расширяет Inning и реализует Storm, вы увидите, что метод event( ) в Storm не может изменить исключение интерфейса event( ) в Inning. Кроме того, в этом есть здравый смысл, потому что, в противном случае, вы никогда не узнаете, что поймали правильную вещь, работая с базовым классом. Конечно, если метод, описанный как интерфейс, не существует в базовом классе, такой как rainHard( ), то нет проблем, если он выбросит исключения.

Ограничения для исключений не распространяются на конструкторы. Вы можете видеть в StormyInning, что конструктор может выбросить все, что хочет, не зависимо от того, что выбрасывает конструктор базового класса. Но, так как конструктор базового класса всегда, так или иначе, должен вызываться (здесь автоматически вызывается конструктор по умолчанию), конструктор наследованного класса должен объявить все исключения конструктора базового класса в своей спецификации исключений. Заметьте, что конструктор наследованного класса не может ловить исключения, выброшенные конструктором базового класса.



Причина того, что StormyInning.walk( ) не будет компилироваться в том, что она выбрасывает исключение, которое Inning.walk( ) не выбрасывает. Если бы это допускалось, то вы могли написать код, вызывающий Inning.walk( ), и не иметь обработчика для любого исключения, а затем, когда вы заменили объектом класса, унаследованного от Inning, могло начать выбрасываться исключение и ваш код сломался бы. При ограничивании методов наследуемого класса в соответствии со спецификацией исключений методов базового класса замена объектов допустима.

Перегрузка метода event( ) показывает, что версия метода наследованного класса может не выбрасывать исключение, даже если версия базового класса делает это. Опять таки это хорошо, так как это не нарушит ни какой код, который написан с учетом версии базового класса с выбрасыванием исключения. Сходная логика применима и к atBat( ), которая выбрасывает PopFoul - исключение, унаследованное от Foul, выбрасываемое версией базового класса в методе atBat( ). Таким образом, если кто-то напишет код, который работает с классом Inning и вызывает atBat( ), он должен ловить исключение Foul. Так как PopFoul наследуется от Foul, обработчик исключения также поймает PopFoul.

Последнее, что нас интересует - это main( ). Здесь вы можете видеть, что если вы имеете дело с объектом StormyInning, компилятор заставит вас ловить только те исключения, которые объявлены для этого класса, но если вы выполните приведение к базовому типу, то компилятор (что совершенно верно) заставит вас ловить исключения базового типа. Все эти ограничения производят более устойчивый код обработки исключений [55].

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


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