W3docs

Переопределение методов в Java

Переопределение методов родительского класса в подклассах Java с аннотацией @Override и динамической диспетчеризацией.

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

В этой главе изложены правила: что считается переопределением, что нет, что компилятор позволит изменить в версии подкласса, и что на самом деле даёт аннотация @Override.

Как выглядит переопределение

Метод переопределяет унаследованный, когда выполняются все следующие условия:

  • Одинаковое имя.
  • Одинаковый список параметров (типы и порядок, после стирания обобщений).
  • Тип возвращаемого значения совпадает с родительским или является его подтипом (ковариантное возвращение).
  • Видимость совпадает с родительской или шире.
  • Метод не объявлен final, static или private в родительском классе.
class Animal {
  String speak() { return "(noise)"; }
}
class Cat extends Animal {
  @Override
  String speak() { return "meow"; }
}

Это корректное переопределение. Вызов speak() на объекте Cat возвращает "meow"; вызов через ссылку типа Animal, указывающую на Cat, также возвращает "meow" — диспетчеризация определяется классом реального объекта.

@Override — используйте всегда

@Override — это аннотация, которая сообщает компилятору: «Я намерен переопределить унаследованный метод». Если на самом деле переопределения не происходит — неправильное имя, неправильные параметры, неправильный тип возвращаемого значения — компилятор выдаёт ошибку:

class Cat extends Animal {
  @Override
  String Speak() { return "meow"; }    // ERROR — no Speak() in Animal
}

Без аннотации этот код молча скомпилировался бы как совершенно новый метод Speak, и ((Animal)cat).speak() продолжал бы возвращать родительский "(noise)". Аннотация ничего не стоит и позволяет поймать целый класс молчаливых ошибок. Всегда пишите её при переопределениях.

Примечание

Переопределения, которые вы будете писать чаще всего, — это не методы ваших собственных классов, а методы, унаследованные от Object, корня любого класса. Переопределение toString(), equals(Object) и hashCode() — это то, как ваши объекты читаемо выводятся, сравниваются по значению и корректно работают в качестве ключей в HashMap или HashSet. @Override на них помогает поймать классическую ошибку: написать equals(Cat other) (это перегрузка) вместо equals(Object other) (настоящее переопределение).

Переопределение и перегрузка

АспектПереопределениеПерегрузка
ГдеМежду подклассом и суперклассомВ одном классе
СигнатураДолжна совпадать с родительскойДолжна отличаться от остальных
ДиспетчеризацияВо время выполнения, по фактическому объектуВо время компиляции, по типам аргументов
Аннотация@Overrideнет

Их часто путают. Переопределение — один метод, несколько типов объектов. Перегрузка — несколько методов, один тип, выбор по передаваемым аргументам.

Ковариантные типы возвращаемых значений

Подкласс может объявить более узкий тип возвращаемого значения, чем у родителя:

class Animal {
  Animal makeBaby() { return new Animal(); }
}
class Cat extends Animal {
  @Override
  Cat makeBaby() { return new Cat(); }      // returns Cat, not Animal — fine
}

Это ковариантность — тип возвращаемого значения переопределённого метода является подтипом родительского. Это безопасно, потому что любой вызывающий код, ожидающий Animal от makeBaby(), с удовольствием примет Cat. Преимущество в том, что вызывающий код, знающий, что работает с Cat, получает обратно Cat без приведения типов:

Cat kitten = new Cat().makeBaby();    // no cast needed

Обратное направление (расширение типа возвращаемого значения в подклассе) не допускается — это нарушило бы код вызывающей стороны, ожидающей более узкий тип.

Видимость можно только расширять

Переопределение должно быть не менее видимым, чем метод родителя:

class A { public  void hi() { } }
class B extends A { protected void hi() { } }   // ERROR — reduces visibility
class C extends A { public    void hi() { } }   // ok

Сужение видимости означало бы, что вызывающий код со ссылкой типа A мог бы вызвать hi(), но после присвоения A a = new B() уже не смог бы — это нарушило бы принцип подстановки.

Исключения можно только сужать

Если родительский метод не объявляет проверяемых исключений, переопределение тоже не может их объявлять. Если родитель выбрасывает проверяемое исключение, переопределение может выбрасывать то же самое или более конкретный подкласс — но не более широкое:

class A {
  void run() throws IOException { ... }
}
class B extends A {
  @Override void run() throws FileNotFoundException { ... }    // ok — FileNotFoundException extends IOException
}
class C extends A {
  @Override void run() throws Exception { ... }                // ERROR — too broad
}

Непроверяемые исключения (подклассы RuntimeException) не ограничены — их всегда можно выбрасывать из переопределения.

Что нельзя переопределить

  • private-методы. Они вообще не видны подклассу, поэтому метод с таким же именем в подклассе — это просто новый метод.
  • final-методы. Помечены именно для того, чтобы предотвратить переопределение.
  • static-методы. Подкласс может объявить статический метод с тем же именем и сигнатурой, но это называется сокрытием метода, а не переопределением. Здесь нет полиморфизма — JVM разрешает вызов на основе типа ссылки во время компиляции:
class A { static String klass() { return "A"; } }
class B extends A { static String klass() { return "B"; } }

A a = new B();
System.out.println(a.klass());   // "A" — static, not polymorphic

Это одно из немногих мест, где правило «диспетчеризация метода выбирает версию фактического объекта» не применяется.

Вызов родителя из переопределения

Распространённый паттерн — расширить поведение родителя, а не заменить его, с помощью super.method(...):

class Logger {
  void log(String s) { System.out.println(s); }
}
class TimestampedLogger extends Logger {
  @Override
  void log(String s) {
    super.log("[" + System.currentTimeMillis() + "] " + s);
  }
}

Переопределение дополняет поведение родителя вместо его дублирования. Подробнее рассматривается в главе о ключевом слове super.

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

java— editable, runs on the server

Что дальше

Вы увидели, как подклассы заменяют методы родителя. Следующая идея — абстракция — это обратная сторона: объявление метода, у которого нет тела по умолчанию, с обязательной реализацией в каждом конкретном подклассе. Переходите к абстракции в Java.

Практика

Практика
Почему в подклассе стоит писать @Override на каждом переопределении, даже если код компилируется без неё?
Почему в подклассе стоит писать @Override на каждом переопределении, даже если код компилируется без неё?
Was this page helpful?