# Лекция 4

## Тема: Повторное использование кода. Композиция и наследование. Наследование в Java. Наследование членов суперкласса. Инициализация базового класса. Ключевое слово super. Запрет наследования с помощью ключевого слова final. Класс Object. Сравнение композиции и наследования.

## Повторное использование кода

Возможность повторного использования кода принадлежит к числу важнейших преимуществ языков объектно-ориентированного программирования.

В Java, вместо того чтобы создавать новый класс "с чистого листа", вы берете за основу уже существующий класс, который кто-то уже создал и проверил на работоспособность. Существую два пути реализации этой идеи.

* **композиция (composition)** - объекты уже имеющихся классов просто создаются внутри нового класса. Программист просто использует функциональность уже готового кода;
* **наследование (inheritance)** - новый класс создается как _специализация_ уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса.

**Композиция** - использование функционала одних объектов в составе других объектов. Рассмотрим пример класса `FileManager`, в котором определен метод для сохранения текстовых данных в файл. Класс `Document` использует функционал класса `FileManager`, чтобы сохранить текстовый документ на жесткий диск.

```java
class FileManager {
    public void saveToFile(String text, String path) {
        // тело метода
    }
}

class Document {
    
    // класс Document содержит ссылку на объект
    // класса FileManager
    private FileManager manager;
    private StringBuilder contents;
    private String path;

    public Document(FileManager manager, String path) {
        this.manager = manager;
        this.contents = new StringBuilder();
        this.path = path;
    }

    public void saveDocument() {
        manager.saveToFile(contents.toString(), path);
    }
}
```

## Базовые понятия механизма наследования

**Наследование** - отношение между классами, в котором один класс повторяет структуру и поведение другого класса (или нескольких других классов).

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

Класс, на основе которого создается новый класс, называется **суперклассом** (базовым классом, родительским классом). Новый создаваемый класс называется **подклассом** (дочерним классом, производным классом, классом-наследником и так далее).

