JavaRush/Java блог/Java Developer/Механизм переопределения методов или Override в Java
Автор
Aditi Nawghare
Инженер-программист в Siemens

Механизм переопределения методов или Override в Java

Статья из группы Java Developer
участников
Привет! Ты уже используешь методы в Java и знаешь о них многое. Наверняка ты сталкивался с ситуацией, когда в одном классе было много методов с одинаковым названием, но разными аргументами. Если помнишь, в тех случаях мы использовали механизм перегрузки методов. Сегодня рассмотрим другую ситуацию. Представь, что у нас есть один общий метод, но он должен делать разные вещи в зависимости от того, в каком классе он был вызван. Как реализовать такое поведение? Чтобы разобраться, возьмем родительский класс Animal, обозначающий животных, и создадим в нем метод voice — «голос»:
public class Animal {

   public void voice() {

       System.out.println("Голос!");
   }
}
Хотя мы только начали писать программу, потенциальная проблема тебе, скорее всего, видна: животных в мире очень много, и все «говорят» по-разному: кошки мяукают, утки крякают, змеи шипят. Как устроен механизм переопределения методов  - 2 Наша цель проста: избежать создания кучи методов для подачи голоса. Вместо того, чтобы создавать методы voiceCat() для мяуканья, voiceSnake() для шипения и т.д., мы хотим, чтобы при вызове метода voice() змея шипела, кошка мяукала, а собака лаяла. Мы легко добьемся этого с помощью механизма переопределения методов (Override в Java). Википедия дает такое пояснение термина «переопределение»: Переопределение метода (англ. Method overriding) в объектно-ориентированном программировании — одна из возможностей языка программирования, позволяющая подклассу или дочернему классу обеспечивать специфическую реализацию метода, уже реализованного в одном из суперклассов или родительских классов. Оно, в общем-то, правильное. Переопределение позволяет взять какой-то метод родительского класса и написать в каждом классе-наследнике свою реализацию этого метода. Новая реализация «заменит» родительскую в дочернем классе. Рассмотрим, как это выглядит на примере. Создадим 4 класса-наследника для нашего класса Animal:
public class Bear extends Animal {
   @Override
   public void voice() {
       System.out.println("Р-р-р!");
   }
}
public class Cat extends Animal {

   @Override
   public void voice() {
       System.out.println("Мяу!");
   }
}

public class Dog extends Animal {

   @Override
   public void voice() {
       System.out.println("Гав!");
   }
}


public class Snake extends Animal {

   @Override
   public void voice() {
       System.out.println("Ш-ш-ш!");
   }
}
Небольшой лайфхак на будущее: чтобы переопределить методы родительского класса, перейди в код класса-наследника в Intellij IDEa, нажми Ctrl+O и выбери в меню «Override methods...». Привыкай пользоваться горячими клавишами с начала, это ускоряет написание программ! Чтобы задать нужное нам поведение, мы сделали несколько вещей:
  1. Создали в каждом классе-наследнике метод с таким же названием, как и у метода в родительском классе.
  2. Сообщили компилятору, что мы не просто так назвали метод так же, как в классе-родителе: хотим переопределить его поведение. Для этого «сообщения» компилятору мы поставили над методом аннотацию @Override («переопределен»).
    Проставленная над методом аннотация @Override сообщает компилятору (да и читающим твой код программистам тоже): «Все ок, это не ошибка и не моя забывчивость. Я помню, что такой метод уже есть, и хочу переопределить его».

  3. Написали нужную нам реализацию для каждого класса-потомка. Змея при вызове voice() должна шипеть, медведь — рычать и т.д.
Давай посмотрим, как это будет работать в программе:
public class Main {

   public static void main(String[] args) {

       Animal animal1 = new Dog();
       Animal animal2 = new Cat();
       Animal animal3 = new Bear();
       Animal animal4 = new Snake();

       animal1.voice();
       animal2.voice();
       animal3.voice();
       animal4.voice();
   }
}
Вывод в консоль: Гав! Мяу! Р-р-р! Ш-ш-ш! Отлично, все работает как надо! Мы создали 4 переменных-ссылки родительского класса Animal, и присвоили им 4 разных объекта классов-наследников. В результате каждый объект ведет себя по-своему. Для каждого из классов-наследников переопределенный метод voice() заменил «родной» метод voice() из класса Animal (который выводит в консоль просто «Голос!»). Как устроен механизм переопределения методов  - 3 У переопределения есть ряд ограничений:
  1. У переопределенного метода должны быть те же аргументы, что и у метода родителя.

    Если метод voice родительского класса принимает на вход String, переопределенный метод в классе-потомке тоже должен принимать на вход String, иначе компилятор выдаст ошибку:

    public class Animal {
    
       public void voice(String s) {
    
           System.out.println("Голос! " + s);
       }
    }
    
    public class Cat extends Animal {
    
       @Override//ошибка!
       public void voice() {
           System.out.println("Мяу!");
       }
    }

  2. У переопределенного метода должен быть тот же тип возвращаемого значения, что и у метода родителя.

    В ином случае мы получим ошибку компиляции:

    public class Animal {
    
       public void voice() {
    
           System.out.println("Голос!");
       }
    }
    
    
    public class Cat extends Animal {
    
       @Override
       public String voice() {         //ошибка!
           System.out.println("Мяу!");
           return "Мяу!";
       }
    }

  3. Модификатор доступа у переопределенного метода также не может отличаться от «оригинального»:

    public class Animal {
    
       public void voice() {
    
           System.out.println("Голос!");
       }
    }
    
    public class Cat extends Animal {
    
       @Override
       private void voice() {      //ошибка!
           System.out.println("Мяу!");
       }
    }
