Якийсь час тому я написав кілька постів про успадкування , інтерфейсі і композиції у Java. У цій статті ми розглянемо множинне спадкування, а потім дізнаємося про переваги композиції порівняно з спадкуванням. Множинне успадкування - це можливість створення одного класу з кількома суперкласами. На відміну від деяких інших популярних мовно-орієнтованих мов програмування, таких як C++, Java не забезпечує підтримку множинного успадкування у класах . Java не підтримує множинне спадкування в класах, тому що це може призвести до проблемі з діамантами, і замість того, щоб надавати якийсь складний спосіб її вирішення, існують ефективніші способи, за допомогою яких ми можемо досягти того ж результату, що й множинне спадкування. Щоб легко зрозуміти проблему алмазів, припустимо, що множинне успадкування було підтримано Java. У цьому випадку ми можемо мати ієрархію класів, як показано нижче. Припустимо, SuperClass - це абстрактний клас, який оголошує деякий метод, а ClassA, ClassB - це конкретні класи. Одним із ключових аспектів об'єктно-орієнтованого програмування є успадкування. За допомогою успадкування можна розширити функціонал вже існуючих класів за рахунок додавання нового функціоналу або зміни старого. Наприклад, є наступний клас Person, який описує окрему людину: І, можливо, згодом ми захочемо додати ще один клас, який описує співробітник підприємства - клас Employee.Так як цей клас реалізує той же функціонал, що і клас Person, оскільки співробітник - це також і людина, то було б раціонально зробити клас Employee похідним (спадкоємцем, підкласом) від класу Person, який, своєю чергою, називається базовим класом, батьком або суперкласом: Щоб оголосити один клас спадкоємцем від іншого, треба використовувати після імені класу-спадкоємця ключове слово extends , після якого йде ім'я базового класу. . Якщо в базовому класі визначені конструктори, то в конструкторі похідного класи необхідно викликати один із конструкторів базового класу за допомогою ключового слова super . є виклик super(name) представлятиме виклик конструктора класу Person. При виклику конструктора після слова super в дужках йде перерахування аргументів, що передаються При цьому виклик конструктора базового класу повинен йти на самому початку в конструкторі похідного класу. Причому навіть якщо похідний клас жодної іншої роботи не виробляє в конструкторі, як у прикладі вище, однаково необхідно викликати конструктор базового класу. Похідний клас має доступ до всіх методів та полів базового класу (навіть якщо базовий клас знаходиться в іншому пакеті), крім тих, що визначені з модифікатором private. При цьому похідний клас також може додавати свої поля та методи: У разі клас Employee додає поле company, яке зберігає місце роботи співробітника, і навіть метод work. Похідний клас може визначати свої методи, а може перевизначати методи, успадковані від базового класу. Наприклад, перевизначимо в класі Employee метод display: Перед методом, що перевизначається, вказується анотація @Override . Ця інструкція в принципі необов'язкова. При перевизначенні методу він повинен мати рівень доступу не менше, ніж рівень доступу до базового класу. Наприклад, якщо у базовому класі метод має модифікатор public, то й у похідному класі метод повинен мати модифікатор public. Однак у цьому випадку бачимо, що частина методу display в Employee повторює дії з методу display базового класу. Тому ми можемо скоротити клас Employee: За допомогою ключового слова super ми можемо також звернутися до реалізації методів базового класу. Хоча успадкування є дуже цікавим і ефективним механізмом, але в деяких ситуаціях його застосування може бути небажаним. І в цьому випадку можна заборонити наслідування за допомогою ключового слова final. Наприклад: Якби клас Person був визначений таким чином, то наступний код був би помилковим і не спрацював, тому що ми тим самим заборонили успадкування: Крім заборони успадкування, можна також заборонити перевизначення окремих методів.Наприклад, у прикладі вище перевизначено метод display(), заборонимо його перевизначення: І тут клас Employee зможе перевизначити метод display. Наслідування та можливість перевизначення методів відкривають нам великі можливості. Насамперед ми можемо передати змінній суперкласу посилання на об'єкт підкласу: Оскільки Employee успадковується від Person, об'єкт Employee є водночас і об'єктом Person. Грубо кажучи, будь-який працівник підприємства одночасно є людиною. Однак, незважаючи на те, що змінна представляє об'єкт Person, віртуальна машина бачить, що насправді вона вказує на об'єкт Employee. Тому при викликі методів цього об'єкта буде викликатися та версія методу, яка визначена в класі Employee, а не в Person. Наприклад: Консольний висновок цієї програми: При виклику перевизначеного методу віртуальна машина динамічно знаходить і викликає саме ту версію методу, що у підкласі. Цей процес ще називається dynamic method lookup або динамічний пошук методу або динамічна диспетчеризація методів. Простіше кажучи, Java клас може успадковувати інший клас і кілька інтерфейсів, а інтерфейс може успадковувати інші інтерфейси. У цій статті ми почнемо з необхідності успадкування та перейдемо до того, як успадкування працює з класами та інтерфейсами. Потім ми розглянемо, як імена змінних/методів та модифікатори доступу впливають на успадковані члени. І наприкінці ми побачимо, що означає наслідувати тип. Уявіть, що ви є виробником автомобілів і пропонуєте своїм клієнтам кілька моделей автомобілів. Незважаючи на те, що різні моделі автомобілів можуть пропонувати різні функції, такі як люк на даху або куленепробивні вікна, всі вони включатимуть загальні компоненти та функції, такі як двигун та колеса. Має сенс створювати базовий дизайн та розширювати його для створення своїх спеціалізованих версій, а не проектувати кожну модель автомобіля окремо з нуля. Аналогічно з наслідуванням ми можемо створити клас з базовими функціями та поведінкою та створити його спеціалізовані версії, створивши класи, які успадковують цей базовий клас. Так само інтерфейси можуть розширювати існуючі інтерфейси. Ми помітимо використання кількох термінів для позначення типу, що успадковується іншим типом, а саме: Клас може успадковувати інший клас та визначати додаткові члени. Почнемо з визначення базового класу Car: Клас ArmoredCar може успадковувати членів класу Car, використовуючи ключове слово extends у своєму оголошенні : Тепер ми можемо сказати, що клас ArmoredCar є підкласом Car, а останній є надкласом ArmoredCar. Класи в Java підтримують поодиноке успадкування ; Клас ArmoredCar не може розширювати кілька класів. Також зауважте, що за відсутності ключового слова extends клас неявно успадковує клас java.lang.Object . Клас підкласу успадковує нестатичні захищені та відкриті члени від класу суперкласу. Крім того, члени з доступом за замовчуванням (package-private) успадковуються, якщо два класи знаходяться в одному пакеті. З іншого боку, приватні та статичні члени класу не успадковуються. Щоб отримати доступ до успадкованих властивостей або методів, ми можемо просто використовувати їх безпосередньо: Зверніть увагу, що нам не потрібне посилання на суперклас для доступу до його членів. Хоча класи можуть успадковувати лише один клас, вони можуть реалізовувати кілька інтерфейсів. Уявіть собі, що ArmoredCar, який ми визначили в попередньому розділі, потрібний для супершпигуна. Тому компанія-виробник автомобілів подумала про додавання функцій польоту та плавання: У наведеному вище прикладі ми помітили використання ключового слова "реалізує" для успадкування від інтерфейсу. Java допускає множинне наслідування з використанням інтерфейсів. До Java 7 це було проблемою.Інтерфейси могли визначати лише абстрактні методи, тобто методи без будь-якої реалізації. Тому, якщо клас реалізовував кілька інтерфейсів з тією самою сигнатурою методу, це було проблемою. Зрештою реалізуючий клас мав реалізувати лише один метод. Давайте подивимося, як це просте рівняння змінилося з введенням в інтерфейси методів за замовчуванням Java 8. Починаючи з Java 8, інтерфейси могли визначати за промовчанням для своїх методів (інтерфейс, як і раніше, може визначати абстрактні методи). Це означає, що якщо клас реалізує кілька інтерфейсів, які визначають методи з однією і тією самою сигнатурою, дочірній клас успадкує окремі реалізації. Це звучить складно та не дозволено. Java забороняє успадкування кількох реалізацій тих самих методів, визначених окремих інтерфейсах. Якщо ми дійсно хочемо реалізувати обидва інтерфейси, нам доведеться перевизначити метод repair(). Якщо інтерфейси в попередніх прикладах визначають змінні з однаковими іменами, скажімо, duration, ми не зможемо отримати до них доступу, якщо перед ім'ям змінної не буде вказано ім'я інтерфейсу: Інтерфейс може розширювати кілька інтерфейсів. Ось приклад: Інтерфейс успадковує інші інтерфейси за допомогою ключового слова extends . Коли клас успадковує інший клас або інтерфейси, крім успадкування їх членів, він також успадковує їх тип. Це дуже потужна концепція, яка дозволяє розробникам програмувати інтерфейс (базовий клас чи інтерфейс) , а не програмування їх реалізації. Наприклад, уявіть умову, при якій організація веде список автомобілів, що належать її співробітникам. Звичайно, у всіх співробітників можуть бути різні моделі автомобілів. Оскільки всі похідні класи Car успадковують тип Car, на екземпляри похідних класів можна посилатися за допомогою змінної класу Car: Що станеться, якщо і в суперкласі, і в підкласі буде визначено змінну або метод з однаковим ім'ям ?Не хвилюйся; ми все ще можемо отримати доступ до них обом. Ключове слово this відноситься до екземпляра, в якому воно використовується. Багато розробників використовують ключові слова this і super, щоб явно вказати, на яку змінну чи метод вони посилаються. Однак використання їх з усіма членами може зробити наш код захаращеним. Що відбувається, коли наш базовий клас та підкласи визначають статичні змінні та методи з однаковими іменами ? Чи можемо ми отримати доступ до статичного члена з базового класу у похідному класі так само, як ми робимо це для змінних екземплярів? Розберемося на прикладі: Ні, ми не можемо. Статичні члени належать класу, а чи не екземплярам. Тому ми можемо використовувати нестатичне ключове слово super в msg() . Оскільки статичні члени належать до класу, ми можемо змінити попередній виклик наступним чином: Розглянемо наступний приклад, у якому і базовий клас, і похідний клас визначають статичний метод msg() з тією самою сигнатурою: Ось як ми можемо їх назвати: Для попереднього коду first.msg() виведе "Car", а second.msg() виведе "ArmoredCar". Статичне повідомлення, яке викликається, залежить від типу змінної, яка використовується для посилання на екземпляр ArmoredCar. У цій статті ми розглянули ключовий аспект Java - успадкування. Ми побачили, як Java підтримує одиночне успадкування з класами та множинне успадкування з інтерфейсами, та обговорили тонкощі того, як цей механізм працює у мові. Як завжди, повний вихідний код прикладів доступний на GitHub.Множинне успадкування в Java та композиція проти успадкування
Множинне успадкування в Java
Алмазна проблема
Як у Java підтримує множинне спадкування?
class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >>
class Employee extends Person < public Employee(String name)< super(name); // якщо базовий клас визначає конструктор // похідний клас повинен його викликати >>
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Employee sam = New Employee("Sam"); sam.display(); >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < public Employee(String name)< super(name); // якщо базовий клас визначає конструктор // то похідний клас має його викликати >>
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam sam.work(); // Sam works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >public void work() < System.out.printf("%s works in %s \n", getName(), company); >>
Перевизначення методів
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam // Works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < System.out.printf("Name: %s \n", getName()); System.out.printf("Works in %s \n", company); >>
class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < super.display(); System.out.printf("Works in %s \n", company); >>
Заборона наслідування
Динамічна диспетчеризація методів
Person sam = New Employee("Sam", "Oracle");
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Person sam = New Employee("Sam", "Oracle"); sam.display(); >> class Person < String name; public String getName() < return name; >public Person(String name) < this.name=name; >public void display() < System.out.printf("Person %s \n", name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company = company; >@Override public void display() < System.out.printf("Employee %s works in %s \n", super.getName(), company); >>
Person Tom Employee Sam works in Oracle
Посібник з наслідування в Java
2. Потреба у спадщині
3. Спадкування класів
3.1. Розширення класу
public
class
Car
int
wheels;
String
model;
void
start()
// Check essential parts
>
>
public
class
ArmoredCar
extends
Car
int
bulletProofWindows;
void
remoteStartCar()
// Цей автомобіль може бути здійснений за допомогою remote control
>
>
3.2. Доступ до батьківських елементів із дочірнього класу
public
class
ArmoredCar
extends
Car
public
String
registerModel()
return
model;
>
>
4. Спадкування інтерфейсу
4.1. Реалізація кількох інтерфейсів
public
interface
Floatable
void
floatOnWater();
>
public
interface
Flyable
void
fly();
>
public
class
ArmoredCar
extends
Car
implements
Floatable,
Flyable
public
void
floatOnWater()
System.out.println("I can float!");
>
public
void
fly()
System.out.println("I can fly!");
>
>
4.2. Проблеми з множинним успадкуванням
public
interface
Floatable
default
void
repair()
System.out.println("Repairing Floatable object");
>
>
public
interface
Flyable
default
void
repair()
System.out.println("Repairing Flyable object");
>
>
public
class
ArmoredCar
extends
Car
implements
Floatable,
Flyable
// this won't compile
>
public
interface
Floatable
int
duration
=
10;
>
public
interface
Flyable
int
duration
=
20;
>
public
class
ArmoredCar
extends
Car
implements
Floatable,
Flyable
public
void
aMethod()
System.out.println(duration);
// won't compile
System.out.println(Floatable.duration);
// outputs 10
System.out.println(Flyable.duration);
// outputs 20
>
>
4.3. Інтерфейси, що розширюють інші інтерфейси
public
interface
Floatable
void
floatOnWater();
>
interface
interface
Flyable
void
fly();
>
public
interface
SpaceTraveller
extends
Floatable,
Flyable
void
remoteControl();
>
5. Спадкування типу
public
class
Employee
private
String
name;
private
Car
car;
// standard constructor
>
Employee
e1
=
new
Employee("Shreya",
new
ArmoredCar());
Employee
e2
=
new
Employee("Paul",
new
SpaceCar());
Employee
e3
=
new
Employee("Pavni",
new
BMW());
6. Приховані члени класу
6.1.
public
class
ArmoredCar
extends
Car
private
String
model;
public
String
getAValue()
return
super.model;
// returns значення modelу, визначене в class base Car
// return this.model; // will return value of model defined in ArmoredCar
// return model; // will return value of model defined in ArmoredCar
>
>
6.2. Приховані статичні члени
public
class
Car
public
static
String
msg()
return
"Car";
>
>
public
class
ArmoredCar
extends
Car
public
static
String
msg()
return
super.msg();
// this won't compile.
>
>
public
class
Car
public
static
String
msg()
return
"Car";
>
>
public
class
ArmoredCar
extends
Car
public
static
String
msg()
return
"ArmoredCar";
>
>
Car
first
=
new
ArmoredCar();
ArmoredCar
second
=
new
ArmoredCar();
7. Висновок