Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
class Person {
String name;
void print(){
System.out.println("Name: " + name);
}
}
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия - класс Employee:
class Employee {
String name;
void print(){
System.out.println("Name: " + name);
}
}
Однако так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник - это также и человек, то было бы целесообразнее не определять повторно один и тот же функционал, а как-то передать этот функционал из класса Person в класс Employee. И для этого можно унаследовать класс Employee от класса Person:
class Employee extends Person { }
Чтобы унаследовать один класс от другого, надо использовать после имени класса-наследника ключевое слово extends, после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Таким образом, наследование реализует отношение is-a (является). То есть объект класса Employee также является объектом класса Person. В этом отношении класс Employee еще называют производным классом (а также дочерним классом, классом-наследником, подклассом) а класс Person - базовым классом (а также родительским классом или суперклассом).
Затем мы можем использовать функционал базового класса (Person), обращаясь к объекту производного класса (Employee):
public class Program{
public static void main (String args[]){
Person tom = new Person();
tom.name = "Tom";
tom.print();
Employee bob = new Employee();
bob.name = "Bob";
bob.print();
}
}
class Person {
String name;
void print(){
System.out.println("Name: " + name);
}
}
class Employee extends Person{}
То есть в данном случае благодаря наследованию у объекта класса Employee мы можем обратиться и к полю name, и к методу print, хотя они определены в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super, в который передаются значения для параметров конструкторв:
super(аргументы);
Например, определим к классе Person конструктор:
class Person {
private String name;
Person(String name){
this.name = name;
}
void print(){
System.out.println("Name: " + name);
}
}
class Employee extends Person{
// надо вызвать конструктор базового класса
Employee(String name){
super(name); // вызываем конструктор базового класса
}
}
Здесь конструктор класса Person принимает один параметр. Поэтому в классе
Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов.
Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
Использование классов:
public class Program{
public static void main (String args[]){
Person tom = new Person("Tom");
tom.print();
Employee bob = new Employee("Bob");
bob.print();
}
}
class Person {
private String name;
Person(String name){
this.name = name;
}
void print(){
System.out.println("Name: " + name);
}
}
class Employee extends Person{
Employee(String name){
super(name);
}
}
Стоит отметить, что при определении конструктора в производном классе до версии Java 25 вызов конструктора суперкласса должны был выполняться ДО всех остальных инструкций. Например:
Employee(String name){
super(name); // До Java 25 вызов конструктора мог быть только в самом начале
// все остальные инструкции
System.out.println("Employee created");
}
Начиная с версии Java 25 вызов конструктора суперкласса может располагаться в любом месте конструктора производного класса. Например:
Employee(String name){
System.out.println("Employee creation started");
super(name); // Начиная с Java 25 можно поместить вызов конструктора в любое место
}
Производный класс не только наследует функционал от базового класса, но и также может расширять его - добавлять свои поля и методы. Например, класс Employee представляет работника некоторой компании, и соответственно нам бы хотелось хранить в нем данные об этой компании:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
bob.print(); // Name: Bob
bob.work(); // Company: Google
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
// выводим данные о компании
void work(){
System.out.println("Company: " + company);
}
}
class Person {
private String name;
Person(String name){
this.name = name;
}
void print(){
System.out.println("Name: " + name);
}
}
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work для вывода названия компании.
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private.
Так в примере выше мы сделали поле name в классе Person приватным (определенным с модификатором private). Поэтому класс Employee не имеет к нему доступа:
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
// выводим данные о компании
void work(){
System.out.println("Name: " + name); // ! Ошибка: так нельзя - переменная name - приватная
System.out.println("Company: " + company);
}
}
При попытке скомпилировать этот класс мы столкнемся с ошибкой, так как в методе work идет обращение к переменной name базового класса Person, которая является приватной.
Определение полей приватными представляет стандартную практику лучше инкапсулировать и защитить данные от некорретного вмешательства извне.
Однако бывают случаи, когда требуется открыть доступ к компоненту (полю или методу) базового класса только для производных классов. В этом случае компонент класса объявляется с помощью модификатора
protected. Например, если суперкласс Person объявляет поле name с модификатором protected, а не приватным, то методы класса Employee могут обращаться к нему напрямую:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
bob.work(); // Name: Bob
// Company: Google
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
// выводим данные о компании
void work(){
System.out.println("Name: " + name); // так можно - переменная name - protected
System.out.println("Company: " + company);
}
}
class Person {
protected String name;
Person(String name){ this.name = name; }
void print(){
System.out.println("Name: " + name);
}
}
В Java поле с модификатором protected доступно любому классу в том же пакете. Поэтому для надлежащей инкапсуляции нам надо поместить класс Person в отдельный пакет. В этом случае поля и методы с модификатором protected
будут доступны только классу Employee и другим унаследованным классам.
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. При переопределении метода производный класс определяет метод с тем же именем, что и базовый класс. Например:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
bob.print();
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
@Override
void print(){
System.out.println("Company: " + company);
}
}
class Person {
private String name;
Person(String name){ this.name = name; }
void print(){
System.out.println("Name: " + name);
}
}
Здесь производный класс Employee переопределяет метод print из базового класса. Перед переопределяемым методом указывается аннотация @Override. Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
В классе Employee реализация метода print выводит значение поля company на консоль. Но что делать, если мы хотим, чтобы переопределенный метод print также выводил и значение поля name из базового класса? Напрямую в классе Employee мы не можем обращаться к полю name, потому что оно приватное. Однако мы можем вызвать в производном классе реализацию метода базового класса с помощью слова super:
super.метод(аргументы)
Например:
public class Program{
public static void main (String args[]){
Employee bob = new Employee("Bob", "Google");
bob.print(); // Name: Bob
// Company: Google
}
}
class Employee extends Person{
private String company;
Employee(String name, String company){
super(name);
this.company = company;
}
@Override
void print(){
super.print(); // вызов реализации метожжа print из класса Person
System.out.println("Company: " + company);
}
}
class Person {
private String name;
Person(String name){ this.name = name; }
void print(){
System.out.println("Name: " + name);
}
}
Здесь в методе print в классе Employee помощью ключевого слова super мы обращаемся к реализации метода print из базового класса, которая выводит имя:
super.print();