![](https://1377473627-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnhxGhlv6e_zwoZYywB%2F-Lop2-tdR8J9HhgaG8u_%2F-LoplTi78DGXVf9DaTMl%2Fassets_-LLMA-cFvx_keJaoUgkS_-LMu9xugWbHdaUgG_LH5_-LMuC4QUjlc3XdR72Icm_51%20\(1\).png?alt=media\&token=00f3c12a-8c66-4f0d-90d6-abceb60af559)

### Создание подкласса в Java

Создание подкласса практически не отличается от создания обычного класса, кроме необходимости указать суперкласс, на основе которого создается подкласс. В Java для этого существует ключевое слово **extends**:

```java
// Суперкласс
class Person {
    String firstName;
    String lastName;
}

// Подкласс
class Student extends Person {
    String group;
    long id;
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        student.firstName = "Иван";
        student.lastName = "Иванов";
        student.id = 10000L;
    }
}
```

В Java, в отличие от C++, отсутствует множественное наследование,  то есть подкласс может создаваться на основе только одного суперкласса.

```java
// Суперкласс
class Person {
    String firstName;
    String lastName;
}

class UniversityMember{}

// МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ ЗАПРЕЩЕНО!
// ЭТОТ КОД ВЫЗОВЕТ ОШИБКУ КОМПИЛЯТОРА
class Student extends Person, UniversityMember {
    String group;
    long id;
}
```

В Java присутствует многоуровневое наследование: подкласс может быть суперклассом для другого класса. Благодаря этому можно создавать целые цепочки классов, связанные механизмом наследования

```java
class Vehicle {

    public void moveTo(Point destination) {
        // тело метода
    }
}

class Truck extends Vehicle {

    public void carryWeight(double weight) {
        // тело метода
    }
}

class DumpTruck extends Truck {

    public void dumpWeight() {
        // тело метода
    }
}
```

### Наследование членов суперкласса

Если член класса определен как `private`, то при наследовании доступ к нему со стороны подкласса закрыт. Важно понимать, что приватный член суперкласса в подклассе есть, только он закрыт для прямого доступа. К примеру, данный код не скомпилируется

```java
class Shape2D {
    private double width;
    private double height;
}

class Rectangle extends Shape2D {

    // ОШИБКА НА ЭТАПЕ КОМПИЛЯЦИИ
    public double getArea() {
        return width * height;
    }
}
```

Закрытыми могут быть как поля класса, так и его методы. Если необходимо открыть поля или методы для доступа к ним со стороны подкласса, при объявлении членов суперкласса используют слово `protected` либо создают геттеры и сеттеры для доступа к полям. К примеру, данный код скомпилируется и будет работать корректно

```java
class Shape2D {
    protected double width;
    protected double height;
}

class Rectangle extends Shape2D {

    // Данный код корректен
    public double getArea() {
        return width * height;
    }
}
```

Данный пример демонстрирует доступ к полям с помощью геттеров

```java
class Shape2D {
    private double width;
    private double height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }
}

class Rectangle extends Shape2D {

    // Данный код корректен
    public double getArea() {
        return getWidth() * getHeight();
    }
}
```

### Инициализация базового класса

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

Для упрощения можете представить, что когда вы создаете объект производного класса, внутри него якобы "содержится" объект базового класса. Этот объект "выглядит" точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса "упакован" объект базового класса.

{% hint style="warning" %}
Данная аналогия не соответствует тому, как наследование реализуется "внутри" языка Java, но пока будем мыслить таким образом.
{% endhint %}

Чтобы объект базового класса был правильно инициализирован, при вызове конструктора подкласса, сначала вызывается конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса.

При использовании конструкторов без параметров, у компилятора не возникает проблем с вызовом таких конструкторов, так как нет нужды передавать аргументы. В этом случае Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса.

```java
class Animal {
    public Animal() {
        System.out.println("Конструктор класса Animal");
    }
}

class Mammal extends Animal {
    public Mammal() {
        System.out.println("Конструктор класса Mammal");
    }
}

class Cat extends Mammal {
    public Cat() {
        System.out.println("Конструктор класса Cat");
    }
}

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat();
    }
}
```

Результат работы такого приложения будет следующим

```
Конструктор класса Animal
Конструктор класса Mammal
Конструктор класса Cat
```

Как видно из данного примера, цепочка вызовов конструкторов начинается с самого базового класса. Таким образом, подобъект базового класса инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса `Cat` не будет определен, Java сгенерирует конструктор по умолчанию, в котором также будет вызван конструктор базового класса.

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

```java
public class Main {
    public static void main(String[] args) {
        Box3D box = new Box3D(100);
    }
}

class Box {
    public double width;
    public double height;

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

class Box3D extends Box {
    public double depth;

    // НЕТ ЯВНОГО ВЫЗОВА КОНСТРУКТОРА СУПЕРКЛАССА !
    public Box3D(double depth) {
        this.depth = depth;
    }
}
```

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

```java
public class Main {
    public static void main(String[] args) {
        Box3D box = new Box3D(100, 200, 300);
    }
}

class Box {
    public double width;
    public double height;

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

class Box3D extends Box {
    public double depth;
    
    public Box3D(double width, double height, double depth) {
        super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
        this.depth = depth;
    }
}
```

### Ключевое слово super

Как было сказано ранее, наследование в Java реализуется следующим образом - в объект производного класса добавляется скрытый объект базового класса, который и обеспечивает вызов методов суперкласса.&#x20;

Ключевое слово `super` как раз и ссылается на этот скрытый объект суперкласса. Используя это ключевое слово, можно получить доступ к членам суперкласса (если позволяет их модификатор доступа)

```java
class Box {
    private double width;
    private double height;

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }

    // Площадь прямоугольника
    public double getArea() {
        return width * height;
    }
}

class Box3D extends Box {
    private double depth;

    public Box3D(double width, double height, double depth) {
        super(width, height); // <-- ВЫЗОВ КОНСТРУКТОРА СУПЕРКЛАССА
        this.depth = depth;
    }
    
    // Мы используем метод суперкласса, чтобы
    // посчитать площадь трехмерной коробки
    public double get3DArea() {
        double area2D = super.getArea(); // <---- Вызов метода суперкласса
        return area2D * depth;
    }
}
```

Как видно из примера, ключевое слово `super` имеет что-то общее с ключевым словом `this`.

### Переопределение методов

При использовании механизма наследования возникает проблема с использованием методов суперкласса. Часто метод суперкласса не отражает изменения и нововведения, внесенные в подклассе и вызов таких методов дает некорректную информацию об объекте. Рассмотрим следующий пример

```java
public class Main {
    public static void main(String[] args) {
        Box3D box = new Box3D(100, 200, 300);
        System.out.println(box.getInfo());
    }
}

class Box {
    private double width;
    private double height;

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public String getInfo() {
        return "Объект Box {" +
                "ширина = " + width +
                ", высота = " + height +
                '}';
    }
}

class Box3D extends Box {
    private double depth;

    public Box3D(double width, double height, double depth) {
        super(width, height);
        this.depth = depth;
    }
}
```

Результат работы такого приложения будет следующим

```
Объект Box {ширина = 100.0, высота = 200.0}
```

Как вы уже понимаете, в данной части кода

```java
Box3D box = new Box3D(100, 200, 300);
System.out.println(box.getInfo());
```

был вызван метод `getInfo()` суперкласса, который выводит информацию только о двух параметрах коробки, тогда как класс `Box3D` содержит три параметра. Таким образом, метод `getInfo()` для класса `Box3D` становится как бы некорректным, неправильным, он выдает неполную информацию об объекте. Как решить эту проблему?

Первый вариант - создать в подклассе `Box3D` свой метод для вывода информации

{% tabs %}
{% tab title="Box3D.java" %}
```java
class Box3D extends Box {
    private double depth;

    public Box3D(double width, double height, double depth) {
        super(width, height);
        this.depth = depth;
    }
    
    public String get3DInfo() {
        return "Объект Box3D {" +
                "ширина = " + super.getWidth() +
                ", высота = " + super.getHeight() +
                ", глубина = " + depth +
                '}';
    }
}
```
{% endtab %}

{% tab title="Box.java" %}
```java
class Box {
    private double width;
    private double height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public String getInfo() {
        return "Объект Box {" +
                "ширина = " + width +
                ", высота = " + height +
                '}';
    }
}
```
{% endtab %}
{% endtabs %}

Тогда мы будем вызывать метод `get3DInfo()`

```java
public class Main {
    public static void main(String[] args) {
        Box3D box = new Box3D(100, 200, 300);
        System.out.println(box.get3DInfo());
    }
}
```

что даст нам корректный результат

```
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
```

Такой вариант является интуитивно понятным, но все-таки не совсем корректным. Теперь у объекта `Box3D` существует аж целых два метода для получения информации об объекте, один из которых является некорректным, что вносит путаницу для нас и для тех программистов, которые будут использовать наш код.

Самым правильным вариантом будет как бы "переписать" метод **getInfo()**, предоставить версию метода **getInfo()** для класса `Box3D`. Таким образом и класс `Box` и класс `Box3D` будет иметь метод `getInfo()`, просто в классе `Box3D` он будет иначе реализован, с учетом появления третьего параметра. Кроме того, объект класса `Box3D` теперь будет содержать только один метод для получения информации об объекте и этот метод будет корректно работать.

Для реализации своеобразного "переписывания" метода в подклассе, в Java существует **механизм переопределения метода (method overriding)**.

Воспользуемся механизмом переопределения метода, чтобы корректно решить проблему с классами `Box` и `Box3D` (в примере для наглядности опущены некоторые члены классов)

```java
public class Main {
    public static void main(String[] args) {
        
        Box box = new Box(600,600);
        System.out.println(box.getInfo());
        
        Box3D box3D = new Box3D(100, 200, 300);
        System.out.println(box3D.getInfo());
    }
}

class Box {
    
    // Поля, конструктор и геттеры\сеттеры

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public String getInfo() {
        return "Объект Box {" +
                "ширина = " + width +
                ", высота = " + height +
                '}';
    }
}

class Box3D extends Box {

    // Поля, конструктор и геттеры\сеттеры
    
    @Override
    public String getInfo() {
        return "Объект Box3D {" +
                "ширина = " + super.getWidth() +
                ", высота = " + super.getHeight() +
                ", глубина = " + depth +
                '}';
    }
}
```

Обратите внимание что сигнатуры двух методов `getInfo()` полностью совпадают - **это обязательное условие** для срабатывания механизма переопределения метода. Метод в суперклассе называется **переопределенным**.

Также обратите внимание на строку 33, где записано `@Override`. Такая запись называется **аннотацией**.

**Аннотация** - это дополнительное пояснение для компилятора и для различных утилит, которые работают с кодом (анализаторы, генераторы документации и так далее). Указание аннотации не является обязательным, код будет работать и без аннотации, но указание аннотации является одним из важных правил грамотного написания кода.

Результат работы нашего примера будет следующим

```
Объект Box {ширина = 600.0, высота = 600.0}
Объект Box3D {ширина = 100.0, высота = 200.0, глубина = 300.0}
```

Обратите внимание, что и для класса `Box` и для класса `Box3D` мы вызываем метод с одним и тем же названием и с одной и той же сигнатурой

```java
Box box = new Box(600, 600);
System.out.println(box.getInfo());

Box3D box3D = new Box3D(100, 200, 300);
System.out.println(box3D.getInfo());
```

Но в зависимости от того, для какого класса мы вызываем этот метод, в первом случае отрабатывает метод в классе `Box`, а во втором случае - метод в классе `Box3D`.

{% hint style="warning" %}
Следует отличать механизм **перегрузки** метода от механизма **переопределения** метода.

Метод может быть как перегруженным, так и переопределенным.
{% endhint %}

### Запрет наследования с помощью ключевого слова final

Иногда бывает необходимо запретить наследоваться от какого-то класса либо запретить переопределять метод. В этом случае, в объявлении класса или метода укажите ключевое слово `final`

```java
final class A {}

class B extends A {
    // ВЫЗОВЕТ ОШИБКУ КОМПИЛЯЦИИ !
}

class C {
    final public void foo() {}
}

class D extends C {
    @Override
    public void foo() {} // <-- Ошибка компиляции !
}
```

### Класс Object

В Java определен специальный класс `Object`, который **является суперклассом для всех классов Java**. Иными словами, все классы в языке Java являются подклассами, производными от класса `Object`.&#x20;

В классе Object определены перечисленные ниже методы, которые **доступны в любом объекте**

| **Метод**                                                                                                                    | Описание                                                                        |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Object clone()`                                                                                                             | Создает новый объект, аналогичный клонируемому объекту                          |
| `boolean equals(Object объект)`                                                                                              | Определяет равнозначность объектов                                              |
| `void finalize()`                                                                                                            | Вызывается перед тем, как неиспользуемый объект будет удален "сборщиком мусора" |
| `Class<?> getClass()`                                                                                                        | Определяет класс объекта во время выполнения                                    |
| `int hashCode()`                                                                                                             | Возвращает хеш-код, связанный с вызывающим объектом                             |
| `void notify()`                                                                                                              | Возобновляет работу потока, ожидающего уведомления от вызывающего объекта       |
| `void notifyAll()`                                                                                                           | Возобновляет работу всех потоков, ожидающих уведомления от вызывающего объекта  |
| `String toString()`                                                                                                          | Возвращает символьную строку, описывающую объект                                |
| <p><code>void wait()</code></p><p><code>void wait(long мсек)</code></p><p><code>void wait(long мсек, int наносек)</code></p> | Ожидает исполнения другого потока                                               |

С некоторыми методами класса `Object` мы встретимся позже, а сейчас нам интересен только метод `toString()`.

#### Метод toString()

Метод `toString()` призван возвращать строковое представление объекта (список значений полей). Рассмотрим пример

```java
public class Main {
    public static void main(String[] args) {
        
        Box box = new Box();
        System.out.println(box.toString());
    }
}

class Box{}
```

Обратите внимание, что класс `Box` не содержит никаких членов, однако, так как Box наследуется от класса `Object`, ему доступны методы суперкласса и метод `toString()` в частности.

Результатом работы данного примера является следующая строка

```
com.company.Box@1540e19d
```

Метод `toString()` в классе `Object` выводит полное название класса и 16-ричное представление хеш-кода объекта.

{% hint style="info" %}
Если вам необходимо вывести значения полей объекта в виде строки, используйте метод `toString()`, он для этого и был предназначен, его использование является общепринятым правилом.&#x20;

Метод `getInfo()` в примере выше был использован только в демонстрационных целях!
{% endhint %}

Чтобы метод `toString()` выводил нужную информацию, его необходимо пепреопределить как в следующем примере

```java
public class Main {
    public static void main(String[] args) {

        Box box = new Box(100, 200);
        System.out.println(box.toString());
    }
}

class Box {
    private double width;
    private double height;

    public Box(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public String toString() {
        return "Box{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}
```

Результат будет следующим

```
Box{width=100.0, height=200.0}
```

Также метод `toString()` имеет одну примечательную особенность - его можно явно не вызывать, а просто указывать ссылочную переменную, Java сама вызовет метод `toString()`. Например, следующий код

```java
public class Main {
    public static void main(String[] args) {

        Box box = new Box(100, 200);
        System.out.println(box.toString());
        System.out.println(box);
    }
}
```

выдаст следующий результат

```
Box{width=100.0, height=200.0}
Box{width=100.0, height=200.0}
```

Как вы видите, результат работы строк 5 и 6 является идентичным.

## Сравнение композиции и наследования

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

Композиция в основном используется, когда в новом классе необходимо задействовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит только определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором **private**.

При использовании наследования, вы берете уже существующий класс и создаете eго специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи.

{% hint style="info" %}
Неплохая статья по поводу отличия композиции и наследования - [https://habr.com/ru/post/325478/](https://habr.com/ru/post/325478/)
{% endhint %}
