«1. Обзор

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

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

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

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

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

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

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

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

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

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

    базовый тип также называется супер или родительским типом производный тип называется расширенным , sub или дочерний тип

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

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

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

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

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

Класс ArmoredCar может наследовать членов класса Car, используя ключевое слово extends в своем объявлении:

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

Теперь мы можем сказать, что ArmoredCar class является подклассом Car, а последний является надклассом ArmoredCar.

Классы в Java поддерживают одиночное наследование; класс ArmoredCar не может расширять несколько классов.

Также обратите внимание, что при отсутствии ключевого слова extends класс неявно наследует класс java.lang.Object.

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

С другой стороны, закрытые и статические члены класса не наследуются.

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

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

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

Обратите внимание, что нам не нужна ссылка на суперкласс для доступа к его членам.

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

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

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

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

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!");
    }
}

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

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

Java допускает множественное наследование с использованием интерфейсов.

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

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

Начиная с Java 8, интерфейсы могли определять реализации по умолчанию для своих методов (интерфейс по-прежнему может определять абстрактные методы) . Это означает, что если класс реализует несколько интерфейсов, которые определяют методы с одной и той же сигнатурой, дочерний класс унаследует отдельные реализации. Это звучит сложно и не разрешено.

«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
}

Вот пример:

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
    }
}

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

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

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

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

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

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

public class Employee {
    private String name;
    private Car car;
    
    // standard constructor
}

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

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

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

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

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

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

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
    }
}

6. Скрытые члены класса

6.1. Скрытые члены экземпляра

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

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

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, чтобы явно указать, к какой переменной или методу они относятся. Однако использование их со всеми членами может сделать наш код загроможденным.

6.2. Скрытые статические члены

return Car.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();

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

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

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