Абстрактные классы и методы

Продолжаем тему наследования и на этом занятии поговорим об абстрактных классах. Абстрактные классы есть во многих языках программирования, в том числе и в Java. Они позволяют описывать поля, методы, но не требуют их конкретизации и реализации. Например, мы описываем класс автомобиль (Car), но пока незнаем как будут реализованы его методы. В этом случае можно объявить его как абстрактный и просто прописать переменные и методы без конкретного наполнения:

abstract class Car {
    String model;
 
    public abstract void go();
    public abstract void stop();
    public abstract void draw();
}

Смотрите, мы здесь определили одно поле model и три абстрактных метода: go, stop и draw. Абстракция в данном случае означает, что мы знаем что хотим от автомобиля, но пока незнаем как это будем делать. Своего рода, это некий набросок – абстракция, причем, абстракция на уровне класса и методов.

В действительности, если класс содержит хотя бы один абстрактный метод, то он должен быть объявлен как абстрактный. И, если мы попытаемся убрать ключевое слово abstract в определении класса, то возникнет ошибка. С другой стороны, если бы данный класс не содержал бы ни одного абстрактного метода, например, вот так:

abstract class Car {
    String model;
 
    public void go() {}
    public void stop() {}
    public void draw() {}
}

То ключевое слово abstract можно как использовать, так и не использовать. Тогда в чем отличие этого абстрактного класса Car от такого же, но не абстрактного? В действительности, только одним: для абстрактных классов нельзя создавать экземпляры, то есть, вот такая строчка приведет к ошибке:

Car car = new Car();

Но, если убрать слово abstract, то ошибки уже не будет. Конечно, это скорее искусственный пример, показывающий отличие в определении абстрактного и не абстрактного классов. В реальности, абстрактные классы, как правило, содержат абстрактные методы, как мы это сделали вначале. Но наряду с абстрактными методами можно записывать и обычные, например, так:

abstract class Car {
    String model;
 
    public abstract void go();
    public abstract void stop();
    public abstract void draw();
 
    public void setModel(String model) {
        this.model = model;
    }
}

Здесь используется сеттер для задания поля model.

Ну, хорошо, мы можем объявлять абстрактные классы и методы, но зачем они нужны, если нельзя ни создавать объекты такого класса, ни вызывать такие методы? Все верно. И такие классы создаются исключительно для дальнейшего наследования и конкретизации их работы уже в дочерних классах. А абстракция позволяет сразу описать необходимые интерфейсы (то есть, методы), через которые в дальнейшем будут вызываться соответствующие переопределенные методы дочерних классов. Это часто очень удобно при реализации больших проектов, когда в целом определены информационные потоки через абстрактные классы и интерфейсы, а далее, создаются производные классы для их конкретного наполнения, реализации.

Итак, давайте тоже наполним конкретикой наш абстрактный класс Car. Для этого определим дочерний класс и назовем его, например, ToyotaCorolla. Если написать вот такие строчки:

class ToyotaCorolla extends Car {
}

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

class ToyotaCorolla extends Car {
    public void go() {
        System.out.println("Toyota Corolla едет");
    }
 
    public void stop() {
        System.out.println("Toyota Corolla остановилась");
    }
 
    public void draw() {
        System.out.println("Рисование машины Toyota Corolla");
    }
}

Теперь никаких ошибок не будет и мы можем создать экземпляр этого класса:

ToyotaCorolla car1 = new ToyotaCorolla();

Или, используя обобщенный тип ссылок на абстрактный класс:

Car car1 = new ToyotaCorolla();

И, далее, мы можем вызывать методы go, stop и draw, определенные в дочернем классе:

car1.go();
car1.stop();
car1.draw();

Давайте для примера добавим еще один дочерний класс ToyotaCamry:

class ToyotaCamry extends Car {
    public void go() {
        System.out.println("Toyota Camry едет");
    }
 
    public void stop() {
        System.out.println("Toyota Camry остановилась");
    }
 
    public void draw() {
        System.out.println("Рисование машины Toyota Camry");
    }
}

Определим массив обобщенных ссылок в функции main:

final int N = 4;
Car cars[] = new Car[N];

Присвоим им экземпляры дочерних классов:

cars[0] = new ToyotaCorolla();
cars[1] = new ToyotaCamry();
cars[2] = new ToyotaCorolla();
cars[3] = new ToyotaCamry();

И вызовем общие методы базового класса Car, реализованные в дочерних классах:

        for(int i = 0;i < N; ++i) {
            cars[i].go();
            cars[i].stop();
            cars[i].draw();
        }

Видите, благодаря тому, что в базовом классе прописаны виртуальные методы go, stop и draw, мы имеем возможность вызывать их, используя единый интерфейс – ссылки на экземпляры базового класса Car. Это еще один пример полиморфизма в ООП.

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

class Car {
    String model;
 
    public void go() {}
    public void stop() {}
    public void draw() {}
 
    public void setModel(String model) {
        this.model = model;
    }
}

И все тоже работало бы. Но в такой реализации есть принципиальные отличия от предыдущей (с абстрактным классом). Во-первых, здесь нам приходится прописывать пустые реализации у методов и такие методы можно уже не переопределять в дочерних классах. Абстрактные же методы обязаны быть определены в производных классах и это, с одной стороны, гарантирует их требуемое наполнение, а с другой, помогает избежать ошибок, если программист забудет их определить.

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

Вот эти моменты обусловили появление абстрактных классов и методов в языке Java.

Путь кодера

Подвиг 1. Объявите абстрактный класс Geom для представления геометрических фигур с полями: width, color для определения толщины и цвета линии, а также с абстрактным методом draw() для рисования конкретного графического примитива. Затем, запишите дочерние классы Line, Rect, Ellipse для представления линий, прямоугольников и эллипсов. Определите в них поля для хранения координат этих фигур и метод draw() для их рисования. Создайте обобщенные ссылки Geom на объекты дочерних классов и вызовите у них метод draw().

Подвиг 2. Объявите абстрактный класс Recipes (рецепты) с полями: название, тип (вегетарианский/обычный). И абстрактными методами: showIngredients (показать ингредиенты), showRecipe (показать рецепт). Описать несколько дочерних классов: Salad (для салатов), Pizza (для пицц), Porridge (для каш). В каждом дочернем классе определить поле для списка ингредиентов (в виде строки) и описания самого рецепта (в виде строки). А также реализовать абстрактные методы базового класса Recipes. Создать несколько экземпляров дочерних классов и через общий интерфейс (в виде ссылок типа Recipes) вызвать методы showRecipe и showIngredients.

Видео по теме