Переопределение методов в Java — один из инструментов для реализации идеи полиморфизма (принципа ООП, о котором мы рассказывали в прошлой лекции). Поэтому главным преимуществом его использования будет та же гибкость, о которой мы говорили ранее. Мы можем выстроить простую и логичную систему классов, каждый из которых будет обладать специфическим поведением (собаки лают, кошки мяукают), но единым интерфейсом — один метод voice() на всех вместо кучи методов voiceDog(), voiceCat() и т.д.
Комментарии (175)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Anonymous #3346123
Уровень 14
15 марта, 21:10
public class Main { public static void main(String[] args) { Animal animal1 = new Bear(); Animal animal2 = new Cat(); Animal animal3 = new Dog(); Animal animal4 = new Snake(); } } Intellij не принимает - Cannot resolve symbol 'Bear', 'Cat', ' Dog', 'Snake' . Why? Почему?
Anton Rekuts
Уровень 19
31 марта, 10:49
А вы добавили классы-потомки, объекты которых вы создали?
Максим Li Java Developer
12 ноября 2023, 21:30
Всё понятно!
chess.rekrut
Уровень 25
21 августа 2023, 14:55
easy
Alexander Rozenberg
Уровень 32
3 августа 2023, 19:03
fine
Rustam
Уровень 35
Student
31 июля 2023, 19:28
👍
Mikhail
Уровень 10
30 июня 2023, 07:04
Спасибо за статью! Интересно и познавательно!
Lexoid
Уровень 40
23 июня 2023, 11:03
Статья очень хорошая, но в корне не согласен с утверждением относительно того, что модификатор доступа у переопределённого метода также не может отличаться от «оригинального». Это утверждение справедливо лишь в том случае, если модификатор доступа переопределяемого метода изначально был public. Суть заключается в том, что модификатор доступа переопределённого метода (в классе-наследнике) всегда должен быть не менее открытым, чем в базовом классе. Если мы переопределяем protected-метод, то можем сделать наш переопределённый метод либо protected, либо же public. Если же метод имел уровень доступа package-private (доступ по умолчанию, в пределах пакета, в котором располагается класс), то в классе-потомке после переопределения метод может иметь модификаторы package-private, protected или public.
Антон Скрытный
Уровень 46
6 августа 2023, 13:16
Спасибо за уточнение, проверил и все действительно так!
Kirill
Уровень 33
10 марта, 12:25
Кроме того, утверждение, что у переопределяемого метода тип возвращаемого значения должен быть строго таким же, как и у метода в родительском классе, тоже не верно. Поскольку допускается использовать подтип возвращаемого значение из метода класса родителя.
Lexoid
Уровень 40
12 марта, 08:34
Совершенно верно! Это называется ковариантностью типа возвращаемого значения. Имеет самое прямое отношение к вопросу переопределения методов. Суть в том, что вместо базового типа допускается указание любых совместимых типов-потомков, которые могли бы быть расширены до базового типа возвращаемого значения, указанного в переопределяемом методе. К примеру, если исходный метод возвращает некоторый экземпляр, который реализует интерфейс List<E>, то при переопределении мы можем сузить тип возвращаемого значения до ArrayList<E>, так как ArrayList<E> является конкретной реализацией интерфейса List<E>. Такое переопределение накладывает определённые ограничения, но и даёт возможность расширить имеющийся интерфейс и конкретизировать тип возвращаемого значения. Какой-нибудь LinkedList<E> уже не получится вернуть в контексте рассматриваемого примера.
Lexoid
Уровень 40
12 марта, 09:00
Кроме того, стоит упомянуть о пропагации исключений, которые мы не обрабатываем явно и указываем после ключевого слова throws в заголовке метода. Пробрасываемые исключения вместе с типом возвращаемого значения и сигнатурой метода образуют контракт метода. Это означает, что при переопределении метода мы должны соблюдать согласованность. Если в методе базового класса не указаны исключения, то при переопределении мы не можем добавлять новые. Мы можем либо повторить перечень исключений из базового метода, либо указать их подтипы, либо вообще не указывать исключения. Например:
public void someMethod() throws Exception {}
При переопределении допустимы следующие варианты:
@Override
public void someMethod() throws Exception {}

@Override
public void someMethod() {}

@Override
public void someMethod() throws InterruptedException {}
Однако следующий вариант будет ошибочным, так как Throwable не является подтипом Exception:
@Override
public void someMethod() throws Throwable {}
Ислам
Уровень 33
8 июня 2023, 12:16
Nice
Глеб
Уровень 2
4 апреля 2023, 20:02
Помогите разобраться: если мы не используем переопределение... срабатывает полиморфизм и медведь, например, все равно рычит... 🤷‍♂️
Anonymous #1218025
Уровень 23
13 июня 2023, 17:38
Тоже не совсем понял. В предыдущей лекции про полиморфизм не писали никаких &оверайдов и ничего ведь. Или причина в том, что сейчас не обязательно писать &оверайд, код сработает и так?
Lexoid
Уровень 40
12 марта, 09:31
Если ты не переопределяешь метод в дочернем классе, то при его вызове будет использоваться реализация из родительского класса. Это происходит потому, что метод автоматически наследуется дочерним классом, и если он не переопределён, используется именно наследованная реализация.
Василий
Уровень 25
2 апреля 2023, 20:08
Кто вернулся после задачи, где изначальный метод возвращает DBObject, а переопределенный должен возвращать User. Не удивляйтесь, статья устарела, в комментах ниже все объясняют. - Если метод в базовом классе возвращает void, переопределенный метод должен возвращать void - Если метод в базовом классе возвращает примитив, переопределенный метод должен возвращать тот же примитив - Если метод в базовом классе возвращает определенный тип, переопределенный метод должен возвращать тот же тип или подтип (он же ковариантный возвращаемый тип)