Наследование в Java

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

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

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

Ключевые участники механизма наследования в Java

  • Наследование — это концепция, согласно которой класс может частично или полностью повторять свойства и методы своего родителя (класса, от которого он наследуется).
  • Класс-потомок (child class), дочерний класс, также называемый подклассом (subclass), расширяющим (extended class) или производным (derived class), — это класс, который наследует другой класс.
  • Класс-родитель (parent class), родительский класс, также называемый суперклассом (superclass) или базовым классом (base class), — это класс, который имеет ряд функций, и эти функции могут быть переданы (унаследованы) другим классом (дочерним классом).
  • Переопределение метода (method overriding) — это изменение поведения метода в производном классе. Как правило, это более конкретное, уточненное поведение. Если в наследнике переопределить метод, уже реализованный в родителе, он, по сути, “заменяет” родительскую версию.
  • У класса может быть только один родительский класс, но также у каждого класса может быть много наследников.

Как это работает

В Java наследование идет сверху вниз: от самого общего класса к более конкретным. Самый верхний класс в цепочке обычно абстрактный — он задает структуру, но сам по себе не используется. Такие классы обыччно обозначаются как abstract. Все последующие классы уже конкретизированы. Допустим, есть базовый класс Gadget. Он может содержать поля вроде веса, ёмкости батареи и уровня заряда, а также методы — work() и charge(). При этом методы могут быть абстрактными, то есть не иметь конкретной реализации.

Пока мы не уточнили, что именно за устройство перед нами, это просто абстрактный перезаряжаемый гаджет. Создадим подкласс Phone класса Gadget. Ему не нужно снова объявлять вес и батарею — всё наследуется. Но поведение при работе (метод work()) уже нужно будет уточнить. Также в Phone можно добавить новые поля — например, диагональ экрана, типы разъемов и т.д. — и новый метод, например, updateOperatingSystem().

Далее можно создать еще два класса, например, Android и iPhone, которые наследуются от Phone. Они оба получат поля и методы от Gadget и Phone, при этом методы могут быть переопределены. В классе Android можно добавить поле “Название бренда” (brandName), а в iPhone оно не требуется, так как такие устройства производит только одна компания.

class Gadget {

}
}
//subclass of Gadget class
class Phone extends Gadget {

}
//subclass of Phone class
class IPhone extends Phone {

}
//subclass of Phone class
class AndroidPhone extends Phone {

}
Run Code

Дочерний класс наследует все public и protected поля и методы от родительского класса — неважно, находятся они в одном пакете или нет. Если дочерний класс находится в том же пакете, что и родитель, он также получает доступ к элементам с модификатором доступа по умолчанию (package-private). Вы можете использовать унаследованные элементы как есть, переопределять их, скрывать или добавлять новые:

  • Унаследованные поля можно использовать напрямую, как и любые другие.
  • Можно объявить в дочернем классе поле с тем же именем, что и у родителя — это называется скрытие (hiding). Лучше этого не делать.
  • Можно добавлять новые поля, которых нет в родительском классе.
  • Унаследованные методы можно использовать без переопределения.
  • Если вы объявляете в подклассе метод с такой же сигнатурой, как у родителя, он переопределяет родительский (overriding).
  • Можно добавлять новые методы в дочернем классе, которые не были объявлены в родительском.
  • Конструктор подкласса может вызывать конструктор родителя — либо неявно (по умолчанию), либо явно с помощью ключевого слова super.

Пример

Создадим базовый класс MusicalInstrument с полями weight (вес) и trademark (торговая марка), а также методом work(). Кроме того, не забываем про конструктор, чтобы инициализировать поля при создании объекта..

public class MusicalInstrument {
   int weight;
   String tradeMark;

   public MusicalInstrument(int weight, String tradeMark) {
       this.weight = weight;
       this.tradeMark = tradeMark;
   }

   public void work() {
       System.out.println("the instrument is playing...");
   }
}
Run Code

Пока совершенно непонятно, что это за музыкальный инструмент и как на нем играть. Давайте создадим конкретный инструмент — скрипку. Она будет иметь те же поля, что и MusicalInstrument (они будут вызываться в конструкторе с помощью ключевого слова super). Также мы можем переопределить метод work() и добавить метод для настройки скрипки по струнам.

public class Violin extends MusicalInstrument {
   String master;
   String owner;
   int age;
   boolean isTuned;

   public Violin(int weight, String tradeMark, String master, String owner, int age, boolean isTuned) {
       super(weight, tradeMark);
       this.master = master;
       this.owner = owner;
       this.age = age;
       this.isTuned = isTuned;
   }

   @Override
   public void work() {
       System.out.println("THe violin's playing");

   }

   public void violinTuning () {
     System.out.println("I'm tuning 1st string...");
     System.out.println("I'm tuning 2nd string...");
     System.out.println("I'm tuning 3rd string...");
     System.out.println("I'm tuning 4th string...");
    }
}
Run Code

Создадим класс Demo, чтобы протестировать класс Violin и посмотреть, как работает наследование.

public class InheritDemo {

   public static void main(String[] args) {

       Violin violin = new Violin(1, null, "Amati", "Kremer", 285, false);
       violin.violinTuning();
       violin.work();
   }
}
Run Code

В этом случае программа выведет следующее:

I'm tuning 1st string...
I'm tuning 2nd string...
I'm tuning 3rd string...
I'm tuning 4th string...
THe violin's playing

Если в дочернем классе есть переопределённый метод, то метод из родительского класса вызываться уже не будет.
А что если такого метода в дочернем классе нет?
Тогда класс Violin будет выглядеть вот так:

public class Violin extends MusicalInstrument {
   String master;
   String owner;
   int age;
   boolean isTuned;

   public Violin(int weight, String tradeMark, String master, String owner, int age, boolean isTuned) {
       super(weight, tradeMark);
       this.master = master;
       this.owner = owner;
       this.age = age;
       this.isTuned = isTuned;
   }

  // @Override


 //  }

   public void violinTuning () {
       System.out.println("I'm tuning 1st string...");
       System.out.println("I'm tuning 2nd string...");
       System.out.println("I'm tuning 3rd string...");
       System.out.println("I'm tuning 4th string...");
   }

}
Run Code

Вывод в консоли:

I'm tuning 1st string...
I'm tuning 2nd string...
I'm tuning 3rd string...
I'm tuning 4th string...
the instrument is playing...

Метод родителя вызывается автоматически, если не переопределен. Кстати, дочерний класс можно представить через родительский — это называется восходящим преобразованием (upcasting):

Parent parent = new Child()

Такой способ инициализации позволяет работать только с тем, что есть в родителе — полями и переопределёнными методами.
В нашем примере это будет выглядеть так:

public class InheritDemo {

   public static void main(String[] args) {

       MusicalInstrument violin = new Violin(1, null, "Amati", "Kremer", 285, false);
       //violin.violinTuning();
       violin.work();
   }
}
Run Code

Методы, определённые только в Violin, будут недоступны. Однако при этом будет вызван метод work() из дочернего класса, если он переопределен.

Иерархия классов в Java

В Java все состоит из классов, и все они подчинены определенной иерархии. Возникает вопрос: существует ли класс, от которого наследуются все остальные? Ответ — да, такой класс действительно существует. И называется он просто Object.

Класс Object из пакета java.lang определяет и реализует поведение, общее для всех классов, включая те, которые вы создаёте сами. Многие классы наследуются от него напрямую, другие — от этих классов и так далее, формируя иерархию.

Типы наследования в Java

Рассмотрим подробнее основные типы наследования в Java.

1. Одиночное (Single Inheritance)
Это самый базовый тип — подкласс наследует свойства и методы одного суперкласса. На схеме ниже класс A служит базовым для производного класса B.

2. Многоуровневое (Multilevel Inheritance)
Это цепочка наследования: есть базовый класс A, от него наследуется класс B, а от B — класс C. В Java при этом класс не может напрямую обращаться к элементам прародителя.

3. Иерархическое наследование (Hierarchical Inheritance)
В этом случае один класс выступает суперклассом для нескольких подклассов. Как в примере с классом Phone, у которого есть два наследника — AndroidPhone и IPhone.

class A {
    public void printA() {
System.out.println("A");
 }
}

class B extends A {
    public void printB() {
 System.out.println(" B"); }
}

class C extends A {
    public void printC() {
System.out.println("C");
}
}

class D extends A {
    public void printD() {
System.out.println("D");
 }
}

public class Demo {
    public static void main(String[] args)
    {
        B objB = new B();
        objB.printA();
        objB.printB();

        C objC = new C();
        objC.printA();
        objC.printC();

        D objD = new D();
        objD.printA();
        objD.printD();
    }
}
Run Code

Вывод в консоли:

A
B
A
C
A
D

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

interface A {
   public void printA();
}

interface B {
   public void printB();
}

interface C extends A, B {
   public void print();
}
class InheritDemo implements C {
   @Override
   public void print()
   {
       System.out.println("Print something");
   }

   @Override
   public void printA() {
   }

   @Override
   public void printB() {
   }
}
Run Code

Перевод статьи «Inheritance in Java».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх