Главная » Java » Расширение класса

0

Одним из самых больших достоинств объектно-ориентированного программирования является возможность такого расширения, или создания подкласса, существующего класса, при котором можно использовать код, написанный для исходного класса.

При расширении класса на его основе создается новый класс, наследующий все поля и методы расширяемого класса. Исходный класс, для которого проводилось расширение, называется суперклассом.

Если подкласс не переопределяет (override) поведение суперкласса, то он наследует все свойства суперкласса, поскольку, как уже говорилось, расширенный класс наследует поля и методы суперкласса.

Примером с плеерами Walkman можно воспользоваться и здесь. В последних моделях плееров устанавливаются  два разъема для наушников, чтобы одну и ту же кассету могли слушать сразу двое. В объектно-ориентированном мире модель с двумя разъемами расширяет базовую модель. Эта модель наследует все характеристики и поведение базовой модели и добавляет к ним свои собственные.

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

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

У Sony имеются и другие модели плееров. Более поздние серии расширяют возможности базовой модели — они создают подклассы на ее основе и наследуют от нее свойства и поведение.

Давайте посмотрим, как происходит наследование в Java. Расширим наш класс Point, чтобы он представлял пиксель на экране монитора. В новом классе Pixel к координатам x и y добавляется информация о цвете пикселя:

class Pixel extends Point { Color color;

public void clear() { super.clear(); color = null;

}

}

Класс Pixel расширяет как данные, так и поведение своего суперкласса Point. Для данных это означает, что в классе Pixel появляется дополнительное поле color. Pixel также расширяет поведение Point, переопределяя метод clear класса Point. Эта концепция наглядно изображена на рисунке:

Объект Pixel может использоваться в любой программе, которая рассчитана на работу с объектами Point. Если методу необходимо передать параметр типа Point, можно вместо него передать объект Pixel — все будет нормально. Вместо объекта класса Point можно пользоваться объектом подкласса Pixel; это явление известно под названием

“полиморфизм” — один и то же объект (Pixel) выступает в нескольких (поли-) формах (-

морф) и может использоваться и как Pixel, и как Point.

Поведение Pixel расширяет поведение Point. Оно может совершенно преобразиться (например, работа с цветами в нашем примере) или будет представлять собой некоторое ограничение старого поведения, удовлетворяющее  всем исходным требованиям. Примером последнего может служить объект класса Pixel, принадлежащий некоторому объекту Screen (экран), в котором значения координат x и y ограничиваются размерами экрана. В исходном классе Point значения координат могли быть произвольными, поэтому ограниченные значения координат все равно лежат в исходном (неограниченном) диапазоне.

Расширенный класс часто переопределяет поведение своего суперкласса (то есть класса, на основе которого он был создан), по-новому реализуя один или несколько унаследованных методов. В приведенном выше примере мы переопределили метод clear, чтобы он вел себя так, как того требует объект Pixel, — метод clear, унаследованный  от Point, знает лишь о существовании полей Point, но, разумеется, не догадывается о присутствии поля color, объявленного в подклассе Pixel.

Упражнение 1.12

Напишите набор классов, отражающих структуру семейства плееров Sony Walkman. Воспользуйтесь методами, чтобы скрыть все данные, объявите последние с ключевым словом private, а методы — public. Какие методы должны принадлежать базовому классу Walkman? Какие методы добавятся в расширенных классах?

1.10.1. Класс Object

Классы, для которых не указан расширяемый класс, являются неявным расширением класса Object. Все ссылки на объекты полиморфно относятся к классу Object, который является базовым классом для всех ссылок, которые могут относиться к объектам любого класса:

Object oref = new Pixel();

oref = “Some String”;

В этом примере объекту oref вполне законно присваиваются ссылки на объекты Pixel и String, невзирая на то что эти классы не имеют между собой ничего общего — за исключением неявного суперкласса Object.

В классе Object также определяется несколько важных методов, рассмотренных в главе 3.

1.10.2. Вызов методов суперкласса

Чтобы очистка объектов класса Pixel происходила правильно, мы заново реализовали метод clear. Его работа начинается с того, что с помощью ссылки super вызывается метод clear суперкласса. Ссылка super во многих отношениях напоминает уже упоминавшуюся ранее ссылку this, за тем исключением, что super используется для ссылок на члены суперкласса, тогда как this ссылается на члены текущего объекта.

Вызов super.clear() обращается к суперклассу для выполнения метода clear точно так же, как он обращался бы к любому объекту суперкласса — в нашем случае, класса Point. После вызова super.clear() следует новый код, который должен присваивать color некоторое разумное начальное значение. Мы выбрали null — то есть отсутствие ссылки на какой-либо объект.

