Простой пример интерфейса и его реализации java. Области видимости переменных

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

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

Например, классы 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)

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

Представьте себе будущее, где автомобилями управляют компьютеры без участия человека. Производители автомобилей пишут программное обеспечение (на Java, конечно 🙂), которое управляет машиной — остановиться, поехать, повернуть и т.д. Другие разработчики делают системы, которые принимают данные GPS (Global Positioning System) и используют эти данные для управления автомобилем. Производители автомобилей публикуют интерфейс-стандарт, который описывает методы для управления машиной. Таким образом сторонние разработчики могут знать какие методы вызывать, чтобы заставить автомобиль двигаться, а производители автомобилей могут изменять внутреннюю реализацию своего продукта в любое время. Ни одна из групп разработчиков не знает как написаны их программы.

Интерфейсы Java

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

Public interface OperateCar { // константы, если есть // enum перечисление направлений RIGHT, LEFT // сигнатуры методов int turn(Direction direction, double radius, double startSpeed, double endSpeed); int changeLanes(Direction direction, double startSpeed, double endSpeed); int signalTurn(Direction direction, boolean signalOn); int getRadarFront(double distanceToCar, double speedOfCar); int getRadarRear(double distanceToCar, double speedOfCar); }

Сигнатуры методов объявляются без скобок и заканчиваются точкой с запятой. Для использования интерфейса вы должны написать класс, который будет реализовывать ваш интерфейс. Класс который реализует интерфейс, должен описывать все методы, объявленные в интерфейсе. Например:

Public class OperateBMW760i implements OperateCar { // здесь методы интерфейса OperateCar с описанием // например: int signalTurn(Direction direction, boolean signalOn) { // поворот RIGHT или LEFT поворотник on или off } }

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

Интерфейсы в качестве API

Пример с автомобилями показывает как интерфейсы могут использоваться в качестве API (Application Programming Interface ) или интерфейса программирования приложений. Использование API — обычная практика при разработке коммерческого ПО. Обычно компании-разработчики продают программное обеспечение, содержащее набор методов, которые другие компании хотят использовать в своих продуктах.

Интерфейсы и множественное наследование

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

Объявление интерфейса

Объявление интерфейса содержит ключевое слово interface , имя интерфейса, список родительских интерфейсов через запятую (если есть) и тело интерфейса. Например:

Public interface GroupedInterface extends Interface1, Interface2, Interface3 { // число e double E = 2.718282; // сигнатуры методов void doSomething (int i, double x); int doSomethingElse(String s); }

Модификатор доступа public означает что интерфейс может быть использован любым классом в любом пакете. Если не определить интерфейс как публичный, он будет доступен только в рамках своего пакета. Интерфейс может наследовать другие интерфейсы, как классы могут наследовать другой класс. В отличие от классов, интерфейсы могут наследовать любое количество других интерфейсов.

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

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

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

Рассмотрим интерфейс, который определяет метод для сравнения объектов.

Public interface Relatable { // this (объект, который вызывает isLargerThan) // объект-параметр должен быть того же класса // возвращет 1, 0, -1 // если объект больше, равен или // меньше чем other public int isLargerThan(Relatable other); }

Чтобы иметь возможность сравнивать объекты мы должны реализовать интерфейс Relatable . Любой класс может реализовать интерфейс Relatable , если есть способ сравнить объекты. Для строк сравнивать можно количество символов, для книг — количество страниц, для студентов — вес и т.д. Для плоских геометрических фигур отличной характеристикой будет площадь, для трехмерных — объем. Все эти классы могут реализовать метод isLargerThan() . Если вы знаете, что класс реализует интерфейс Relatable , то вы смело можете сравнивать объекты этого класса.

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

Напишем класс Rectangle , реализующий интерфейс Relatable .

Public class RectanglePlus implements Relatable { public int width = 0; public int height = 0; public Point origin; // четыре конструктора public RectanglePlus() { origin = new Point(0, 0); } public RectanglePlus(Point p) { origin = p; } public RectanglePlus(int w, int h) { origin = new Point(0, 0); width = w; height = h; } public RectanglePlus(Point p, int w, int h) { origin = p; width = w; height = h; } // метод для передвижения прямоугольника public void move(int x, int y) { origin.x = x; origin.y = y; } // вычисление площади public int getArea() { return width * height; } // метод для реализации интерфейса Relatable public int isLargerThan(Relatable other) { RectanglePlus otherRect = (RectanglePlus)other; if (this.getArea() < otherRect.getArea()) return -1; else if (this.getArea() > otherRect.getArea()) return 1; else return 0; } }

Так как класс RectanglePlus реализует Relatable , размеры любых двух объектов типа RectanglePlus можно сравнивать.

Метод isLargerThan в качестве аргумента принимает объекты типа Relatable . При реализации метода в примере выше мы используем приведение типов, потому что компилятор не поймет что, other — объект типа RectanglePlus и вызов метода other.getArea() без приведения типов приведет к ошибке.

Использование интерфейса в качестве типа

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

Рассмотрим пример — метод, который ищет больший объект из двух любых объектов, класс которых реализует интерфейс Relatable:

Public Object findLargest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) > 0) return object1; else return object2; }

Приведением object1 к типу Relatable , мы делаем возможным вызов метода isLargerThan .

Так же для любого класса, реализующего интерфес Relatable, можно реализвать методы:

Public Object findSmallest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) < 0) return object1; else return object2; } public boolean isEqual(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) == 0) return true; else return false; }

Переопределение интерфейсов

Допустим, вы написали интерфейс DoIt:

Public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); }

Предположим, позже, вы захотели добавить в него третий метод:

Public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); boolean didItWork(int i, double x, String s); }

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

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

Public interface DoItPlus extends DoIt { boolean didItWork(int i, double x, String s); }

Теперь пользователи вашего интерфейса смогут перейти к использованию нового интерфейса или остаться со старым без боли.

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

8.2.1. Оператор interface

Определение интерфейса сходно с определением класса, отличие состоит в том, что в интерфейсе отсутствуют объявления данных и конструкторов. Общая форма интерфейса приведена ниже:

interface имя {
тип_результата имя_метода1 (список параметров);
тип имя_ finall _переменной = значение;
}

Обратите внимание - у объявляемых в интерфейсе методов отсутствуют операторы тела. Объявление методов завершается символом «;» (точка с запятой). В интерфейсе можно объявлять и переменные, при этом они неявно объявляются переменными типа final. Это означает, что класс реализации не может изменять их значения. Кроме того, при объявлении переменных в интерфейсе их обязательно нужно инициализировать константными значениями. Ниже приведен пример определения интерфейса, содержащего единственный метод с именем callback и одним параметром типа int.

interface Callback {
void callback(int param);
}

8.2.2. Оператор implements

Оператор implements - это дополнение к определению класса, реализующего некоторый интерфейс.
class имякласса [ extends суперкласс]
[ implements интерфейс0 [, интерфейс 1...]]
{тело класса}

Если в классе реализуется несколько интерфейсов, то их имена разделяются запятыми. Ниже приведен пример класса, в котором реализуется определенный нами интерфейс:

class Client implements Callback {
void callback(int p) {
System.out.println("callback вызван с " + p);
}
}

В очередном примере метод callback интерфейса, определенного ранее, вызывается через переменную-ссылку на интерфейс:

class Testlface {
public static void main(String args)
{

switch(result) {
case NO:
System.out.println("He т ");
break;
case YES:
System.out.println(" Д a");
break;
case MAYBE:
System.out.println("Mo ж e т быть ");
break;
case LATER:
System.out.println(" Позже ");
break;
case SOON:
System.out.priniln("C к opo");
break;
case NEVER:
System.out.println(" Никогда ");
break;
}
}

public static void main(String args) {
Question q = new Question();
answer(q.ask());
answer(q.ask());
answer(q.ask());
answer (q . ask ());
}
}

Обратите внимание на то, что результаты при разных запусках программы отличаются, поскольку в ней используется класс генерации случайных чисел Random пакета java.util.

Позже
Скоро
Нет
Да

