Java — разница между extends и implements на примерах
После продолжительного программирования на C++ переходить на Java бывает болезненно. С одной стороны прославленный сборщик мусора, а с другой огромное множество принципиальных различий в подходах к программированию. Об одном таком отличии я сегодня расскажу подробнее.
Речь пойдет о наследовании в Java. В отличии от C++, где наследование могло быть множественным, здесь это не совсем так. Кроме того, привычный синтаксис через «:» заменился на целых два ключевых слова: extends и implements. Начну с первого.
Ключевое слово extends в Java
Действие ключевого слова в точности совпадает с его переводом, один класс расширяет другой, что является классическим наследованием. Правила видимости полей и методов сохранились: private доступны только в самом классе, protected в самом классе и во всех наследниках, к public методам и полям можно обращаться откуда угодно. Главное отличие от «сишного» наследования в том, что можно расширять только один класс. Я сейчас не буду рассуждать о том, насколько это удобно, скажу только, что со множественным наследованием в C++ постоянно творилась какая-то каша.
Небольшой пример наследования с помощью ключевого слова extends. Напишем класс Door, который будет описывать характеристики двери, мы можем создать объект этого класса и работать с ним, как с «просто дверью». С другой стороны напишем еще два класса: IronDoor и WoodDoor, которые будут расширять класс Door(== наследуются от класса Door), т.е. добавят свои характеристики к базовым.
//Базовый класс "дверь" public class Door < //Допустим, что у любой двери есть цена protected int price; //Этот метод тоже наследуется protected void doSomething() < System.out.println("Door is doing something"); >//Этот метод доступен исключительно в классе Door private void onlyForDoor() < >> //Железная дверь public class IronDoor extends Door < //Уровень защиты определен только для железных дверей private int protectionLvl; IronDoor(int price, int protectionLvl) < this.price = price; this.protectionLvl = protectionLvl; >> //Деревянная дверь public class WoodDoor extends Door < //Характеристика "порода древесины" доступна только деревянной двери private String woodType; WoodDoor(int price, String woodType) < this.price = price; this.woodType = woodType; >>
Ключевое слово implements в Java
С ключевым словом implements связано чуть больше хитростей. Слово «имплементировать» можно понимать, как «реализовывать», а в тот самый момент, когда возникает слово «реализовывать», где-то недалеко появляются интерфейсы. Так вот конструкция public class Door implements Openable означает, что класс дверь реализует интерфейс «открывающийся». Следовательно класс должен переопределить все методы интерфейса. Главная фишка в том, что можно реализовывать сколь угодно много интерфейсов.
Зачем это нужно? Самый простой пример, который приходит в голову, два интерфейса: Openable и Closeble. В первом метод open, и метод close во втором. Они помогут научить нашу дверь закрываться и открываться.
public interface Openable < void open(); >public interface Closeble < void close(); >public class Door implements Openable, Closeble < protected int price; protected void doSomething() < System.out.println("Door is doing something"); >//Реализованные методы @Override public void open() < >@Override public void close() < >>
В классах-потомках двери(железная и деревянная двери) тоже появятся методы открыть/закрыть, реализованные в классе Door. Но никто нам не запрещает их переопределить.
public class IronDoor extends Door < private int protectionLvl; IronDoor(int price, int protectionLvl) < this.price = price; this.protectionLvl = protectionLvl; >//Переопределяем методы @Override public void open() < System.out.println("The IRON door is opened"); >@Override public void close() < System.out.println("The IRON door is closed"); >>
Заключение
Итак, главное отличие в том, что extends используется для наследования от класса в прямом смысле этого слова, а implements позволяет «реализовать интерфейс». На первый взгляд это кажется лишним, неудобным и непонятным, но стоит пару раз использовать по назначению и все встает на свои места. На сегодня у меня все, спасибо за внимание!
Наследование
Наследование является неотъемлемой частью Java. При использовании наследования вы говорите: Этот новый класс похож на тот старый класс. В коде это пишется как extends, после которого указываете имя базового класса. Тем самым вы получаете доступ ко всем полям и методам базового класса. Используя наследование, можно создать общий класс, которые определяет характеристики, общие для набора связанных элементов. Затем вы можете наследоваться от него и создать новый класс, который будет иметь свои уникальные характеристики. Главный наследуемый класс в Java называют суперклассом. Наследующий класс называют подклассом. Получается, что подкласс — это специализированная версия суперкласса, которая наследует все члены суперкласса и добавляет свои собственные уникальные элементы. К примеру, в Android есть класс View и подкласс TextView.
Чтобы наследовать класс, достаточно вставить имя наследуемого класса с использованием ключевого слова extends:
public class MainActivity extends Activity
В этом коде мы наследуемся от класса Activity и добавляем свой код, который будет отвечать за наше приложение.
Подкласс в свою очередь может быть суперклассом другого подкласса. Так например, упоминавший ранее класс TextView является суперклассом для EditText.
В производный класс можно добавлять новые методы.
Для каждого создаваемого подкласса можно указывать только один суперкласс. При этом никакой класс не может быть собственным суперклассом.
Хотя подкласс включает в себя все члены своего суперкласса, он не может получить доступ к тем членам суперкласса, которые объявлены как private.
Помните, мы создавали класс Box для коробки кота. Давайте наследуемся от этого класса и создадим новый класс, который будет иметь не только размеры коробки, но и вес.
В том же файле Box.java после последней закрывающей скобки добавьте новый код:
class HeavyBox extends Box < int weight; // вес коробки // конструктор HeavyBox(int w, int h, int d, int m) < width = w; height = h; depth = d; weight = m; // масса >>
Возвращаемся в главную активность и пишем код:
HeavyBox box = new HeavyBox(15, 10, 20, 5); int vol = box.getVolume(); mInfoTextView.setText("Объём коробки: " + vol + " Вес коробки: " + box.weight);
Обратите внимание, что мы вызываем метод getVolume(), который не прописывали в классе HeavyBox. Однако мы можем его использовать, так как мы наследовались от класса Box и нам доступны все открытые поля и методы. Заодно мы вычисляем вес коробки с помощью новой переменной, которую добавили в подкласс.
Теперь у нас появилась возможность складывать в коробку различные вещи. В хозяйстве всё пригодится.
При желании вы можете создать множество разных классов на основе одного суперкласса. Например, мы можем создать цветную коробку.
class ColorBox extends Box < int color; // цвет коробки // конструктор ColorBox(int w, int h, int d, int c) < width = w; height = h; depth = d; color = c; // цвет >>
Ключевое слово super
В Java существует ключевое слово super, которое обозначает суперкласс, т.е. класс, производным от которого является текущий класс. В данном случае, супер не означает превосходство, скорее даже наоборот, дочерний класс имеет больше методов, чем родительский. Само слово пошло из теории множеств, где используется термин супермножество. Посмотрим, зачем это нужно.
В конструкторе HeavyBox мы дублировали поля width,height и depth, которые уже есть в классе Box. Это не слишком эффективно. Кроме того, возможны ситуации, когда суперкласс имеет закрытые члены данных, но мы хотим иметь к ним доступ. Через наследование это не получится, так как закрытые члены класса доступны только родному классу. В таких случаях вы можете сослаться на суперкласс.
Ключевое слово super можно использовать для вызова конструктора суперкласса и для обращения к члену суперкласса, скрытому членом подкласса.
Использование ключевого слова super для вызова конструктора суперкласса
class HeavyBox extends Box < int weight; // вес коробки // конструктор // инициализируем переменные с помощью ключевого слова super HeavyBox(int w, int h, int d, int m) < super(w, h, d); // вызов конструктора суперкласса weight = m; // масса >>
Вызов метода super() всегда должен быть первым оператором, выполняемым внутри конструктора подкласса.
При вызове метода super() с нужными аргументами, мы фактически вызываем конструктор Box, который инициализирует переменные width, height и depth, используя переданные ему значения соответствующих параметров. Вам остаётся инициализировать только своё добавленное значение weight. При необходимости вы можете сделать теперь переменные класса Box закрытыми. Проставьте у полей класса Box модификатор private и убедитесь, что вы можете обращаться к ним без проблем.
У суперкласса могут быть несколько перегруженных версий конструкторов, поэтому можно вызывать метод super() с разными параметрами. Программа выполнит тот конструктор, который соответствует указанным аргументам.
Вторая форма ключевого слова super действует подобно ключевому слову this, только при этом мы всегда ссылаемся на суперкласс подкласса, в котором она использована. Общая форма имеет следующий вид:
super.член
Здесь член может быть методом либо переменной экземпляра.
Подобная форма подходит в тех случаях, когда имена членов подкласса скрывают члены суперкласса с такими же именами.
class A < int i; >// наследуемся от класса A class B extends A < int i; // имя переменной совпадает и скрывает переменную i в классе A B(int a, int b) < super.i = a; // обращаемся к переменной i из класса A i = b; // обращаемся к переменной i из класса B >void show() < System.out.println("i из суперкласса: " + super.i); System.out.println("i в подклассе: " + i); >> class MainActivity
В результате мы должны увидеть:
i из суперкласса: 1 i в подклассе: 2
Таким образом, знакомое нам выражение super.onCreate(savedInstanceState) обращается к методу onCreate() из базового класса.
Создание многоуровневой иерархии
Мы использовали простые примеры, состоящие из суперкласса и подкласса. Можно строить более сложные конструкции, содержащие любое количество уровней наследования. Например, класс C может быть подклассом класса B, который в свою очередь является подклассом класса A. В подобных ситуациях каждый подкласс наследует все характеристики всех его суперклассов.
Напишем пример из трёх классов. Суперкласс Box, подкласс HeavyBox и подкласс MoneyBox. Последний класс наследует все характеристики классов Box и HeavyBox, а также добавляет поле cost, которое содержит стоимость коробки.
Box.java
package ru.alexanderklimov.expresscourse; class Box < private int width; // ширина коробки private int height; // высота коробки private int depth; // глубина коробки // Конструктор для создания клона объекта Box(Box ob) < // передача объекта конструктору width = ob.width; height = ob.height; depth = ob.depth; >// Конструктор, используемый при указании всех измерений Box(int w, int h, int d) < width = w; height = h; depth = d; >// Конструктор, используемый, когда ни одно из измерений не указано Box() < // значение -1 используется // для указания неинициализированного параллелепипеда width = -1; height = -1; depth = -1; >// Конструктор для создания куба Box(int len) < width = height = depth = len; >// вычисляем объём коробки int getVolume() < return width * height * depth; >>
HeavyBox.java
package ru.alexanderklimov.expresscourse; //Добавление веса class HeavyBox extends Box < int weight; // вес коробки // Конструктор клона объекта HeavyBox(HeavyBox ob) < // передача объекта конструктору super(ob); weight = ob.weight; >// Конструктор, используемый // при указании всех параметров HeavyBox(int w, int h, int d, int m) < super(w, h, d); // вызов конструктора суперкласса weight = m; // масса >// Конструктор по умолчанию HeavyBox() < super(); weight = -1; >// Конструктор для создания куба HeavyBox(int len, int m) < super(len); weight = m; >>
MoneyBox
package ru.alexanderklimov.expresscourse; //Цена коробки class MoneyBox extends HeavyBox < int cost; // Конструирование клона объекта MoneyBox(MoneyBox ob) < // передача объекта конструктору super(ob); cost = ob.cost; >// Конструктор, используемый // при указании всех параметров MoneyBox(int w, int h, int d, int m, int c) < super(w, h, d, m); // вызов конструктора суперкласса cost = c; >// Конструктор по умолчанию MoneyBox() < super(); cost = -1; >// Конструктор для создания куба MoneyBox(int len, int m, int c) < super(len, m); cost = c; >>
Код для основной активности, например, при щелчке кнопки:
public void onClick(View v)
В результате мы получим различные значения, вычисляемые в коде. Благодаря наследованию, класс MoneyBox может использовать классы Box и HeavyBox, добавляя только ту информацию, которая нам требуется для его собственного специализированного применения. В этом и состоит принцип наследования, позволяя повторно использовать код.
Метод super() всегда ссылается на конструктор ближайшего суперкласса в иерархии. Т.е. метод super() в классе MoneyBox вызывает конструктор класса HeavyBox, а метод super() в классе HeavyBox вызывает конструктор класса Box.
Если в иерархии классов конструктор суперкласса требует передачи ему параметров, все подклассы должны передавать эти параметры по эстафете.
В иерархии классов конструкторы вызываются в порядке наследования, начиная с суперкласса и заканчивая подклассом. Если метод super() не применяется, программа использует конструктор каждого суперкласса, заданный по умолчанию или не содержащий параметров.
Вы можете создать три класса A, B, C, которые наследуются друг от друга (A←B←C), у которых в конструкторе выводится текст и вызвать в основном классе код:
C c = new C();
Вы должные увидеть три строчки текста, определённые в каждом конструкторе класса. Поскольку суперкласс ничего не знает о своих подклассах, любая инициализация полностью независима и, возможно, обязательна для выполнения любой инициализацией, выполняемой подклассом.
Переопределение методов
Если в иерархии классов имя и сигнатура типа метода подкласса совпадает с атрибутами метода суперкласса, то метод подкласса переопределяет метод суперкласса. Когда переопределённый метод вызывается из своего подкласса, он всегда будет ссылаться на версию этого метода, определённую подклассом. А версия метода из суперкласса будет скрыта.
Если нужно получить доступ к версии переопределённого метода, определённого в суперклассе, то используйте ключевое слово super.
Не путайте переопределение с перегрузкой. Переопределение метода выполняется только в том случае, если имена и сигнатуры типов двух методов идентичны. В противном случае два метода являются просто перегруженными.
В Java SE5 появилась запись @Override; она не является ключевым словом. Если вы собираетесь переопределить метод, используйте @Override, и компилятор выдаст сообщение об ошибке, если вместо переопределения будет случайно выполнена перегрузка.
Для закрепления материала создадим класс Animal с одним методом.
package ru.alexanderklimov.expresscourse; public class Animal < String sleep()< return "Животные иногда спят"; >>
Теперь создадим класс Cat, наследующий от первого класса.
package ru.alexanderklimov.expresscourse; public class Cat extends Animal
Java знает, у родительского класса есть метод sleep(). Удостовериться можно следующим образом. Находясь в классе Cat, выберите в меню Source | Override/Implement Methods. . Появится диалоговое окно, где можно отметить флажком нужный метод.

В результате в класс будет добавлена заготовка:
@Override String sleep() < // TODO Auto-generated method stub return super.sleep(); >
Попробуем вызвать данный метод в основном классе активности:
public void onClick(View v)
Мы получим текст, который определён в суперклассе, хотя вызывали метод дочернего класса.
Но если мы хотим получить другой текст, совсем не обязательно придумывать новые методы. Достаточно закомментировать вызов метода из суперкласса и добавить свой вариант.
@Override String sleep() < //return super.sleep(); return "Коты постоянно спят!"; >
Запускаем программу и нажимаем на кнопку. И получим уже другой ответ, более соответствующий описанию среднестатистического кота. Заметьте, что код для щелчка кнопки мы не меняем, но система сама разберётся, что выводить нужно текст не из суперкласса, а из дочернего класса.
Рассмотрим другой пример переопределения методов. Создадим суперкласс Figure, который будет содержать размеры фигуры, а также метод для вычисления площади. А затем создадим два других класса Rectangle и Triangle, у которых мы переопределим данный метод.
class Figure < double dim1; double dim2; Figure(double a, double b) < dim1 = a; dim2 = b; >double area() < System.out.printLn("Площадь фигуры"); return 0; >> class Rectangle extends Figure < Rectangle(double a, double b) < super(a, b); >// Переопределяем метод double area() < System.out.println("Площадь прямоугольника"); return dim1 * dim2; >> class Triangle extends Figure < Triangle(double a, double b) < super(a, b); >// переопределяем метод double area() < System.out.println("Площадь треугольника"); return dim1 * dim2 / 2; >> // В главной активности Figure figure = new Figure(10, 10); Rectangle rectangle = new Rectangle(8, 5); Triangle triangle = new Triangle(10, 6); Figure fig; fig = figure; mInfoTextView.setText("Площадь равна " + fig.area); fig = rectangle; mInfoTextView.setText("Площадь равна " + fig.area); fig = triangle; mInfoTextView.setText("Площадь равна " + fig.area);
Как видите, во всех классах используется одно и тоже имя метода, но каждый класс по своему вычисляет площадь в зависимости от фигуры. Это очень удобно и позволяет не придумывать новые названия методов в классах, которые наследуются от базового класса.
Наследование классов
Наследование классов – это способ расширения одного класса другим классом.
Таким образом, мы можем добавить новый функционал к уже существующему.
Ключевое слово «extends»
Допустим, у нас есть класс Animal :
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > let animal = new Animal("Мой питомец");
Вот как мы можем представить объект animal и класс Animal графически:
…И мы хотели бы создать ещё один class Rabbit .
Поскольку кролики – это животные, класс Rabbit должен быть основан на Animal , и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать «общие» животные.
Синтаксис для расширения другого класса следующий: class Child extends Parent .
Давайте создадим class Rabbit , который наследуется от Animal :
class Rabbit extends Animal < hide() < alert(`$прячется!`); > > let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.hide(); // Белый кролик прячется!
Объект класса Rabbit имеет доступ как к методам Rabbit , таким как rabbit.hide() , так и к методам Animal , таким как rabbit.run() .
Внутри ключевое слово extends работает по старой доброй механике прототипов. Оно устанавливает Rabbit.prototype.[[Prototype]] в Animal.prototype . Таким образом, если метода не оказалось в Rabbit.prototype , JavaScript берет его из Animal.prototype .
Например, чтобы найти метод rabbit.run , движок проверяет (снизу вверх на картинке):
- Объект rabbit (не имеет run ).
- Его прототип, то есть Rabbit.prototype (имеет hide , но не имеет run ).
- Его прототип, то есть (вследствие extends ) Animal.prototype , в котором, наконец, есть метод run .
Как мы помним из главы Встроенные прототипы, сам JavaScript использует наследование на прототипах для встроенных объектов. Например, Date.prototype.[[Prototype]] является Object.prototype , поэтому у дат есть универсальные методы объекта.
После extends разрешены любые выражения
Синтаксис создания класса допускает указывать после extends не только класс, но и любое выражение.
Пример вызова функции, которая генерирует родительский класс:
function f(phrase) < return class < sayHi() < alert(phrase); >>; > class User extends f("Привет") <> new User().sayHi(); // Привет
Здесь class User наследует от результата вызова f(«Привет») .
Это может быть полезно для продвинутых приёмов проектирования, где мы можем использовать функции для генерации классов в зависимости от многих условий и затем наследовать их.
Переопределение методов
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе Rabbit , берутся непосредственно «как есть» из класса Animal .
Но если мы укажем в Rabbit собственный метод, например stop() , то он будет использован вместо него:
class Rabbit extends Animal < stop() < // . теперь это будет использоваться для rabbit.stop() // вместо stop() из класса Animal >>
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
У классов есть ключевое слово «super» для таких случаев.
- super.method(. ) вызывает родительский метод.
- super(. ) для вызова родительского конструктора (работает только внутри нашего конструктора).
Пусть наш кролик автоматически прячется при остановке:
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > class Rabbit extends Animal < hide() < alert(`$прячется!`); > stop() < super.stop(); // вызываем родительский метод stop this.hide(); // и затем hide >> let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.stop(); // Белый кролик стоит. Белый кролик прячется!
Теперь у класса Rabbit есть метод stop , который вызывает родительский super.stop() в процессе выполнения.
У стрелочных функций нет super
Как упоминалось в главе Повторяем стрелочные функции, стрелочные функции не имеют super .
При обращении к super стрелочной функции он берётся из внешней функции:
class Rabbit extends Animal < stop() < setTimeout(() =>super.stop(), 1000); // вызывает родительский stop после 1 секунды > >
В примере super в стрелочной функции тот же самый, что и в stop() , поэтому метод отрабатывает как и ожидается. Если бы мы указали здесь «обычную» функцию, была бы ошибка:
// Unexpected super setTimeout(function() < super.stop() >, 1000);
Переопределение конструктора
С конструкторами немного сложнее.
До сих пор у Rabbit не было своего конструктора.
Согласно спецификации, если класс расширяет другой класс и не имеет конструктора, то автоматически создаётся такой «пустой» конструктор:
class Rabbit extends Animal < // генерируется для классов-потомков, у которых нет своего конструктора constructor(. args) < super(. args); >>
Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим собственный конструктор.
Давайте добавим конструктор для Rabbit . Он будет устанавливать earLength в дополнение к name :
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < this.speed = 0; this.name = name; this.earLength = earLength; >// . > // Не работает! let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not defined.
Упс! При создании кролика – ошибка! Что не так?
Если коротко, то:
- Конструкторы в наследуемых классах должны обязательно вызывать super(. ) , и (!) делать это перед использованием this ..
…Но почему? Что происходит? Это требование кажется довольно странным.
Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
В JavaScript существует различие между «функцией-конструктором наследующего класса» и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством [[ConstructorKind]]:»derived» .
Разница в следующем:
- Когда выполняется обычный конструктор, он создаёт пустой объект и присваивает его this .
- Когда запускается конструктор унаследованного класса, он этого не делает. Вместо этого он ждёт, что это сделает конструктор родительского класса.
Поэтому, если мы создаём собственный конструктор, мы должны вызвать super , в противном случае объект для this не будет создан, и мы получим ошибку.
Чтобы конструктор Rabbit работал, он должен вызвать super() до того, как использовать this , чтобы не было ошибки:
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < super(name); this.earLength = earLength; >// . > // теперь работает let rabbit = new Rabbit("Белый кролик", 10); alert(rabbit.name); // Белый кролик alert(rabbit.earLength); // 10
Переопределение полей класса: тонкое замечание
Продвинутое замечание
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования.
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто).
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время.
Мы можем переопределять не только методы, но и поля класса.
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования.
Рассмотрим этот пример:
class Animal < name = 'animal'; constructor() < alert(this.name); // (*) >> class Rabbit extends Animal < name = 'rabbit'; >new Animal(); // animal new Rabbit(); // animal
Здесь, класс Rabbit расширяет Animal и переопределяет поле name своим собственным значением.
В Rabbit нет собственного конструктора, поэтому вызывается конструктор Animal .
Что интересно, в обоих случаях: new Animal() и new Rabbit() , alert в строке (*) показывает animal .
Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не переопределённое.
Что же в этом странного?
Если это ещё не ясно, сравните с методами.
Вот тот же код, но вместо поля this.name , мы вызываем метод this.showName() :
class Animal < showName() < // вместо this.name = 'animal' alert('animal'); >constructor() < this.showName(); // вместо alert(this.name); >> class Rabbit extends Animal < showName() < alert('rabbit'); >> new Animal(); // animal new Rabbit(); // rabbit
Обратите внимание: теперь результат другой.
И это то, чего мы, естественно, ожидаем. Когда родительский конструктор вызывается в производном классе, он использует переопределённый метод.
…Но для полей класса это не так. Как уже было сказано, родительский конструктор всегда использует родительское поле.
Почему же наблюдается разница?
Что ж, причина заключается в порядке инициализации полей. Поле класса инициализируется:
- Перед конструктором для базового класса (который ничего не расширяет),
- Сразу после super() для производного класса.
В нашем случае Rabbit – это производный класс. В нем нет конструктора constructor() . Как было сказано ранее, это то же самое, как если бы был пустой конструктор, содержащий только super(. args) .
Итак, new Rabbit() вызывает super() , таким образом, выполняя родительский конструктор, и (согласно правилу для производных классов) только после этого инициализируются поля его класса. На момент выполнения родительского конструктора ещё нет полей класса Rabbit , поэтому используются поля Animal .
Это тонкое различие между полями и методами характерно для JavaScript.
К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь.
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
Устройство super, [[HomeObject]]
Продвинутая информация
Если вы читаете учебник первый раз – эту секцию можно пропустить.
Она рассказывает о внутреннем устройстве наследования и вызов super .
Давайте заглянем «под капот» super . Здесь есть некоторые интересные моменты.
Вообще, исходя из наших знаний до этого момента, super вообще не может работать!
Ну правда, давайте спросим себя – как он должен работать, чисто технически? Когда метод объекта выполняется, он получает текущий объект как this . Если мы вызываем super.method() , то движку необходимо получить method из прототипа текущего объекта. И как ему это сделать?
Задача может показаться простой, но это не так. Движок знает текущий this и мог бы попытаться получить родительский метод как this.__proto__.method . Однако, увы, такой «наивный» путь не работает.
Продемонстрируем проблему. Без классов, используя простые объекты для наглядности.
Вы можете пропустить эту часть и перейти ниже к подсекции [[HomeObject]] , если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.
В примере ниже rabbit.__proto__ = animal . Попробуем в rabbit.eat() вызвать animal.eat() , используя this.__proto__ :
let animal = < name: "Animal", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // вот как предположительно может работать super.eat() this.__proto__.eat.call(this); // (*) >>; rabbit.eat(); // Кролик ест.
В строке (*) мы берём eat из прототипа ( animal ) и вызываем его в контексте текущего объекта. Обратите внимание, что .call(this) здесь неспроста: простой вызов this.__proto__.eat() будет выполнять родительский eat в контексте прототипа, а не текущего объекта.
Приведённый выше код работает так, как задумано: выполняется нужный alert .
Теперь давайте добавим ещё один объект в цепочку наследования и увидим, как все сломается:
let animal = < name: "Животное", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, eat() < // . делаем что-то специфичное для кролика и вызываем родительский (animal) метод this.__proto__.eat.call(this); // (*) >>; let longEar = < __proto__: rabbit, eat() < // . делаем что-то, связанное с длинными ушами, и вызываем родительский (rabbit) метод this.__proto__.eat.call(this); // (**) >>; longEar.eat(); // Error: Maximum call stack size exceeded
Теперь код не работает! Ошибка возникает при попытке вызова longEar.eat() .
На первый взгляд все не так очевидно, но если мы проследим вызов longEar.eat() , то сможем понять причину ошибки. В обеих строках (*) и (**) значение this – это текущий объект ( longEar ). Это важно: для всех методов объекта this указывает на текущий объект, а не на прототип или что-то ещё.
Итак, в обеих линиях (*) и (**) значение this.__proto__ одно и то же: rabbit . В обоих случаях метод rabbit.eat вызывается в бесконечном цикле не поднимаясь по цепочке вызовов.
Картина того, что происходит:
- Внутри longEar.eat() строка (**) вызывает rabbit.eat со значением this=longEar .
// внутри longEar.eat() у нас this = longEar this.__proto__.eat.call(this) // (**) // становится longEar.__proto__.eat.call(this) // то же что и rabbit.eat.call(this);
// внутри rabbit.eat() у нас также this = longEar this.__proto__.eat.call(this) // (*) // становится longEar.__proto__.eat.call(this) // или (снова) rabbit.eat.call(this);
Проблема не может быть решена с помощью одного только this .
[[HomeObject]]
Для решения этой проблемы в JavaScript было добавлено специальное внутреннее свойство для функций: [[HomeObject]] .
Когда функция объявлена как метод внутри класса или объекта, её свойство [[HomeObject]] становится равно этому объекту.
Затем super использует его, чтобы получить прототип родителя и его методы.
Давайте посмотрим, как это работает – опять же, используя простые объекты:
let animal = < name: "Животное", eat() < // animal.eat.[[HomeObject]] == animal alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // rabbit.eat.[[HomeObject]] == rabbit super.eat(); >>; let longEar = < __proto__: rabbit, name: "Длинноух", eat() < // longEar.eat.[[HomeObject]] == longEar super.eat(); >>; // работает верно longEar.eat(); // Длинноух ест.
Это работает как задумано благодаря [[HomeObject]] . Метод, такой как longEar.eat , знает свой [[HomeObject]] и получает метод родителя из его прототипа. Вообще без использования this .
Методы не «свободны»
До этого мы неоднократно видели, что функции в JavaScript «свободны», не привязаны к объектам. Их можно копировать между объектами и вызывать с любым this .
Но само существование [[HomeObject]] нарушает этот принцип, так как методы запоминают свои объекты. [[HomeObject]] нельзя изменить, эта связь – навсегда.
Единственное место в языке, где используется [[HomeObject]] – это super . Поэтому если метод не использует super , то мы все ещё можем считать его свободным и копировать между объектами. А вот если super в коде есть, то возможны побочные эффекты.
Вот пример неверного результата super после копирования:
let animal = < sayHi() < alert("Я животное"); >>; // rabbit наследует от animal let rabbit = < __proto__: animal, sayHi() < super.sayHi(); >>; let plant = < sayHi() < alert("Я растение"); >>; // tree наследует от plant let tree = < __proto__: plant, sayHi: rabbit.sayHi // (*) >; tree.sayHi(); // Я животное (. )
Вызов tree.sayHi() показывает «Я животное». Определённо неверно.
- В строке (*) , метод tree.sayHi скопирован из rabbit . Возможно, мы хотели избежать дублирования кода?
- Его [[HomeObject]] – это rabbit , ведь он был создан в rabbit . Свойство [[HomeObject]] никогда не меняется.
- В коде tree.sayHi() есть вызов super.sayHi() . Он идёт вверх от rabbit и берёт метод из animal .
Вот диаграмма происходящего:
Методы, а не свойства-функции
Свойство [[HomeObject]] определено для методов как классов, так и обычных объектов. Но для объектов методы должны быть объявлены именно как method() , а не «method: function()» .
Для нас различий нет, но они есть для JavaScript.
В приведённом ниже примере используется синтаксис не метода, свойства-функции. Поэтому у него нет [[HomeObject]] , и наследование не работает:
let animal = < eat: function() < // намеренно пишем так, а не eat() < . // . >>; let rabbit = < __proto__: animal, eat: function() < super.eat(); >>; rabbit.eat(); // Ошибка вызова super (потому что нет [[HomeObject]])
Итого
- Чтобы унаследовать от класса: class Child extends Parent :
- При этом Child.prototype.__proto__ будет равен Parent.prototype , так что методы будут унаследованы.
- При переопределении конструктора:
- Обязателен вызов конструктора родителя super() в конструкторе Child до обращения к this .
- При переопределении другого метода:
- Мы можем вызвать super.method() в методе Child для обращения к методу родителя Parent .
- Внутренние детали:
- Методы запоминают свой объект во внутреннем свойстве [[HomeObject]] . Благодаря этому работает super , он в его прототипе ищет родительские методы.
- Поэтому копировать метод, использующий super , между разными объектами небезопасно.
- У функций-стрелок нет своего this и super , поэтому они «прозрачно» встраиваются во внешний контекст.
Задачи
Ошибка создания экземпляра класса
важность: 5
В коде ниже класс Rabbit наследует Animal .
К сожалению, объект класса Rabbit не создаётся. Что не так? Исправьте ошибку.
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < this.name = name; this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // Error: this is not defined alert(rabbit.name);
Ошибка возникает потому, что конструктор дочернего класса должен вызывать super() .
Вот правильный код:
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < super(name); this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // ошибки нет alert(rabbit.name); // White Rabbit
Улучшенные часы
важность: 5
У нас есть класс Clock . Сейчас он выводит время каждую секунду
class Clock < constructor(< template >) < this.template = template; >render() < let date = new Date(); let hours = date.getHours(); if (hours < 10) hours = '0' + hours; let mins = date.getMinutes(); if (mins < 10) mins = '0' + mins; let secs = date.getSeconds(); if (secs < 10) secs = '0' + secs; let output = this.template .replace('h', hours) .replace('m', mins) .replace('s', secs); console.log(output); >stop() < clearInterval(this.timer); >start() < this.render(); this.timer = setInterval(() =>this.render(), 1000); > >
Создайте новый класс ExtendedClock , который будет наследоваться от Clock и добавьте параметр precision – количество миллисекунд между «тиками». Установите значение в 1000 (1 секунда) по умолчанию.
- Сохраните ваш код в файл extended-clock.js
- Не изменяйте класс clock.js . Расширьте его.
class ExtendedClock extends Clock < constructor(options) < super(options); let < precision = 1000 >= options; this.precision = precision; > start() < this.render(); this.timer = setInterval(() =>this.render(), this.precision); > >;
Что означает ключевое слово extends
В этой статье сделана попытка объяснить некоторые термины объектно-ориентированного программирования Java, и ответить на вопросы: что значит слово extends в определении класса? Что значит слово implements в определении класса? В чем разница между extends и implements? Что такое interface? Что такое @Override?
Если коротко, то:
extends это ключевое слово, предназначенное для расширения реализации какого-то существующего класса. Создается новый класс на основе существующего, и этот новый класс расширяет (extends) возможности старого.
implements это ключевое слово, предназначенное для реализации интерфейса (interface).
Оба ключевых слова extends и implements используются, когда Вы создаете свой собственный класс на языке Java. Различие между ними в том, что implements означает, что Вы используете элементы интерфейса в Вашем классе, а extends означает, что Вы создаете подкласс от класса, который расширяете (extend). В новом классе Вы можете расширить только один класс, но Вы можете реализовать столько интерфейсов, сколько захотите.
Тут появилось словечко интерфейс (interface). Разница между interface и обычным классом (regular class) — то, что в интерфейсе Вы не можете определить определенную реализацию (только ее «интерфейс»), а в классе можете. Если сказать точнее, то это означает, что в интерфейсе Вы можете только указать методы, но не реализовывать их. Только класс может реализовать (implement) интерфейс. Класс также может расширить (extend) другой класс. Аналогично, интерфейс может расширить другой интерфейс. Реализация (implements) используется для интерфейса, и расширение (extends) используется для расширения класса. Когда Вы должны выбрать между реализацией интерфейса или расширением класса, пойдите по пути реализации интерфейса, так как класс может реализовать множество интерфейсов, но расширить можно только один класс.
Java не поддерживает множественное наследование (multiple inheritance) для классов. Эта проблема также решается путем использования нескольких интерфейсов.
@Override ключевое слово, которое позволяет в дочернем классе заново создать реализацию метода родительского класса.
Пример реализации интерфейса (как используется ключевое слово implements):
//Интерфейс, здесь нет реализации методов, // только их объявления: public interface ExampleInterface public void do(); public String doThis(int number); >
Интерфейс также может содержать в себе декларации полей констант, аннотации, интерфейсы и даже классы.
//А вот это уже реализация (применение ключевого слова implements): public class sub implements ExampleInterface public void do() //определите то, что должно произойти . > public String doThis(int number) //определите то, что должно произойти . > >
Теперь пример расширения класса (применение ключевого слова extends):
//Исходный класс, который будет расширен: public class SuperClass public int getNb() //определите то, что должно произойти return 1; > public int getNb2() //определите то, что должно произойти return 2; > >
//Производный класс, расширяющий исходный: public class SubClass extends SuperClass //Вы можете переназначить (override) реализацию метода: @Override public int getNb2() return 3; > >
Вот что получится в результате:
SubClass s = new SubClass(); s.getNb(); //возвращает 1 s.getNb2(); //возвращает 3
SuperClass sup = new SuperClass(); sup.getNb(); //возвращает 1 sup.getNb2(); //возвращает 2
Чтобы лучше понять работу терминов extends, implements, interface, @Override, необходимо изучить принципы объектно-ориентированного программирования: динамическое связывание (dynamic binding), полиморфизм (polymorphism) и общее наследование (general inheritance) [1].
[Ссылки]
1. Lesson: Interfaces and Inheritance site:docs.oracle.com .