Всем привет!
Данный пост написан, скорее, для тех, кто уже видел и даже решил на 12 уровне (2 уровень Java Core) несколько задач на Интерфейсы, но всё еще задаётся вопросом:
"Так зачем же они нужны, если в них все равно нужно реализовывать методы, объявленные в интерфейсе??? То есть, они же не сокращают количество кода!!!".
Не сомневаюсь, что эта тема раскрывается в последующих лекциях, и раскрываются более глубокие смыслы и варианты использования интерфейсов, но если вам совсем невтерпёж, то милости прошу.
Изначально я хотела написать комментарий, но поняла, что это будет ооочень длинный комментарий, поэтому мы здесь.
Это моя первая статья, разумную критику приветствую. Если я где-то не права - можете смело поправлять.
Итак, как мы знаем, при создании своего класса мы можем наследоваться только от одного абстрактного или не абстрактного класса. Но при этом в нашем классе можно реализовывать довольно большое количество интерфейсов.
При наследовании нужно руководствоваться здравым смыслом и в потомки записывать всё же родственные сущности (не будем же мы наследовать котика от бульдозера и наоборот).
С интерфейсами логика немного иная: здесь важны не сами "сущности", а их "умения" или что можно с ними делать.
Например, и котик, и бульдозер могут перемещаться в пространстве. То есть, в принципе, могут реализовать интерфейс CanMove или Moveable.
Или же человек и кот. Они оба могут пить, только делают это по-разному: человек пьёт чай или кофе из чашки, а кот лакает водичку или молоко из миски. Но в целом они оба пьют, так что у каждого из них можно сделать свою реализацию интерфейса CanDrink.
Какая нам от этого польза?
Представьте, что вы делаете игру. Есть у вас, допустим, локация: река, с обеих сторон от нее берега, а дальше лес и горы. На берегу отдыхают разные виды живых существ. Внезапно приближается наводнение. Все, кто может летать - улетают. Кто не может лететь, но может бежать - бегут. Кто-то умеет плавать, так что им в принципе всё равно на это ваше наводнение (ну или они смогут выплыть на берег), хотя, некоторые из них сначала могут попытаться убежать (если умеют). Остальные, как это ни грустно, погибнут.
Попробуем это реализовать.
(Не пугайтесь сразу большого количества кода =) Там бОльшая часть - комментарии)
Какие классы нам могут понадобиться?
Начнем с абстрактного класса разных персонажей или юнитов (извините, не сильна в игровой терминологии, если ошибаюсь - поправьте). Пусть это будет класс Unit. Будем считать, что абсолютно все юниты могут перемещаться в пространстве и издавать звуки.
Абстрактный класс Unit:
// Абстрактный класс для всех юнитов
public static abstract class Unit {
// Базовый метод движения.
// По умолчанию (если не переопределено) будет у всех наследников.
public void move (int x, int y) {
System.out.println("Я ( " + getClassName() + " ) просто брожу по полю на " +
x + " метров вправо и " + y + " метров вперед");
}
// Абстрактный метод, который ДОЛЖЕН ПЕРЕОПРЕДЕЛИТЬ у себя
// КАЖДЫЙ КЛАСС, унаследованный от Unit.
public abstract void makeSound();
// Вспомогательный метод получения имени класса
// без всей лишней информации.
public String getClassName() {
return this.getClass().getSimpleName();
}
}
Какие интерфейсы ("навыки") нужны нашим юнитам? Они могут бегать (CanRun), плавать (CanSwim) или летать (CanFly). Кто-то может обладать несколькими навыками сразу, а у некоторых несчастных может не быть ни одного.
// Интерфейсы. КАЖДЫЙ КЛАСС, "наследующий" какой-то интерфейс,
// ДОЛЖЕН РЕАЛИЗОВАТЬ его у себя.
interface CanRun {
void run(String action);
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
Дальше мы создаем классы-наследники абстрактного класса Unit.
Путь это будет класс Человек (Human):
// Человек НАСЛЕДУЕТ абстрактный класс Unit,
// а также РЕАЛИЗУЕТ интерфейсы CanRun, CanSwim
public static class Human extends Unit implements CanRun, CanSwim {
// Переопределяем метод public void makeSound()
// родительского абстрактного класса Unit
@Override
public void makeSound() {
System.out.print("Йу-хуу! ");
}
// РЕАЛИЗУЕМ метод public void run(String action) интерфейса CanRun
@Override
public void run(String action) {
System.out.println("Я ( " + this.getClassName() + " ) " + action +
" отсюда на двух ногах ");
}
// РЕАЛИЗУЕМ метод public void swim() интерфейса CanSwim
@Override
public void swim() {
System.out.println("Я ( " + this.getClassName() + " ) " +
"с двумя руками и двумя ногами " +
"хорошо плаваю");
}
}
Класс Птица (Bird). Да, я понимаю, что просто птиц не бывает, но для упрощения пусть будет просто птица, не будем делать ее абстрактной:
// Птица НАСЛЕДУЕТ абстрактный класс Unit,
// и РЕАЛИЗУЕТ интерфейс CanFly
public static class Bird extends Unit implements CanFly {
// Переопределяем абстрактный метод public void makeSound()
// родительского абстрактного класса Unit
@Override
public void makeSound() {
System.out.print("Курлык! ");
}
// РЕАЛИЗУЕМ метод public void fly() интерфейса CanFly
@Override
public void fly() {
System.out.println("Я ( " + this.getClassName() + " ) улетел отсюда");
}
}
А теперь создаём еще один абстрактный класс Животные (Animal), который будет унаследован от Unit:
// Абстрактный класс Животных, НАСЛЕДУЕТ абстрактный класс Unit
public static abstract class Animal extends Unit {
// тут могут быть какие-то данные и/или методы
}
И уже его наследники Барашек (Sheep):
// Баран НАСЛЕДУЕТ класс Animal,
// и РЕАЛИЗУЕТ интерфейс CanRun
public static class Sheep extends Animal implements CanRun {
// Переопределяем абстрактный метод public void makeSound()
// родительского абстрактного класса Unit
@Override
public void makeSound() {
System.out.print("Беееее! ");
}
// РЕАЛИЗУЕМ метод public void run(String action) интерфейса CanRun
@Override
public void run(String action) {
System.out.println("Я ( "+ this.getClassName() + " ) " + action +
" отсюда на четырёх копытах");
}
}
и Ленивец (Sloth):
// Ленивец НАСЛЕДУЕТ класс Animal
// и внутри себя ПЕРЕОПРЕДЕЛЯЕТ метод
// void move(int x, int y) абстрактного класса Unit
public static class Sloth extends Animal {
// Переопределяем абстрактный метод public void makeSound()
// родительского абстрактного класса Unit
@Override
public void makeSound() {
System.out.print("Зевает... ");
}
// Переопределяем метод public void move(int x, int y)
// родительского абстрактного класса Unit
@Override
public void move(int x, int y) {
System.out.println("Я ( "+ getClassName() + " ) очень медленный, поэтому " +
"переместился на " + (int)(x/12) + " вправо " +
"и на " + (int)(y/12) + " вперед");
}
}
Метод move(int x, int y) у абстрактного класса Unit не является абстрактным, поэтому мы не обязаны его переопределять, но для Ленивца сделали небольшое замедление.
Теперь переходим к действиям.
public static void main(String[] args) {
// создаём список юнитов
// и добавляем туда представителей разных классов
List<Unit> units = new ArrayList<unit>();
units.add(new Human());
units.add(new Bird());
units.add(new Sheep());
units.add(new Sloth());
// проходим в цикле по всем юнитам и вызываем метод move(int x, int y).
// У всех, кроме ленивца, будет вызван метод базового класса.
// У Ленивца будет вызван его переопределенный метод
for (Unit unit : units) {
// в самом начале ничего не происходит, юниты просто двигаются.
unit.move((int)(Math.random()*50), (int)(Math.random()*50));
}
System.out.println("\n...Наводнение приближается....");
int distanceOfFlood = (int)(Math.random()*1000); // Число от 0 до 1000
System.out.println("...Наводнение на " + distanceOfFlood + " от берега...\n");
// проходим в цикле по всем юнитам
for (Unit unit : units) {
unit.makeSound(); // у каждого юнита свой, переопределенный, звук
// смотрим, кто что "умеет":
// если юнит умеет летать
if(unit instanceof CanFly)
((CanFly) unit).fly(); // проблем нет, улетает
// если юнит не умеет летать, но умеет бегать
else if(unit instanceof CanRun) {
// смотрим на какое расстояние разлилась вода
if(distanceOfFlood < 400) {
// если меньше 400 (условно метров)
((CanRun) unit).run("убежал"); // юнит убежал
}
else {
// если больше 400, юнит безуспешно пытается убежать
((CanRun) unit).run("пытался убежать");
// если юнит может не только бегать, но и плавать
if (unit instanceof CanSwim) {
((CanSwim) unit).swim(); // плывёт
}
// иначе умирает (он хотя бы пытался)
else unitIsDead(unit);
}
}
// если юнит не летает, не бегает, но может плавать
else if (unit instanceof CanSwim)
((CanSwim) unit).swim(); // плывёт
else
// если юнит не умеет совсем ничего - грустненько :(
unitIsDead(unit); // умирает
System.out.println();
}
}
public static void unitIsDead(Unit unit) {
System.out.println("Извините, я ( " + unit.getClassName() + " ) умер");
}
Числовые литералы 12, 50, 400 и 1000 взяты навскидку, можно задать и другие, но логика, надеюсь, понятна.
Итак, как мы можем увидеть, имея абстрактный родительский класс с общими методами, мы можем вообще не задумываться какого конкретно класса тот или иной юнит, а просто вызывать эти методы (makeSound() и move()).
После первого прохода в цикле, когда у всех юнитов вызывается метод move(), на экран будет выведено следующее:
Очевидно, что у всех объектов, кроме ленивца выведено стандартное сообщение из метода move() абстрактного родительского класса Unit, а у ленивца метод move() был переопределен.
Однако, абстрактный класс не поможет нам узнать, что "умеет" тот или иной юнит.
Тут как раз в дело вступают интерфейсы.
С помощью instanceof мы узнаём, может ли этот юнит совершать те или иные действия (поддерживает ли он нужный нам интерфейс), и если да, используем знакомое уже нам приведение типов, например, с помощью ((CanFly) unit).fly() приводим наш объект типа Unit к типу интерфейса CanFly и вызываем у него метод fly().
И не имеет значения какой класс у нашего объекта, важно лишь то, что он в своём "резюме" указал способность летать. Мы ему и говорим: "Ты же умеешь, вот и лети! Нам всё равно как ты это сделаешь".
То есть для нас, как для разработчиков, это значит, что классы, реализующие интерфейс CanFly, могут когда угодно и как угодно менять реализацию метода fly(), могут появляться новые классы, реализующие его или же, наоборот, разработчики могут удалить некоторые из старых классов. Но до тех пор, пока этот метод выполняет заявленные функции, и объект летит, нас это не волнует. Наш код, работающий с объектами, реализующими этот интерфейс, останется прежним, нам ничего не придётся изменять.
После второго цикла, когда все пытаются спастись, в зависимости от масштаба наводнения, на экране мы увидим либо
либо
Без интерфейса нам бы пришлось проверять каждый объект на соответствие какому-то классу (а проверить бы пришлось все) и держать в голове навыки каждого конкретного класса. То есть вот перед нами сейчас барашек и он вроде бы умеет бегать.
А если у вас таких персонажей несколько десятков или сотен разных видов (классов)? А если еще и написали их не Вы, а другой программист, так что вы даже понятия не имеете кто и что там умеет? Это было бы куда сложнее...
И небольшое дополнение уже после публикации:
В реальной жизни над проектом работаете не вы один. Допустим, вы делаете только логику. Все объекты, с которыми вы взаимодействуете, пишут другие программисты. Вы можете даже не знать все классы, с которыми работает ваш код. Вам нужно от них только то, чтобы они выполняли то, что вам надо. При этом все они это могут делать совершенно по-разному.
Но вы, допустим, в своём коде делаете метод, который работает только с объектами классов, поддерживающих определённый интерфейс
void doSomething(CanFly f)
то есть параметром методa устанавливаете интерфейс. Не конкретный класс, а интерфейс. И все другие программисты, вызывая у себя этот ваш метод void doSomething(CanFly ) аргументами должны передать либо явно объект класса, реализующего CanFly, либо какой-то объект какого-то класса, который может быть к нему приведен:
Object obj = new Bird();
doSomething(obj); // ошибка компиляции Object cannot be converted to CanFly
doSomething((CanFly) obj); // нет ошибки, потому что obj у нас класса Bird и реализует CanFly
Bird b = new Bird();
doSomething(b); // нет ошибки
Human h = new Human() ;
doSomething(h); // ошибка компиляции
doSomething((CanFly) h); // ошибка времени выполнения ClassCastException
Вот так интерфейсы и могут быть полезны.
И это далеко не все их возможности и способы применения. Дальше по курсу, наверняка, узнаем больше =)
Спасибо, что дочитали до конца =)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
возрастууровню. некоторые и на 20х плохо их понимают. ну и тут написано общее описание же. как раз для тех уровней. без учета использования интерфейсов для аннотаций, паттернов и тд, так что кто хочет, тот почитает. не поймёт с первого раза, почитает ещё раз. когда человек покупает книгу, того же Шилдта, там тоже можно в конец книги заглянуть и ужаснуться от конструкций, но человек читает по мере понимания. а хорошая статья в закладке браузера ещё никому не помешала.