Интерфейсы Java — что это такое?

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

Вы можете использовать их как способ достижения полиморфизма.

Пример интерфейса Java

Вот простой пример интерфейса Java:

public interface MyInterface {

    public String hello = "Hello";

    public void sayHello();
}

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

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

System.out.println(MyInterface.hello);

Как видите, доступ к переменной очень похож на доступ к статической переменной в классе.

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

Реализация интерфейса

Вот класс, который реализует интерфейс MyInterface, показанный выше:

public class MyInterfaceImpl implements MyInterface {

    public void sayHello() {
        System.out.println(MyInterface.hello);
    }
}

Обратите внимание, реализует часть MyInterface вышеупомянутого объявления класса. Это сообщает компилятору Java, что класс MyInterfaceImpl реализует интерфейс MyInterface.

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

Экземпляры интерфейса

Как только класс Java реализует интерфейс Java, вы можете использовать экземпляр этого класса в качестве экземпляра. Вот пример:

MyInterface myInterface = new MyInterfaceImpl();

myInterface.sayHello();

Обратите внимание, как объявлена переменная типа интерфейса MyInterface, в то время как созданный объект имеет тип MyInterfaceImpl. Java позволяет это, потому что класс MyInterfaceImpl реализует MyInterface. Затем вы можете ссылаться на экземпляры класса MyInterfaceImpl как на экземпляры MyInterface.

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

Реализация нескольких интерфейсов

Класс Java может реализовывать несколько интерфейсов. В этом случае класс должен реализовать все методы, объявленные во всех реализованных интерфейсах. Вот пример:

public class MyInterfaceImpl
    implements MyInterface, MyOtherInterface {

    public void sayHello() {
        System.out.println("Hello");
    }

    public void sayGoodbye() {
        System.out.println("Goodbye");
    }
}

Этот класс реализует два, называемых MyInterface и MyOtherInterface. Вы перечисляете имена для реализации после ключевого слова Implements, разделенных запятой.

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

import com.jenkov.package1.MyInterface;
import com.jenkov.package2.MyOtherInterface;

public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
    ...
}

реализованные классом выше:

public interface MyInterface {

    public void sayHello();
}
public interface MyOtherInterface {

    public void sayGoodbye();
}

Как видите, каждый интерфейс содержит один метод. Эти методы реализуются классом MyInterfaceImpl.

Перекрывающиеся подписи метода

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

Спецификация не дает никакого решения этой проблемы. Вам решать, что делать в этой ситуации.

Какие типы Java могут реализовывать интерфейсы?

Следующие типы могут реализовывать интерфейсы:

Переменные интерфейса

Интерфейс может содержать как переменные, так и константы. Однако часто не имеет смысла помещать переменные. В некоторых случаях может иметь смысл определять константы.

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

Все переменные в интерфейсе являются открытыми, даже если вы опустите ключевое слово public в объявлении переменной.

Методы интерфейса

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

Все методы являются открытыми, даже если вы опустите ключевое слово public в объявлении метода.

Методы интерфейса по умолчанию

До Java 8 интерфейсы не могли содержать реализацию методов, а содержали только сигнатуры методов. Однако это приводит к некоторым проблемам, когда API необходимо добавить метод.

Если API просто добавляет метод, все классы, которые реализуют интерфейс, должны реализовать этот новый метод. Это хорошо, если все реализующие классы расположены в API. Но если некоторые реализующие классы находятся в клиентском коде API(код, который использует API), то этот код нарушается.

Посмотрите на этот интерфейс и представьте, что он является частью, например, API с открытым исходным кодом, который многие приложения используют внутри:

public interface ResourceLoader {

    Resource load(String resourcePath);

}

Теперь представьте, что проект использует этот API и реализовал ResourceLoader следующим образом:

public class FileLoader implements ResourceLoader {

    public Resource load(String resourcePath) {
        // in here is the implementation +
        // a return statement.
    }
}

Если разработчик API хочет добавить еще один метод в ResourceLoader, то класс FileLoader будет нарушен при обновлении этого проекта до новой версии API.

Чтобы облегчить эту проблему, Java 8 добавила концепцию. Метод интерфейса по умолчанию может содержать реализацию этого метода по умолчанию. Классы, которые реализуют интерфейс, но не содержат реализации по умолчанию, автоматически получат реализацию метода по умолчанию.

Вы помечаете метод как метод по умолчанию, используя ключевое слово. Вот пример:

public interface ResourceLoader {

    Resource load(String resourcePath);

    default Resource load(Path resourcePath) {
        // provide default implementation to load
        // resource from a Path and return the content
        // in a Resource object.
    }

}

В этом примере добавляется метод загрузки по умолчанию(Path). В примере не учитывается фактическая реализация(внутри тела метода), потому что это не очень интересно. Важно то, как вы объявляете метод по умолчанию.

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

Статические методы интерфейса

Статические методы в Java должны иметь реализацию.

public interface MyInterface {

    public static void print(String text){
        System.out.print(text);
    }
}

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

MyInterface.print("Hello static method!");

Статические методы в интерфейсах могут быть полезны, когда у вас есть несколько служебных методов, которые вы хотели бы сделать доступными. Например, Vehicle может иметь статический метод printVehicle(Vehicle v).

Интерфейсы и наследование

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

public interface MySuperInterface {

    public void saiHello();

}
public interface MySubInterface extends MySuperInterface {

    public void sayGoodbye();
}

MySubInterface расширяет MySuperInterface. Это означает, что MySubInterface наследует все поля и методы от MySuperInterface. Это означает, что если класс реализует MySubInterface, этот класс должен реализовать все методы, определенные как в MySubInterface, так и в MySuperInterface.

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

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

Вот пример интерфейса Java, который наследуется от нескольких интерфейсов:

public interface MySubInterface extends
    SuperInterface1, SuperInterface2 {

    public void sayItAll();
}

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

Наследование и методы по умолчанию

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

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

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

Интерфейсы и полиморфизм

Java-интерфейсы — это способ достижения полиморфизма. Полиморфизм — это концепция, которая требует некоторой практики и мысли, чтобы овладеть ею. По сути, полиморфизм означает, что экземпляр класса(объекта) можно использовать так, как если бы он был разных типов. Здесь тип означает либо класс, либо интерфейс.

Посмотрите на эту простую диаграмму классов:

Two parallel class hierarchies used in the same application.

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

Теперь представьте, что вам нужно иметь возможность хранить эти объекты в базе данных, а также сериализовать их в XML, JSON или другие форматы. Вы хотите, чтобы это было реализовано с использованием одного метода для каждой операции, доступного для каждого объекта Car, Truck или Vehicle. Метод store(), метод serializeToXML() и метод serializeToJSON().

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

Где в приведенной выше схеме вы бы поместили эти три метода, чтобы они были доступны для всех классов?

Одним из способов решения этой проблемы было бы создание общего суперкласса для класса Vehicle и Driver, который имеет методы хранения и сериализации. Однако это приведет к концептуальному беспорядку. Иерархия классов больше не будет моделировать транспортные средства и водителей, но также будет привязана к механизмам хранения и сериализации, используемым в вашем приложении.

Лучшим решением было бы создать некоторые интерфейсы с включенными методами хранения и сериализации и позволить классам реализовать эти интерфейсы. Вот примеры таких интерфейсов:

public interface Storable {

    public void store();
}
public interface Serializable {
    public void serializeToXML(Writer writer);
    public void serializeToJSON(Writer writer);
}

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

Car car = new Car();

Storable storable =(Storable) car;
storable.store();

Serializable serializable =(Serializable) car;
serializable.serializeToXML(new FileWriter("car.xml"));
serializable.serializeToJSON(new FileWriter("car.json"));

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

Универсальные интерфейсы

public interface MyProducer() {

    public Object produce();

}

Этот интерфейс представляет интерфейс, который содержит единственный метод, который называется производством(), который может произвести единственный объект. Так как возвращаемое значение yield() — Object, он может возвращать любой объект Java.

public class CarProducer implements MyProducer{
    public Object produce() {
        return new Car();
    }
}
MyProducer carProducer = new CarProducer();

Car car =(Car) carProducer.produce();
public interface MyProducer {
    
    public T produce();
    
}
public class CarProducer implements MyProducer{

    @Override
    public T produce() {
        return(T) new Car();
    }
}
MyProducer myCarProducer = new CarProducer();

Car produce = myCarProducer.produce();

Как вы можете видеть, поскольку универсальный тип для экземпляра CarProducer установлен на Car, больше нет необходимости приводить объект, возвращенный из метода yield(), поскольку в объявлении исходного метода в интерфейсе MyProducer указано, что этот метод возвращает тот же тип, который указан в универсальном типе при использовании.

MyProducer myStringProducer = new CarProducer();

String produce1 = myStringProducer.produce();
public class CarProducer implements MyProducer{

    @Override
    public Car produce() {
        return new Car();
    }
}
MyProducer myCarProducer = new CarProducer();

Car produce = myCarProducer.produce();

Как вы можете видеть, по-прежнему нет необходимости приводить объект, возвращаемый функциейручки(), поскольку реализация CarProducer объявляет, что это экземпляр Car.

Обобщения Java более подробно описаны в моем руководстве по обобщению Java.

Функциональные интерфейсы

С Java 8 была введена новая концепция, называемая функциональными интерфейсами. Короче говоря, функциональный интерфейс — это интерфейс с единственным нереализованным методом(нестандартный, нестатический метод). Я объяснил функциональные интерфейсы в моем учебнике по функциональному интерфейсу Java, который является частью моего учебника по функциональному программированию на Java. ,

Оцените статью