Содержание | Рекомендации по изучению | Задания

Занятие 1 | Занятие 2 | Занятие 3 | Занятие 4 | Занятие 6 | Занятие 7 | Занятие 8 | Занятие 9 | Занятие 10 | Занятие 11 | Занятие 12 | Занятие 13

Занятие 4

Интерфейсы

Абстрактные классы

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

Например, классы Point (точка), Circle (круг) и Rectangle (прямоугольник) унаследованы от класса Figure (фигура). В классе Figure есть метод paint(), общий для всех подклассов — он нужен, чтобы нарисовать фигуру. Но между рисованием круга, точки и прямоугольника нет ничего общего, поэтому бессмысленно программировать этот метод в классе Figure.

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

Класс, в котором есть хотя бы один абстрактный метод, называется абстрактным классом и перед словом class должно также стоять слово abstract.

Создать объект абстрактного класса нельзя. Можно только унаследовать от этого класса другие классы, переопределить в них абстрактные методы (наполнив их конкретным содержимым) и создавать объекты уже этих классов.*

Множественное наследование

о множественном наследовании

Множественным наследованием называется ситуация, когда класс наследует от двух или более классов.

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

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

Например, в классе Clock есть метод ring(), который вызывается, когда срабатывает таймер будильника. Но в классе Phone тоже есть метод ring(), который вызывается, когда кто-то звонит по телефону и надо оповестить об этом владельца. Когда класс Cellular наследует от классов Clock и Phone, он получает метод ring(). Но какой из его вариантов?*

У этой ситуации нет безупречного решения. Поэтому многие языки программирования запрещают множественное наследование.

Java множественное наследование не поддерживает.

Заметим, однако, что если метод ring() хотя бы в одном из классов Clock и Phone является абстрактным, то конфликта возникнуть не может. Абстрактный метод не имеет реализации, а следовательно «побеждает» тот метод, который абстрактным не является. Если же метод является абстрактным в обоих классах, он останется таким же и в их потомке.

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

Понятие интерфейса в Java. Описание интерфейса

Интерфейс представляет собой класс, в котором все поля — константы (т.е. статические — static и неизменяемые — final), а все методы абстрактные.

При описании интерфейса вместо ключевого слова class используется ключевое слово interface, после которого указывается имя интерфейса, а затем, в фигурных скобках список полей-констант и методов. Никаких модификаторов перед объявлением полей и методов ставить не надо: все поля автоматически становятся public static final, а методы — public abstract. Методы не могут иметь реализации, т.е. после закрывающей круглой скобки сразу ставится точка с запятой.

Опишем, например, интерфейс для объекта, который «умеет» сообщать информацию о себе в формате прайс-листа (т.е. сообщать свое название, цену, и краткое описание).

interface PriceItem { String getTitle(); int getPrice(int count); String getDescription(); }

Для разнообразия метод getPrice() в этом примере требует один целочисленный параметр (количество единиц товара).

Такой интерфейс полезен для программы типа Интернет-магазин, которая должна по запросу пользователя формировать прайс — перечень товаров с указанием их цены и описания.

Реализация интерфейса

Класс может реализовывать интерфейс, т.е. перенимать его поведение. Эта процедура аналогична наследованию, только вместо ключевого слова extends используется ключевое слово implements. Но если после extends может быть указан только один класс, то после implements можно перечислить через запятую произвольное количество интерфейсов. Класс может одновременно наследовать от другого класса и реализовывать интерфейсы.

Мы можем взять любой существующий в программе класс и «научить» его сообщать о себе все необходимые сведения. Например, класс Dog, с которым мы работали на прошлом занятии. Можно изменить этот класс, заставив его реализовывать наш интерфейс, а можно оставить в неприкосновенности* и создать на его базе новый класс. В новом классе обязательно требуется переопределить абстрактные методы интерфейса.

class Dog2 extends Dog implements PriceItem { private int price; String getTitle() { return ("Умная собака"); }; int getPrice(int count) { return price * count; }; int setPrice(int p) { price = p; } String getDescription() { return ("Умная собака, которая знает свой возраст и умеет сообщать его с помощью лая"); }; }

Класс Dog2 «умеет» то же самое, что и старый класс Dog, но помимо этого его можно использовать в программе Интернет-магазина для формирования прайса. Обратите внимание, класс Dog ничего не знал о цене, поэтому понадобилось добавить метод setPrice() и поле price, чтобы эту цену можно было бы изменять. А изменять описание собаки не понадобится, поэтому метод getDescription() просто выводит одну и ту же сроку.

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

Переменные интерфейсного типа

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

