Отношения между классами. Наследование, композиция и агрегация

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

Привет! Сегодня мы более подробно рассмотрим один из принципов объектно-ориентированного программирования (ООП): наследование. А заодно познакомимся и с другими видами связей между классами: композицией и агрегацией.

Эта тема не будет сложной: с наследованием и примерами наследования мы уже рассказывали в прошлых статьях. Сегодня наша главная цель – закрепить полученные знания, более подробно рассмотреть механизм наследования и еще раз пробежаться по примерам. 🙂

Что ж, поехали!

Наследование в Java и его преимущества

Как вы наверняка помните, наследование – это механизм, позволяющий описать новый класс на основе существующего класса (родительского). При этом новый класс перенимает свойства и функциональность родительского класса.

Давайте вспомним пример наследования, который мы разбирали в предыдущих статьях:

public class Car {

   private String model;
   private int maxSpeed;
   private int yearOfManufacture;

   public Car(String model, int maxSpeed, int yearOfManufacture) {
       this.model = model;
       this.maxSpeed = maxSpeed;
       this.yearOfManufacture = yearOfManufacture;
   }


public void gas() {
       // Газ
   }

   	public void brake() {
       // Тормоз
   }
}


public class Truck extends Car {

   public Truck(String model, int maxSpeed, int yearOfManufacture) {
       super(model, maxSpeed, yearOfManufacture);
   }
}



public class Sedan extends Car {
   public Sedan(String model, int maxSpeed, int yearOfManufacture) {
       super(model, maxSpeed, yearOfManufacture);
   }
}
Run Code

У нас есть определенная программа, которая предполагает работу с различными типами автомобилей. Даже если вы не являетесь автолюбителем, вы наверняка знаете, что в мире существует огромное количество типов автомобилей. 🙂

Поэтому общие свойства всех машин мы вынесли в родительский класс Car. Итак, что же объединяет все машины, независимо от их разновидности?

У каждого автомобиля есть год выпуска, название модели и максимальная скорость. Эти характеристики мы разместили в полях model, maxSpeed и yearOfManufacture. Что касается поведения – любая машина умеет разгоняться и тормозить 🙂 Эти действия мы описали в методах gas() и brake().

Зачем нужно наследование?

Какие преимущества это нам дает? Прежде всего, это уменьшает количество кода.

Конечно, можно было бы обойтись без родительского класса. Но тогда в каждом классе Truck, Sedan, F1Car и SportsCar пришлось бы заново писать методы gas() и brake(). А еще создавать поля model, maxSpeed и yearOfManufacture. Представьте, сколько бы мы набрали лишнего кода, если в проекте десятки типов авто.

Когда у нас есть несколько десятков классов автомобилей, количество дублирующегося кода становится действительно серьезным.

Вынося общие поля и методы (их ещё называют состояния и поведения) в родительский класс, мы серьезно экономим и время, и место. А если какой-то тип машины имеет уникальные свойства или методы, которых нет у других? Нет проблем! Их можно спокойно добавить в дочерний класс.

Пример:

public class F1Car extends Car {

   public void pitStop() {

       // Только гоночные машины делают пит-стопы
   }

   public static void main(String[] args) {

       F1Car formula1Car = new F1Car();
       formula1Car.gas();
       formula1Car.pitStop();
       formula1Car.brake();
   }
}
Run Code

На примере болида Формулы-1 видно: в отличие от других машин, у него есть особенное поведение – пит-стоп.

Это не мешает общей структуре наследования: общие действия описаны в родительском Car, а специфичные методы можно спокойно добавлять в потомке. То же самое и с полями: если у дочернего класса появляются уникальные свойства, мы просто объявляем их внутри него – и все.

Relationships between classes. Inheritance, composition, and aggregation - 3

То же самое касается и полей: если у дочернего класса есть уникальные свойства, мы спокойно объявляем эти поля внутри дочернего класса и перестаем беспокоиться 🙂 Возможность повторного использования кода – главное преимущество наследования.

Для программиста это особенно важно: писать как можно меньше лишнего кода. С этим принципом вы еще не раз столкнетесь на практике.

Особенности наследования в Java

Еще одна важная вещь, которую нужно запомнить – в Java нет множественного наследования. Каждый класс может наследовать только один родительский класс. Почему так устроено, мы обсудим позже. Сейчас просто стоит это запомнить.

С наследованием все более-менее понятно. Давайте двигаться дальше.

Композиция и агрегация

Классы и объекты могут быть связаны друг с другом. Наследование описывает связь типа “является“. Например: лев — это животное. Такую связь легко выразить через наследование, где Animal – родительский класс, а Lion – дочерний.

Однако не все отношения описываются таким образом. Например, клавиатура определенно связана с компьютером, но это не компьютер. Руки как-то связаны с человеком, но являются человеком.

В таких случаях связь описывается по-другому: не “является“, а “имеет“. Рука не является человеком, но является частью человека. Клавиатура не является компьютером, но является его частью.

Такую “has-a” связь можно выразить в коде с помощью агрегации и композиции. Главное отличие между ними – в “строгости” этой связи.

Приведем простой пример: у нас есть класс Car. У каждого автомобиля есть двигатель. Кроме того, в каждом автомобиле есть пассажиры.

У нас есть класс Car. У каждого автомобиля есть двигатель. Кроме того, в каждом автомобиле есть пассажиры.

В чем принципиальная разница между движком Engine и полями пассажиров Passenger[]? Если в машине сидит пассажир А, это не мешает сидеть в ней пассажирам В и С. Машина может одновременно везти несколько пассажиров. Более того, если все пассажиры выйдут из автомобиля, он все равно будет работать без сбоев.

Отношения между классом Car и массивом Passenger[] passengers менее строгие. Такую связь называют агрегацией.

Другой пример агрегации: предположим, у нас есть класс Student и класс StudentGroup. Студент может вступить в несколько студенческих организаций: клуб физиков, клуб фанатов “Звездных войн” и/или студенческий комедийный клуб.

Композиция – это более строгий вид отношений. Когда мы говорим о композиции, объект является частью другого объекта и не может одновременно принадлежать другому объекту того же типа.

Самый простой пример – двигатель автомобиля. Двигатель является частью автомобиля и не может быть частью другого автомобиля. Как видите, их отношения гораздо строже, чем отношения между Car и Passengers.

Теперь вы знаете, как классы могут быть связаны друг с другом с помощью наследования, композиции и агрегации. Эти механизмы помогают строить более гибкие и понятные программы. Дальше будет еще интереснее!

Перевод статьи «Relationships between classes. Inheritance, composition, and aggregation».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх