Разработка программ. Мои заметки.

February 15, 2017 at 23:55

Эволюция интерфейсов в Java. Нововведения в Java 9.

Как менялись возможности интерфейсов в Java. Текущие итоги.

Interface Тут я не буду вдаваться в ООП теорию для чего нужны интерфейсы, я лишь постараюсь максимально полно перечислить синтаксические правила их использования и новые возможности, добавленные в процессе эволюции Java-платформы. Также остановлюсь на несложных моментах, которые порой не упоминаются в учебниках.

До Java 8, Java интерфейс — это конструкция, похожая на класс в котором отсутствуют реализации методов:


    public interface MyStrongInterface 
    {
        public int howManyTimes = 15;

        public void doIt();
    }

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

Перед словом interface может стоять любой модификатор видимости (private, protected, public, default — так будем называть отсутствие модификатора), но не всегда и не везде:

  • Если interface объявлен в отдельном файле (имя_интерфейса.java), то допускаются только модификаторы public (полная видимость интерфейса везде) или default (видимость интерфейса внутри пакета).

  • Если interface объявлен внутри другого класса (не интерфейса!), то допускаются все модификаторы видимости public, default, private и protected. Но, такой же вложенный интерфейс, объявленный внутри другого интерфейса (не класса!), может иметь только модификатор public или default. В случае использования модификаторов public или default, встроенный интерфейс будет виден с префиксом имени окружающего его класса или интерфейса.


    public interface SomeInterface extends Main.Printable
    {
        final int max = 5;

        public abstract void method();

        public interface SomeAnother
        {
            ...
        }
    }

    //  Где-то в коде

    interface Showable extends SomeInterface.SomeAnother
    {
        void show();
    }    

  • Interface объявленный внутри другого класса или интерфейса также может иметь слово static в модификаторе доступа. Static ничего не меняет, но ошибкой не является.

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


    public interface Printable 
    {
        int borderLeft = 20;
        int borderTop = 15;

        void print( int count );
        void preview();
        ...
    }

  • перед переменной интерфейса может стоять модификатор доступа public static final (или одно из этих слов). Но может и ничего не стоять. Вне зависимости от этого, любая переменная, объявленная в интерфейсе — всегда трактуется компилятором как public static final. Другими словами — для компилятора это всегда константа. И состояния она хранить не может. Таким образом — интерфейс не может иметь своего состояния. Это есть принцип.

  • перед сигнатурой метода может стоять модификатор public abstract (или одно из этих слов). Но может и ничего не стоять. Вне зависимости от этого, любой метод, объявленный в интерфейсе — всегда трактуется компилятором как public abstract. Приведу тут незамысловатую, но удачную картинку с сайта javapoint.com:

    Interface

Один интерфейс может расширять другой интерфейс:


    interface Showable extends Printable 
    {  
        void show();  
    }

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

  • Один класс может реализовать несколько интерфейсов:


    class NewPower implements Printable, Comparable, Serializable
    {
        //  Тут обязаны быть реализации всех методов всех интерфейсов 
        ...        
    }

И вот пришла Java 8.

В Java 8 стало возможным добавлять реализацию методов в интерфейсах! Что бы это сделать, перед методом нужно поставить ключевое слово default или static. То есть, если вы хотите иметь в интерфейсе реализацию нестатического метода, то перед ним ставится модификатор default и теперь можно вставить код метода. А если хотите иметь реализацию статического метода, то пишете ключевое слово static. И снова можно вписать код.

Таким образом, если вы поставили default или static, то вы обязаны предоставить реализацию метода. Статический метод интерфейса можно вызвать так же, как и статический метод класса, указав впереди имя интерфейса. Оба новых модификатора всегда предполагают модификатор public. Его можно указывать, а можно и не указывать. Но он всегда подразумевается. Другими словами, эти интерфейсные методы, содержащие реализацию, видны всем.


    public interface SomeInterface
    {
        default int addOne( int i ) {
            return i + 1;
        }
    
        static int addTwo( int i ) {
            return i + 2;
        }

        public default int addThree( int i ) {
            return i + 3;
        }
    
        public static int addFour( int i ) {
            return i + 4;
        }
    }
    ...
    SomeInterface.addTwo( 22 );

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


    public interface SomeInterface1 extends SomeInterface
    {
        int addOne( int i );
    }
    
    //  Или вот так
    public abstract class SomeAClass implements SomeInterface
    {
        public abstract int addOne( int i );
    }

Проделать подобное со статическим методом — не получится. Вы можете описать статический метод с такой же сигнатурой как у предка, но, тем самым, вы лишь скроете статический метод предка. Это будет другой метод. Вызываться будет строго тот статический метод, который определён в том типе, который вы используете для его вызова. Если вы не объявили идентичный статический метод в потомке, то через тип потомка вызовется статический метод его предка. А если объявили, то вызовется статический метод потомка. Это работает только для классов. Вызвать статический метод интерфейса предка, через интерфейс потомка — не получится. Детальнее в вызов статических методов вдаваться не будем, так как это лежит за рамками данной статьи.

А скоро придёт Java 9. И сделает реализацию методов интерфейсов ещё более удобной.

В Java 9 можно создавать private и private static методы. И писать их реализацию. Эти методы используются другими методами интерфейса, позволяя выносить туда дублирующийся код. Методы private и default могут обращаться к любым другим методам интерфейса. Методы private static и static только к статическим. Что понятно. Небольшое синтаксическое улучшение. Концептуально это ничего не меняет, но хороший стиль поддерживать проще.


    public interface SomeUseful 
    {
        default int doWork() {
            return process( calculateTime() );
        }
        
        private int calculateTime() {
            ...
        }

        private int process() {
            int value = calculateSomething();
            ...
        }

        private static int calculateSomething() {
            ...
        }
    }

В сухом остатке можно сказать, что методы, которые перечисляются в интерфейсах, могут быть public и private. И могут иметь весь список модификаторов, хотя это и не обязательно. Те методы, которые помечены как private, не могут быть переопределены наследниками, ибо они им не видны. А значит, обязательно должны быть определены в интерфейсе. То есть, они в принципе не могут быть abstract. Хорошей практикой, кстати, считается указывать формально полный список комбинаций модификаторов перед методом или переменной.

Попробуем их перечислить, выделив те из них, которые синтаксически корректны:

  • public static — корректен. Требует реализации метода.
  • public abstract — корректен. Не требует реализации метода. Должен быть определён в потомке.
  • public default — корректен. Требует реализации метода. Может быть переопределён в потомке.
  • private static — корректен. Требует реализации метода.
  • private abstract — невозможен. private не виден потомкам, а значит должен быть определён тут же, в интерфейсе. И abstract, постулирующий реализацию в потомке, не может быть назначен.
  • private default — невозможен. Ибо default виден всем и может быть переопределён в потомке. А private — не виден потомкам. Противоречие.
  • private — корректен.

Особенности реализации методов интерфейсов в Java 8 и Java 9.

В этом важном абзаце я оттеню крайне важную мысль, если кто-то, случайно, не обратил на неё внимания. Итак, осталась ли разница между интерфейсом и абстрактным классом после того, как у интерфейса появилась возможность определять методы? Да. Разница осталась. И довольно существенная.

Суть её в том, что интерфейс по прежнему не имеет полей (fields). Ни полей класса (статических), ни полей объекта (экземпляров класса). Исключительно и только константы. А значит — никакой метод интерфейса не может что-то сохранить между своими вызовами. Говоря формально — у интерфейса по прежнему нет состояний. Методы интерфейса могут хранить лишь логику работы алгоритма, используя вызовы других своих методов для более аккуратного её представления. Всё. Интерфейс по прежнему является концептуально другой сущностью языка. И, в силу описанных выше ограничений, не способен сломать ранее реализованную логику. Не взирая на новые возможности. Что хорошо и важно.

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

Подытожим. Итак, когда мы реализуем/расширяем интерфейс, содержащий методы с модификатором default у нас есть следующие варианты обращения с каждым из таких методов:

  • Использовать метод предка как есть
  • Переопределить (override — реализовать заново) метод
  • Переобъявить метод как абстрактный, тем самым затребовав его реализацию потомком

Множественное наследование.

Ещё один интересный момент, возникающий с появлением возможности методов интерфейса содержать программный код.

Любой Java класс может наследовать другой Java класс. Но всего один! А вот унаследовать и реализовать интерфейсов Java класс может сколько угодно. Интерфейс также может расширять много интерфейсов. Это всегда было и есть сейчас. Поскольку ранее интерфейсы не содержали кода, то никаких проблем не возникало. Теперь, как мы уже знаем, начиная с Java 8 интерфейсы могут содержать методы с кодом (реализованные методы). Другими словами — появилось то, что уже можно назвать множественным наследованием. И это значит, что ситуация приобрела и проблемы, привносимые этим множественным наследованием.

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

Описать основную проблему несложно. Предположим ваш класс намерен реализовать два интерфейса, каждый из которых имеет какой-то default метод с сигнатурой, полностью идентичной методу из другого реализуемого интерфейса. То есть, ваш новый класс наследует две разные реализации одного и того же метода. Из одного и из другого интерфейса. И сам ваш класс/интерфейс пока не переопределяет этот метод. Ну и какой из них должен быть вызван если это понадобится? Из какого интерфейса?

Ответ — прост. Ни из какого. В Java вы получите незамысловатую ошибку: class … inherits unrelated defaults for …. Другими словами - не знаю кого вызвать. Определись. Для разрешения ситуации нужно действовать без фантазий — реализовать свой вариант метода. А в нём или написать свой код или внятно указать из какого предка код вызываем. Можно и то и другое. Выглядит это так:


    public interface InterfaceA { 
        default void defaultMethod() {
            ... 
        } 
    }

    public interface InterfaceB {
        default void defaultMethod() {
            ...
        }
    }

    public class Impl implements InterfaceA, InterfaceB  {
        public void defaultMethod() {
            //  Тут своя реализация
            ...
        }

        //  Как вариант, можно использовать реализацию одного из предков.

        public void defaultMethod() {
                InterfaceB.super.defaultMethod();
                ...
            }
    }

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

В конце, для логической точки, хотелось бы коротко ответить на сакральный вопрос отличия интерфейса от абстрактного класса с учётом последних веяний от Java 8 и Java 9:

  • У абстрактного класса могут быть конструкторы.
  • У абстрактного класса помимо констант могут быть поля (уровня класса или объекта).
  • У абстрактного класса методы могут изменять значения полей класса/объекта. По умному - класс может иметь состояния.

Автор — Владимир Рыбов