Что бы случилось, если бы мы не вызвали super.clear()? Метод clear класса Pixel присвоил бы полю цвета значение null, но переменные x и y, унаследованные  от класса Point,

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

При вызове метода super.method() runtime-система просматривает иерархию классов до первого суперкласса, содержащего method(). Например, если бы метод clear отсутствовал в классе Point, то runtime-система попыталась бы найти такой метод в его суперклассе и (в случае успеха) вызвала бы его.

Во всех остальных ссылках при вызове метода используется тип объекта, а не тип ссылки

на объект. Приведем пример:

Point point = new Pixel();

point.clear(); // используется метод clear() класса Pixel

В этом примере будет вызван метод clear класса Pixel, несмотря на то что переменная,

содержащая объект класса Pixel, объявлена как ссылка на Point.

1.11. Интерфейсы

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

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

структурой данных.

Java использует так называемый интерфейс — некоторое подобие класса, где методы лишь объявляются, но не определяются. Разработчик интерфейса решает, какие методы должны поддерживаться в классах, реализующих данный интерфейс, и что эти методы должны делать. Приведем пример интерфейса Lookup:

interface Lookup {

/** Вернуть значение, ассоциированное с именем, или

null, если такого значения не окажется */ Object find(String name);

}

В интерфейсе Lookup объявляется всего один метод find, который получает значение (имя) типа String и возвращает значение, ассоциированное  с данным именем, или null, если такого значения не найдется. Для объявленного метода не предоставляется никакой конкретной реализации — она полностью возлагается на класс, в котором реализуется данный интерфейс. Во фрагменте программы, где используются ссылки на объекты Lookup (объекты, реализующие интерфейс Lookup), можно вызвать метод find и получить ожидаемый результат независимо от конкретного типа объекта:

void processValues(String[] names, Lookup table) {

for (int i = 0; i < names.length; i++) { Object value = table.find(names[i]); if (value != null)

processValue(names[i], value);

}

}

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

class SimpleLookup implements Lookup {

private String[] Names;

private Object[] Values;

public Object find(String name) {

for (int i = 0; i < Names.length; i++) {

if (Names[i].equals(name))

return Values[i];

}

return null;

}

// . . .

}

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

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

Упражнение 1.13

Напишите расширенный интерфейс Lookup с добавлением методов add и remove.

Реализуйте его в новом классе.

1.12. Исключения

Что делать, если в программе произошла ошибка? Во многих языках о ней свидетельствуют необычные значения кодов возврата — например, –1. Программисты нередко не проверяют свои программы на наличие исключительных состояний, так как они полагают, что ошибок “быть не должно”. С другой стороны, поиск опасных мест и восстановление нормальной работы даже в прямолинейно построенной программе может затемнить ее логику до такой степени, что все происходящее в ней станет совершенно непонятным. Такая простейшая задача, как считывание файла в память, требует около семи строк в программе. Обработка ошибок и вывод сообщений о них увеличивает код до

40 строк. Суть программы теряется в проверках как иголка в стоге сена — это, конечно же, нежелательно.

При обработке ошибок в Java используются проверяемые исключения (checked exceptions). Исключение заставляет программиста предпринять какие-то действия при возникновении ошибки. Исключительные ситуации в программе обнаруживаются при их возникновении, а не позже, когда необработанная ошибка приведет к множеству проблем.

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

может сообщить о возникновении исключительной ситуации и завершить работу потока в программе.

Исключения в Java являются объектами — у них имеется тип, методы и данные. Представление исключения в виде объекта оказывается полезным, поскольку объект- исключение может обладать данными или методами (или и тем и другим), которые позволят справиться с конкретной ситуацией. Объекты-исключения обычно порождаются от класса Exception, в котором содержится строковое поле для описания ошибки. Java требует, чтобы все исключения были расширениями класса с именем Throwable.

Основная парадигма работы с исключениями Java заключена в последовательности try- catch-finally. Сначала программа пытается (try) что-то сделать; если при этом возникает исключение, она его перехватывает (catch); и наконец (finally), программа предпринимает некоторые итоговые действия в стандартном коде или в коде обработчика исключения —

в зависимости от того, что произошло.

Ниже приводится метод averageOf, который возвращает среднее арифметическое  двух элементов массива. Если какой-либо из индексов выходит за пределы массива, программа запускает исключение, в котором сообщает об ошибке. Прежде всего следует определить новый тип исключения Illegal AverageException  для вывода сообщения об ошибке. Затем необходимо указать, что метод averageOf возбуждает это исключение, при помощи ключевого слова throws:

class IllegalAverageException extends Exception {

}

class MyUtilities {

public double averageOf(double[] vals, int i, int j)

throws IllegalAverageException

{

try {

return (vals[i] + vals[j]) / 2;

} catch (IndexOutOfBounds e) {

throw new IllegalAverageException();

}

}

}

Если при определении среднего арифметического оба индекса i и j оказываются в пределах границ массива, вычисление происходит успешно и метод возвращает полученное значение. Однако, если хотя бы один из индексов выходит за границы массива, возбуждается исключение IndexOutOfBounds и выполняется соответствующий оператор catch. Он создает и возбуждает новое исключение IllegalAverageException — в сущности, общее исключение нарушения границ массива превращается в конкретное исключение, более точно описывающее истинную причину. Методы, находящиеся дальше в стеке выполнения, могут перехватить новое исключение и должным образом прореагировать на него.

Если выполнение метода может привести к возникновению проверяемых исключений, последние должны быть объявлены после ключевого слова throws, как показано на примере метода averageOf. Если не считать исключений RuntimeException  и Error, а также подклассов этих типов исключений, которые могут возбуждаться в любом месте программы, метод возбуждает лишь объявленные в нем исключения — как прямо, посредством оператора throw, так и косвенно, вызовом других методов, возбуждающих исключения.

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

проверка предотвращает ошибки в тех случаях, когда метод должен обрабатывать исключения от других методов, однако не делает этого. Кроме того, метод, вызывающий ваш метод, может быть уверен, что это не приведет к возникновению неожиданных исключений. Именно поэтому исключения, которые должны быть объявлены после ключевого слова throws, называются проверяемыми исключениями. Исключения, являющиеся расширениями RuntimeException  и Error, не нуждаются в объявлении и проверке; они называются непроверяемыми исключениями.

Упражнение 1.14

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

1.13. Пакеты

Конфликты имен становятся источником серьезных проблем при разработке повторно используемого кода. Как бы тщательно вы ни подбирали имена для своих классов и методов, кто-нибудь может использовать это же имя для других целей. При использовании простых названий проблема лишь усугубляется — такие имена с большей вероятностью будут задействованы кем-либо еще, кто также захочет пользоваться простыми словами. Такие имена, как set, get, clear и т. д., встречаются очень часто, и конфликты при их использовании оказываются практически неизбежными.

Во многих языках программирования  предлагается стандартное решение — использование “префикса пакета” перед каждым именем класса, типа, глобальной функции и так далее. Соглашения о префиксах создают контекст имен (naming context), который предотвращает конфликты имен одного пакета с именами другого. Обычно такие префиксы имеют длину в несколько символов и являются сокращением названия

пакета — например, Xt для “X Toolkit” или WIN32 для 32-разрядного Windows API.

Если программа состоит всего из нескольких пакетов, вероятность конфликтов префиксов невелика. Однако, поскольку префиксы являются сокращениями, при увеличении числа пакетов вероятность конфликта имен повышается.

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

пакета.

Приведем пример метода, в котором полные имена используются для вывода текущей даты и времени с помощью вспомогательного  класса Java с именем Date (о котором рассказано в главе 12):

class Date1 {

public static void main(String[] args) { java.util.Date now = new java.util.Date(); System.out.println(now);

}

}

Теперь сравните этот пример с другим, в котором для объявления типа Date используется ключевое слово import:

import java.util.Date;

class Date2 {

public static void main(String[] args) { Date now = new Date(); System.out.println(now);

}

}

Пакеты Java не до конца разрешают проблему конфликтов имен. Два различных проекта могут присвоить своим пакетам одинаковые имена. Эта проблема решается только за счет использования общепринятых соглашений об именах. По наиболее распространенному  из таких соглашений в качестве префикса имени пакета используется перевернутое имя домена организации в Internet. Например, если фирма Acme Corporation содержит в Internet домен с именем acme.com, то разработанные ей пакеты будут иметь имена типа COM.acme.package.

Точки, разделяющие компоненты имени пакета, иногда могут привести к недоразумениям, поскольку те же самые точки используются при вызове методов и доступе к полям в ссылках на объекты. Возникает вопрос — что же именно импортируется? Новички часто пытаются импортировать объект System.out, чтобы не вводить его имя перед каждым вызовом println. Такой вариант не проходит, поскольку System является классом, а out — его статическим полем, тип которого поддерживается методом println.

С другой стороны, java.util является пакетом, так что допускается импортирование java.util.Date (или java.util.*, если вы хотите импортировать все содержимое пакета). Если у вас возникают проблемы с импортированием  чего-либо, остановитесь и убедитесь в том, что вы импортируете тип.

Классы Java всегда объединяются в пакеты. Имя пакета задается в начале файла:

package com.sun.games;

class Card

{

}

// …

// …

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

Источник: Арнольд К., Гослинг Д. – Язык программирования Java (1997)

По теме:

  • Комментарии