PriceItem pi; // переменная интерфейсного типа Dog2 dog = new Dog2(); // создается объект класса Dog2, на него ссылается переменная dog dog.voice(); // можно вызвать метод лая System.out.println(dog.getTitle()); // можно вывести название товара Dog oldDog = dog; // переменная oldDog ссылается на тот же самый объект oldDog.voice(); // можно работать с объектом нового класса по-старому pi = dog; // переменная pi рассматривает тот же самый объект как товар для прайса pi.voice(); // НЕ ПОЛУЧИТСЯ. Этого метода нет в интерфейсе PriceItem*

Мы можем поместить собак, велосипеды и компьютеры в один массив goods (товары) и в цикле сформировать прайс:

PriceItem[] goods; ... // создание и заполнение массива элементами, // поддерживающими интерфейс PriceItem for (int i = 0; i < googs.length; i++) { System.out.println("Название: " + goods[i].getTitle() + ", цена за единицу товара: " + goods[i].getPrice(1) + ", описание: " + goods[i].getDescription() + "."); }
об использовании интерфейсов

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

Приемы программирования: пример применения интерфейсов

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

Один объект может «сообщить» что-то другому объекту, вызвав его метод. Пусть информацию о времени обрабатывает метод sayTime(int hours, int minutes). Для того, чтобы вызвать этот метод у какого-то объекта, надо быть уверенным, что такой метод описан в классе этого объекта. Можно определить интерфейс, скажем TimeListener, и реализовать его во всех классах, которым нужно следить за временем, не вмешиваясь в основную иерархию этих классов. И тогда у нас может быть разновидность умной собаки, которая лает ровно в полночь и разновидность кнопки, которая может автоматически срабатывать через заданный промежуток времени.

Класс, следящий за временем будет иметь внутренний список объектов типа TimeListener, методы для добавления и удаления объектов в этот список (те объекты, которые «захотят» следить за временем, вызовут этот метод) и каждую минуту объект этого класса (достаточно одного такого объекта на всю программу) будет вызывать метод sayTime(int hours, int minutes) для каждого из объектов в этом списке.

Пакеты и области видимости

Пакеты

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

Каждый пакет имеет имя. Имя представляет собой обычный идентификатор Java. Особенность заключается в том, что это имя одновременно является названием папки, в которой хранятся файлы классов, входящие в пакет. А точка в имени преобразуется в разделитель имен файловой системы. То есть пакет с именем java.util будет представлен папкой util, находящейся внутри папки java.

В папке хранятся файлы с расширением .java, содержащие описания классов, входящих в пакет. В начале такого файла должен стоять оператор package, после которого записывается имя пакета.

Импортирование пакетов

Полное имя класса состоит из идентификатора, указанного после ключевого слова class и предшествующего ему имени пакета, в котором этот класс находится. Классы ClassA и ClassB, описанные в пакете package1, имеют полные имена package1.ClassA и package1.ClassB.

Классы, находящиеся внутри одного пакета могут пользоваться сокращенными именами друг друга (что мы до сих пор всегда и делали). В одном из методов класса ClassA можно определить переменную класса ClassB, создать объект класса ClassB и вызвать его метод (например, f()) командами:

ClassB varb = new ClassB(); varb.f();

вместо команд:

package1.ClassB varb = new package1.ClassB(); varb.f();

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

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

Для импортирования класса используется ключевое слово import, после которого указывается его полное имя. Например, можно импортировать класс Vector из пакета java.util:

import java.util.Vector;

Теперь можно пользоваться именем Vector вместо java.util.Vector.

о рекомендациях по именованию пакетов

Чтобы импортировать пакет полностью (то есть, ко всем классам пакета можно будет обращаться с помощью сокращенного имени), после имени пакета ставится точка и звездочка. Так, команда

import java.util.*;

импортирует все файлы из пакета java.util. Но таким способом пользоваться не рекомендуется, так как при этом из разных пакетов могут импортироваться файлы с одинаковыми именами.*

Eclipse позволяет облегчить жизнь разработчику. Если в программе используется класс с неизвестным именем, на полях редактора кода появляется значок предупреждения об ошибке. Щелчок по этому значку выводит варианты решения проблемы. Например, создать новый класс. Или импортировать существующий (при этом выводится список всех доступных пакетов, содержащих класс с таким именем). Если выбрать вариант "Import" соответствующая директива import будет автоматически добавлена в начало пакета.

Все классы, находящиеся в одном пакете, должны иметь разные имена, иначе возникнет ошибка. К классам, которые находятся в разных пакетах, такого требования предъявить, очевидно, невозможно (поскольку пакеты могут разрабатываться совершенно разными людьми).

Поэтому при импортировании нескольких пакетов, в которых содержатся классы с одинаковыми именами, может возникнуть путаница в этих именах. Однако Java справляется с этой проблемой, «отдавая» сокращенное имя первому из импортированных классов. К другому классу можно обратиться с помощью его полного имени.

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

