JavaRush /Курсы /Java Core /Полиморфизм и переопределение

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

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();
}
На экран будет выведена надпись
Я – белый
Это неправда: Я – корова
Я – кит

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

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

Комментарии (562)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3585174 Уровень 33
30 июля 2025
like
Максим Антонов Уровень 27
13 мая 2025
по моему тут ошибка написана в лекций в самом начале во втором пункте про наследовательность, сказано мы не можем наследовать статические методы и переопределять их, только что проверил все работает
Андрей Уровень 26
16 июня 2025
Не совсем Статический метод нельзя переопределить, но джава позволяет его "затенить" или скрыть Мутная тема на самом деле =) То есть, в классе наследнике может быть метод с такой же сигнатурой что в классе родителе и все будет работать, но это не будет переопределением Вот, немного нагруженная, но подробная статья обо всем этом Тык
Cryptosin Уровень 24
21 июля 2025
в Java нельзя переопределить статические методы — но здесь есть важное уточнение: их можно "скрыть" (hide), и это часто воспринимается как "работает" или "переопределяется", хотя технически это не так.,

class Parent {
    static void sayHello() {
        System.out.println("Hello from Parent");
    }
}

class Child extends Parent {
    static void sayHello() {
        System.out.println("Hello from Child");
    }
}

public class Test {
    public static void main(String[] args) {
        Parent p = new Child();
        p.sayHello();  // Выведет "Hello from Parent"
        
        Child c = new Child();
        c.sayHello();  // Выведет "Hello from Child"
    }
}
Что происходит:

 Parent p = new Child(); p.sayHello();
— вызовется Parent.sayHello(), потому что статические методы привязаны к классу, а не к объекту. Здесь p имеет тип Parent, значит будет вызван метод sayHello() именно из Parent.

Child c = new Child(); c.sayHello();
— а тут уже тип Child, значит вызовется метод из Child. Вывод: ✅ Можно создать статический метод с тем же именем в подклассе — это не переопределение, а скрытие (method hiding). ❌ Полиморфизм (динамический вызов) на статические методы не работает. То есть JVM не выбирает метод на основе объекта во время выполнения, как с обычными методами. ✅ Да, "всё работает" — но не так, как с обычными (нестатическими) методами. НО! static void sayHello()

Parent c = new Child();
c.sayHello(); // Выведет: "Hello from Parent"
ЕЩЕ ОДНО НО! static void sayHello()

Parent c = new Child();
c.sayHello(); // Выведет "Child" — потому что это полиморфизм
illia Уровень 30
4 марта 2025
Тип ссылки (Cow) ограничивает, какие методы можно вызывать. Тип объекта (Whale) определяет, какая реализация методов будет выполнена (полиморфизм). Если метод есть только в Whale, его нельзя вызвать через Cow, пока не сделано приведение типа.
neptun020202 Уровень 32
27 февраля 2025
На предыдущем уровне нас учили наследованию всяких зверушек от Animal и Pet. А сами тут кита от коровы наследуют. Сказочные...
ゾルディック キルア Уровень 37
9 января 2025
👁
Сергей Воронов Уровень 13
27 декабря 2024
А есть ли смысл в создании статического метода внутри класса Cow, и если да, что он мог бы делать?
Vitalii Shevchenko Уровень 1
1 апреля 2025
Наприклад, рахувать кількість створених корів
Zick_Zibn Уровень 5
12 декабря 2024
Повторение мать учения! Так что ли?
ВК Уровень 36
5 декабря 2024
Где комиксы???
valodya_93 Уровень 33
24 октября 2024
а можно ли сделать так чтобы у объекта whale при вызове printAll игнорился метод color?без переопределения printAll у whale
Данил Уровень 29
16 октября 2024
КоровныйКИТ , где-то я тебя видел ...