Привет! Сегодня поговорим о важном понятии в Java - интерфейсы. Слово тебе наверняка знакомо. Например, интерфейсы есть у большинства компьютерных программ и игр. В широком смысле интерфейс - некий «пульт», который связывает две взаимодействующие друг с другом стороны. Простой пример интерфейса из повседневной жизни - пульт от телевизора. Он связывает два объекта, человека и телевизор, и выполняет разные задачи: прибавить или убавить звук, переключить каналы, включить или выключить телевизор. Одной стороне (человеку) нужно обратиться к интерфейсу (нажать на кнопку пульта), чтобы вторая сторона выполнила действие. Например, чтобы телевизор переключил канал на следующий. При этом пользователю не обязательно знать устройство телевизора и то, как внутри него реализован процесс смены канала. Все, к чему пользователь имеет доступ - это интерфейс . Главная задача - получить нужный результат. Какое это имеет отношение к программированию и Java? Прямое:) Создание интерфейса очень похоже на создание обычного класса, только вместо слова class мы указываем слово interface . Давай посмотрим на простейший Java-интерфейс, и разберемся, как он работает и для чего нужен: public interface Swimmable { public void swim () ; } Мы создали интерфейс Swimmable - «умеющий плавать ». Это что-то вроде нашего пульта, у которого есть одна «кнопка»: метод swim() - «плыть». Как же нам этот «пульт » использовать? Для этого метод, т.е. кнопку нашего пульта, нужно имплементировать . Чтобы использовать интерфейс, его методы должны реализовать какие-то классы нашей программы. Давай придумаем класс, объекты которого подойдут под описание «умеющий плавать». Например, подойдет класс утки - Duck: public class Duck implements Swimmable { public void swim () { System. out. println ("Уточка, плыви!" ) ; } public static void main (String args) { Duck duck = new Duck () ; duck. swim () ; } } Что же мы здесь видим? Класс Duck «связывается» с интерфейсом Swimmable при помощи ключевого слова implements . Если помнишь, мы использовали похожий механизм для связи двух классов в наследовании, только там было слово «extends ». « public class Duck implements Swimmable » можно для понятности перевести дословно: «публичный класс Duck реализует интерфейс Swimmable ». Это значит, что класс, связанный с каким-то интерфейсом, должен реализовать все его методы. Обрати внимание: в нашем классе Duck прямо как в интерфейсе Swimmable есть метод swim() , и внутри него содержится какая-то логика. Это обязательное требование. Если бы мы просто написали « public class Duck implements Swimmable » и не создали бы метод swim() в классе Duck , компилятор выдал бы нам ошибку: Duck is not abstract and does not override abstract method swim() in Swimmable Почему так происходит? Если объяснять ошибку на примере с телевизором, получится, что мы даем человеку в руки пульт с кнопкой «переключить канал» от телевизора, который не умеет переключать каналы. Тут уж нажимай на кнопку сколько влезет, ничего не заработает. Пульт сам по себе не переключает каналы: он только дает сигнал телевизору, внутри которого реализован сложный процесс смены канала. Так и с нашей уткой: она должна уметь плавать, чтобы к ней можно было обратиться с помощью интерфейса Swimmable . Если она этого не умеет, интерфейс Swimmable не свяжет две стороны - человека и программу. Человек не сможет использовать метод swim() , чтобы заставить объект Duck внутри программы плыть. Теперь ты увидел более наглядно, для чего нужны интерфейсы. Интерфейс описывает поведение , которым должны обладать классы, реализующие этот интерфейс. «Поведение» - это совокупность методов. Если мы хотим создать несколько мессенджеров, проще всего сделать это, создав интерфейс Messenger . Что должен уметь любой мессенджер? В упрощенном виде, принимать и отправлять сообщения. public interface Messenger { public void sendMessage () ; public void getMessage () ; } И теперь мы можем просто создавать наши классы-мессенджеры, имплементируя этот интерфейс. Компилятор сам «заставит» нас реализовать их внутри классов. Telegram: public class Telegram implements Messenger { public void sendMessage () { System. out. println ("Отправляем сообщение в Telegram!" "Читаем сообщение в Telegram!" ) ; } } WhatsApp: public class WhatsApp implements Messenger { public void sendMessage () { System. out. println ("Отправляем сообщение в WhatsApp!" ) ; } public void getMessage () { System. out. println ("Читаем сообщение в WhatsApp!" ) ; } } Viber: public class Viber implements Messenger { public void sendMessage () { System. out. println ("Отправляем сообщение в Viber!" ) ; } public void getMessage () { System. out. println ("Читаем сообщение в Viber!" ) ; } } Какие преимущества это дает? Самое главное из них - слабая связанность . Представь, что мы проектируем программу, в которой у нас будут собраны данные клиентов. В классе Client обязательно нужно поле, указывающее, каким именно мессенджером клиент пользуется. Без интерфейсов это выглядело бы странно: public class Client { private WhatsApp whatsApp; private Telegram telegram; private Viber viber; } Мы создали три поля, но у клиента запросто может быть всего один мессенджер. Просто мы не знаем какой. И чтобы не остаться без связи с клиентом, приходится «заталкивать» в класс все возможные варианты. Получается, один или два из них всегда будут null , и они вообще не нужны для работы программы. Вместо этого лучше использовать наш интерфейс: public class Client { private Messenger messenger; } Это и есть пример «слабой связанности»! Вместо того, чтобы указывать конкретный класс мессенджера в классе Client , мы просто упоминаем, что у клиента есть мессенджер. Какой именно - определится в ходе работы программы. Но зачем нам для этого именно интерфейсы? Зачем их вообще добавили в язык? Вопрос хороший и правильный! Того же результата можно добиться с помощью обычного наследования, так ведь? Класс Messenger - родительский, а Viber , Telegram и WhatsApp - наследники. Действительно, можно и так. Но есть одна загвоздка. Как ты уже знаешь, множественного наследования в Java нет . А вот множественная реализация интерфейсов - есть. Класс может реализовывать сколько угодно интерфейсов . Представь, что у нас есть класс Smartphone , у которого есть поле Application - установленное на смартфоне приложение. public class Smartphone { private Application application; } Приложение и мессенджер, конечно, похожи, но все-таки это разные вещи. Мессенджер может быть и мобильным, и десктопным, в то время как Application - это именно мобильное приложение. Так вот, если бы мы использовали наследование, не смогли бы добавить объект Telegram в класс Smartphone . Ведь класс Telegram не может наследоваться одновременно от Application и от Messenger ! А мы уже успели унаследовать его от Messenger , и в таком виде добавить в класс Client . Но вот реализовать оба интерфейса класс Telegram запросто может! Поэтому в классе Client мы сможем внедрить объект Telegram как Messenger , а в класс Smartphone - как Application . Вот как это делается: public class Telegram implements Application , Messenger { //...методы } public class Client { private Messenger messenger; public Client () { this . messenger = new Telegram () ; } } public class Smartphone { private Application application; public Smartphone () { this . application = new Telegram () ; } } Теперь мы используем класс Telegram как захотим. Где-то он будет выступать в роли Application , где-то - в роли Messenger . Наверняка ты уже обратил внимание, что методы в интерфейсах всегда «пустые», то есть они не имеют реализации. Причина этого проста: интерфейс описывает поведение, а не реализует его. «Все объекты классов, имплементирующих интерфейс Swimmable , должны уметь плавать»: вот и все, что говорит нам интерфейс. Как там конкретно будет плавать рыба, утка или лошадь - вопрос к классам Fish , Duck и Horse , а не к интерфейсу. Также как переключение канала - задача телевизора. Пульт просто предоставляет тебе кнопку для этого. Впрочем, в Java8 появилось интересное дополнение - методы по умолчанию (default method). Например, в твоем интерфейсе есть 10 методов. 9 из них реализованы по-разному в разных классах, но один реализован одинаково у всех. Раньше, до выхода Java8, методы внутри интерфейсов вообще не имели реализации: компилятор сразу выдавал ошибку. Теперь же можно сделать вот так: public interface Swimmable { public default void swim () { System. out. println ("Плыви!" ) ; } public void eat () ; public void run () ; } Используя ключевое слово default , мы создали в интерфейсе метод с реализацией по умолчанию. Два других метода, eat() и run() , нам необходимо будет реализовать самим во всех классах, которые будут имплементировать Swimmable . С методом swim() этого делать не нужно: реализация будет во всех классах одинаковой. Кстати, ты уже не раз сталкивался с интерфейсами в прошлых задачах, хоть и не замечал этого сам:) Вот очевидный пример: Ты работал с интерфейсами List и Set ! Точнее, с их реализациями - ArrayList , LinkedList , HashSet и прочими. На этой же схеме видно пример, когда один класс реализует сразу несколько интерфейсов. Например, LinkedList реализует интерфейсы List и Deque (двусторонняя очередь). Ты знаком и с интерфейсом Map , а точнее, с его реализаций - HashMap . Кстати, на этой схеме ты можешь увидеть одну особенность: интерфейсы могут быть унаследованы друг от друга. Интерфейс SortedMap унаследован от Map , а Deque наследуется от очереди Queue . Это нужно, если ты хочешь показать связь интерфейсов между собой, но при этом один интерфейс является расширенной версией другого. Давай рассмотрим пример с интерфейсом Queue - очередь. Мы пока не проходили коллекции Queue , но они достаточно простые и устроены как обычная очередь в магазине. Добавлять элементы можно только в конец очереди, а забирать - только из начала. На определенном этапе разработчикам понадобился расширенный вариант очереди, чтобы добавлять и получать элементы можно было с обеих сторон. Так создали интерфейс Deque - двустороннюю очередь. В нем присутствуют все методы обычной очереди, ведь она является «родителем» двусторонней, но при этом добавлены новые методы.

Чтобы было проще понять интерфейсы в Java, можно их представлять как полностью абстрактные классы, то есть классы все методы которых не имеют реализации, а только лишь объявлены. Но так было до Java 8, в которой появилась возможность создавать в интерфейсе реализацию метода по умолчанию. Интерфейсы так же могут содержать поля, но все они будут объявлены как final и static , то есть по существу являются константами. Кроме того, интерфейсы служат для реализации подобия множественного наследования в Java, которое в чистом виде в Java не поддерживается.

Как уже говорилось, унаследоваться (extends ) в Java можно только от одного класса, каждый класс В или С происходит из неполной семьи, как показано на рисунке а ниже. Все классы происходят только от "Адама", от класса Object. Но часто возникает необходимость породить класс D от двух классов B и С, как показано на рисунке б . Это называется множественным наследованием (multiple inheritance ). В множественном наследовании нет ничего плохого. Трудности возникают, если классы B и C сами порождены от одного класса А, как показано на рисунке в . Это так называемое "ромбовидное" наследование.

В самом деле, пусть в классе А определен метод f(), к которому мы обращаемся из некоторого метода класса D. Можем мы быть уверены, что метод f() выполняет то, что написано в классе А, т. е. это метод A.f()? Может, он переопределен в классах B и С? Если так, то каким вариантом мы пользуемся: В.f() или C.f()? Конечно, допустимо определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другая ситуация.

В различных языках программирования этот вопрос решается по-разному, главным образом уточнением имени метода f(). Но при этом всегда нарушается принцип KISS. Вокруг множественного наследования всегда много споров, есть его ярые приверженцы и столь же ярые противники. Не будем встревать в эти споры, наше дело — наилучшим образом использовать средства языка для решения своих задач.

Создатели языка Java после долгих споров и размышлений поступили радикально — запретили множественное наследование классов вообще. При расширении класса после слова extends можно написать только одно имя суперкласса. С помощью уточнения super можно обратиться только к членам непосредственного суперкласса.

Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей Automobile, от которого можно породить класс грузовиков Truck и класс легковых автомобилей Car. Но вот надо описать пикап Pickup. Этот класс должен наследовать свойства и грузовых, и легковых автомобилей.

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

Интерфейс (interface ), в отличие от класса, содержит только константы, заголовки методов, и с Java 8 может содержать реализацию методов по умолчанию.

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

Описание интерфейса начинается со слова interface , перед которым может стоять модификатор public , означающий, как и для класса, что интерфейс доступен всюду. Если же модификатора public нет, интерфейс будет виден только в своем пакете.

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

Затем в фигурных скобках записываются в любом порядке константы, заголовки методов и реализации методов по умолчанию. Можно сказать, что в интерфейсе все методы абстрактные (те у которых нет реализации по умолчанию), но слово abstract писать не надо. Константы всегда статические, но слова static и final указывать не нужно. Все эти модификаторы принимаются по умолчанию. Все константы и методы в интерфейсах всегда открыты, не обязательно даже указывать модификатор public .

Вот какую схему можно предложить для иерархии автомобилей:

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
interface Pickup extends Car, Truck{ . . . }

Таким образом, интерфейс — это только набросок, эскиз. В нем указано, что делать, но не указано, как это делать (если нет реализации по умолчанию).

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

Как же тогда использовать интерфейсы, если невозможно создать объекты на их основе?

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

Вот как можно реализовать иерархию автомобилей:

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
class Truck implements Automobile{ . . . }
class Pickup extends Truck implements Car{ . . . }

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
class Pickup implements Car, Truck{ . . . }

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

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

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

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

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

Стоит обратить внимание что в классе AnimalSound мы создали массив объектных ссылок типа Soud (интерфейсный тип) и поместили туда ссылки на объекты, реализующие данный интерфейс.

Программа генерирует следующий вывод:

И теперь посмотрим на классы Cow, Cat и Dog:

Как видно наши классы Cow, Cat и Dog которые имплементируют интерфейс Sound имеют каждый свою реализацию метода getSound().

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

Как видно из примера класс MaxMin реализует два интрефейса.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: