Заведём жабу? Часть 9. Наследование.

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

Прежде, чем приступить к практике наследования следует повторить знния по модификаторам доступа. Всего существует 4 модификатора доступа.

public — доступ с любого места. Чаще всего используется для внешнего интерфейса.

protected — доступ внутри пакета и в дочерних классах. Чаще всего используется при наследовании.

без указания (или по умолчанию) — доступ внутри пакет. Такой вариант использовать нежелательно. Лучше указывать явно модификатор доступа.

private — доступ только внутри класса. Чаще всего используется для скрытия реализации (инкапсуляции). То есть для каких-либо служебных методов, которые должны работать только внутри класса.

Список представлен в порядке уменьшения области видимости (сверху-вниз).

Давайте попробуем использовать наследование в программном коде.

Наш файл MyFirstProgram.java выглядит примерно так:


package myfirstprogram;
 
import testobject.Computer;
 
public class MyFirstProgram {
 
    public static void main(String[] args) {
       
      Computer comp = new Computer("IBM", 2048, 320, 2);
       
//     comp.setName("IBM");
//       
//      comp.setRam(2048);
//      
//      comp.setHdd(320);
       
      comp.on();
       
      comp.load();
       
      comp.off();
       
    }
     
}


а файл Computer.java примерно так:


package testobject;
 
public class Computer {
     
    private String name;    
      
    private int ram;  
         
    private int hdd;
     
    private double weight;
    
    
    public Computer() {
        
    }
    
    public Computer (String name, int ram, int hdd, double weight) {
        
        this.name = name;
        
        this.ram = ram;
        
         this.hdd = hdd;
         
         this.weight = weight;
    }
      
    
    
    public String getName(){
        return name;
    }
    
    public void setName(String newName){
        
        name = newName;
        
    }
    
    public int getRam(){
        
        return ram;
        
    }
    
   public void setRam(int newRam){
        if(newRam>0){
        
        ram = newRam;
   
 } else {
        
         print("Переданное значение " +newRam+ " не может быть отрицательным!");   
            
        }
    }
   
  public void setHdd(int newHdd){
        if(newHdd>0){
        
        hdd = newHdd;
   
 } else {
        
        print("Переданное значение " +newHdd+ " не может быть отрицательным!");   
            
        }
    }
   
   public void setWeight(int newWeight){
        if(newWeight>0){
        
        weight = newWeight;
   
 } else {
        
        print("Переданное значение " +newWeight+ "не может быть отрицательным!");   
            
        }
    }
   
    
    public void on(){
        
       int time;
         
       print("Я включился. Моя модель " + name);  
         
    }
     
      public void off(){
         
      print("Я выключился");  
           
    }
     
    public void load(){
       
    print("Я загружаюсь. Мой объём жесткого диска равен " + hdd + " ГБ");  
      
 }
    
      private void print (String str){
    
      System.out.println(str);
}
     
}

Откроем NetBeans и перейдём в класс MyFirstProgram.

Здесь мы создали конструктор Computer с какими-то параметрами и использовали сервисные методы — включить, загрузить, отключить. Теперь наша задача создать объект, который будет наследоваться от объекта Computer. Давайте создадим объект Notebook, который расширит наш объект Computer. Для этого:

1) В Netbeans нажмите ПКМ по пакету (папке) testobject.

2) В открывшемся окне выберите Новый — класс Java.

3) В открывшемся окне в строке напишите имя класса Notebook, нажмите кнопку Готово.

Ноутбук является расширенным вариантом компьютера, то есть он умеет всё то, что умеет компьютер, плюс некоторые из методов он будет переопределять. Также он добавит свои какие-то методы. То есть по другому можно сказать, что ноутбук — это усовершенствованный объект компьютер. Чтобы произвести наследование от компьютера, нам нужно написать ключевое слово Extends (то есть расширить), а также указать название класса, от которого расширяемся.


package testobject;

public class Notebook extends Computer{
    
}

 

Так как наши два класса Computer и Notebook находятся в одном пакете testobject, то при использовании класса Computer в классе Notebok нам нет необходимости добавлять его при помощи оператор import. Классы, находящиеся в одном пакете прекрасно «видят» друг друга.

Не станем ничего добавлять в класс Notebook, однако проверим, как ноутбук унаследовал методы от компьютера. Для этого переходим в myfirstprogram, закомментируем строчку «Computer comp = new Computer(«IBM», 2048, 320, 2);» а также строки «comp.on(); comp.load(); comp.off();» (напоминаю, чтобы закомментировать можно выделить нужно строку, нажать Ctrl + правый слэшь, находится возле правого шифта).

Теперь давайте создадим объект Notebook.

 Notebook notebook = new Notebook(); 

Не обязательно переменную называть также, как и объект. Можно написать и так:

 Notebook note = new Notebook(); 

Можно даже назвать просто «n», однако в этом случае интуитивно не ясно, что именно эта «n» ссылка на объект Notebook.

Доступные методы объекта Notebook в Netbeans можно просмотреть через точку (пишем «notebook.» вот так с точкой и ждём всплывающее окно). Несмотря на то, что тело нашего класса осталось пустым, нам доступны все методы Computer и внутри класса Notebook.

Это «сетеры», «гетеры», методы on, off и т.д., то есть наш объект Notebook унаследовал все доступные методы из объекта Computer. Давайте выполнимен метод on. Напишем

notebook.on();

и запустим программу.

Должно появиться «Я включился. Моя модель null.» Метод on(); выполнился нормально. Также можно вызвать метод load(); и метод off();

 notebook.on();
notebook.load();
notebook.off(); 

Снова запустим и увидим что-то вроде: «Я включился. Моя модель null
Я загружаюсь. Мой объём жесткого диска равен 0 ГБ
Я выключился »

Объект Notebook унаследовал все доступные методы от объекта Computer. Если бы механизм наследования отсутствовал, нам бы пришлось вручную копировать методы из класса Computer в класс Notebook, что не так удобно
, к примеру если нужно что-то поменять в методе on(); класса Computer, то также вручную пришлось бы вносить изменения в метод on(); класса Notebook.

Наследование избавляет от повторного написания кода и делает наш код более гибким и оптимальным. Но почему наши методы вывели значение null и 0 ГБ, то есть значения по умолчанию? Тут нужно знать особенности поведения конструкторов при наследовании. В класса Notebook у нас нет конструктора, а это значит, что автоматически создан конструктор «по умолчанию» без параметров.

То есть это тоже самое, как будто написано вот так:


public Notebook(){

}

То есть ничего не поменялось, просто тоже самое мы прописали вручную явно. Но даже если этого конструктора нет, он всё равно присутствует, но не виден для глаз, поскольку автоматически создаётся компилятором. Давайте изменим класс Notebook так, чтобы он выводил надпись в конструкторе.


public class Notebook extends Computer{
    
public Notebook() {
        
      System.out.println("Notebokk: конструктор");
    }
    
}

В классе Computer у нас есть два конструктора. Их также можно просмотреть слева в навигаторе (они выделяются ромбиком).


public Computer() {
        
    }

а также

public Computer (String name, int ram, int hdd, double weight) {
        
        this.name = name;
        
        this.ram = ram;
        
         this.hdd = hdd;
         
         this.weight = weight;
    }

Первый конструктор без параметров, второй с 4 параметрами. Давайте в конструктор без параметров класса Computer также добавим вывод текста:


  public Computer() {
        
        System.out.println("Computer: конструктор");
        
    }

Теперь ещё раз запустим наш проект. Получим что-то вроде:

Computer: конструктор
Notebook: конструктор
Я включился. Моя модель null
Я загружаюсь. Мой объём жесткого диска равен 0 ГБ
Я выключился

Мы видим, что при создании объекта Notebook вначале выполнился конструктор объекта Computer и только после этого конструктор объекта Notebook. Отсюда следует важное правило: при создании дочернего объекта вызов конструкторов происходит по цепочке, начиная от самого верхнего родительского объекта.

У нас сначала вызывался конструктор Компьютер, потом конструктор Ноутбук. Ещё одно правило говорит, что дочерний класс должен иметь хотя бы один конструктор, который имеет родительский!

В нашем случае объект Ноутбук имеет конструктор без параметров и объект Компьютер имеет конструктор без параметров. Если удалить конструктор без параметров у класса Computer (то есть удалить эту часть кода

  public Computer() {
        
        System.out.println("Computer: конструктор");
        
    } 

), то сразу же появится ошибка, потому что в классе Ноутбук нет ни одного конструктора класса Компьютер. То есть в дочернем классе должен быть хотя бы один конструктор родительского класса. В классе Notebook должен быть хотя бы один конструктор, который полностью совпадает с параметрами конструктора родительского класса Computer.

Конструкторы не наследуются! Они вызываются. Это правило наследования в JAVA.

При создании объекта Notebook вызываются все конструкторы по цепочке, начиная с самого верхнего родительского объекта и так далее до вызываемого объекта Notebook.

Теперь поговорим о переопределении методов. Или скрытие методов. Переопределение метода означает, что если в родительском классе у нас есть какие-либо методы (например load(); on(), off();) и если мы напишем в дочернем классе напишем такой же метод с аналогичной сигнатурой, что и в родительском классе (к примеру скопируем метод on(); из класса Computer в класс Notebook(); кстати в этом случае нам будет недоступен метод print, в классе Computer можно изменить модификатор доступа метода print с private на protected, аналогично с переменной name, тогда всё будет в порядке), то при вызове метода в дочернем классе выполнится переопределённый.

Изменим файл Notebook.java следующим образом:


public class Notebook extends Computer{
    
public Notebook() {
        
      System.out.println("Notebook: конструктор");
    }
    
  public void on(){
                       
       print("Notebook. Я включился. Моя модель " + getName());  
         
    }


}

Теперь запустим программу. Получим что-то вроде:

Computer: конструктор
Notebook: конструктор
Notebook. Я включился. Моя модель null
Я загружаюсь. Мой объём жесткого диска равен 0 ГБ
Я выключился

