Абстрактные классы и методы. Интерфейсы. Множественное наследование интерфейсов

Абстрактные классы

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

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

Рассмотрим пример абстрактного класса A:

abstract class A {
    int p1;
    A() {
        p1 = 1;
    }
    void print() {
        System.out.println(p1);
    }
}
class B extends A {
}
public class Main {
    public static void main(String[] args) {
        A ob1;
        // ошибка: ob1 = new A();
        B ob2 = new B(); // будет вызван конструктов по умолчанию из A
        ob2.print();
    }
}

Java разрешит описать конструкторы в классе A, но не разрешит ими воспользоваться (потому что запрещено создавать объекты абстрактного класса).

Обратите внимание на то, что объявление переменной ob1 как ссылки, на объект класса A тоже не запрещается.

Приведение классов

Зачем же может потребоваться ссылка ob1, какой объект с ней удастся связать? Ну, например, объект класса-потомка B. Дело в том, что класс A, как родитель, является более универсальным, чем потомок B. Это значит, что любой объект класса потомка может быть явно или даже автоматически приведён к классу родителю.

То есть следующее содержание метода main было бы вполне корректным:

A ob1;
B ob2 = new B();
ob1 = (A) ob2; // явное приведение
ob1.print();

Более того, приведение могло быть и неявным (автоматическим):

ob1 = ob2; // автоматическое приведение

Как для встроенных типов, так и для классов автоматическое приведение всегда возможно, когда переменную или объект мы пытаемся привести к более универсальному типу (целые числа к вещественным, объект потомка B к классу родителю A и пр.).

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

Абстрактным называется метод, который не имеет реализации в данном классе. После круглых скобок, где перечислены его аргументы, ставится не открывающая фигурная скобка, чтобы начать блок описания метода, а точка с запятой. То есть описание у абстрактноно метода отсутствует. Перед именем метода указывается при этом модификатор abstract.

Какой смысл в создании метода без реализации? Ведь его нельзя будет использовать. Для объектов того класса, где метод описан – конечно же использовать нельзя, но вот если унаследовать класс и в потомках переопределить метод, задав там его описание, то для объектов классов потомков метод можно будет вызывать (и работать будут описанные в классах потомках реализации).

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

Когда же уместно использовать абстрактные методы и классы? Сначала рассмотрим пример иерархии классов домашних животных, где нет ни абстрактных классов, ни абстрактных методов.

class Pet {
    String name;
    int age;
    boolean hungry;
    void voice() {
    }
    void food() {
        hungry = false;
    }
}
class Snake extends Pet {
    double length;
    void voice() {
        System.out.println("Шшш-ш-ш");
    }
}
class Dog extends Pet {
    void voice() {
        System.out.println("Гав-гав");
    }
}
class PatrolDog extends Dog {
    void voice() {
        System.out.println("Ррр-р-р");
    }
}
class Cat extends Pet {
    void voice() {
        System.out.println("Мяу-мяу");
    }
}
class Fish extends Pet {
}
public class Main {
    public static void main(String[] args) {
        Pet zorka = new Pet();
        zorka.food();
        Fish nemo = new Fish();
        nemo.voice();
    }
}

Поскольку нет какого-то общего звука, который издавали бы все домашние животные, то мы в классе Pet не стали задавать какую-то реализауию методу voice(), внутри метода не делается совсем ничего, но тем не менее у него есть тело, обособленное блоком из фигурных скобок. Метод voice() хороший претендент на то, чтобы стать абстрактным.

Кроме того, вряд ли можно завести себе домашнее животного неопределенного вида, то есть у вас дома вполне могли бы жить Cat, Dog или даже Snake, но вряд ли вы бы смогли завести животное Pet, являющееся непонятно кем.

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

Рассмотрим пример с участием абстрактного класса и абстрактного метода:

abstract class Pet {
    String name;
    int age;
    boolean hungry;
    abstract void voice();
    void food() {
        hungry = false;
    }
}
class Snake extends Pet {
    double length;
    void voice() {
        System.out.println("Шшш-ш-ш");
    }
}
class Dog extends Pet {
    void voice() {
        System.out.println("Гав-гав");
    }
}
class PatrolDog extends Dog {
    void voice() {
        System.out.println("Ррр-р-р");
    }
}
class Cat extends Pet {
    void voice() {
        System.out.println("Мяу-мяу");
    }
}
class Fish extends Pet {
    void voice() {
    }
}
public class Main {
    public static void main(String[] args) {
        // ошибка: Pet zorka = new Pet();
        Fish nemo = new Fish();
        nemo.voice();
    }
}

Обратите внимание, что теперь, во-первых, мы не можем создавать объекты абстрактного класса Pet, а, во-вторых, реализация метода voice() должна иметься во всех его потомках (хотя бы пустая реализация), не являющихся абстрактными классами.

Хотя, мы могли бы создать абстрактного потомка:

abstract class Fish extends Pet {
}

Но не могли бы создавать объектов класса Fish, нам пришлось бы расширять класс, чтоб в итоге получить не абстрактный и создавать на его основе объекты. Например:

class GoldenFish extends Fish {
    void voice() {
    }
}

Интерфейсы

Интерфейс это консутрукция языка программирования Java, в рамках которой могут описываться только абстрактные публичные (abstract public) методы и статические константyst свойства (final static). То есть также, как и на основе абстрактных классов, на основе интерфейсов нельзя порождать объекты.

Один интерфейс может быть наследником другого интерфейса.

Классы могут реализовывать интерфейсы (т. е. получать от интерфейса список методов и описывать реализацю каждого из них), притом, что особенно важно, один класс может реализовывать сразу несколько интерфейсов.

Перед описанием интерфейса указывается ключевое слово interface. Когда класс реализуемт интерфейс, то после его имени указывается ключевое слово implements и далее через запятую перечисляются имена тех интерфейсов, методы которых будут полностью описаны в классе.

Пример:

interface Instruments {
    final static String key = "До мажор";
    public void play();
}
class Drum implements Instruments {
    public void play() {
        System.out.println("бум бац бац бум бац бац");
    }
}
class Guitar implements Instruments {
    public void play() {
        System.out.println("до ми соль до ре до");
    }
}

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

interface Instruments {
    static public String key = "До мажор";
    void play();
}

Но когда метод play() будет описываться в реализующем интерфейс классе, перед ним всё равно необходимо будет явно указать модификатор public.

Множественное наследование интерфейсов

Java не поддерживает множественное наследование классов. Это объясняется тем, что такое наследование порождает некоторые проблемы.

Чаще всего указываются неоднозначности, возникающие при так называемом «ромбовидном» наследовании, когда один класс «A» является наследником двух других классов «B» и «C», которые в свою очередь оба являются наследниками класса «D». Проблема допустимости множественного наследования кроется в следующем. Предположим, что в родителе A определялся какой-то метод m1(). И этот же метод мы вызываем для объекта класса D. А что если m1() был переопределён в классах B и С. Какая реализация из трёх будет работать при вызове метода m1() для объекта класса D? От неодназначности можно было бы избавиться потребовав в описанном случае при вызове уточнять при вызове, какой из методов требуется (так и сделано в некоторых языках), но в Java от множественного наследования классов решили отказаться.

Вместо множественного наследования классов в Java введено множественное наследование интерфейсов, которое часично решает проблемы (но, как будет показано в примере далее, к сожалению, не все).

Рассмотрим пример, где реализовано два интерфейса с методами доступными для грузового и для легкового транспорта. Класс Pickup (пикап) должен обладать как возможностью перевозки грузов, так и пассажиров, поэтому он реализует сразу оба интерфейса:

interface PassangersAuto {
    void transportPassangers();
}
interface CargoAuto {
    void transportCargo();
}
class Truck implements CargoAuto {
    final static int a = 1;
    public void transportCargo() {
        System.out.println("Везу груз");
    }
}
class Sedan implements PassangersAuto {
    public void transportPassangers() {
        System.out.println("Везу пассажиров");
    }
}
class Pickup implements CargoAuto, PassangersAuto {
    public void transportCargo() {
        System.out.println("Везу груз");
    }
    public void transportPassangers() {
        System.out.println("Везу пассажиров");
    }
}

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

Но интерфейсы, как говорилось выше, не являются совершенным инструментом лишенным всяких недостатков. Рассмотрим пример, когда у нас имеются два интерфейса, в каждом из которых есть свойства с одинаковыми именами (но, возможно, разными значениями) и методы с одинаковыми именами.

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

Также к свойству можно обратиться как к статическому свойству одного из интерфейсов (разумеется, это можно делать и если у свойств были бы разные имена).

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

К сожалению, создать отдельные реализации для двух одноимённых методов из разных интерфейсов в классе наследнике не получится (чтобы потом ими можно было пользоваться через то же приведение объектов к нужному интерфейсу). Если класс реализует несколько интерфейсов, в которых есть одноимённые методы, то в нём может задаваться лишь одна общая для всех реализация этих методов (и это уже ограничивает полиморфизм при множественном наследовании через интерфейсы в Java).

Итак, код примера:

interface Interface1 {
    int someField = 100;
    String someMethod();
}
interface Interface2 {
    int someField = 200;
    String someMethod();
}
class SomeClass implements Interface1, Interface2 {
    public String someMethod() {
        return "It Works";
    }
}
public class Main {
    public static void main(String[] args) {
        SomeClass a = new SomeClass();
        System.out.println( a.someMethod() ); // It works
        System.out.println( a.someField ); // ошибка
        System.out.println( ( (Interface1) a).someField ); // 100
        System.out.println( Interface1.someField ); // 100
    }
}