Найти тему
Изучаем Java

Java12: полиморфизм

Оглавление

Допустим у нас есть 2 класса - предок и потомок. И в обоих определен метод с одинаковой сигнатурой. Мы создаем переменную с типом предка и можем присвоить ей объект созданный из класса предка, либо потомка. Дальше мы вызываем метод у этой переменной и, в зависимости от того на какой объект переменная ссылается в данный момент, это приведет к вызову метода либо у предка, либо у потомка.

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

Что здесь происходит:

  • В методе main мы определяем переменную
    Animal animal;
  • Присваиваем ей объект, созданный из класса Animal
    animal = new Animal();
  • Вызываем метод say
    animal.say();
    срабатывает метод определенный в классе Animal
  • Присваиваем той же переменной новый объект Cat
    animal = new Cat();
  • Выполняем опять тот же самый код
    animal.say();
    срабатывает метод определенный в классе Cat

Т.е. полиморфизм здесь, когда вызов одного и того же кода animal.say()приводит к выполнению методов у разных объектов.

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

Например, давайте “покормим” питомцев в следующем коде:

-2

(ссылка на исходник)

В этом примере у нас иерархия из 3-х классов - базовый Animal и два его потомка Cat, Dog.

-3

В методе feedAnimal у нас единый алгоритм, который используется и для Cat и для Dog:

  • Отделяем начало действий
    System.out.println("---------------");
  • Запрашиваем имя питомца, чтобы подозвать
    System.out.printf("Подзываем питомца: - %1$s, %1$s !\n", animal.sayName());
  • Питомец подбегает
    animal.run();
  • Опять запрашиваем имя, чтобы обратиться к питомцу
    System.out.printf("Хочешь вкусненького, %s ?\n", animal.sayName());
  • Питомец выражает одобрение идее
    animal.say();
  • Насыпаем корма
    System.out.printf("Насыпаем корма для %s\n", animal.sayName());
  • Питомец ест
    animal.eat();

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

Что происходит в методе main в этом примере:

  • Мы набираем массив объектов, с которыми собираемся работать
    Animal[] animals = new Animal[]{new Cat(), new Dog()};
    Обратите внимание, что массив объявлен для объектов базового класса и мы можем складировать туда не только базовый Animal, но и любых потомков.
  • Дальше итерируем по массиву и вызываем для каждого объекта feedAnimal(animal);
-4

Анотация @Override

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

Пример с опечаткой в потомке:

-5

(ссылка на исходник)

Здесь мы смоделировали, что опечатались в 2 методах - Cat.sayz, а в базовом Animal.say и Dog.sayNamez, а в базовом Animal.sayName.

При выполнение программы вызовется базовый метод say для кота и sayName для собаки. В консоли кот скажет Метод в классе Animal, а собаку вместо Шарика будут звать Абстрактный зверь.

Задание:

  • добавьте пропущенный класс Main ориентируясь на предыдущий пример
  • запустите програму, проанализируйте результат

Пример с новым именем с базовом классе:

-6

(ссылка на исходник)

Здесь мы поменяли метод с sayName на getName в базовом классе Animal, поправили соотвественно метод feedAnimal

-7

но забыли изменить классы Cat и Dog. И наши Барсик и Шарик при выполнении программы превратились в “Абстрактный зверь”.

В этих ситуациях на помощь нам приходит анотация @Override, ориентируясь на которую компилятор проверяет, что метод переопределяется в потомке. Компилятор смотрит сигнатуру метода, где есть анотация и проверяет, что в базовом классе есть метод с точно такой сигнатурой. Если нет, то мы видим ошибку на этапе компиляции.

Задание:

  • добавьте пропущенный класс Main, ориентируясь на предыдущий пример
  • замените метод feedAnimal на новую версию
  • запустите програму, проанализируйте результат

Наш “правильный” пример с питомцами, где мы используем анотацию:

-8

(ссылка на исходник)

Задание:

  • попробуйте добавить опечатку в метод с анотацией и запустить программу
  • попробуйте изменить название базового метода и запустить программу

Абстрактные методы

Кроме анотации @Override мы можем воспользоваться механизмом абстрактных методов в базовом классе. Такой метод помечается ключевым словом abstract и состоит только из декларации, без тела метода. Потомки обязаны переопределить такой метод и компилятор отслеживает пропуски и опечатки.

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

Пример с питомцами, где мы используем абстрактные методы.

-9

(ссылка на исходник)

Полезные ссылки: