Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

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

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

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

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

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

2. Потребность в наследстве

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

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

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

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

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

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 является подклассом Car, а последний является надклассом ArmoredCar.

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

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

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

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

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
}

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

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. Скрытые члены класса

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

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

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

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

Разберемся на примере:

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 .

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

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

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

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