Inheritance
In the preceding lessons, you have seen inheritance mentioned several times. In the Java language, classes can be derived from other classes, thereby inheriting fields and methods from those classes.
Definitions: A class that is derived from another class is called a subclass (also a derived class, extended class, or child class). The class from which the subclass is derived is called a superclass (also a base class or a parent class).
Excepting Object , which has no superclass, every class has one and only one direct superclass (single inheritance). In the absence of any other explicit superclass, every class is implicitly a subclass of Object .
Classes can be derived from classes that are derived from classes that are derived from classes, and so on, and ultimately derived from the topmost class, Object . Such a class is said to be descended from all the classes in the inheritance chain stretching back to Object .
The idea of inheritance is simple but powerful: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug!) them yourself.
A subclass inherits all the members (fields, methods, and nested classes) from its superclass. Constructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.
The Java Platform Class Hierarchy
The Object class, defined in the java.lang package, defines and implements behavior common to all classes—including the ones that you write. In the Java platform, many classes derive directly from Object , other classes derive from some of those classes, and so on, forming a hierarchy of classes.
All Classes in the Java Platform are Descendants of Object
At the top of the hierarchy, Object is the most general of all classes. Classes near the bottom of the hierarchy provide more specialized behavior.
An Example of Inheritance
Here is the sample code for a possible implementation of a Bicycle class that was presented in the Classes and Objects lesson:
A class declaration for a MountainBike class that is a subclass of Bicycle might look like this:
MountainBike inherits all the fields and methods of Bicycle and adds the field seatHeight and a method to set it. Except for the constructor, it is as if you had written a new MountainBike class entirely from scratch, with four fields and five methods. However, you didn't have to do all the work. This would be especially valuable if the methods in the Bicycle class were complex and had taken substantial time to debug.
What You Can Do in a Subclass
A subclass inherits all of the public and protected members of its parent, no matter what package the subclass is in. If the subclass is in the same package as its parent, it also inherits the package-private members of the parent. You can use the inherited members as is, replace them, hide them, or supplement them with new members:
- The inherited fields can be used directly, just like any other fields.
- You can declare a field in the subclass with the same name as the one in the superclass, thus hiding it (not recommended).
- You can declare new fields in the subclass that are not in the superclass.
- The inherited methods can be used directly as they are.
- You can write a new instance method in the subclass that has the same signature as the one in the superclass, thus overriding it.
- You can write a new static method in the subclass that has the same signature as the one in the superclass, thus hiding it.
- You can declare new methods in the subclass that are not in the superclass.
- You can write a subclass constructor that invokes the constructor of the superclass, either implicitly or by using the keyword super .
The following sections in this lesson will expand on these topics.
Private Members in a Superclass
A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.
A nested class has access to all the private members of its enclosing class—both fields and methods. Therefore, a public or protected nested class inherited by a subclass has indirect access to all of the private members of the superclass.
Casting Objects
We have seen that an object is of the data type of the class from which it was instantiated. For example, if we write
then myBike is of type MountainBike .
MountainBike is descended from Bicycle and Object . Therefore, a MountainBike is a Bicycle and is also an Object , and it can be used wherever Bicycle or Object objects are called for.
The reverse is not necessarily true: a Bicycle may be a MountainBike , but it isn't necessarily. Similarly, an Object may be a Bicycle or a MountainBike , but it isn't necessarily.
Casting shows the use of an object of one type in place of another type, among the objects permitted by inheritance and implementations. For example, if we write
then obj is both an Object and a MountainBike (until such time as obj is assigned another object that is not a MountainBike ). This is called implicit casting.
If, on the other hand, we write
we would get a compile-time error because obj is not known to the compiler to be a MountainBike . However, we can tell the compiler that we promise to assign a MountainBike to obj by explicit casting:
This cast inserts a runtime check that obj is assigned a MountainBike so that the compiler can safely assume that obj is a MountainBike . If obj is not a MountainBike at runtime, an exception will be thrown.
Here the instanceof operator verifies that obj refers to a MountainBike so that we can make the cast with knowledge that there will be no runtime exception thrown.
Как унаследовать класс в java

Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия — класс Employee. Так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник — это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса Person, который, в свою очередь, называется базовым классом, родителем или суперклассом:
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов. При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display:
Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final . Например:
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:
В этом случае класс Employee не сможет переопределить метод display.
Динамическая диспетчеризация методов
Наследование и возможность переопределения методов открывают нам большие возможности. Прежде всего мы можем передать переменной суперкласса ссылку на объект подкласса:
Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.
Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:
Консольный вывод данной программы:
При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.
Master Inheritance In Java With Examples
![]()
Object-Oriented Programming or better known as OOPs is one of the major pillars of Java that has leveraged its power and ease of usage. To become a professional Java developer, you must get a flawless control over the various Java OOPs concepts like Inheritance, Abstraction, Encapsulation, and Polymorphism. Through the medium of this article, I will give you a complete insight into one of the most important concepts of OOPs i.e Inheritance in Java and how it is achieved.
Below are the topics, I will be discussing in this article:
- Introduction to Inheritance in Java
- Types of Inheritance in Java
- Single Inheritance
- Multi-level Inheritance
- Hierarchical Inheritance
- Hybrid Inheritance
- Rules of Inheritance in Java
Introduction To Inheritance in Java
In OOP, computer programs are designed in such a way where everything is an object that interacts with one another. Inheritance is an integral part of Java OOPs which lets the properties of one class to be inherited by the other. It basically, helps in reusing the code and establish a relationship between different classes.
As we know, a child inherits the properties of his parents. A similar concept is followed in Java, where we have two classes:
1. Parent class ( Super or Base class )
2. Child class ( Subclass or Derived class )
A class that inherits the properties is known as Child Class whereas a class whose properties are inherited is known as Parent class.
Syntax:
Now, to inherit a class we need to use extends keyword. In the below example, class Son is the child class and class Mom is the parent class. The class Son is inheriting the properties and methods of Mom class.
Let’s see a small program and understand how it works. In this example, we have a base class Teacher and a subclass HadoopTeacher. Since class HadoopTeacher extends the properties from the base class, we need not declare these properties and method in the subclass.
Output:
Now let’s move further and see the various types of Inheritance supported by Java.
Types of Inheritance in Java
Below figure depicts the types of Inheritance:
Single Inheritance
In single inheritance, one class inherits the properties of another. It enables a derived class to inherit the properties and behavior from a single parent class. This will, in turn, enable code reusability as well as add new features to the existing code.
Here, Class A is your parent class and Class B is your child class which inherits the properties and behavior of the parent class. A similar concept is represented in the below code:
Multi-level Inheritance
When a class is derived from a class which is also derived from another class, i.e. a class having more than one parent class but at different levels, such type of inheritance is called Multilevel Inheritance.
If we talk about the flowchart, class B inherits the properties and behavior of class A and class C inherits the properties of class B. Here A is the parent class for B and class B is the parent class for C. So in this case class C implicitly inherits the properties and methods of class A along with Class B. That’s what is multilevel inheritance.
Hierarchical Inheritance
When a class has more than one child classes (subclasses) or in other words, more than one child classes have the same parent class, then such kind of inheritance is known as hierarchical.
In the above flowchart, Class B and C are the child classes which are inheriting from the parent class i.e Class A.
Hybrid Inheritance
Hybrid inheritance is a combination of two or more types of inheritance.
Now that we know what is Inheritance and its various types, let’s move further and see some of the important rules that should be considered while inheriting classes.
Rules of Inheritance in Java
RULE 1: Multiple Inheritance is NOT permitted in Java.
Multiple inheritance refers to the process where one child class tries to extend more than one parent class. In the above illustration, Class A is a parent class for Class B and C, which are further extended by class D. This is results in Diamond Problem. Why? Say you have a method show() in both the classes B and C, but with different functionalities. When Class D extends class B and C, it automatically inherits the characteristics of B and C including the method show(). Now, when you try to invoke show() of class B, the compiler will get confused as to which show() to be invoked ( either from class B or class C ). Hence it leads to ambiguity.
For Example:
In the above code, Demo3 is a child class which is trying to inherit two parent classes Demo1 and Demo2. This is not permitted as it results in a diamond problem and leads to ambiguity.
NOTE: Multiple inheritance is not supported in Java but you can still achieve it using interfaces.
RULE 2: Cyclic Inheritance is NOT permitted in Java.
It is a type of inheritance in which a class extends itself and form a loop itself. Now think if a class extends itself or in any way, if it forms cycle within the user-defined classes, then is there any chance of extending the Object class. That is the reason it is not permitted in Java.
For Example:
In the above code, both the classes are trying to inherit each other’s characters which is not permitted as it leads to ambiguity.
RULE 3: Private members do NOT get inherited.
For Example:
When you execute the above code, guess what happens, do you think the private variables an and pw will be inherited? Absolutely not. It remains the same because they are specific to the particular class.
RULE 4: Constructors cannot be Inherited in Java.
A constructor cannot be inherited, as the subclasses always have a different name.
You can do only:
Methods, instead, are inherited with “the same name” and can be used. You can still use constructors from A inside B’s implementation though:
RULE 5: In Java, we assign parent reference to child objects.
Parent is a reference to an object that happens to be a subtype of Parent, i.e.a Child Object. Why is this used? Well, in short, it prevents your code to be tightly coupled with a single class. Since the reference is of a parent class, it can hold any of its child class object i.e., it can refer to any of its child classes.
It has the following advantages:-
- Dynamic method dispatch allows Java to support overriding of a method, which is central for run-time polymorphism.
- It allows a class to specify methods that will be common to all of its derivatives while allowing subclasses to define the specific implementation of some or all of those methods.
- It also allows subclasses to add its specific methods subclasses to define the specific implementation of some.
Imagine you add getEmployeeDetails to the class Parent as shown in the below code:
We could override that method in Child to provide more details.
Now you can write one line of code that gets whatever details are available, whether the object is a Parent or Child, like:
Then check the following code:
This will result in the following output:
Here we have used a Child class as a Parent class reference. It had a specialized behavior which is unique to the Child class, but if we invoke getEmployeeDetails(), we can ignore the functionality difference and focus on how Parent and Child classes are similar.
RULE 6: Constructors get executed because of super() present in the constructor.
As you already know, constructors do not get inherited, but it gets executed because of the super() keyword. ‘super()’ is used to refer the extended class. By default, it will refer to the Object class. The constructor in Object does nothing. If a constructor does not explicitly invoke a super-class constructor, then Java compiler will insert a call to the no-argument constructor of the super-class by default.
This brings us to the end of this article on “Inheritance in Java”. Hope, you found it informative and it helped in adding value to your knowledge.
If you wish to check out more articles on the market’s most trending technologies like Artificial Intelligence, DevOps, Ethical Hacking, then you can refer to Edureka’s official site.
Do look out for other articles in this series which will explain the various other aspects of Java.
Name already in use
java_for_beginners_book / c7.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Основные навыки и понятия
- Основы наследования
- Вызов конструктора суперкласса
- Обращения к членам суперкласса с помощью ключевого слова super
- Создание многоуровневой иерархии классов
- Порядок вызова конструкторов
- Представление о ссылках на объекты подкласса из переменной суперкласса
- Переопределение методов
- Применение переопределяемых методов для организации динамического доступа
- Абстрактные классы
- Использование ключевого слова final
- Представление о классе Object
Наследование является одним из трех основополагающих принципов объектно-ориентированного программирования, поскольку оно допускает создание иерархических классификаций. Благодаря наследованию можно создать общий класс, в котором определяются характерные особенности, присущие множеству связанных элементов. От этого класса могут затем наследовать другие, более конкретные классы, добавляя в него свои индивидуальные особенности.
В языке Java наследуемый класс принято называть суперклассом, а наследующий от него класс — подклассом. Следовательно, подкласс — это специализированный вариант суперкласса. Он наследует все переменные и методы, определенные в суперклассе, дополняя их своими элементами.
Наследование одних классов от других отражается в Java при объявлении класса. Для этой цели служит ключевое слово extends. Подкласс дополняет суперкласс, расширяя его.
Рассмотрим простой пример программы, демонстрирующий некоторые свойства наследования. В этой программе определен суперкласс TwoDShape, хранящий сведения о ширине и высоте двумерного объекта. Там же определен и его подкласс Triangle. Обратите внимание на то, что в определении подкласса присутствует ключевое слово extends.
Ниже приведен результат выполнения данной программы.
Здесь в классе TwoDShape определены атрибуты обобщенной двумерной фигуры, конкретным воплощением которой может быть квадрат, треугольник, прямоугольник и т.д. Класс Triangle представляет конкретную разновидность объекта типа TwoDShape, в данном случае — треугольник. Класс Triangle включает в себя все элементы класса TwoDObject, а в дополнение к ним — поле style и методы area() и showStyle(). Описание треугольника хранится в переменной экземпляра style, метод area() вычисляет и возвращает площадь треугольника, а метод showStyle() отображает геометрическую форму треугольника.
В класс Triangle входят все члены суперкласса TwoDShape, и поэтому в теле метода area() доступны переменные экземпляра width и height. Кроме того, с помощью объектов tl и t2 в методе main() можно непосредственно обращаться к переменным width и height, как будто они принадлежат классу Triangle. На рис. 7.1 схематически показано, каким образом суперкласс TwoDShape включается в состав класса Triangle.