Рекомендуется для названия пакета использовать адрес сайта фирмы-разработчика. Адреса сайтов есть практически у всех серьезных разработчиков программ (и, что самое главное, адреса сайтов не могут совпадать). Адрес сайта рекомендуется записывать наоборот. То есть, если адрес — sun.com, то имя пакета должно начинаться с com.sun. Кстати, таких пакетов довольно много в вашей системе, их поставляет фирма Sun Microsystems, разработчик языка Java.

Файловая структура Java-проекта

Итак, Java-проект может состоять из нескольких пакетов. Каждому пакету в файловой структуре операционной системы соответствует одна папка.

В пакете могут содержаться классы и интерфейсы. Они хранятся в файлах с расширением .java.

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

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

Eclipse создает новый файл с расширением .java автоматически, если выполнить команду New --> Class или New --> Interface.

Файл с расширением .java — это обычный текстовый файл. Его можно открывать и редактировать как с помощью Eclipse, так и в любом другом текстовом редакторе (даже в Блокноте).

Для каждого класса (открытого или закрытого) Java создает файл с расширением .class. Это двоичный файл, в котором хранятся команды на внутреннем языке Java. Эти файлы недоступны для редактирования в Eclipse (если попытаться их открыть, Eclipse на самом деле откроет соответствующий .java-файл). Чтобы они не мешали, их можно скрыть с помощью фильтра. Для этого в представлении Navigator нажмите маленькую треугольную кнопку справа (menu) и выберите команду Filters... В открывшемся окне поставьте галочку напротив расширения .class, чтобы скрыть из панели Navigator соответствующие файлы.

Области видимости классов

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

об объектах "невидимых" классов

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

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

Вложенные классы, объявленные без модификатора public, видны только в методах содержащего их класса. Классы, описанные в методах, видны только в пределах этих методов.

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

Области видимости членов класса

Члены класса (методы и атрибуты), объявленные как public, видны везде, где виден сам класс.

Члены класса, объявленные как protected видны в самом классе и его потомках.

Члены класса, объявленные как private, видны только в пределах класса.

Если к члену класса не применяется ни один из модификаторов public, private, protected, он виден в пределах текущего пакета.

"Видимость" поля означает, что его можно использовать в выражениях, передавать в качестве аргумента в методы, изменять его значение с помощью присваивания.

"Видимость" метода означает возможность его вызова.

Для обращения к полям и методам класса используется полное имя, состоящее из имени объекта соответствующего класса и собственно имени атрибута или метода. В теле метода того же класса имя объекта можно опускать (если подразумевается this — объект, для которого вызван данный метод).

Области видимости переменных

Переменные, объявленные в теле метода, видны от места объявления до конца блока, в котором это объявление находится. Границы блока задаются фигурными скобками {}. Поэтому в следующем примере:

{ int x = 0; } { int x = 2; }

используются две разные переменные x (первая переменная, равная 0, перестает существовать за границами своего блока).

Переменные, являющимися параметрами метода, видны во всем теле метода.

Перечисленные переменные называются локальными переменными. Двух локальных переменных с одинаковыми именами и перекрывающимися областями видимости быть не должно. Нельзя, например, объявить две переменных следующим образом:

int x = 0; { int x = 2; }

Нельзя, в частности, объявлять в теле метода переменную, совпадающую (по имени) с одним из параметров метода.

Конфликты имен

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

Конфликт имен возникает, когда импортируются два пакета, содержащие классы с одинаковыми именами.

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

Java "просматривает" имена классов в следующем порядке. Сначала — классы, импортированные поодиночке. Потом — классы, определенные в данном пакете. В последнюю очередь классы из пакетов, импортируемых полностью в порядке следования команд import.

Конфликт имен возникает так же, когда имя одного из параметров метода совпадает с именем одного из атрибутов этого же класса. Такая ситуация возникает довольно часто, поскольку для метода, изменяющего значение этого атрибута, такое называние параметра довольно наглядно. Например:

class Dog { int age; ... public void setAge(int age) { ... }; ... }

Такой заголовок метода setAge(int age) лучше, чем использовавшийся нами на прошлом занятии setAge(int a), поскольку сразу позволяет судить о назначении параметра. Однако возникает вопрос: к чему будет относиться имя age в теле этого метода — к атрибуту или к параметру.

Ответ: к параметру. Имя параметра «перекрывает» имя атрибута.

Для того, чтобы обратиться к атрибуту, следует использовать полное имя (т.е. указатель на объект this).

Реализация метода должна выглядеть следующим образом:

public void setAge(int age) { this.age = age; // проверку диапазона параметра в этом примере проигнорируем };
Дополнительная литература

1. Вязовик Н.А. Программирование на Java. (глава 8)

2. Хабибуллин И.Ш. Самоучитель Java 2. (глава 3)

Содержание | Рекомендации по изучению | Задания

Занятие 1 | Занятие 2 | Занятие 3 | Занятие 4 | Занятие 6 | Занятие 7 | Занятие 8 | Занятие 9 | Занятие 10 | Занятие 11 | Занятие 12 | Занятие 13