Руководство по наследованию в Java

Руководство по наследованию в Java

1. обзор

Один из основных принципов объектно-ориентированного программирования -inheritance – enables us to reuse existing code or extend an existing type.

Проще говоря, в Java класс может наследовать другой класс и несколько интерфейсов, в то время как интерфейс может наследовать другие интерфейсы.

В этой статье мы начнем с необходимости наследования и перейдем к тому, как наследование работает с классами и интерфейсами.

Затем мы рассмотрим, как имена переменных / методов и модификаторы доступа влияют на наследуемые члены.

И в конце мы увидим, что означает наследование типа.

2. Необходимость наследования

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

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

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

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

  • базовый тип также называется супер или родительский тип

  • производный тип называется расширенным, дочерним или дочерним типом

3. Наследование с классами

3.1. Расширение класса

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

Начнем с определения базового классаCar:

public class Car {
    int wheels;
    String model;
    void start() {
        // Check essential parts
    }
}

КлассArmoredCar может наследовать члены классаCar отusing the keyword extends in its declaration:

public class ArmoredCar extends Car {
    int bulletProofWindows;
    void remoteStartCar() {
    // this vehicle can be started by using a remote control
    }
}

Classes in Java support single inheritance; классArmoredCar не может расширять несколько классов. В отсутствие ключевого словаextends класс неявно наследует классjava.lang.Object.

3.2. Что унаследовано

По сути, производный класс наследует членыprotected иpublic от базового класса, которые не являютсяstatic.. Кроме того, члены с доступомdefault иpackage наследуются, если два класса находятся в одном пакете.

Базовый класс не позволяет производным классам получить доступ ко всему своему коду.

Членыprivate иstatic класса не наследуются производным классом. Кроме того, если базовый класс и производные классы определены в отдельных пакетах, члены с доступомdefault илиpackage в базовом классе не наследуются в производном классе.

3.3. Доступ к членам родительского класса из производного класса

Это просто. Просто используйте их (нам не нужна ссылка на базовый класс для доступа к его членам). Вот быстрый пример:

public class ArmoredCar extends Car {
    public String registerModel() {
        return model;
    }
}

3.4. Скрытые члены экземпляра базового класса

Что произойдет, если и наш базовый класс, и производный класс определяют переменную или метод с одинаковым именем? Не волнуйтесь; мы все еще можем получить доступ к ним обоим. Однако мы должны прояснить наши намерения для Java, добавив к переменной или методу префикса ключевых словthis илиsuper.

Ключевое словоthis относится к экземпляру, в котором оно используется. Ключевое словоsuper (как кажется очевидным) относится к экземпляру родительского класса:

public class ArmoredCar extends Car {
    private String model;
    public String getAValue() {
        return super.model;   // returns value of model defined in base class Car
        // return this.model;   // will return value of model defined in ArmoredCar
        // return model;   // will return value of model defined in ArmoredCar
    }
}

Многие разработчики используют ключевые словаthis иsuper, чтобы явно указать, на какую переменную или метод они ссылаются. Однако использование их со всеми членами может сделать наш код загроможденным.

3.5. Скрытые статические члены базового класса

Что происходит, когда наш базовый класс и производные классы определяют статические переменные и методы с одинаковыми именами? Можем ли мы получить доступ к членуstatic из базового класса в производном классе, как мы это делаем для переменных экземпляра?

Давайте выясним это на примере:

public class Car {
    public static String msg() {
        return "Car";
    }
}
public class ArmoredCar extends Car {
    public static String msg() {
        return super.msg(); // this won't compile.
    }
}

Нет, не можем. Статические члены принадлежат классу, а не экземплярам. Таким образом, мы не можем использовать нестатическое ключевое словоsuper вmsg().

Поскольку статические члены принадлежат классу, мы можем изменить предыдущий вызов следующим образом:

return Car.msg();

Рассмотрим следующий пример, в котором и базовый класс, и производный класс определяют статический методmsg() с одной и той же сигнатурой:

public class Car {
    public static String msg() {
        return "Car";
    }
}
public class ArmoredCar extends Car {
    public static String msg() {
        return "ArmoredCar";
    }
}

Вот как мы можем их назвать:

Car first = new ArmoredCar();
ArmoredCar second = new ArmoredCar();

Для предыдущего кодаfirst.msg() выведет «Car, аsecond.msg() выведет« ArmoredCar ». Вызываемое статическое сообщение зависит от типа переменной, используемой для ссылки на экземплярArmoredCar.