Рис. 7.1. Схематическое представление класса Triangle
Несмотря на то что TwoDShape является суперклассом для класса Triangle, он по-прежнему остается независимым классом. Тот факт, что один класс является суперклассом другого класса, совсем не означает, что он не может быть использован самостоятельно. Например, следующий фрагмент кода считается вполне допустимым:
Разумеется, объекту типа TwoDShape ничего не известно о подклассах своего класса TwoDShape, и он не может даже обратиться к ним.
Ниже приведена общая форма объявления класса, наследующего от суперкласса.
Для каждого создаваемого подкласса можно указать только один суперкласс. Множественное наследование в Java не поддерживается, т.е. у подкласса не может быть несколько суперклассов. (Этим Java отличается от языка C++, где можно создать класс, производный сразу от нескольких классов. Об этом не следует забывать, преобразуя код C++ в код Java.) С другой стороны, вполне допустима многоуровневая иерархия, в которой один подкласс является суперклассом другого подкласса. И конечно же, класс не может быть суперклассом для самого себя.
Главное преимущество наследования заключается в следующем: как только будет создан суперкласс, в котором определены общие для множества объектов атрибуты, он может быть использован для создания любого числа более конкретных подклассов. А в каждом подклассе может быть точно выстроена своя собственная классификация. В качестве примера ниже приведен еще один подкласс, производный от суперкласса TwoDShape и инкапсулирующий прямоугольники.
В класс Rectangle входят все члены класса TwoDShape. Кроме того, он содержит метод is Square(), определяющий, является ли прямоугольник квадратом, а также метод area(), вычисляющий площадь прямоугольника.
Доступ к членам класса и наследование
Как пояснялось в главе 6, члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. Но наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в подкласс и входят все члены его суперкласса, то в нем все равно оказываются недоступными те члены суперкласса, которые являются закрытыми. Так, если сделать закрытыми переменные экземпляра width и height в классе TwoDShape, они станут недоступными в классе Triangle, как показано ниже.
Класс Triangle не будет скомпилирован, поскольку ссылки на переменные экземпляра width и height в методе area() нарушают правила доступа. Эти переменные объявлены закрытыми (private), и поэтому они доступны только членам собственного класса. А его подклассам запрещено обращаться к ним.
Напомним, что член класса, объявленный закрытым (private), недоступен за пределами своего класса. Это ограничение распространяется и на подклассы.
На первый взгляд, ограничение на доступ к закрытым членам суперкласса из подкласса кажется трудно преодолимым, поскольку оно не дает во многих случаях возможности пользоваться закрытыми членами этого класса. Но на самом деле это не так. Как пояснялось в главе 6, для обращения к закрытым членам класса в программах на Java обычно используются специальные методы доступа. Ниже в качестве примера приведены видоизмененные классы TwoDShape и Triangle, в которых обращение к переменным экземпляра width и height осуществляется с помощью специальных методов доступа.
Конструкторы и наследование
В иерархии классов допускается, чтобы у суперклассов и подклассов были свои собственные конструкторы. В связи с этим возникает следующий резонный вопрос: какой конструктор отвечает за построение объекта подкласса: конструктор суперкласса, конструктор подкласса или же оба вместе? На этот вопрос можно ответить так: конструктор суперкласса конструирует родительскую часть объекта, а конструктор подкласса — производную часть этого объекта. И в этом есть своя логика, поскольку суперклассу неизвестны и недоступны любые элементы подкласса, а следовательно, их конструирование должно происходить раздельно. В приведенных выше примерах данный вопрос не возникал, поскольку они опирались на автоматическое создание конструкторов, используемых в Java по умолчанию. Но на практике конструкторы определяются явным образом в большинстве классов. Ниже будет показано, каким образом разрешается подобная ситуация.
Если конструктор определен только в подклассе, то все происходит очень просто: конструируется объект подкласса, а родительская часть объекта автоматически конструируется конструктором суперкласса, используемым по умолчанию. В качестве примера ниже приведен переработанный вариант класса Triangle, в котором определяется конструктор, а член style этого класса делается закрытым, так как теперь он устанавливается конструктором.
Здесь конструктор класса Triangle, помимо поля style, инициализирует также унаследованные члены класса TwoDClass.
Если конструкторы объявлены как в подклассе, так и в суперклассе, то дело несколько усложнятся, поскольку должны быть выполнены оба конструктора. В таком случае на помощь приходит ключевое слово super, доступное в двух общих формах. С помощью первой формы вызывается конструктор суперкласса. А вторая форма служит для доступа к членам суперкласса, скрываемым членами подкласса. Рассмотрим первое применение ключевого слова super.
Применение ключевого слова super для вызова конструктора суперкласса
Для вызова конструктора суперкласса служит следующая общая форма ключевого слова super:
где список_параметров обозначает параметры, необходимые для нормальной работы конструктора суперкласса. Вызов конструктора super() должен быть первым оператором в теле конструктора подкласса. Для того чтобы лучше понять особенности вызова super(), рассмотрим вариант класса TwoDShape из следующего примера программы, где определен конструктор, инициализирующий переменные экземпляра width и height:
В конструкторе Triangle присутствует вызов конструктора super() с параметрами w и h. В результате управление получает конструктор TwoDShape(), инициализирующий переменные width и height значениями, передаваемыми ему в качестве параметров. Теперь класс Triangle уже не занимается инициализацией элементов суперкласса. Он должен инициализировать только собственную переменную экземпляра style. Конструктору TwoDShape() предоставляется возможность построить соответствующий подобъект так, как требуется для данного класса. Более того, в суперклассе TwoDShape можно реализовать функции, о которых не будут знать его подклассы. Благодаря этому код становится более устойчивым к ошибкам.
Любая форма конструктора, определенного в суперклассе, может быть вызвана с помощью оператора super(). Для выполнения выбирается тот вариант конструктора, который соответствует указываемым аргументам. В качестве примера ниже приведена расширенная версия классов TwoDShape и Triangle, содержащих конструкторы по умолчанию и конструкторы, принимающие один или более аргумент.
Выполнение этой версии программы дает следующий результат:
Еще раз напомним основные свойства вызова конструктора super(). Когда этот вызов присутствует в конструкторе подкласса, происходит обращение к конструктору его непосредственного суперкласса. Таким образом, вызывается конструктор того класса, который непосредственно породил вызывающий класс. Это справедливо и при многоуровневой иерархии. Кроме того, вызов конструктора super() должен быть первым оператором в теле конструктора подкласса.
Применение ключевого слова super для доступа к членам суперкласса
Существует еще одна общая форма ключевого слова super, которая применяется подобно ключевому слову this, но ссылается на суперкласс данного класса. Эта общая форма обращения к члену суперкласса имеет следующий вид:
где член_класса обозначает метод или переменную экземпляра.
Данная форма ключевого слова super применяется в тех случаях, если член подкласса скрывает член суперкласса. Рассмотрим следующий пример несложной иерархии классов:
Результат выполнения данной программы выглядит следующим образом:
Несмотря на то что переменная экземпляра i в классе В скрывает одноименную переменную в классе А, ключевое слово super позволяет обращаться к переменной i из суперкласса. Аналогичным образом ключевое слово super можно использовать для вызова методов суперкласса, скрываемых методами подкласса.
Пример для опробования 7.1. Расширение класса Vehicle
Для того чтобы продемонстрировать возможности наследования, расширим класс Vehicle, созданный в главе 4. Напомним, что класс Vehicle инкапсулирует данные о транспортных средствах и, в частности, сведения о количестве пассажиров, объеме топливного бака и потреблении топлива. Воспользуемся классом Vehicle в качестве заготовки для создания более специализированных классов. Например, транспортным средством, помимо прочих, является грузовик. Одной из важных характеристик грузовика является его грузоподъемность. Поэтому для создания класса Truck можно расширить класс Vehicle, добавив переменную экземпляра, хранящую сведения о допустимом весе перевозимого груза. В этом проекте переменные экземпляра будут объявлены в классе Vehicle как закрытые (private), а для обращения к ним будут созданы специальные методы доступа.
- Создайте новый файл TruckDemo.java и скопируйте в него исходный код последней версии класса Vehicle, разработанной в главе 4.
- Создайте класс Truck, исходный код которого приведен ниже.
Создание многоуровневой иерархии классов
В представленных до сих пор примерах программ использовались простые иерархии классов, состоявшие только из суперкласса и подкласса. Но в Java можно также строить иерархии, состоящие из любого числа уровней наследования. Как упоминалось выше, многоуровневая иерархия идеально подходит для использования одного подкласса в качестве суперкласса для другого подкласса. Так, если имеются три класса, А, в и С, то класс С может наследовать от класса В, а тот, в свою очередь, от класса А. В таком случае каждый подкласс наследует характерные особенности всех своих суперклассов. В частности, класс С наследует все члены классов В и А.
Для того чтобы стало понятнее назначение многоуровневой иерархии, рассмотрим следующий пример программы. В этой программе подкласс Triangle выступает в роли суперкласса для класса ColorTriangle. Класс ColorTriangle наследует все свойства классов Triangle и TwoDShape, а также содержит поле color, определяющее цвет треугольника.
Результат выполнения данной программы выглядит следующим образом:
Благодаря наследованию в классе ColorTriangle можно использовать ранее определенные классы Triangle и TwoDShape, дополняя их лишь данными, необходимыми для конкретного применения класса ColorTriangle. Таким образом, наследование способствует повторному использованию кода.
Данный пример демонстрирует еще одну важную деталь: оператор super() всегда обращается к конструктору ближайшего суперкласса. Иными словами, оператор super() в классе ColorTriangle означает вызов конструктора класса Triangle, а в классе Triangle — вызов конструктора класса TwoDShape. Если в иерархии классов для конструктора суперкласса предусмотрены параметры, то все суперклассы должны передавать их вверх по иерархической структуре. Это правило действует независимого от того, нужны ли параметры самому подклассу или не нужны.
Порядок вызова конструкторов
В связи с изложенным выше в отношении наследования и иерархии классов может возникнуть следующий резонный вопрос: когда создается объект подкласса и какой конструктор выполняется первым: тот, что определен в подклассе, или же тот, что определен в суперклассе? Так, если имеется суперкласс А и подкласс В, то вызывается ли конструктор класса А раньше конструктора класса В, или же наоборот? Ответ на этот вопрос состоит в том, что в иерархии классов конструкторы вызываются по порядку выведения классов: от суперкласса к подклассу. Более того, оператор super() должен быть первым в конструкторе подкласса, и поэтому порядок, в котором вызываются конструкторы, остается неизменным, независимо от того, используется ли оператор super() или нет. Если оператор super() отсутствует, то выполняется конструктор каждого суперкласса по умолчанию (т.е. конструктор без параметров). В следующем примере программы демонстрируется порядок вызова конструкторов:
Ниже приведен результат выполнения данной программы.
Как видите, конструкторы вызываются по порядку выведения их классов.
По зрелом размышлении можно прийти к выводу, что вызов конструкторов по порядку выведения их классов имеет определенный смысл. Ведь суперклассу ничего не известно ни об одном из производных от него подклассов, и поэтому любая инициализация, которая требуется его членам, осуществляется совершенно отдельно от инициализации членов подкласса, а возможно, это и необходимое условие. Следовательно, онадолжна выполняться первой.
Ссылки на суперкласс и объекты подклассов
Как вам должно быть уже известно, Java является строго типизированным языком программирования. Помимо стандартных преобразований и автоматического продвижения простых типов данных, в этом языке строго соблюдается принцип совместимости типов. Это означает, что переменная ссылки на объект класса одного типа, как правило, не может ссылаться на объект класса другого типа. В качестве примера рассмотрим следующую простую программу:
Несмотря на то что классы X и Y содержат одинаковые члены, переменной типа X нельзя присвоить ссылку на объект типа Y, поскольку типы объектов отличаются. Вообще говоря, переменная ссылки на объект может указывать только на объекты своего типа.
Но из этого строгого правила соблюдения типов имеется следующее важное исключение: переменной ссылки на объект суперкласса может быть присвоена ссылка на объект любого производного от него подкласса. Следовательно, по ссылке на объект суперкласса можно обращаться к объекту подкласса. Ниже приведен соответствующий пример.
В данном примере класс Y является подклассом X. Следовательно, переменной х2 можно присвоить ссылку на объект типа Y.
Следует особо подчеркнуть, что доступ к конкретным членам класса определяется типом переменной ссылки на объект, а не типом объекта, на который она ссылается. Это означает, что если ссылка на объект подкласса присваивается переменной ссылки на объект суперкласса, то доступ разрешается только к тем частям этого объекта, которые определяются суперклассом. Именно поэтому переменной х2 недоступен член b класса Y, когда она ссылается на объект этого класса. И в этом есть своя логика, поскольку суперклассу ничего не известно о тех членах, которые добавлены в производный от него подкласс. Именно поэтому последняя строка кода в приведенном выше примере была закомментирована.
Несмотря на кажущийся несколько отвлеченным характер приведенных выше рассуждений, им можно найти ряд важных применений на практике. Одно из них рассматривается ниже, а другое — далее в этой главе, когда речь пойдет о переопределении методов.
Один из самых важных моментов для присваивания ссылок на объекты подкласса переменным суперкласса наступает тогда, когда конструкторы вызываются в иерархии классов. Как вам должно быть уже известно, в классе нередко определяется конструктор, принимающий объект своего класса в качестве параметра. Благодаря этому в классе может быть сконструирована копия его объекта. Этой особенностью можно выгодно воспользоваться в подклассах, производных от такого класса. В качестве примера рассмотрим очередные версии классов TwoDShape и Triangle. В оба класса добавлены конструкторы, принимающие объект своего класса в качестве параметра.
В приведенном выше примере программы объект t2 конструируется на основании объекта tl, и поэтому они идентичны. Результат выполнения данной программы выглядит следующим образом:
Обратите внимание на конструктор класса Triangle, код которого приведен ниже.
В качестве параметра данному конструктору передается объект Triangle, который затем с помощью вызова super() передается конструктору TwoDShape, как показано ниже.
Следует заметить, что конструктор TwoDshape() должен получить объект типа TwoDShape, но конструктор Triangle() передает ему объект типа Triangle. Тем не менее никаких недоразумений не возникает. Ведь, как пояснялось ранее, переменная ссылки на суперкласс может ссылаться на объект производного от него подкласса. Следовательно, конструктору TwoDShape() можно передать ссылку на экземпляр подкласса, производного от класса TwoDShape. Конструктор TwoDShape() инициализирует лишь те части передаваемого ему объекта подкласса, которые являются членами класса TwoDShape, и поэтому не имеет значения, содержит ли этот объект дополнительные члены, добавленные в производных подклассах.
В иерархии классов часто присутствуют методы с одинаковой сигнатурой и одинаковым возвращаемым значением как в суперклассе, так и в подклассе. В этом случае говорят, что метод суперкласса переопределяется в подклассе. Если переопределяемый метод вызывается из подкласса, то всегда выбирается тот вариант метода, который определен в подклассе. А вариант метода, определенный в суперклассе, скрывается. Рассмотрим в качестве примера следующий фрагмент кода:
Выполнение этого фрагмента кода дает следующий результат:
Когда метод show() вызывается для объекта типа В, выбирается вариант этого метода, определенный в классе В. Таким образом, вариант метода show() в классе В переопределяет вариант одноименного метода, объявленный в классе А.
Если требуется обратиться к исходному варианту переопределяемого метода, т.е. тому, который определен в суперклассе, следует воспользоваться ключевым словом super. Например, в приведенном ниже варианте класса В из метода show() вызывается вариант того же метода, определенный в суперклассе. При этом отображаются все переменные экземпляра.
Если подставить новый вариант метода show() в предыдущую версию рассматриваемого здесь фрагмента кода, результат его выполнения изменится и будет иметь следующий вид:
В данном случае super. show() — это вызов метода show(), определенного в суперклассе.
Переопределение метода происходит только в том случае, если сигнатуры переопределяемого и переопределяющего методов совпадают. В противном случае происходит обычная перегрузка методов. Рассмотрим следующую видоизмененную версию предыдущего примера:
Выполнение этого фрагмента кода дает следующий результат:
На этот раз в варианте метода show() из класса В предусмотрен строковый параметр. И благодаря этому сигнатура данного метода отличается от сигнатуры метода show() из класса А, для которого параметры не предусмотрены. В результате переопределение метода не происходит.
в переопределяемых методах Примеры из предыдущего раздела демонстрируют переопределение методов, но по ним трудно судить, насколько богатые возможности предоставляет этот механизм. В самом деле, если переопределение методов используется только для соблюдения соглашений о пространствах имен, то его можно рассматривать как любопытную, но почти бесполезную особенность языка программирования. Но это совсем не так. Переопределение методов лежит в основе одного из наиболее эффективных языковых средств Java: динамической диспетчеризации методов, представляющей собой механизм вызова переопределяемых методов, когда выбор конкретного метода осуществляется не на этапе компиляции, а в процессе выполнения программы. Динамическая диспетчеризация методов имеет очень большое значение, поскольку именно с ее помощью принцип полиморфизма реализуется на стадии выполнения программ на Java.
Прежде всего напомним один очень важный принцип: переменная ссылки на суперкласс может ссылаться на объект подкласса. В Java этот принцип используется для вызова переопределяемых методов во время выполнения. Если переопределяемый метод вызывается по ссылке на суперкласс, то исполняющая система Java определяет по типу объекта, какой именно вариант метода следует вызвать, причем делает это во время выполнения программы. Если ссылки указывают на разные типы объектов, то и вызываться будут разные версии переопределенных методов. Иными словами, вариант переопределенного метода для вызова определяет не тип переменной, а тип объекта, на который она ссылается. Так, если суперкласс содержит метод, переопределяемый в подклассе, то вызывается метод, соответствующий тому типу объекта, на который указывает переменная ссылки на суперкласс.
Ниже приведен простой пример, демонстрирующий динамическую диспетчеризацию методов в действии.
Результат выполнения данной программы выглядит следующим образом:
В данном примере программы определяются суперкласс Sup и два его подкласса Subl и Sub2. В классе Sup объявляется метод who(), переопределяемый в его подклассах. В методе main() создаются объекты типа Sup, Subl и Sub2. Там же объявляется переменная supRef ссылки на объект типа Sup. Затем переменной supRef в методе main() поочередно присваиваются ссылки на объекты разного типа, и далее эти ссылки используются для вызова метода who(). Как следует из результата выполнения данной программы, вызываемый вариант метода who() определяется типом объекта, на который ссылается переменная supRef в момент вызова, а не типом самой этой переменной.
Причины для переопределения методов
Как упоминалось выше, переопределяемые методы обеспечивают соблюдение принципа полиморфизма при выполнении программ на Java. Полиморфизм в объектно-ориентированных программах имеет большое значение потому, что благодаря ему появляется возможность объявить в суперклассе методы, общие для всех его подклассов, а в самих подклассах — определить специфические реализации всех этих методов или некоторых из них. Переопределение методов — один из способов, которыми в Java реализуется принцип полиморфизма “один интерфейс — множество методов”.
Залогом успешного применения полиморфизма является, в частности, понимание того, что суперклассы и подклассы образуют иерархию в направлении от меньшей специализации к большей. Если суперкласс организован правильно, он предоставляет своему подклассу все элементы, которыми тот может пользоваться непосредственно. В нем также определяются те методы, которые должны быть по-своему реализованы в порожденных классах. Таким образом, подклассы получают достаточную свободу определять собственные методы, реализуя в то же время согласованный интерфейс. Сочетая наследование с переопределением методов, в суперклассе можно определить общую форму методов для использования во всех его подклассах.
Демонстрация механизма переопределения методов на примере класса TwoDShape Для того чтобы стало понятнее, насколько эффективным является механизм переопределения методов, продемонстрируем его на примере класса TwoDShape. В приведенных ранее примерах в каждом классе, порожденном от класса TwoDShape, определялся метод area(). Теперь мы знаем, что в этом случае имеет смысл включить метод area() в состав класса TwoDShape и позволить каждому его подклассу переопределить этот метод: в частности, реализовать вычисление площади в зависимости от конкретного типа геометрической фигуры. Именно такой подход и воплощен в приведенном ниже примере программы. Для удобства в класс TwoDShape добавлено поле name, упрощающее написание демонстрационных программ.
Ниже приведен результат выполнения данной программы.
Рассмотрим код данной программы более подробно. Теперь, как и предполагалось при написании программы, метод area() входит в состав класса TwoDShape и переопределяется в классах Triangle и Rectangle. В классе TwoDShape метод area() играет роль заполнителя и лишь уведомляет пользователя о том, что этот метод должен быть переопределен в подклассе. При каждом переопределении метода area() в нем реализуются средства, необходимые для того типа объекта, который инкапсулируется в подклассе. Так, если требуется реализовать класс для эллипсов, метод area() придется переопределить таким образом, чтобы он вычислял площадь этой фигуры.
У рассматриваемой здесь программы имеется еще одна важная особенность. Обратите внимание на то, что в методе main() геометрические фигуры объявляются как массив объектов типа TwoDShape. Но на самом деле элементами массива являются ссылки на объекты Triangle, Rectangle и TwoDShape. Это вполне допустимо. Ведь, как пояснялось ранее, переменная ссылки на суперкласс может ссылаться на объект его подкласса. В этой программе организован перебор элементов массива в цикле и вывод сведений о каждом объекте. Несмотря на всю простоту данного примера, он наглядно демонстрирует потенциальные возможности как наследования, так и переопределения методов. Тип объекта, на который указывает переменная ссылки на суперкласс, определяется на этапе выполнения программы и обрабатывается соответствующим образом. Если объект является производным от класса TwoDShape, его площадь вычисляется при вызове метода area(). Интерфейс для данной операции оказывается общим и не зависит от того, с какой именно геометрической фигурой приходится иметь дело каждый раз.
Применение абстрактных классов
Иногда требуется создать суперкласс, в котором определяется лишь самая общая форма для всех его подклассов, а наполнение ее деталями предоставляется каждому из этих подклассов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в подклассах, а не в самом суперклассе. Подобная ситуация возникает, например, в связи с невозможностью получить содержательную реализацию метода в суперклассе. Именно такая ситуация была продемонстрирована в варианте класса TwoDShape из предыдущего примера, где метод area() был просто определен как заполнитель. Такой метод не вычисляет и не выводит площадь двумерной геометрической формы любого типа.
Создавая собственные библиотеки классов, вы можете сами убедиться в том, что у метода зачастую отсутствует содержательное определение в контексте его суперкласса. Подобное затруднение разрешается двумя способами. Один из них, как показано в предыдущем примере, состоит в том, чтобы просто выдать предупреждающее сообщение. И хотя такой способ может пригодиться в некоторых случаях, например при отладке, в практике программирования он обычно не применяется. Ведь в суперклассе могут быть объявлены методы, которые должны быть переопределены в подклассе, чтобы этот класс стал содержательным. Рассмотрим для примера класс Triangle. Он был бы неполным, если бы в нем не был переопределен метод area(). В подобных случаях требуется какой-то способ, гарантирующий, что в подклассе действительно будут переопределены все необходимые методы. И такой способ в Java имеется. Он состоит в использовании абстрактного метода.
Абстрактный метод создается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в суперклассе. Это означает, что он должен быть переопределен в подклассе, поскольку его вариант из суперкласса просто непригоден для использования. Для определения абстрактного метода служит приведенная ниже общая форма,
Как видите, у абстрактного метода отсутствует тело. Модификатор abstract может применяться только к обычным методам, но не к статическим методам (static) и конструкторам.
Класс, содержащий один или более абстрактный метод, должен быть также объявлен как абстрактный, для чего перед его объявлением class указывается модификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект абстрактного класса с помощью оператора new приведет к ошибке во время компиляции.
Когда подкласс наследует абстрактный класс, в нем должны быть реализованы все абстрактные методы суперкласса. В противном случае подкласс должен быть также определен как abstract. Таким образом, атрибут abstract наследуется до тех пор, пока не будет достигнута полная реализация класса.
Используя абстрактный класс, мы можем усовершенствовать рассматривавшийся ранее класс TwoDShape-Для неопределенной двумерной геометрической фигуры понятие площади не имеет никакого смысла, поэтому в приведенной ниже версии предыдущей программы метод area() и сам класс TwoDShape объявляются как abstract. Это, конечно, означает, что во всех классах, производных от класса TwoDShape, должен быть переопределен метод area().
Как показывает представленный выше пример программы, во всех подклассах, производных от класса TwoDShape, метод area() должен быть непременно переопределен. Убедитесь в этом сами, попробовав создать подкласс, в котором не переопределен метод area(). В итоге вы получите сообщение об ошибке во время компиляции. Конечно, возможность создавать ссылки на объекты типа TwoDShape по-прежнему существует, и это было сделано в приведенном выше примере программы, но объявлять объекты типа TwoDShape уже нельзя. Именно поэтому массив shapes сокращен в методе main() до 4 элементов, а объект типа TwoDShape для общей двумерной геометрической формы больше не создается.
И еще одно, последнее замечание. Обратите внимание на то, что в классе TwoDShape по-прежнему присутствуют определения методов showDim() и getName() и перед их именами нет модификатора abstract. В абстрактные классы вполне допускается (и часто практикуется) включать конкретные методы, которые могут быть использованы в своем исходном виде в подклассе. А переопределению в подклассах подлежат только те методы, которые объявлены как abstract.
Использование ключевого слова final
Какие бы богатые возможности ни представляли механизмы наследования и переопределения методов, иногда требуется запретить их действие. Допустим, создается класс, в котором инкапсулированы средства управления некоторым устройством. Кроме того, в этом классе пользователю может быть разрешено инициализировать устройство, чтобы воспользоваться некоторой секретной информацией. В таком случае пользователи данного класса не должны иметь возможность переопределять метод инициализации устройства. Для этой цели в Java предоставляется ключевое слово final, позволяющее без труда запретить переопределение метода или наследование класса.
Предотвращение переопределения методов
Для того чтобы предотвратить переопределение метода, в начале его объявления нужно указать модификатор доступа final. Переопределять объявленные подобным образом методы нельзя. Ниже приведен фрагмент кода, демонстрирующий использование ключевого слова final для подобных целей.
Поскольку метод meth() объявлен как final, его нельзя переопределить в классе В. Если вы попытаетесь сделать это, возникнет ошибка при компиляции программы.
Предотвратить наследование класса можно, указав в определении класса ключевое слово final. В этом случае считается, что данное ключевое слово применяется ко всем методам класса. Очевидно, что не имеет никакого смысла применять ключевое слово final к абстрактным классам. Ведь абстрактный класс не завершен по определению, и объявленные в нем методы должны быть реализованы в подклассах.
Ниже приведен пример класса, подклассы которого создавать запрещено.
Как следует из комментариев к данному примеру, недопустимо, чтобы класс В наследовал от класса А, так как последний определен как final.
Применение ключевого слова final к переменным экземпляра
Помимо рассмотренных ранее примеров использования, ключевое слово final можно применять и к переменным экземпляра. Подобным способом создаются именованные константы. Если имени переменной предшествует модификатор final, то значение этой переменной не может быть изменено на протяжении всего времени выполнения программы. Очевидно, что подобным переменным нужно присваивать начальные значения. В главе 6 был рассмотрен простой класс ErrorMsg для обработки ошибок. В нем устанавливается соответствие между кодами ошибок и символьными строками сообщений об ошибках. Ниже приведен усовершенствованный вариант этого класса, в котором для создания именованных констант применяется модификатор final. Теперь, вместо того чтобы передавать методу getErrorMsg() числовое значение, например 2, достаточно указать при его вызове именованную целочисленную константу DISKERR.
Обратите внимание на то, как используются константы в методе main(). Они являются членами класса ErrorMsg, и поэтому для доступа к ним требуется ссылка на объект этого класса. Разумеется, константы могут быть унаследованы подклассами и непосредственно доступными в них.
Многие программирующие на Java пользуются именами констант типа final, составленными полностью из прописных букв, как в предыдущем примере. Но это не строгое правило, а только принятый стиль программирования.
В Java определен специальный класс Object. По умолчанию он считается суперклассом всех остальных классов. Иными словами, все классы являются подклассами, производными от класса Object. Это означает, что переменная ссылки на объект типа Object может ссылаться на объект любого класса. Более того, переменная ссылки на объект типа Object может также ссылаться на любой массив, поскольку массивы реализованы в виде классов.
В классе Object определены перечисленные ниже методы, доступные в любом объекте.
| Метод | Назначение |
|---|---|
| Object clone() | Создает новый объект, аналогичный клонируемому объекту |
| boolean equals (Object объект) | Определяет равнозначность объектов |
| void finalize() | Вызывается перед тем, как неиспользуемый объект будет удален системой «сборки мусора» |
| Class<?> getClass() | Определяет класс объекта во время выполнения |
| int hashCode() | Возвращает хеш-код, связанный с вызывающим объектом |
| void notify() | Возобновляет работу потока, ожидающего уведомления от вызывающего объекта |
| void notifyAll() | Возобновляет работу всех потоков, ожидающих уведомления от вызывающего объекта |
| String toString() | Возвращает символьную строку, описывающую объект |
| void wait() | Ожидает исполнения другого потока |
| void wait (long миллисекунды) | Ожидает исполнения другого потока |
| void wait (long миллисекунды, int наносекунды) | Ожидает исполнения другого потока |
Методы getClass(), notify(), notifyAll() и wait() объявлены как final, а остальные методы можно переопределять в подклассах. Некоторые из этих методов будут описаны далее в этой книге. Два из них — equals() и toString() — заслуживают особого внимания. Метод equals() сравнивает два объекта. Если объекты равнозначны, то он возвращает логическое значение true, иначе — логическое значение false. Метод toString() возвращает символьную строку, содержащую описание того объекта, которому принадлежит этот метод. Он автоматически вызывается в том случае, если объект передается методу println() в качестве параметра. Во многих классах этот метод переопределяется. В этом случае описание специально подбирается для конкретных типов объектов, которые в них создаются.
Обратите внимание на необычный синтаксис, описывающий значение, возвращаемое методом getClass(). Это обобщенный тип. С помощью обобщений в Java можно указывать в качестве параметра тип данных, используемый в классе или методе. Более подробно обобщения рассматриваются в главе 13.