Java 8: стандартные и статические методы интерфейса - похожи, но разные!

Итак, Java 8 официально отсутствует уже почти 4 года, и вместе с ней появилось множество интересных вещей. Некоторые из них были усовершенствованы в языке, например, сортировка параллельных массивов и более эффективное разрешение конфликтов в HashMaps. Новые функции включают в себя столь популярные Lambdas и Method Parameter Reflection (для тех, кто плохо работает). В частности, одной новой функцией является возможность иметь методы интерфейса по умолчанию и статические методы интерфейса (методы статического интерфейса фактически были включены в предыдущие версии Java, но мы включим их для сравнения) . Эта статья призвана объяснить, с чем они связаны, на различных примерах.

1. Введение в методы интерфейса по умолчанию и статические интерфейсы

В EC -1 (пример кода 1, показан ниже) мы видим простое объявление интерфейса. Два метода в интерфейсе выполняют одну и ту же операцию, однако один - по умолчанию, а другой - статический.

//EC-1: Intro to default and static interface methods
public interface Foo {
    //Default method declaration
    default int sumDefault(int a, int b) {
        return a + b; //simply adds a + b
    }
    //Static method declaration
    static int sumStatic(int a, int b) {
        return a + b; //simply adds a + b
    }
}

IntelliJ или любая другая IDE, которую вы настроили для использования Java-8, не должны жаловаться на вышеуказанное. Давайте копнем глубже!

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

Если мы попытаемся реализовать интерфейс Foo, как показано в EC-1, не переопределяя какой-либо из методов, наша IDE не будет жаловаться, поскольку знает, что реализация уже предоставлена, как показано в EC-2 ниже.

EC-2: Implementing Foo
public class Bar implements Foo {
   //No implementation required as it is already provided in the interface. IDE remains happy :D.
}

3. Призыв

Давайте посмотрим, как мы можем вызывать как стандартные, так и статические методы интерфейса.

EC-3: Invoking default & static methods.
//Default 
int i = new Bar().sumDefault(2, 2); 
System.out.println("Default: " + i);
//Static    
int i = Foo.sumStatic(2, 2);
System.out.println("Static: " + i);

Как показано в EC-3, мы можем просто вызвать метод по умолчанию, как если бы это был любой другой метод экземпляра в классе, т.е. путем вызова конструктора классов new <Constructor>.<method>, поскольку он уже имеет реализацию.

С другой стороны, поскольку статические методы принадлежат шаблону класса / интерфейса, их можно вызывать так же, как обычно вызывается любой статический метод, т.е. <Class/Interface>.<method>. Оба примера в EC-3 не содержат ошибок.

4. Переопределение стандартных и статических методов в интерфейсе Foo.

Что произойдет, если мы попытаемся переопределить и по умолчанию, и статический метод?

EC-4: Overriding Default & Static Interface Methods
public class Bar implements Foo {
    @Override
    public int sumDefault(int a, int b) {
        return a + b - 1; // adds a + b then subtracts 1
    }
    
    @Override <- Error
    public int sumStatic(int a, int b) {
        return a + b - 1; // adds a + b then subtracts 1
    }
}

Как показано в EC-4, переопределение метода по умолчанию будет работать без сбоев. К сожалению, мы не можем переопределить статические методы, поскольку Java предотвращает переопределение любого метода с помощью ключевого слова static. Ваша IDE укажет на ошибку.

Давайте посмотрим, как вызывать методы по умолчанию и статические методы интерфейса, которые были переопределены. Мы будем использовать код из EC-4.

5. Вызов замещенных методов

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

EC-5: Invoking default and static interface methods
public class Bar implements Foo {
    @Override
    public int sumDefault(int a, int b) {
        return a + b - 1; 
       //Note this overridden version adds a to b then subtracts 1;
       //The original interface method just adds a to b;
    }
    
    public static void main(String args[]) {
        Bar bar = new Bar();
        int i = bar.sumDefault(2, 2);
        System.out.println("Default: " + i);       
    }
}
Output:
Default: 3
#QuickMaths 

Результат sumDefault после переопределения - 3 вместо 4. Это показывает, что методы по умолчанию при переопределении будут принимать реализацию класса, в котором они были переопределены, и не сохранят свою базовую реализацию интерфейса.

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

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

EC-6 Implementing Foo and Goo
public interface Goo {
   //Exact same method signature as that of Foo
    default int sumDefault(int a, int b) {
         return a + b + a; //Adds a + b and then adds a again, unlike Foo that just adds a + b;
    }
}
public class Bar implements Foo, Goo {
     //Which implementation of sumDefault(int a, int b) will this class use, Foo or Goo????
}

EC-6 представляет собой отличный вопрос. Какую реализацию примет класс Bar? Ответ… Ни один из них! Ваша среда IDE будет жаловаться, что Bar наследует несвязанные версии метода sumDefault(int a, int b). Как разработчику вам придется игнорировать их обоих и реализовать свою собственную версию. В этом случае это похоже на традиционный интерфейс, который требует от вас определения собственной реализации метода.

7. Наконец, реализация иерархии интерфейсов.

В нашем последнем примере, что произошло бы, если бы Goo расширился от Foo, а Bar реализовал Goo, как показано в EC-7 ниже.

EC-7: Implementing a hierarchy of interfaces
public interface Foo {
    default int sumDefault(int a, int b) {
        return a + b; // Adds a to b
    }
}

public interface Goo extends Foo {
    default int sumDefault(int a, int b) {
        return a + b - 1; //Adds a + b, then subtracts 1.
    }
}

public class Bar implements Goo {
    public static void main(String args[]) {
        System.out.println("Default: " + new Bar().sumDefault(2, 2));
    }
}
Output:
Default: 3
#MoreQuickMaths

Как видно из EC-7, на выходе всегда получается самая последняя реализация в цепочке. Таким образом, мы можем забыть о реализации, предоставляемой Foo, поскольку она была заменена реализацией Goo.

Заключение

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

Как и в случае с большинством новых функций, для него будут хорошие варианты использования, как отмечает Pankaj из JournalDev: Методы интерфейса Java по умолчанию помогут нам в удалении базовых классов реализации, мы можем предоставить реализацию по умолчанию и классы реализации. можно выбрать, какой из них переопределить. , Я ценю дополнительную гибкость, которая у нас теперь есть.

Спасибо за прочтение!