4. Наследование с интерфейсами

4.1. Реализация нескольких интерфейсов

Хотя классы могут наследовать только один класс, они могут реализовывать несколько интерфейсов.

Представьте, чтоArmoredCar, который мы определили в предыдущем разделе, требуется супершпиону. Компания-производительCar подумала о добавлении летающих и плавающих функций:

public interface Floatable {
    void floatOnWater();
}
public interface Flyable {
    void fly();
}
public class ArmoredCar extends Car implements Floatable, Flyable{
    public void floatOnWater() {
        System.out.println("I can float!");
    }

    public void fly() {
        System.out.println("I can fly!");
    }
}

В приведенном выше примере мы заметили использование ключевого словаimplements для наследования от интерфейса.

4.2. Проблемы с множественным наследованием

Многократное наследование с интерфейсами разрешено в Java.

До Java 7 это не было проблемой. Интерфейсы могут определять только методыabstract, то есть методы без какой-либо реализации. Таким образом, если класс реализовал несколько интерфейсов с одной и той же сигнатурой метода, это не было проблемой. У реализующего класса в конечном итоге был только один метод для реализации.

Давайте посмотрим, как это простое уравнение изменилось с введением методовdefault в интерфейсы с Java 8.

Starting with Java 8, interfaces could choose to define default implementations for its methods (интерфейс все еще может определять методыabstract). Это означает, что если класс реализует несколько интерфейсов, которые определяют методы с одной и той же сигнатурой, дочерний класс будет наследовать отдельные реализации. Это звучит сложно и не допускается.

Java запрещает наследование нескольких реализаций одних и тех же методов, определенных в отдельных интерфейсах.

Вот пример:

public interface Floatable {
    default void repair() {
        System.out.println("Repairing Floatable object");
    }
}
public interface Flyable {
    default void repair() {
        System.out.println("Repairing Flyable object");
    }
}
public class ArmoredCar extends Car implements Floatable, Flyable {
    // this won't compile
}

Если мы действительно хотим реализовать оба интерфейса, нам придется переопределить методrepair().

Если интерфейсы в предыдущих примерах определяют переменные с одинаковыми именами, скажемduration, мы не сможем получить к ним доступ, не указав перед именем переменной имя интерфейса:

public interface Floatable {
    int duration = 10;
}
public interface Flyable {
    int duration = 20;
}
public class ArmoredCar extends Car implements Floatable, Flyable {

    public void aMethod() {
        System.out.println(duration); // won't compile
        System.out.println(Floatable.duration); // outputs 10
        System.out.println(Flyable.duration); // outputs 20
    }
}

4.3. Интерфейсы, расширяющие другие интерфейсы

Интерфейс может расширять несколько интерфейсов. Вот пример:

public interface Floatable {
    void floatOnWater();
}
interface interface Flyable {
    void fly();
}
public interface SpaceTraveller extends Floatable, Flyable {
    void remoteControl();
}

Интерфейс наследует другие интерфейсы с помощью ключевого словаextends. Классы используют ключевое словоimplements для наследования интерфейса.

5. Наследование типа

Когда класс наследует другой класс или интерфейсы, кроме наследования их членов, он также наследует их тип. Это также относится к интерфейсу, который наследует другие интерфейсы.

Это очень мощная концепция, которая позволяет разработчикам программировать на интерфейс (базовый класс или интерфейс), а не программировать на их реализации (конкретные или производные классы).

Например, представьте условие, когда организация ведет список автомобилей, принадлежащих ее сотрудникам. Конечно, все сотрудники могут иметь разные модели автомобилей. Итак, как мы можем ссылаться на разные экземпляры автомобилей? Вот решение:

public class Employee {
    private String name;
    private Car car;

    // standard constructor
}

Поскольку все производные классыCar наследуют типCar, на экземпляры производного класса можно ссылаться с помощью переменной классаCar:

Employee e1 = new Employee("Shreya", new ArmoredCar());
Employee e2 = new Employee("Paul", new SpaceCar());
Employee e3 = new Employee("Pavni", new BMW());

6. Заключение

В этой статье мы рассмотрели ключевой аспект языка Java - как работает наследование.

Мы говорим о том, как Java поддерживает одиночное наследование с классами и множественное наследование с интерфейсами, и обсуждали тонкости работы механизма в языке.

Как всегда, доступен полный исходный код примеровover on GitHub.