undefined

Полиморфизм и переопределение

Java Core
2 уровень , 1 лекция
Открыта

— Амиго, ты любишь китов?

— Китов? Не, не слышал.

— Этот как корова, только больше и плавает. Кстати, киты произошли от коров. Ну, или имели общего с ними предка. Не столь важно.

Полиморфизм и переопределение - 1

— Так вот. Хочу рассказать тебе об еще одном очень мощном инструменте ООП – это полиморфизм. У него есть четыре особенности.

1) Переопределение метода.

Представь, что ты для игры написал класс «Корова». В нем есть много полей и методов. Объекты этого класса могут делать разные вещи: идти, есть, спать. Еще коровы звонят в колокольчик, когда ходят. Допустим, ты реализовал в классе все до мелочей.

Полиморфизм и переопределение - 2

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

Ты начал проектировать класс «Кит» и понял, что он лишь немного отличается от класса «Корова». Логика работы обоих классов очень похожа, и ты решил использовать наследование.

Класс «Корова» идеально подходит на роль класса-родителя, там есть все необходимые переменные и методы. Достаточно только добавить киту возможность плавать. Но есть проблема: у твоего кита есть ноги, рога и колокольчик. Ведь эта функциональность реализована внутри класса «Корова». Что тут можно сделать?

Полиморфизм и переопределение - 3

К нам на помощь приходит переопределение (замена) методов. Если мы унаследовали метод, который делает не совсем то, что нужно нам в нашем новом классе, мы можем заменить этот метод на другой.

Полиморфизм и переопределение - 4

Как же это делается? В нашем классе-потомке мы объявляем такой же метод, как и метод класса родителя, который хотим изменить. Пишем в нем новый код. И все – как будто старого метода в классе-родителе и не было.

Вот как это работает:

Код Описание
class Cow {
  public void printColor() {
    System.out.println("Я - белая");
  }

  public void printName() {
    System.out.println("Я - корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.println("Я - кит");
  }
}
Тут определены два класса Cow и WhaleWhale унаследован от Cow.








В классе Whale переопределен метод printName();

public static void main(String[] args) {
  Cow cow = new Cow();
  cow.printName();
}
Данный код выведет на экран надпись «Я – корова»
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printName();
}
Данный код выведет на экран «Я – кит»

После наследования класса Cow и переопределения метода printName, класс Whale фактически содержит такие данные и методы:

Код Описание
class Whale {
  public void printColor() {
    System.out.println("Я - белая");
  }

  public void printName() {
    System.out.println("Я - кит");
  }
}
Ни о каком старом методе мы и не знаем.

— Честно говоря, ожидаемо.

2) Но это еще не все.

— Предположим в классе Cow есть метод printAll, который вызывает два других метода, тогда код будет работать так:

На экран будет выведена надпись Я – белая Я – кит

Код Описание
class Cow {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я - белая");
  }

  public void printName() {
    System.out.println("Я - корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.println("Я - кит");
  }
}
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printAll();
}
На экран будет выведена надпись
Я – белая
Я – кит

Обрати внимание, когда вызываются метод printAll() написанный в классе Cow, у объекта типа Whale, то будет использован метод printName класса Whale, а не Cow.

Главное, не в каком классе написан метод, а какой тип (класс) объекта, у которого этот метод вызван.

— Ясно.

— Наследовать и переопределять можно только нестатические методы. Статические методы не наследуются и, следовательно, не переопределяются.

Вот как выглядит класс Whale после применения наследования и переопределения методов:

Код Описание
class Whale {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я - белая");
  }

  public void printName() {
    System.out.println("Я - кит");
  }
}
Вот как выглядит класс Whale, после применения наследования и переопределения метода. Ни о каком старом методе printName мы и не знаем.

3) Приведение типов.

Тут есть еще более интересный момент. Т.к. класс при наследовании получает все методы и данные класса родителя, то объект этого класса разрешается сохранять (присваивать) в переменные класса родителя (и родителя родителя, и т.д., вплоть до Object). Пример:

Код Описание
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printColor();
}
На экран будет выведена надпись
Я – белая
public static void main(String[] args) {
  Cow cow = new Whale();
  cow.printColor();
}
На экран будет выведена надпись
Я – белая
public static void main(String[] args) {
  Object o = new Whale();
  System.out.println(o.toString());
}
На экран будет выведена надпись
Whale@da435a
Метод toString() унаследован от класса Object.

— Очень интересно. А зачем это может понадобиться?

— Это ценное свойство. Позже ты поймешь, что очень, очень ценное.

4) Вызов метода объекта (динамическая диспетчеризация методов).

