Допустим у нас есть 2 класса - предок и потомок. И в обоих определен метод с одинаковой сигнатурой. Мы создаем переменную с типом предка и можем присвоить ей объект созданный из класса предка, либо потомка. Дальше мы вызываем метод у этой переменной и, в зависимости от того на какой объект переменная ссылается в данный момент, это приведет к вызову метода либо у предка, либо у потомка.
Рассмотрим на примере:
Что здесь происходит:
- В методе main мы определяем переменную
Animal animal; - Присваиваем ей объект, созданный из класса Animal
animal = new Animal(); - Вызываем метод say
animal.say();
срабатывает метод определенный в классе Animal - Присваиваем той же переменной новый объект Cat
animal = new Cat(); - Выполняем опять тот же самый код
animal.say();
срабатывает метод определенный в классе Cat
Т.е. полиморфизм здесь, когда вызов одного и того же кода animal.say()приводит к выполнению методов у разных объектов.
Таким образом можно абстрагировать алгоритм работы с целой иерархией классов, убрать его в отдельный метод, который на вход принимает переменную базового класса.
Например, давайте “покормим” питомцев в следующем коде:
В этом примере у нас иерархия из 3-х классов - базовый Animal и два его потомка Cat, Dog.
В методе 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);
Анотация @Override
В примере выше наш полиморфизм основывается на достаточно хрупкой конструкции - контроль над сигнатурами методов остается на стороне программиста. Очень легко представить, что человек может опечататься в названии метода при добавлении очередного потомка или изменит название метода в базовом классе на более “правильное”, но забудет поправить в одном из потомков. Проблема в том, что никакой ошибки при этом не возникнет - программа все так же скомпилируется и успешно запустится, но метод “забытого” потомка не вызовется, вместо этого выполнится метод базового класса.
Пример с опечаткой в потомке:
Здесь мы смоделировали, что опечатались в 2 методах - Cat.sayz, а в базовом Animal.say и Dog.sayNamez, а в базовом Animal.sayName.
При выполнение программы вызовется базовый метод say для кота и sayName для собаки. В консоли кот скажет Метод в классе Animal, а собаку вместо Шарика будут звать Абстрактный зверь.
Задание:
- добавьте пропущенный класс Main ориентируясь на предыдущий пример
- запустите програму, проанализируйте результат
Пример с новым именем с базовом классе:
Здесь мы поменяли метод с sayName на getName в базовом классе Animal, поправили соотвественно метод feedAnimal
но забыли изменить классы Cat и Dog. И наши Барсик и Шарик при выполнении программы превратились в “Абстрактный зверь”.
В этих ситуациях на помощь нам приходит анотация @Override, ориентируясь на которую компилятор проверяет, что метод переопределяется в потомке. Компилятор смотрит сигнатуру метода, где есть анотация и проверяет, что в базовом классе есть метод с точно такой сигнатурой. Если нет, то мы видим ошибку на этапе компиляции.
Задание:
- добавьте пропущенный класс Main, ориентируясь на предыдущий пример
- замените метод feedAnimal на новую версию
- запустите програму, проанализируйте результат
Наш “правильный” пример с питомцами, где мы используем анотацию:
Задание:
- попробуйте добавить опечатку в метод с анотацией и запустить программу
- попробуйте изменить название базового метода и запустить программу
Абстрактные методы
Кроме анотации @Override мы можем воспользоваться механизмом абстрактных методов в базовом классе. Такой метод помечается ключевым словом abstract и состоит только из декларации, без тела метода. Потомки обязаны переопределить такой метод и компилятор отслеживает пропуски и опечатки.
Т.к. абстрактные методы состоят только из декларации, то логично, что мы не сможем создать объект такого типа. Класс содержащий абстрактные методы в свою очередь тоже помечается как абстрактный.
Пример с питомцами, где мы используем абстрактные методы.
Полезные ссылки: