Глава 8. Наследование: проблемы и альтернативы. Интерфейсы. Композиция Проблемы множественного наследования классов. Интерфейсы
Достаточно часто требуется совмещать в объекте поведение, характерное для двух или более независимых иерархий. Но ещё чаще требуется писать единый полиморфный код для объектов из таких иерархий в случае, когда эти объекты обладают схожим поведением. Как мы знаем, за поведение объектов отвечают методы. Это значит, что в полиморфном коде требуется для объектов из разных классов вызывать методы, имеющие одинаковую сигнатуру, но разную реализацию. Унарное наследование, которое мы изучали до сих пор, и при котором у класса может быть только один прародитель, не обеспечивает такой возможности. При унарном наследовании нельзя ввести переменную, которая бы могла ссылаться на экземпляры из разных иерархий, так как она должна иметь тип, совместимый с базовыми классами этих иерархий.
В C++ для решения данных проблем используется множественное наследование. Оно означает, что у класса может быть не один непосредственный прародитель, а два или более. В этом случае проблема совместимости с классами из разных иерархий решается путём создания класса, наследующего от необходимого числа классов-прародителей.
Но при множественном наследовании классов возникает ряд трудно разрешимых проблем, поэтому в Java оно не поддерживается. Основные причины, по которым произошёл отказ от использования множественного наследования классов: наследование ненужных полей и методов, конфликты совпадающих имён из разных ветвей наследования.
В частности, так называемое ромбовидное наследование, когда у класса A наследники B и C, а от них наследуется класс D. Поэтому класс D получает поля и методы, имеющиеся в классе A, в удвоенном количестве – один комплект по линии родителя B, другой – по линии родителя C.
Конечно, в C++ имеются средства решать указанные проблемы. Например, проблемы ромбовидного наследования снимаются использованием так называемых виртуальных классов, благодаря чему при ромбовидном наследовании потомкам достаётся только один комплект членов класса A, и различения имён из разных ветвей класса с помощью имён классов. Но в результате получается заметное усложнение логики работы с классами и объектами, вызывающее логические ошибки, не отслеживаемые компилятором. Поэтому в Java используется множественное наследование с помощью интерфейсов, лишённое практически всех указанных проблем.
Интерфейсы являются важнейшими элементами языков программирования, применяемых как для написания полиморфного кода, так и для межпрограммного обмена. Концепция интерфейсов Java была первоначально заимствована из языка Objective-C, но в дальнейшем развивалась и дополнялась. В настоящее время она, фактически, стала основой объектного программирования в Java, заменив концепцию множественного наследования, используемую в C++.
Интерфейсы являются специальной разновидностью полностью абстрактных классов. То есть таких классов, в которых вообще нет реализованных методов - все методы абстрактные. Полей данных в них также нет, но можно задавать константы (неизменяемые переменные класса). Класс в Java должен быть наследником одного класса-родителя, и может быть наследником произвольного числа интерфейсов. Сами интерфейсы также могут наследоваться от интерфейсов, причём также с разрешением на множественное наследование.
Отсутствие в интерфейсах полей данных и реализованных методов снимает почти все проблемы множественного наследования и обеспечивает изящный инструмент для написания полиморфного кода. Например, то, что все коллекции обладают методами, перечисленными в разделе о коллекциях, обеспечивается тем, что их классы являются наследниками интерфейса Collection. Аналогично, все классы итераторов являются наследниками интерфейса Iterator, и т.д.
Декларация интерфейса очень похожа на декларацию класса:
МодификаторВидимости interface ИмяИнтерфейса
extends ИмяИнтерфейса1, ИмяИнтерфейса2,..., ИмяИнтерфейсаN{
декларация констант;
декларация заголовков методов;
}
В качестве модификатора видимости может использоваться либо слово public – общая видимость, либо модификатор должен отсутствовать, в этом случае обеспечивается видимость по умолчанию - пакетная. В списке прародителей, расширением которых является данный интерфейс, указываются прародительские интерфейсы ИмяИнтерфейса1, ИмяИнтерфейса2 и так далее. Если список прародителей пуст, в отличие от классов, интерфейс не имеет прародителя.
Для имён интерфейсов в Java нет специальных правил, за исключением того, что для них, как и для других объектных типов, имя принято начинать с заглавной буквы. Мы будем использовать для имён интерфейсов префикс I (от слова Interface), чтобы их имена легко отличать от имён классов.
Объявление константы осуществляется почти так же, как в классе:
МодификаторВидимости Тип ИмяКонстанты = значение;
В качестве необязательного модификатора видимости может использоваться слово public. Либо модификатор должен отсутствовать - но при этом видимость также считается public, а не пакетной. Ещё одним отличием от декларации в классе является то, что при задании в интерфейсе все поля автоматически считаются окончательными (модификатор final), т.е. без права изменения, и к тому же являющимися переменными класса (модификатор static). Сами модификаторы static и final при этом ставить не надо.
Декларация метода в интерфейсе осуществляется очень похоже на декларацию абстрактного метода в классе – указывается только заголовок метода:
МодификаторВидимости Тип ИмяМетода(списокПараметров)
throws списокИсключений;
В качестве модификатора видимости, как и в предыдущем случае, может использоваться либо слово public, либо модификатор должен отсутствовать. При этом видимость также считается public, а не пакетной. В списке исключений через запятую перечисляются типы проверяемых исключений (потомки Exception), которые может возбуждать метод. Часть throws списокИсключений является необязательной. При задании в интерфейсе все методы автоматически считаются общедоступными (public) абстрактными (abstract) методами объектов.
Пример задания интерфейса:
package figures_pkg;
public interface IScalable {
public int getSize();
public void setSize(int newSize);
}
Класс можно наследовать от одного родительского класса и от произвольного количества интерфейсов. Но вместо слова extends используется зарезервированное слово implements - реализует. Говорят, что класс-наследник интерфейса реализует соответствующий интерфейс, так как он обязан реализовать все его методы. Это гарантирует, что объекты для любых классов, наследующих некий интерфейс, могут вызывать методы этого интерфейса. Что позволяет писать полиморфный код для объектов из разных иерархий классов. При реализации возможно добавление новых полей и методов, как и при обычном наследовании. Поэтому можно считать, что это просто один из вариантов наследования, обладающий некоторыми особенностями.
Уточнение: в абстрактных классах, реализующих интерфейс, реализации методов может не быть – наследуется декларация абстрактного метода из интерфейса.
Замечание: Интерфейс также может реализовывать (implements) другой интерфейс, если в том при задании типа использовался шаблон (generics, template). Но эта тема выходит за пределы данного учебного пособия.
В наследнике класса, реализующего интерфейс, можно переопределить методы этого интерфейса. Повторное указание в списке родителей интерфейса, который уже был унаследован кем-то из прародителей, запрещено.
Реализации у сущности типа интерфейс не бывает, как и для абстрактных классов. То есть экземпляров интерфейсов не бывает. Но, как и для абстрактных классов, можно вводить переменные типа интерфейс. Эти переменные могут ссылаться на объекты, принадлежащие классам, реализующим соответствующий интерфейс. То есть классам-наследникам этого интерфейса.
Правила совместимости таковы: переменной типа интерфейс можно присваивать ссылку на объект любого класса, реализующего этот интерфейс.
С помощью переменной типа интерфейс разрешается вызывать только методы, декларированные в данном интерфейсе, а не любые методы данного объекта. Если, конечно, не использовать приведение типа.
Основное назначение переменных интерфейсного типа – вызов с их помощью методов, продекларированных в соответствующем интерфейсе. Если такой переменной назначена ссылка на объект, можно гарантировать, что из этого объекта разрешено вызывать эти методы, независимо от того, какому классу принадлежит объект. Ситуация очень похожа с полиморфизмом на основе виртуальных и динамических методов объектов. Но гарантией работоспособности служит не одинаковость сигнатуры методов в одной иерархии, а одинаковость сигнатуры методов в разных иерархиях – благодаря совпадению с декларацией одного и того же интерфейса. Обязательно, чтобы методы были методами объектов – полиморфизм на основе методов класса невозможен, так как для вызовов этих методов используется статическое связывание (на этапе компиляции).
- Содержание
- Глава 1. Общие представления о языке Java 6
- Глава 2. Объектно-ориентированное проектирование и платформа NetBeans 26
- Глава 3. Примитивные типы данных и операторы для работы с ними 78
- Глава 4. Работа с числами в языке Java 95
- Глава 5. Управляющие конструкции 112
- Глава 6. Начальные сведения об объектном программировании 128
- Глава 7. Важнейшие объектные типы 175
- Введение
- Глава 1. Общие представления о языке Java
- 1.1. Java и другие языки программирования. Системное и прикладное программирование
- 1.2. Виртуальная Java-машина, байт-код, jit-компиляция. Категории программ, написанных на языке Java
- 1.3.Алфавит языка Java. Десятичные и шестнадцатеричные цифры и целые числа. Зарезервированные слова Алфавит языка Java
- Десятичные и шестнадцатеричные цифры и целые числа
- Зарезервированные слова языка Java
- 1.4. Управляющие последовательности. Символы Unicode. Специальные символы Управляющие последовательности
- Простые специальные символы
- Составные специальные символы
- 1.5.Идентификаторы. Переменные и типы. Примитивные и ссылочные типы
- Краткие итоги по главе 1
- Задания
- Глава 2. Объектно-ориентированное проектирование и платформа NetBeans
- 2.1.Процедурное и объектно-ориентированное программирование. Инкапсуляция
- 2.2. Работа со ссылочными переменными. Сборка мусора
- 2.3. Проекты NetBeans. Пакеты. Уровни видимости классов. Импорт классов
- 2.4. Базовые пакеты и классы Java
- 2.5. Создание в NetBeans простейшего приложения Java
- 2.6. Компиляция файлов проекта и запуск приложения
- 2.7. Структура проекта NetBeans
- 2.8. Создание в NetBeans приложения Java с графическим интерфейсом
- 2.9. Редактор экранных форм
- 2.10. Внешний вид приложения
- 2.11. Ведение проектов
- 2.11. Редактирование меню экранной формы
- 2.12. Создание нового класса
- 2.13. Документирование исходного кода в Java
- 2.14. Основные компоненты пакетов swing и awt
- 2.15. Технологии Java и .Net
- Краткие итоги по главе 2
- Задания
- Глава 3. Примитивные типы данных и операторы для работы с ними
- 3.1.Булевский (логический) тип
- 3.2.Целые типы, переменные, константы
- 3.3.Основные операторы для работы с целочисленными величинами
- 3.4.Вещественные типы и класс Math
- 3.5.Правила явного и автоматического преобразования типа при работе с числовыми величинами
- 3.6. Оболочечные классы. Упаковка (boxing) и распаковка (unboxing)
- 3.7.Приоритет операторов
- 3.8.Типы-перечисления (enum)
- Краткие итоги по главе 3
- Задания
- Глава 4. Работа с числами в языке Java
- 4.1 Двоичное представление целых чисел Позиционные и непозиционные системы счисления
- Двоичное представление положительных целых чисел
- Двоичное представление отрицательных целых чисел. Дополнительный код
- Проблемы целочисленной машинной арифметики
- Шестнадцатеричное представление целых чисел и перевод из одной системы счисления в другую
- 4.2. Побитовые маски и сдвиги
- 4.3. Двоичное представление вещественных чисел Двоичные дроби
- Мантисса и порядок числа
- Стандарт ieee 754 представления чисел в формате с плавающей точкой*
- Краткие итоги по главе 4
- Задания
- Глава 5. Управляющие конструкции Составной оператор
- Условный оператор if
- Оператор выбора switch
- Условное выражение …?... : …
- Оператор цикла for
- Оператор цикла while – цикл с предусловием
- Оператор цикла do...While – цикл с постусловием
- Операторы прерывания continue, break, return, System.Exit
- Краткие итоги по главе 5
- Задания
- Глава 6. Начальные сведения об объектном программировании
- Наследование и полиморфизм. Uml-диаграммы
- Функции. Модификаторы. Передача примитивных типов в функции
- Локальные и глобальные переменные. Модификаторы доступа и правила видимости. Ссылка this
- Передача ссылочных типов в функции. Проблема изменения ссылки внутри подпрограммы
- Наследование. Суперклассы и подклассы. Переопределение методов
- Наследование и правила видимости. Зарезервированное слово super
- Статическое и динамическое связывание методов. Полиморфизм
- Базовый класс Object
- Конструкторы. Зарезервированные слова super и this. Блоки инициализации
- Удаление неиспользуемых объектов и метод finalize. Проблема деструкторов для сложно устроенных объектов
- Перегрузка методов
- Правила совместимости ссылочных типов как основа использования полиморфного кода. Приведение и проверка типов
- Рефакторинг
- Reverse engineering – построение uml-диаграмм по разработанным классам
- Краткие итоги по главе 6
- Задания
- Глава 7. Важнейшие объектные типы Массивы
- Коллекции, списки, итераторы
- Работа со строками в Java. Строки как объекты. Классы String, StringBuffer и StringBuilder
- Работа с графикой
- Исключительные ситуации Обработка исключительных ситуаций
- Иерархия исключительных ситуаций
- Объявление типа исключительной ситуации и оператор throw
- Объявление метода, который может возбуждать исключительную ситуацию. Зарезервированное слово throws
- Работа с файлами и папками
- Краткие итоги по главе 7
- Задания
- Глава 8. Наследование: проблемы и альтернативы. Интерфейсы. Композиция Проблемы множественного наследования классов. Интерфейсы
- Отличия интерфейсов от классов. Проблемы наследования интерфейсов
- Пример на использование интерфейсов
- Композиция как альтернатива множественному наследованию
- Краткие итоги по главе 8
- Задания
- Глава 9. Дополнительные элементы объектного программирования на языке Java Потоки выполнения (threads) и синхронизация
- Преимущества и проблемы при работе с потоками выполнения
- Синхронизация по ресурсам и событиям
- Класс Thread и интерфейс Runnable. Создание и запуск потока выполнения
- Поля и методы, заданные в классе Thread
- Подключение внешних библиотек dll.“Родные” (native) методы*
- Краткие итоги по главе 9
- Задания
- Глава 10. Введение в сетевое программирование Краткая справка по языку html
- Апплеты
- Сервлеты
- Технология jsp – Java Server Pages
- Краткие итоги по главе 10
- Задания
- Глава 11. Встроенные классы Виды встроенных классов
- Вложенные (nested) классы и интерфейсы
- Внутренние (inner) классы
- Локальные (local) классы
- Анонимные (anonimous) классы и обработчики событий
- Анонимные (anonimous) классы и слушатели событий (listeners)
- Краткие итоги по главе 11
- Задания
- Глава 12. Компонентное программирование Компонентная архитектура JavaBeans
- Мастер создания компонента в NetBeans
- Пример создания компонента в NetBeans – панель с заголовком
- Добавление в компонент новых свойств
- Добавление в компонент новых событий
- Краткие итоги по главе 12
- Задания
- Литература
- Дополнительная литература
- 276 Курс подготовлен при поддержке Sun Microsystems