Как мы видим наш метод on(); изменился. Мы его переопределили. И теперь у объекта ноутбук есть переопределённый метод on();, который в данном случае выполнится. Говорят, что объект Ноутбук скрыл метод on(); у родительского класса.

У родительского был такой метод:


 public void on(){
        
       int time;
         
       print("Я включился. Моя модель " + name);  
         
    }

у дочернего такой:

  public void on(){
                       
       print("Notebook. Я включился. Моя модель " + getName());  
         
    }

Теперь при создании объекта Notebook и вызове метода on() всегда будет вызывать переопределённый метод.

В Java есть такое понятие, как аннотация. Обратите внимание Netbeans напротив переопределённого метода отображает жёлтую лампочку с предложением добавить аннотацию Override.

Если нажать на данный пункт, перед методом появится вот такое слово @Override. Желательно использовать аннотацию, поскольку оно облегчает работу программиста.

Также есть такое ключевое слово, как super.

Если написать super в классе Notebook внутри переопределённого метода on() и поставить точку, то мы увидим все доступные методы из родительского класса.

Например мы в переопределённом методе on(); можем вызвать родительский метод on();

public class Notebook extends Computer{
    
public Notebook() {
        
      System.out.println("Notebook: конструктор");
    }
    
@Override
  public void on(){
                     
      super.on();
       print("Notebook. Я включился. Моя модель " + getName());  
         
    }
}

Давайте запустим программу и посмотрим, что произойдёт.

«run:
Computer: конструктор
Notebook: конструктор
Я включился. Моя модель null
Notebook. Я включился. Моя модель null
Я загружаюсь. Мой объём жесткого диска равен 0 ГБ
Я выключился
СБОРКА УСПЕШНО ЗАВЕРШЕНА (общее время: 0 секунд)»

Сначала выполнился метод on(); родительского класса, а уже потом переопределённый метод. Это нужно, чтобы мы в дочернем методе могли вызывать любой метод из родительского класса с помощью ключевого слова «super.» По аналогии со словом this. Только this ссылается на текущий объект/переменную, а super на родительский.

Если поставить курсор в свободное место внутри класса и нажать ctrl + пробел, то нам выведется список методов с пояснением «перепопределить.»

Давайте выберем метод load(); Получим что-то вроде:

  @Override
    public void load() {
        super.load(); //To change body of generated methods, choose Tools | Templates.
    }

Если родительский метод load(); не нужен, можно удалить super.load();

Netbeans позволяет просмотреть дерево зависимости конкретного класса. ТО есть можно увидеть от кого наследуется данный класс. Например можно щёлкнуть правой кнопкой мыши по файлу Notebook.java в Netbeans и выбрать «Иерархия файлов» (или alt + F12)

Можно прочитать описание каждого элемента зависимости. Также важно знать, что в Java все классы наследуются от класса Object. Это задумка разработчиков языка Java, чтобы на самом верхнем уровне находился один объект Object. Если щёлкним на него два раза — сможем перейти и посмотреть, какие у него есть методы и что он из себя представляет.

Методы класса Object Имеют все создаваемые классы в Java, а также имеют возможность переписать, переопределить все доступные методы. Даже если у класса не указан никакой extends, то есть он ни от кого не наследуется, то компилятор добавляет автоматически вот такой кусок кода extends Object. К примеру


public class Computer extends Object {
     
    protected String name;    
      
    private int ram;  
         
    private int hdd;
     
    private double weight;

}

По умолчанию все классы Java наследуются от этого объекта Object. Теперь парочка определений.

Аннотации — это дополнительные служебные данные, которые используются в процессе анализа кода компилятором для указания каких-либо инструкций.

Если метод использует super — компилятор сразу будет искать этот метод в родительском классе, минуя текущий.

Можно и не указывать слово super, тогда компилятор будет искать его в текущем классе, а затем в родительском.

Желательно всего строго указывать super и @Override, если они необходимы. Таким образом вероятность ошибки меньше.

Дочерний класс видит:

Открытые методы и переменные, указанные с модификатором public.

Защищённые (protected) методы и переменные.

Методы и переменные, защищённые на уровне пакета (то есть те, где не указан модификатор доступа), если суперкласс находится в том же пакете, что и дочерний — нежелательно так делать!

Все объекты наследуются от Object, даже если не указано extends Object.

Родительские классы наследуют элементы дочернего класса!

В дочерних классах при наследовании можно расширять модификатор доступа, но нельзя сужать.

Нет множественного наследования как в C++ (дочерний класс может иметь только один родительский класс)

Так делать можно:

Так тоже можно:

А вот так уже нельзя:

Два родительских объекта в Java быть не может.

Когда есть общее поведение для каких-либо объектов — нужно выносить их в родительский класс.

Нужно уметь правильно наследоваться, т.е. уметь выделять общие классы.

Наследование избавляет вашу программу от избыточности.

Если нужно изменить общее поведение — то наследование автоматически передаст это изменение дочерним классам.

Дочерний класс наследует доступные методы и переменные от родительского класса и может прибавлять свои собственные методы и переменные.

Поделиться ссылкой:

Добавить комментарий

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