Вот как это выглядит:

Код Описание
public static void main(String[] args) {
  Whale whale = new Whale();
  whale.printName();
}
На экран будет выведена надпись
Я – кит.
public static void main(String[] args) {
  Cow cow = new Whale();
  cow.printName();
}
На экран будет выведена надпись
Я – кит.

Обрати внимание, что на то, какой именно метод printName вызовется, от класса Cow или Whale, влияет не тип переменной, а тип – объекта, на который она ссылается.

В переменной типа Cow сохранена ссылка на объект типа Whale, и будет вызван метод printName, описанный в классе Whale.

— Это не просто для понимания.

— Да, это не очень очевидно. Запомни главное правило:

Набор методов, которые можно вызвать у переменной, определяется типом переменной. А какой именно метод/какая реализация вызовется, определяется типом/классом объекта, ссылку на который хранит переменная.

— Попробую.

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

5) Расширение и сужение типов.

Для ссылочных типов, т.е. классов, приведение типов работает не так, как для примитивных типов. Хотя у ссылочных типов тоже есть расширение и сужение типа. Пример:

Расширение типа Описание
Cow cow = new Whale();
Классическое расширение типа. Теперь кита обобщили (расширили) до коровы, но у объекта типа Whale можно вызывать только методы, описанные в классе Cow.

Компилятор разрешит вызвать у переменной cow только те методы, которые есть у ее типа — класса Cow.

Сужение типа Описание
Cow cow = new Whale();
if (cow instanceof Whale) {
  Whale whale = (Whale) cow;
}
Классическое сужение типа с проверкой. Переменная cow типа Cow, хранит ссылку на объект класса Whale.
Мы проверяем, что это так и есть, и затем выполняем операцию преобразования (сужения) типа. Или как ее еще называют – downcast.
Cow cow = new Cow();
Whale whale = (Whale) cow; //exception
Ссылочное сужение типа можно провести и без проверки типа объекта.
При этом, если в переменной cow хранился объект не класса Whale, будет сгенерировано исключение – InvalidClassCastException.

6) А теперь еще на закуску. Вызов оригинального метода

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

В этом случае очень хочется исполнить в новом методе свой код и вызвать этот же метод, но базового класса. И такая возможность в Java есть. Делается это так: super.method().

Примеры:

Код Описание
class Cow {
  public void printAll() {
    printColor();
    printName();
  }

  public void printColor() {
    System.out.println("Я – белый");
  }

  public void printName() {
    System.out.println("Я – корова");
  }
}

class Whale extends Cow {
  public void printName() {
    System.out.print("Это неправда: ");
    super.printName();

    System.out.println("Я – кит");
  }
}
public static void main(String[] args){
  Whale whale = new Whale();
  whale.printAll();
}
На экран будет выведена надпись
Я – белый
Это неправда: Я – корова
Я – кит

— Гм. Ничего себе лекция. Мои робо-уши чуть не расплавились.

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

Комментарии (353)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Anonymous #2482341 16 уровень, Москва
19 февраля 2021
Java Syntax Pro?
3 февраля 2021
Ребята, подскажите пожалуйста, что имеется ввиду: "Главное, не в каком классе написан метод, а какой тип (класс) объекта, у которого этот метод вызван."
Anonymous #2491313 27 уровень
28 января 2021
Дежавю, или лекция такая уже была?
Oleksii 14 уровень, Винница
13 января 2021
Почему в лекции переопределение без @Override???
Сергей 22 уровень, Санкт-Петербург
30 декабря 2020
Мое личное понимание полиморфизма: По сути, полиморфизм - это выделение общей логики поведения и состояния в отдельный абстрактный класс или интерфейс, чтобы реализовать логику других классов, которые схожи по смыслу с абстрактным классом или интерфейсом, т.е. обобщенные классы. P.S. Если что - подправьте и дополните ).
Антон 17 уровень, Москва
17 декабря 2020
Умалат 19 уровень, Малгобек
16 декабря 2020
Виктора не видели?
Veygard 22 уровень, Москва
16 декабря 2020
Отличная картинка из топ комента, которую запостил Макс
Vsevolod 13 уровень, Москва
11 декабря 2020
на самом деле, изначально составители курса хотели говорить о котах. Но они вспомнили, что они украинцы, и решили говорить о кiтах
Alexander Kusakin 30 уровень, Astrakhan
7 декабря 2020
После картинки "убираем рога, колокольчик" я подумал, что научат исключать лишние поля и методы, унаследованные от родительского класса. Пока на ум приходит только переопределение пустыми методами.