Привет! Сегодня мы подробно рассмотрим еще один принцип объектно-ориентированного программирования (ООП) — наследование. Заодно изучим другие типы отношений между классами — композицию и агрегирование.
Эта тема не будет сложной: ты уже много раз сталкивался с наследованием и его примерами в прошлых лекциях. Сегодня главным будет закрепить твои знания, подробнее рассмотреть механизм наследования и еще раз пробежаться по примерам :) Итак, поехали!

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

Как ты наверняка помнишь, наследование (inheritance) — механизм, который позволяет описать новый класс на основе существующего (родительского). При этом свойства и функциональность родительского класса заимствуются новым классом. Давай вспомним пример наследования из предыдущих лекций:
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);
   }
}
Есть некая программа, в рамках которой мы работаем с различными типами автомобилей. Даже если ты не автолюбитель, наверняка знаешь, что типов этих самых автомобилей на свете великое множество :) Поэтому общие свойства автомобилей выделяем в общий класс-родитель — Car. А что общего у всех автомобилей вне зависимости от типа? У любой машины есть год выпуска, название модели и максимальная скорость. Эти свойства выносим в поля model, maxSpeed, yearOfManufacture. Что касается поведения, любая машина может газовать и тормозить :) Это поведение мы определяем в методах gas() и brake(). Какие выгоды это нам дает? Прежде всего — сокращение объема кода. Конечно, можем обойтись и без родительского класса. Но поскольку каждая машина должна уметь газовать и тормозить, нам придется создавать методы gas() и brake() в классе Truck, в классе Sedan, в классе F1Car, в классе Sportcar и во всех остальных классах машин. Представь, сколько лишнего кода мы при этом напишем. Не забывай и о полях 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();
   }
}
Возьмем случай с гоночными машинами Формулы-1. У них, в отличие от «сородичей», есть уникальное поведение — время от времени они заезжают на пит-стоп. Нам это не мешает. Общее поведение мы уже описали в родительском классе Car, а специфическое поведение классов-потомков можем добавить внутри классов. Это касается и полей: если у дочернего класса есть уникальные свойства, спокойно объявляем эти поля внутри дочернего него и не переживаем :) Возможность повторного использования кода — главное преимущество наследования. Для программиста очень важно не писать лишний объем кода. Ты не раз столкнешься с этим в работе. Пожалуйста, запомни еще одну крайне важную вещь: в Java нет множественного наследования. Каждый класс наследуется только от одного класса. О причинах этого подробнее поговорим в будущих лекциях, пока просто запомни. Этим Java, кстати, отличается от некоторых других ООП-языков. Например, в С++ множественное наследование есть. С наследованием все более-менее ясно — идем дальше.

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

Классы и объекты могут быть связаны друг с другом. Наследование описывает связь «является» (или по-английски «IS A»). Лев является Животным. Такое отношение легко выразить с помощью наследования, где Animal будет родительским классом, а Lion — потомком. Однако не все связи отношения в мире описываются таким образом. К примеру, клавиатура определенно как-то связана с компьютером, но она не является компьютером. Руки как-то связаны с человеком, но они не являются человеком. В этих случаях в его основе лежит другой тип отношения: не «является», а «является частью» («HAS A»). Рука не является человеком, но является частью человека. Клавиатура не является компьютером, но является частью компьютера. Отношения HAS A можно описать в коде, используя механизмы композиции и агрегирования. Разница между ними заключается в «строгости» этих связей. Приведем простой пример: У нас есть наш Car — машина. У каждой машины есть двигатель. Кроме того, у каждой машины есть пассажиры внутри. В чем же принципиальная разница между полями Engine engine и Passenger [] passengers? Если у машины внутри сидит пассажир А, это не значит, что в ней не могут находиться пассажиры B и C. Одна машина может соответствовать нескольким пассажирам. Кроме того, если всех пассажиров высадить из машины, она продолжит спокойно функционировать. Связь между классом Car и массивом пассажиров Passenger [] passengers менее строгая. Она называется агрегацией. Есть неплохая статья на эту тему: Отношения между классами (объектами). В ней приведен еще один хороший пример агрегации. Допустим, у нас есть класс Student, обозначающий студента, и класс StudentsGroup (группа студентов). Студент может входить и в клуб любителей физики, и в студенческий фан-клуб «Звездных войн» или команду КВН. Композиция — более строгий тип связи. При использовании композиции объект не только является частью какого-то объекта, но и не может принадлежать другому объекту того же типа. Самый простой пример — двигатель автомобиля. Двигатель является частью автомобиля, но не может быть частью другого автомобиля. Как видишь, их связь гораздо более строгая, чем у Car и Passengers.