Java Challengers #3: Полиморфизм и наследование


Мы продолжаем перевод серии статей с задачками по Java. Прошлый пост про строки вызвал на удивление бурную дискуссию. Надеемся, что мимо этой статьи вы тоже не пройдете мимо. И да — мы приглашаем теперь на юбилейный десятый поток нашего курса "Разработчик Java".


Согласно легендарному Венкату Субраманиам (Venkat Subramaniam) полиморфизм является самым важным понятием в объектно — ориентированном программировании. Полиморфизм — или способность объекта выполнять специализированные действия на основе его типа — это то, что делает Java — код гибким. Шаблоны проектирования, такие как Команда (Command), Наблюдатель (Observer), Декоратор (Decorator), Стратегия (Strategy), и многие другие, созданные бандой четырех (Gang Of Four), все используют ту или иную форму полиморфизма. Освоение этой концепции значительно улучшит вашу способность продумывать программные решения.



Вы можете взять исходный код для этой статьи и поэксперементировать здесь: https://github.com/rafadelnero/javaworld-challengers


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


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


public abstract class JavaMascot {
  public abstract void executeAction();
}

public class Duke extends JavaMascot {
  @Override
  public void executeAction() {
    System.out.println("Punch!");
  }
}

public class Juggy extends JavaMascot {
  @Override
  public void executeAction() {
     System.out.println("Fly!");
  }
}

public class JavaMascotTest {
  public static void main(String... args) {
    JavaMascot dukeMascot = new Duke();
    JavaMascot juggyMascot = new Juggy();
    dukeMascot.executeAction();
    juggyMascot.executeAction();
  }
}

Вывод этого кода будет таким:


Punch!
Fly!

Так как определены конкретные реализации, то будут вызваны методы и Duke и Juggy.


Перегрузка (overloading) метода — это полиморфизм? Многие программисты путают отношение полиморфизма с переопределением методов (overriding) и перегрузкой методов (overloading). Фактически, только переопределение метода — это истинный полиморфизм. Перегрузка использует то же имя метода, но разные параметры. Полиморфизм — это широкий термин, поэтому всегда будут дискуссии на эту тему.


Какова цель полиморфизма


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


Чтобы лучше понять цель полиморфизма, взгляните на SweetCreator:


public abstract class SweetProducer {
  public abstract void produceSweet();
}

public class CakeProducer extends SweetProducer {
  @Override
  public void produceSweet() {
    System.out.println("Cake produced");
  }
}
public class ChocolateProducer extends SweetProducer {
  @Override
  public void produceSweet() {
    System.out.println("Chocolate produced");
  }
}
public class CookieProducer extends SweetProducer {
  @Override
  public void produceSweet() {
    System.out.println("Cookie produced");
  }
}

public class SweetCreator {
  private List<SweetProducer> sweetProducer;

  public SweetCreator(List<SweetProducer> sweetProducer) {
    this.sweetProducer = sweetProducer;
  }

  public void createSweets() {
    sweetProducer.forEach(sweet -> sweet.produceSweet());
  }
}

public class SweetCreatorTest {
  public static void main(String... args) {
    SweetCreator sweetCreator = new SweetCreator(Arrays.asList(
      new CakeProducer(),
      new ChocolateProducer(), 
      new CookieProducer()));

     sweetCreator.createSweets();
  }
}

В этом примере вы можете видеть, что класс SweetCreator знает только о классе SweetProducer. Он не знает реализации каждого Sweet. Такое разделение дает нам гибкость для обновления и повторного использования наших классов, а это делает код намного проще в сопровождении. При проектировании кода всегда ищите способы сделать его максимально гибким и удобным. Полиморфизм — это очень мощный способ для использования в этих целях.


Аннотация @Override обязывает программиста использовать такую же сигнатуру метода, которая должна быть переопределена. Если метод не переопределен, будет ошибка компиляции.

Ковариантные возвращаемые типы при переопределении метода


Можно изменить тип возвращаемого значения переопределенного метода если это ковариантный тип. Ковариантный тип в основном является подклассом возвращаемого значения.


Рассмотрим пример:


public abstract class JavaMascot {
  abstract JavaMascot getMascot();
}

public class Duke extends JavaMascot {
  @Override
  Duke getMascot() {
    return new Duke();
  }
}

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


Полиморфизм в базовых классах Java


Мы постоянно используем полиморфизм в базовых классах Java. Один очень простой пример — создание экземпляра класса ArrayList с объявлением типа как интерфейс List.


List<String> list = new ArrayList<>();

Рассмотрим пример кода, использующий Java Collections API без полиморфизма:


public class ListActionWithoutPolymorphism {
  // Пример без полиморфизма 
  void executeVectorActions(Vector<Object> vector) 
  {/* Здесь повтор кода */}

  void executeArrayListActions(ArrayList<Object> arrayList) 
  {/* Здесь повтор кода */}

  void executeLinkedListActions(LinkedList<Object> linkedList) 
  {/* Здесь повтор кода */}

  void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList)
  { /* Здесь повтор кода */}
}

public class ListActionInvokerWithoutPolymorphism {
  listAction.executeVectorActions(new Vector<>());
  listAction.executeArrayListActions(new ArrayList<>());
  listAction.executeLinkedListActions(new LinkedList<>());
  listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

Отвратительный код, не так ли? Представьте себе, что вам нужно его сопровождать! Теперь рассмотрим тот же пример с полиморфизмом:


public static void main(String... polymorphism) {
ListAction listAction = new ListAction();   
  listAction.executeListActions();
}
public class ListAction {
  void executeListActions(List<Object> list) {
    // Выполнение действий с различными списками
  }
}
public class ListActionInvoker {
  public static void main(String... masterPolymorphism) {
    ListAction listAction = new ListAction();
    listAction.executeListActions(new Vector<>());
    listAction.executeListActions(new ArrayList<>());
    listAction.executeListActions(new LinkedList<>());
    listAction.executeListActions(new CopyOnWriteArrayList<>());
  }
}

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


Вызов конкретных методов для полиморфного метода


Можно вызвать конкретные методы при полиморфном вызове метода, это происходит за счет гибкости. Вот пример:


public abstract class MetalGearCharacter {
    abstract void useWeapon(String weapon);
}
public class BigBoss extends MetalGearCharacter {
    @Override
    void useWeapon(String weapon) {
        System.out.println("Big Boss is using a " + weapon);
    }
   void giveOrderToTheArmy(String orderMessage) {
        System.out.println(orderMessage);
    }
}
public class SolidSnake extends MetalGearCharacter {
    void useWeapon(String weapon) {
        System.out.println("Solid Snake is using a " + weapon);
    }
}
public class UseSpecificMethod {
    public static void executeActionWith(MetalGearCharacter metalGearCharacter) {
        metalGearCharacter.useWeapon("SOCOM");
        // Следующая строка не будет работать
        // metalGearCharacter.giveOrderToTheArmy("Attack!");
        if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
public static void main(String... specificPolymorphismInvocation) {
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

Техника, которую мы используем здесь — это приведение типов (casting) или сознательное изменение типа объекта во время выполнения.


Обратите внимание, что вызов определенного метода возможен только при приведении более общего типа к более специфичному типу. Хорошей аналогией было бы сказать явно компилятору: "Эй, я знаю, что я здесь делаю, поэтому я собираюсь привести объект к определенному типу и буду использовать этот метод."


Ссылаясь на приведенный выше пример, у компилятора есть веская причина не принимать вызов определенных методов: класс, который передаётся должен быть SolidSnake. В этом случае, у компилятора нет никакого способа гарантировать, что каждый подкласс MetalGearCharacter имеет метод giveOrderToTheArmy.


Ключевое слово instanceof


Обратите внимание на зарезервированное слово instanceof. Перед вызовом конкретного метода мы спросили, является ли MetalGearCharacter экземпляром (instanceof) BigBoss. Если это не экземпляр BigBoss, мы получим следующее исключение:


Exception in thread `main" java.lang.ClassCastException: 
com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot 
be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss

Ключевое слово super


Что делать, если мы хотим сослаться на атрибут или метод из родительского класса? В этом случае мы можем использовать ключевое слово super.
Например:


public class JavaMascot {
  void executeAction() {
    System.out.println("The Java Mascot is about to execute an action!");
  }
}
public class Duke extends JavaMascot {
  @Override
  void executeAction() {
    super.executeAction();
    System.out.println("Duke is going to punch!");
  }
  public static void main(String... superReservedWord) {
    new Duke().executeAction();
  }
}

Использование зарезервированного слова super в методе executeAction класса Duke вызывает метод родительского класса. Затем мы выполняем конкретное действие из класса Duke. Вот почему мы можем видеть оба сообщения в выводе:


The Java Mascot is about to execute an action!
Duke is going to punch!

Решите задачку по полиморфизму


Давайте проверим, что вы узнали о полиморфизме и наследовании.


В этой задачке Вам дается несколько методов от Matt Groening’s The Simpsons, от вавам требуется разгадать, какой будет вывод для каждого класса. Для начала внимательно проанализируйте следующий код:


public class PolymorphismChallenge {
    static abstract class Simpson {
        void talk() {
            System.out.println("Simpson!");
        }
        protected void prank(String prank) {
            System.out.println(prank);
        }
    }

    static class Bart extends Simpson {
        String prank;
        Bart(String prank) { this.prank = prank; }
        protected void talk() {
            System.out.println("Eat my shorts!");
        }
        protected void prank() {
            super.prank(prank);
            System.out.println("Knock Homer down");
        }
    }

    static class Lisa extends Simpson {
        void talk(String toMe) {
            System.out.println("I love Sax!");
        }
    }

    public static void main(String... doYourBest) {
        new Lisa().talk("Sax :)");
        Simpson simpson = new Bart("D'oh");
        simpson.talk();
        Lisa lisa = new Lisa();
        lisa.talk();
        ((Bart) simpson).prank();
    }
}

Как вы думаете? Каким будет результат? Не используйте IDE, чтобы выяснить это! Цель в том, чтобы улучшить ваши навыки анализа кода, поэтому постарайтесь решить самостоятельно.


Выберите ваш ответ (правильный ответ вы сможете найти в конце статьи).


A)
I love Sax!
D'oh
Simpson!
D'oh


B)
Sax :)
Eat my shorts!
I love Sax!
D'oh
Knock Homer down


C)
Sax :)
D'oh
Simpson!
Knock Homer down


D)
I love Sax!
Eat my shorts!
Simpson!
D'oh
Knock Homer down


Что случилось? Понимание полиморфизма


Для следующего вызова метода:


new Lisa().talk("Sax :)");

вывод будет "I love Sax!". Это потому, что мы передаём строку в метод и у класса Lisa есть такой метод.


Для следующего вызова:


Simpson simpson = new Bart("D'oh");
simpson.talk();

Вывод будет "Eat my shorts!". Это потому, что мы инициализируем тип Simpson с помощью Bart.


Теперь смотрите, это немного сложнее:


Lisa lisa = new Lisa();
lisa.talk();

Здесь мы используем перегрузку метода с наследованием. Мы ничего не передаем методу talk, поэтому вызывается метод talk из Simpson.


В этом случае на выходе будет "Simpson!".


Вот еще один:


((Bart) simpson).prank();

В этом случае строка prank была передана при создании экземпляра класса Bart через new Bart("D'oh");. В этом случае сначала вызывается метод super.prank(), а затем метод prank() из класса Bart. Вывод будет:


"D'oh"
"Knock Homer down"

Распространенные ошибки с полиморфизмом


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


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


Также помните, что переопределение метода не является перегрузкой метода.


Невозможно переопределить метод, если параметры отличаются. Можно изменить тип возвращаемого значения переопределенного метода, если возвращаемый тип является подклассом.


Что нужно помнить о полиморфизме


  • Созданный экземпляр определяет, какой метод будет вызван при использовании полиморфизма.


  • Аннотация @Override обязывает программиста использовать переопределенный метод; в противном случае возникнет ошибка компилятора.


  • Полиморфизм может использоваться с обычными классами, абстрактными классами и интерфейсами.


  • Большинство шаблонов проектирования зависят от той или иной формы полиморфизма.


  • Единственный способ вызвать нужный ваш метод в полиморфном подклассе — это использовать приведение типов.


  • Можно создать мощную структуру кода, используя полиморфизм.


  • Экспериментируйте. Через это, вы сможете овладеть этой мощной концепцией!



Ответ


Ответ — D.


Вывод будет:


I love Sax!
Eat my shorts! 
Simpson!
D'oh
Knock Homer down

Как всегда приветствую ваши комментарии и вопросы. И ждём у Виталия на открытом уроке.

Комментарии (0)