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!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