1. Що таке ієрархія класів?
У програмуванні (і не лише в ньому) ієрархія — це деревоподібна структура: на вершині — «загальні» сутності (базові класи), а нижче — конкретніші (підкласи). У Java ієрархію класів будують за допомогою наслідування: кожен підклас може сам бути батьком для інших підкласів і так далі.
Аналогія:
- Тварина (Animal) — це загальний клас.
- Ссавець (Mammal) — це частковий випадок тварини.
- Собака (Dog) — це частковий випадок ссавця.
- А ось конкретний «Шарік» — це вже обʼєкт класу Dog.
У коді це виглядає так:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
Ієрархія класів дає змогу описувати спільні властивості й поведінку «нагорі», а деталі — «внизу». Це робить код логічнішим і допомагає уникнути дублювання.
Схема ієрархії
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Як будувати ієрархію: логіка й практика
Визначаємо загальне і часткове
Головне правило: базовий клас має містити те, що характерно для всіх його нащадків, а все унікальне переносимо в підкласи.
Приклад:
- Усі тварини можуть дихати та їсти — отже, методи breathe() і eat() мають бути в класі Animal.
- Тільки птахи вміють літати — отже, метод fly() буде в класі Bird, а не в Animal.
- Тільки собаки вміють гавкати — метод bark() буде в класі Dog.
Приклад: Тварини
// Базовий клас
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " їсть.");
}
void makeSound() {
System.out.println(name + " видає звук.");
}
}
// Підклас: Ссавець
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " годує дитинчат молоком.");
}
}
// Підклас: Собака
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " гавкає: Гав-гав!");
}
void wagTail() {
System.out.println(name + " виляє хвостом.");
}
}
// Підклас: Кішка
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " нявкає: Няв!");
}
void purr() {
System.out.println(name + " муркоче.");
}
}
// Підклас: Птах
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " літає.");
}
@Override
void makeSound() {
System.out.println(name + " цвірінькає: Чік-чірік!");
}
}
Виклик у main():
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Шарік");
Cat murka = new Cat("Мурка");
Bird sparrow = new Bird("Горобець");
sharik.eat(); // Шарік їсть.
sharik.makeSound(); // Шарік гавкає: Гав-гав!
sharik.feedMilk(); // Шарік годує дитинчат молоком.
sharik.wagTail(); // Шарік виляє хвостом.
murka.eat(); // Мурка їсть.
murka.makeSound(); // Мурка нявкає: Няв!
murka.feedMilk(); // Мурка годує дитинчат молоком.
murka.purr(); // Мурка муркоче.
sparrow.eat(); // Горобець їсть.
sparrow.makeSound(); // Горобець цвірінькає: Чік-чірік!
sparrow.fly(); // Горобець літає.
}
}
Візуалізація: дерево класів
| Клас | Батьківський клас | Особливості |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Ще приклади з життя
Геометричні фігури
Ієрархії класів чудово підходять для моделювання геометрії.
// Базовий клас
class Shape {
void draw() {
System.out.println("Малюємо фігуру.");
}
}
// Коло
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Малюємо коло радіусом " + radius);
}
}
// Прямокутник
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Малюємо прямокутник " + width + "x" + height);
}
}
Використання:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Малюємо коло радіусом 5.0
s2.draw(); // Малюємо прямокутник 3.0x4.0
}
}
Транспорт
class Vehicle {
void move() {
System.out.println("Транспорт рухається.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("Автомобіль їде дорогою.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("Велосипедист крутить педалі.");
}
}
main():
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // Автомобіль їде дорогою.
v2.move(); // Велосипедист крутить педалі.
Користувачі
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " увійшов у систему."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " видалив користувача " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " здійснив покупку."); }
}
4. Корисні нюанси
Не зловживайте наслідуванням. Наслідування — інструмент для відношення «is-a». Якщо ви хочете сказати «Кішка — це Тварина», використовуйте наслідування. Якщо ж «Кішка містить хвіст», використовуйте композицію («has-a»).
Погано:
class Engine { /* ... */ }
class Car extends Engine { /* Автомобіль — це двигун? Ні, це композиція! */ }
Добре:
class Car {
Engine engine; // Автомобіль містить двигун
}
Не створюйте надто глибоких ієрархій. Чим більше рівнів — тим складніше підтримувати та розуміти код. Зазвичай 2–3 рівні — це максимум для більшості завдань.
Не робіть надто «пласких» ієрархій. Якщо у вас є 20 класів і всі вони наслідують безпосередньо від одного базового, можливо, варто переглянути архітектуру.
Наслідування «заради зручності». Іноді хочеться «позичити» методи або поля, не замислюючись про логіку. Це шлях до хаосу! Якщо class A і class B не повʼязані логічно, не варто робити class B extends A лише заради кількох методів.
Порушення принципу підстановки Лісков. Якщо підклас не може бути використаний замість батьківського класу без несподіванок — ієрархію побудовано неправильно.
Дублювання коду. Якщо ловите себе на думці: «цей метод копіюється з класу в клас», можливо, час винести його в базовий клас.
5. Типові помилки під час побудови ієрархій класів
Помилка № 1: Наслідування без логіки «is-a».
Якщо ви використовуєте наслідування лише заради доступу до методів або полів, а не тому, що підклас справді «є» батьківським, ваш код швидко стане безладним. Наприклад, «Автомобіль наслідує Двигун» — це не is-a, а has-a.
Помилка № 2: Ігнорування унікальних особливостей підкласу.
Якщо всі ваші підкласи виглядають однаково й не додають нічого нового, можливо, ієрархія не потрібна — використовуйте один клас.
Помилка № 3: Дублювання коду в підкласах.
Якщо ви копіюєте одну й ту саму реалізацію в кілька підкласів, варто винести її в батьківський клас.
Помилка № 4: Надто складна або глибока ієрархія.
Багаторівневі ієрархії складно підтримувати й тестувати. Чим простіше — тим краще!
Помилка № 5: Перевизначення методів без анотації @Override.
Без анотації легко помилитися в сигнатурі методу й тоді метод не буде перевизначений, а вважатиметься новим. Завжди використовуйте @Override!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