Ключевое слово super в Java
Обращение к родительскому классу в Java через super — вызов конструкторов и переопределённых методов.
super — это аналог this для родительского класса. Если this означает «текущий объект», то super — «текущий объект, рассматриваемый как экземпляр суперкласса». Ключевое слово super имеет смысл только внутри класса, который расширяет другой, и используется в трёх местах:
super(args)внутри конструктора — вызов конструктора родителя.super.method(args)внутри метода экземпляра — вызов родительской версии переопределённого метода.super.fieldвнутри метода экземпляра — чтение поля родителя, перекрытого подклассом.
Первый вариант встречается намного чаще всего.
super(...) — вызов конструктора родителя
Каждый конструктор подкласса должен начинаться с вызова какого-либо конструктора родителя. Если вы не пишете его явно, Java автоматически вставляет super(); в качестве первой строки — вызов конструктора родителя без аргументов:
public class Animal {
String name;
public Animal() { name = "(unnamed)"; } // no-arg
}
public class Cat extends Animal {
public Cat() {
// super(); ← inserted by the compiler
}
}Когда у родителя нет конструктора без аргументов, необходимо явно вызвать подходящий:
public class Animal {
String name;
public Animal(String name) { this.name = name; } // no no-arg constructor
}
public class Cat extends Animal {
public Cat(String name) {
super(name); // required — there is no Animal()
}
}Самая распространённая ошибка при наследовании — забыть об этом. Когда у родителя нет конструктора без аргументов, компилятор всё равно пытается вставить super(); в любой конструктор подкласса, который не вызывает super(...) явно, — и завершается с ошибкой "there is no default constructor available in Animal". Решение — добавить явный вызов super(...), соответствующий одному из существующих конструкторов родителя.
Правила для super(...):
- Вызов должен быть первым оператором в теле конструктора.
- Конструктор может либо обратиться к другому конструктору того же класса через
this(...), либо вызвать родительский черезsuper(...)— но не оба сразу. - Аргументы должны соответствовать существующему конструктору родителя.
Почему существует правило «первого оператора»?
JVM обязана полностью инициализировать родительскую часть объекта до того, как выполнится какой-либо код подкласса. Это включает инициализацию final-полей, объявленных в родителе. Если бы код подкласса выполнялся первым, он обращался бы к этим полям до того, как им были присвоены значения.
super.method(args) — вызов родительской версии метода
Внутри метода экземпляра super.method(...) вызывает родительскую версию method, даже если текущий класс её переопределил:
public class Animal {
String speak() { return "(some noise)"; }
}
public class Cat extends Animal {
@Override
String speak() {
return super.speak() + " — actually, meow";
// ^^^^^^^^^^^^^ calls Animal.speak(), not Cat.speak()
}
}
new Cat().speak(); // "(some noise) — actually, meow"Без super. обычный вызов speak() внутри Cat.speak обратился бы к самому себе и ушёл в бесконечную рекурсию.
Классический паттерн — расширять поведение родителя, а не заменять его:
@Override
void onSave() {
super.onSave(); // run parent's save logic first
// then add subclass-specific behavior
}super.method(...) не может подняться на два уровня вверх — в Java не существует super.super.method(). Если ваш класс расширяет класс, который расширяет другой, вы можете вызвать метод непосредственного родителя, но не прародителя.
super.field — доступ к перекрытому полю
Если подкласс объявляет поле с тем же именем, что и поле родителя, поле подкласса скрывает (shadows) родительское:
public class A {
String label = "A's label";
}
public class B extends A {
String label = "B's label";
void show() {
System.out.println(label); // B's label
System.out.println(super.label); // A's label
System.out.println(this.label); // B's label
}
}Это почти всегда плохой дизайн — иметь два поля с одинаковым именем в паре родитель–потомок запутывает код, — однако язык предоставляет super.field для разрешения неоднозначности. Чище всего переименовать одно из полей.
Обратите внимание: поля не являются полиморфными. В отличие от переопределённых методов, обращение к полю определяется во время компиляции на основе объявленного типа ссылки. Подробнее об этом рассказывается в главе о полиморфизме.
super и static
super, как и this, существует только в контексте экземпляра. Нельзя использовать super в static-методе или инициализаторе — там нет текущего объекта, а значит, нет и его родительского представления. Для вызова статического метода родителя следует указывать имя класса родителя напрямую:
public class A { static void hi() { System.out.println("hi"); } }
public class B extends A {
static void demo() {
A.hi(); // ok — call by class name
// super.hi(); // ERROR — super in a static context
}
}Подкласс также может объявить static-метод с тем же именем, что и static-метод родителя, — это называется сокрытием метода (method hiding), а не переопределением, и подчиняется правилам времени компиляции. Кратко это рассматривается в главе о переопределении методов.
Цепочки вызовов super
Каждый вызов super(...) активирует следующий уровень цепочки наследования. Трассировка выполнения для глубоко вложенного подкласса:
new SiameseCat("Lulu")
→ SiameseCat(String)
→ super("Lulu") — Cat(String)
→ super("Lulu") — Animal(String)
→ super() — Object()
→ Animal body runs
→ Cat body runs
→ SiameseCat body runsИнициализация всегда идёт от самого дальнего предка вниз к текущему классу. К моменту, когда выполняется тело SiameseCat, все поля родителей полностью инициализированы.
Пример
Что дальше
super — это мост к родительскому классу, но главная идея, которую он открывает, — полиморфизм: возможность вызвать метод через ссылку родительского типа и получить вызов нужной реализации подкласса в момент выполнения. Это тема следующей главы. Продолжайте с полиморфизма в Java.