Заведём жабу? Часть 11. Абстрактные классы и интерфейсы.

Абстрактные классы и интерфейсы.

Начнём с понятия абстрактного класса. Чтобы понять что это такое рассмотрим пример из жизни. При создании ноутбуков фирмы часто дают им конкретные названия моделей. Каждая модель отличается от другой какой-то особенностью. Но смысл их один и тот же. ни выполняют одни и те же действия. По сути они являются ноутбуками, но каждый со своей уникальной реализацией.

Нельзя создать просто ноутбук — это бессмысленно. Фирма-производитель всегда создаёт какую-то конкретную модель. Потому что ноутбук — это абстрактное понятие. Или другой пример — машина. Заводы не производят просто машины — они производят конкретные модели машин. Каждая со своими характеристиками. Но основа у всех одна — машина. То есть поведение, действия у всех машин одинаковые. Машины могут ездить, сигналить, поворачивать и так далее. Но каждая модель делает это по своему, плюс добавляет какие-то свои удобства.

Можно сказать, что все машины используют какую-то одну модель поведения, то есть они должны уметь выполнять определённые действия, характерные для машины. Но как именно выполнять — это уже на усмотрение каждой модели. И здесь мы подошли к понятию абстрактного класса.

Абстрактный класс нужен для того, чтобы задать поведение для всех дочерних объектов или, можно сказать, задать модель поведения. Например абстрактное понятие ноутбук может задать модель поведения, которая будет характерна для всех ноутбуков. Но каждая конкретная модель будет реализовывать её по своему. Рассмотрим схему.

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

Мы не можем создать объект Notebook, Toshiba или Asus. Это общие понятия, а вот конкретную модель ноутбука можем. И каждая конкретная модель будет реализовывать всё, что заложено в уровне абстракции. В предыдущих примерах все эти классы были классами с реализацией, то есть Ноутбук имел какие-то свои реализованные методы. А модели Тошиба и Асус могли переопределить какие-то методы.

Или если рассматривать пример с машиной, то абстрактное понятие машина задаёт поведение, характерное для всех машин. Но каждая конкретная модель машины реализовывает её по своему. Посмотрим как всё это выглядит в программном коде.

В настоящее время у нас существует пакет myfirstprogram с файлом MyFirstProgram.java со следующим кодом:

package myfirstprogram;
import testobject.Computer;
import testobject.Notebook;
 
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 notebook = new Notebook("IBM");

Computer comp = new Computer("Comp");

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

doSmth(notebook);
doSmth(comp);
    }  
    
public static void doSmth(Computer comp){
  
    if(comp instanceof Notebook){
        ((Notebook)comp).on();
        ((Notebook)comp).charge();
      } else if (comp instanceof Computer){
           comp.on();
    }  
  }
    
}

пакет testobject с файлами Computer.java и Notebook.java Код файла Computer.java выглядит следующим образом:


package testobject;
 
public class Computer {
     
    protected String name;    
      
   protected int ram;  
         
    protected int hdd;
     
    protected double weight;
    
      public Computer(String name) {

 this.name = name;
        
    }
  
    
    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 + " ГБ");  
      
 }
    
      protected void print (String str){
    
      System.out.println(str);
}
     
}

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

package testobject;

public class Notebook extends Computer{
         
public Notebook(String name) {
        
        super(name);
    }

 public Notebook (String name, int ram, int hdd, double weight) {
        
       super(name, ram, hdd, weight);
    }
        
@Override
  public void on(){

       print("Notebook. Я включился. Моя модель " + getName());  
        
    }
  
    @Override
    public void load() {
       
    }
    
    public void charge(){
    
        System.out.println("Notebook charging...");
        
    }
    
    
}

Откроем Netbeans. Для создания абстрактного класса создадим класс Tosiba.

Нажмите ПКМ (правая кнопка мыши) по пакету testobject слева.

В открывшемся окне выберите Новый — Класс Java. В открывшемся окне в разделе Имя класса введите Toshiba. Нажмите Готово.

Получим код вида:


package testobject;

public class Toshiba {
    
}

Для создания абстрактного класса перед словом class следует написать abstract. Abstract — это ключевое слово, которое указывает на то, что данный класс является абстрактным и по смыслу в нём должны быть абстрактные методы какие-то.

  
package testobject;

public abstract class Toshiba {
    
}

Создадим несколько абстрактных методов. Создание абстрактных методов похоже на создание обычного метода за исключением одной особенности. Для начала мы указываем модификатор доступа, например public, затем тип метода пусть будет void, затем название метода, например openCD() — то есть открытие CD-привода. Но в отличие от обычных методов абстрактные методы также имеют ключевое слово abstract перед типом. И в конце мы сразу ставим точку с запятой без указания фигурных — {} — скобок.


package testobject;

public abstract class Toshiba {
    
    public abstract void openCD();
    
}

Сравним.

public abstract class Toshiba {
    
public void openCD(){ 
       
   };
    
}

Абстрактный класс Тошиба с обычным методом openCD();.

package testobject;

public abstract class Toshiba {
    
    public abstract void openCD();
    
}

Абстрактный класс Тошиба с абстрактным методом openCD();

Абстрактные методы не содержат реализацию. И реализацию данного метода должен сделать дочерний класс от класса Toshiba, Таким образом мы создали один абстрактный метод. Давайте создадим ещё один. По аналогии пусть это будет метод closeCD();


package testobject;

public abstract class Toshiba {
    
    public abstract void openCD();
    
    public abstract void closeCD();
    
}

Мы создали абстрактный класс Тошиба, который указал поведение для всех дочерних объектов — все дочерние объекты должны уметь выполнять методы openCD(); и closeCD();

Теперь давайте создадим какой-либо дочерний класс от Toshiba. Нажмём правой кнопкой мыши по пакету testobject. Выберем Новый — класс java.

В открывшемся окне в разделе Имя класса введём ToshibaModel1. Для наследования от абстрактоного класса мы пишем уже знакомое нам ключевое слово extends.



package testobject;

public class ToshibaModel1 extends Toshiba{
    
}

После того, как мы написали extends от абстрактного класса, компилятор нам выдал красножёлтую лампочку слева. И если мы на неё нажмём, то увидим, что нам предлагают реализовать все абстрактные методы. Выберем эту опцию.


package testobject;

public class ToshibaModel1 extends Toshiba{

    @Override
    public void openCD() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void closeCD() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
    
}

Netbeans нам автоматически сформировал заглушки для Override-методов (перезаписанных методов). Тела этих методов ( throw new UnsupportedOperationException(«Not supported yet.»); ) мы должны заполнить самостоятельно. Сделаем по аналогии с прошлыми уроками вывод на экран какого-либо сообщения.

package testobject;

public class ToshibaModel1 extends Toshiba{

    @Override
    public void openCD() {
        System.out.println("ToshibaModel1 overrided openCD");
    }

    @Override
    public void closeCD() {
         System.out.println("ToshibaModel1 overrided closeCD");
    }
    
}

Таким образом мы реализовали абстрактный класс Toshiba, у которого было два абстрактных метода openCD и closeCD и мы выполнили требования класса Toshiba по реализации объектов и реализовали два метода openCD и closeCD.

Давайте перейдём в класс MyFirstProgram, закомментируем всё, что было в методе main.


    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 notebook = new Notebook("IBM");
//
//Computer comp = new Computer("Comp");

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

//doSmth(notebook);
//doSmth(comp);

    }  

Попробуем создать экземпляр класса Toshiba;

 Toshiba t = new Toshiba(); 

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

А абстрактный класс — он для того и существует, чтобы от него наследовались и этот унаследованный объект мы уже можем создавать. таким образом данную запись Toshiba t = new Toshiba(); мы не сможем выполнить. Давайте создадим объект ToshibaModel1.

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

Toshiba toshiba = new ToshibaModel1();

Понадобится выполнить рекомендации компилятора по импорту.

Абстрактный класс Toshiba содержит объявления двух методов, то «через точку» в Netbeans мы эти методы увидим.(closeCD и openCD).

Выберем openCD() а затем closeCD.

 
Toshiba toshiba = new ToshibaModel1();     
        
  toshiba.openCD();
  toshiba.closeCD();

Выполнятся методы именно созданного объекта ToshibaModel1, который реализовал эти два метода. То есть:

run:
ToshibaModel1 overrided openCD
ToshibaModel1 overrided closeCD
СБОРКА УСПЕШНО ЗАВЕРШЕНА (общее время: 2 секунды)

Наш унаследованный класс ToshibaModel1 выполнился. Вернёмся в класс ToshibaModel1. Следует помнить, что ToshibaModel1 должен реализовать все абстрактные методы из класса Toshiba. Не только один из двух, а оба.Если мы удалим один из реализованных методов, то компилятор начнёт «ругаться,» поскольку не все абстрактные методы реализованы.

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

Абстрактный класс кроме абстрактных методов может содержать и обычные методы. Напишем метод printMyModel(), выводящий на экран строку My model is toshiba.

package testobject;

public abstract class Toshiba {
    
    public abstract void openCD();
    
    public abstract void closeCD();
    
    public void printMyModel(){
        System.out.println("My model is toshiba");
    }
}

Исходя из правил наследования метод printMyModel будет доступен в классе ToshibaModel1.

Если мы перейдём в класс ToshibaModel1, наберём super. с точкой, то «через точку» метод printMyModel() будет доступен.

public class ToshibaModel1 extends Toshiba{

    @Override
    public void openCD() {
        super.printMyModel();
        System.out.println("ToshibaModel1 overrided openCD");
    }

    @Override
    public void closeCD() {
         System.out.println("ToshibaModel1 overrided closeCD");
    }
    
}

Конечно можно и не использовать ключевое слово super, и метод printMyModel будет относится к родительскому, но с указанием super мы делаем себе подсказку о том, что этот метод родительского класса, во-вторых «помогаем» компилятору.

Также в абстрактном классе мы можем перечислять какие-то поля или свойства объекта Toshiba по аналогии,как мы делали в прошлых уроках. Отличие абстрактного класса от обычного класса в том, что он имеет хотя бы один абстрактный метод и сам этот класс должен помечаться словом abstract и экземпляр этого класса мы создать не можем.

Во всём остальном абстрактный класс похож на обычный класс.


public abstract class Toshiba {
    
    public String shortName = "TSHB";
    
    public abstract void openCD();
    
    public abstract void closeCD();
    
    public void printMyModel(){
        System.out.println("My model is toshiba");
    }
}

То есть создавать переменные в абстрактном классе не возбраняется.

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

Нельзя создать экземпляр абстрактного класса (через ключевое слово new), потому что этот класс ничего не умеет, это шаблон поведения для дочерних классов.

Если в классе есть хотя бы один абстрактный метод — весь класс будет абстрактным.

Любой дочерний класс должен реализовать все абстрактные методы родительского, либо он сам должен быть абстрактным.

Абстрактный класс может быть абстрактным, и в то же время не иметь ни одного абстрактного метода. То есть вот такая запись тоже разрешается:

public abstract class Toshiba {

]

но особого смысла в этом нет, хотя данная запись допустима. Теперь поговорим об интерфейсе.

Интерфейс.

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

Интерфейс — более «строгий» вариант абстрактного класса. Методы могут быть ТОЛЬКО абстрактными.

Интерфейс задаёт только поведение каких-либо объектов, без реализации.

Важное отличие Интерфейса от Абстрактного класса в том, что Интерфейс может наследоваться от одного или нескольких интерфейсов через ключевое слово extends. А абстрактный класс может наследоваться только от одного абстрактного класса.

На схеме абстракция также может быть реализована в виде интерфейсов, например Интерфейс Toshiba наследует Интерфейс Notebook. Model Toshiba 1 реализовывает два Интерфейса — интерфейс Toshiba и интерфейс Notebook. Таким образом Интерфейсы также используются для создания абстракций, но с некоторыми отличиями.

Для создания Интерфейса сделаем щелчок правой кнопкой мыши по пакету testobject. В открывшемся меню выберем Создать — Интерфейс Java.

В открывшемся окне в разделе Имя класса введём называние: Planshet.

package testobject;

public interface Planshet {
    
}

у данного Интерфейса, так как мы описываем планшет, должна быть возможность навигации по интерфейсу с помощью касания пальцев. Создадим метод navigateByScreen — то есть навигация по экрану с помощью касания. Также указывается тип метода, пусть будет void. ВАЖНЫЙ МОМЕНТ: для Интерфейса не важно какой мы указываем модификатор доступа, потому что он всегда будет public в отличие от абстрактного класса, где мы вручную должны были указывать модификатор доступа. В интерфейсе это делать необязательно.


package testobject;

public interface Planshet {
    
    void navigateByScreen();
    
}

Для компилятора это будет написано вот так: public void navigateByScreen(); А что будет, если написать private void navigateByScreen(); в Интерфейсе? Компилятор начнёт «ругаться» — модификатор доступа private неприменим к методам интерфейса. Все методы интерфейса являются публичными.

Также по аналогии с абстрактным классом, у нас отсутствует тело класса. То есть после скобок у нас сразу идёт точка с запятой. Давайте в объекте ToshibaModel1 реализуем интерфейс Planshet. То есть наш ноутбук ToshibaModel1 будет одновременно наследоваться от абстрактного класса Toshiba и в тоже время реализовывать поведение планшета. То есть это модель будет по сути планшетом Тошиба.

Для этого после extends Toshiba мы пишем implemets Planshet. Класс может наследоваться от одного класса, но в тоже время реализовывать какой-либо интерфейс. Причём интерфейсов он может реализовывать несколько. Через запятую можно добавить и какие-то иные интерфейсы, которые должен реализовать ToshibaModel1. (public class ToshibaModel1 extends Toshiba implements Planshet, Kommynikator, Router и так далее)


package testobject;

public class ToshibaModel1 extends Toshiba implements Planshet{

    @Override
    public void openCD() {
        super.printMyModel();
        System.out.println("ToshibaModel1 overrided openCD");
    }

    @Override
    public void closeCD() {
         System.out.println("ToshibaModel1 overrided closeCD");
    }
    
}

Компилятор подсказывает, что у нас есть один нереализованный метод из интерфейса Planshet. Нажмём на «лампочку» и выберем реализовать все абстрактные методы.



package testobject;

public class ToshibaModel1 extends Toshiba implements Planshet{

    @Override
    public void openCD() {
        super.printMyModel();
        System.out.println("ToshibaModel1 overrided openCD");
    }

    @Override
    public void closeCD() {
         System.out.println("ToshibaModel1 overrided closeCD");
    }

    @Override
    public void navigateByScreen() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
    
}

Netbeans снова создал заглушку для метода navigateByScreen. Изменим метод NavigateByScreen следующим образом:


  public void navigateByScreen() {
       System.out.println("Navigate by screen");
    }

То есть ToshibaModel1 реализовал несколько поведений. Первое поведение класса Toshiba, а второе поведения интерфейса Planshet. Если мы хотим сделать из ToshibaModel1 кроме ноутбука ещё, к примеру, телефон, можно создать ещё один интерфейс.

ПКМ по testobject — Создать — Интерфейс Java. В строке имя класса напишем Phone.


package testobject;

public interface Phone {
    
}


Внутри напишем какой-либо метод, скажем void call();



package testobject;

public interface Phone {
    
 void call();

}

В классе ToshibaModel1 через запятую указываем этот интерфейс и реализовываем его метод Call();


package testobject;

public class ToshibaModel1 extends Toshiba implements Planshet, Phone{

    @Override
    public void openCD() {
        super.printMyModel();
        System.out.println("ToshibaModel1 overrided openCD");
    }

    @Override
    public void closeCD() {
         System.out.println("ToshibaModel1 overrided closeCD");
    }

    @Override
    public void navigateByScreen() {
       System.out.println("Navigate by screen");
    }

    @Override
    public void call() {
        
      System.out.println("call");  
        
    }
    
}

ToshibaModel1 реализовывает три поведения. Первое от абстрактного класса Toshiba, второе от Planshet, третье от Phone.

Отличие абстрактного класса от интерфейса

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

Абстрактный класс — это некое промежуточное звено между интерфейсом и объектом с конкретной реализацией.

Интерфейс может наследоваться от множества интерфейсов, а абстрактный класс — только от одного абстрактного класса.

Существует совет, что если есть возможность — лучше вообще отказаться от абстрактных классов, а использовать только интерфейсы. Но сё же применение абстрактных классов в некоторых случаях предпочтительнее. Всё зависит от конкретной задачи.

Несколько примечаний.

Не нужно строить большие иерархии из 20 ступеней.

Уровни абстракции вырисовываются чаще не сразу, а по ходу написания программы.

Проблема множественного наследования решается с помощью интерфейсов.

Класс может наследоваться только от одного абстрактного класса, но реализовывать множество интерфейсов.

С помощью абстрактных классов и интерфейсов вы обязываете разработчика, который создает дочерний класс, реализовать все нужные методы, чтобы была определенная вами модель поведения.

Абстрактный класс может быть пустым, но всё равно он останется абстрактным.

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

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

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