Наследование в Java
В случае наследования в Java для указания родительского класса используется ключевое слово extends, которое следует после имени дочернего, т. е. производного, класса. После extends пишется родительский класс. Другими словами, «дочерний класс расширяет родительский».
В Java нет множественного наследования от классов, но есть множественное наследование интерфейсов.
В Java есть особенность, связанная с наследованием конструкторов и их вызовом. В конструкторе дочернего класса, перед тем как будут выполнены выражения тела, неявно вызывается дефолтный (без параметров) конструктор предка.
Причем конструктор без параметров обязательно должен быть в родительском классе, если в дочернем предполагаются только конструкторы с аргументами, а родительские конструкторы не вызываются явно. В этом случае, если в родительском классе будут только конструкторы с аргументами, то в дочернем будет возникать ошибка.
public class Main public static void main(String[] args) Child child = new Child(10); System.out.println(child.a); System.out.println(child.b); > >
class Parent int a; Parent(int a) this.a = 1; > >
class Child extends Parent int b; Child(int b) this.b = b; > >
В приведенном выше примере ошибка возникает на уровне конструктора класса Child. Перед тем как выполнится выражение this.b = b, будет неявно вызван конструктор Parent без параметров. Однако такого конструктора в родительском классе нет.
Если мы закомментируем или удалим конструктор Parent,
class Parent int a; // Parent(int a) // this.a = 1; // > >
то ошибка в дочернем классе исчезнет. Дело в том, что в Java любой класс есть потомок базового класса Object, у которого всегда имеется пустой дефолтный конструктор. В данном случае, двигаясь по дереву наследования к корню, из Child будет вызываться конструктор базового для всех класса.
Почему же он не вызывался, когда в Parent был определен конструктор с параметрами? В Java, как только вы определяете в классе любой конструктор, дефолтный родительского уже не наследуется напрямую, а начинает неявно вызываться из собственных конструкторов.
Заметим, что у объектов Child поле this.a существует, даже если ему не было присвоено значение. В данном случае значение будет нулевым.
Обычным вариантом недопущения подобных ошибок является объявление в Parent конструктора без аргументов. Он может быть пустым, или содержать какой-либо код:
import java.util.Random; class Parent int a; Parent() Random random = new Random(); this.a = random.nextInt(10); > >
Другой распространенный вариант – когда в дочернем классе явно вызывается конструктор родительского класса с помощью ключевого слова super. Причем вызывать всегда надо первой строчкой тела конструктора.
public class Main public static void main(String[] args) Child child = new Child(10, 20); System.out.println(child.a); System.out.println(child.b); > >
class Parent int a; Parent(int a) this.a = a; > >
class Child extends Parent int b; Child(int a, int b) super(a); this.b = b; > >
Обычные методы наследуются и переопределяются стандартно для объектно-ориентированного программирования. При этом в Java наследовать и переопределять можно только нестатические методы. Таким образом, если в родительском классе объявлен статический метод, то дочерние классы его не наследуют.
В Java объекты дочернего класса можно присваивать переменным родительского. Подобное называется расширением типа. При этом в случае вызова переопределенных методов будут вызываться методы дочернего класса, а не родительского. Однако набор допустимых для вызова методов определяется родительским классом, то есть типом переменной.
Также возможно сужение типа, когда объект, связанный с переменной родительского типа, присваивается переменной дочернего, то есть своего типа. В этих случаях, чтобы избежать ошибок, следует проверять тип объекта. Например, пусть класс B производный от класса A. Тогда:
A var = new B(); if (var instanceof B) B var1 = (B) var; >
Если в методе дочернего класса требуется использовать возможности родительского, то обращение ко второму происходит через слово super, после которого пишется необходимый метод.
public class ParentMeth public static void main(String[] args) ChildMeth ch = new ChildMeth(); ch.PrintStr(); > public void PrintStr() System.out.println("Parent class"); > > class ChildMeth extends ParentMeth public void PrintStr() super.PrintStr(); System.out.println("Child class"); > >
Программирование на Java. Курс
Java наследование от базового
Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >>
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия — класс Employee. Так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник — это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса Person, который, в свою очередь, называется базовым классом, родителем или суперклассом:
class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов. При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Employee sam = new Employee("Sam"); sam.display(); >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam sam.work(); // Sam works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >public void work() < System.out.printf("%s works in %s \n", getName(), company); >>
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam // Works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < System.out.printf("Name: %s \n", getName()); System.out.printf("Works in %s \n", company); >>
Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < super.display(); System.out.printf("Works in %s \n", company); >>
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final . Например:
public final class Person
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
class Employee extends Person
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:
В этом случае класс Employee не сможет переопределить метод display.
Динамическая диспетчеризация методов
Наследование и возможность переопределения методов открывают нам большие возможности. Прежде всего мы можем передать переменной суперкласса ссылку на объект подкласса:
Person sam = new Employee("Sam", "Oracle");
Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.
Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Person sam = new Employee("Sam", "Oracle"); sam.display(); >> class Person < String name; public String getName() < return name; >public Person(String name) < this.name=name; >public void display() < System.out.printf("Person %s \n", name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company = company; >@Override public void display() < System.out.printf("Employee %s works in %s \n", super.getName(), company); >>
Консольный вывод данной программы:
Person Tom Employee Sam works in Oracle
При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.
Java наследование от базового
Исходя из схемы выше, реализуем 4 класса: Pixel , Snake , Food , Bonus .
Получаем дилемму: если объект класса Enemy вызывает метод canBeEaten () , определенный в классе Pixel и при этом не переопределенный в классе Enemy , а классы Snake и Food определили этот метод каждый по-своему. Возникает вопрос: от какого класса должен наследовать свое поведение экземпляр класса Enemy : Snake или Food ? Такая конфигурация наследования называется «проблемой множественного наследования».
В Java для решения этой проблемы применяются интерфейсы.
Что такое интерфейс?
Интерфейс – это объект языка Java схожий по своей сути с абстрактным классом. Для определения интерфейса в языке есть отдельное ключевое слово interface.
Интерфейс не обязан иметь в себе сигнатуры методов. Пустые интерфейсы называются интерфейсами-маркерами. Классическим примером интерфейса-маркера можно считать Cloneable из пакета java.lang .
Отличия абстрактного класса от интерфейса
Если интерфейс так похож на абстрактный класс, зачем тогда в языке присутствуют обе эти конструкции? Для этого есть несколько причин:
- Идеологическая. В парадигме ООП все классы – существительные. Абстрактный класс не исключение. Следовательно, он описывает общие свойства объектов. Интерфейс же — это контракт. Он описывает общие действия доступные всем классам, реализующим его. Интерфейс не располагает реализацией – он лишь гарантирует наличие действия у объекта. Именно поэтому в некоторых нотациях принято именовать интерфейсы как Noun-able.
- Практическая. Класс может расширять только один класс и имплементировать множество интерфейсов. С помощью интерфейса можно решить проблему множественного наследования.
Пример интерфейса
Создадим интерфейс, определяющий возможность перемещаться, и имплементируем его:
Проведем рефакторинг в соответствии со схемой. Вынесем из абстрактного класса метод canBeEaten () в отдельный интерфейс и имплементируем его в абстрактном классе. Таким образом, мы реализовали контракт о том, что все классы дочерние от абстрактного обязаны у себя реализовать метод canBeEaten () что решает проблему выбора родителя для неопределенного метода и, как следствие, саму проблему ромба.
interface Eatable < boolean canBeEaten(); >abstract class Pixel implements Eatable < >
Добавим также классу Snake интерфейс-маркер, чтобы отличать «живые» объекты. Например, змеек и, возможно, других персонажей от еды.
interface Alivable<> class Snake extends Pixel implements Movable, Alivable < >
Таким образом, видно, что класс может расширять только один другой класс, но имплементировать множество интерфейсов.
Ссылки
Тема данной статьи достаточно обширна, и в ней остался не раскрыт ряд вопросов: применение интерфейсов маркеров или примесей, однако я надеюсь, что мне удалось осветить основные аспекты наследования в Java.
Подведем итог. В этой статье мы:
- Научились создавать интерфейсы, абстрактные классы и методы.
- Поняли, чем отличается абстрактный класс от интерфейса.
- Познакомились с проблемой множественного наследования, а также методами ее решения.