C, PHP, VB, .NET

Дневникът на Филип Петров


* Методи по подразбиране

Публикувано на 07 март 2015 в раздел ПИК3 Java.

Както вече знаем, ламбда изразите работят само и единствено с функционални интерфейси. Това е логично, защото ако един интерфейс има повече от един метод, компилатора няма как да знае кой от методите да използва при конвертирането на ламбда израза. Това е нормално и не подлежи на коментар. Интересен е обаче въпроса как да разширим съществуващи интерфейси, като им добавим допълнителни методи, които да могат да работят с ламбда изрази.

Нека дадем един класически пример - добре познатият интерфейс Collection. Можем да проверим, че той наследява интерфейс Iterable. Нека видим как са реализирани тези интерфейси в Java 7:

// jdk7-src->java/lang/Iterable.java
package java.lang;
import java.util.Iterator;
public interface Iterable<T> {
 Iterator<T> iterator();
}

// jdk7-src->java/util/Collection.java
package java.util;
public interface Collection<E> extends Iterable<E> {
 int size();
 boolean isEmpty();
 boolean contains(Object o);
 Iterator<E> iterator();
 Object[] toArray();
 <T> T[] toArray(T[] a);
 boolean add(E e);
 boolean remove(Object o);
 boolean containsAll(Collection<?> c);
 boolean addAll(Collection<? extends E> c);
 boolean removeAll(Collection<?> c);
 boolean retainAll(Collection<?> c);
 void clear();
 boolean equals(Object o);
 int hashCode();
}

Ние много добре знаем как да обходим един списък и да отпечатаме всичките му елементи на екрана:

import java.util.Collection;
import java.util.ArrayList;
public class Example{
 public static void main(String [] args) {
   Collection<String> c = new ArrayList<>();
   c.add("aaa"); c.add("bbb"); c.add("ccc");
   for(String s: c){
     System.out.println(s);
   }
 }
}

Това е добре познатия цикъл foreach. А не би ли било по-добре ако можем да направим нещо като следното:

c.forEach(System.out::println);

Видимо това ще е много по-стегнат и по-удобен запис, защото ще премахне нуждата от още едни блокови скоби (а в Java особено когато се намешат try-catch блокове скобите стават винаги по много). И внимание - не само, че можем да го направим, а това вече е направено в Java 8! Изпълнете горния ред и ще видите, че списъка ще бъде отпечатан!

Преди да отговорим на въпроса "как са го направили" ще кажем защо е проблем да бъде направено. Първоначално ще си кажем, че чисто и просто в интерфейс Collection са добавили още един нов абстрактен метод, който получава някакъв удобен за случая функционален интерфейс като входен параметър и готово. Да, но това би било пагубно за всички стари съществуващи програми писани на Java 7 или по-стара версия, които използват интерфейса Collection - те просто няма да работят в Java 8, защото не са реализирали този нов метод. По философия това не е допустимо за разработчиците на Java - те винаги са се стремяли към пълна обратна съвместимост с предишни версии.

Как тогава са направили хем метод forEach да го има в интерфейс Collection, хем старите софтуери да работят по този нов интерфейс? Отговорът се крие в още едно нововъведение в Java 8 - методи по подразбиране (default methods). В Java 8 просто е прекъсната традицията на правилото "интерфейсите дефинират само и единствено абстракти методи" - там вече е позволено да има и неабстрактни (т.е. реализирани и работещи) методи. Ето как са реализирали въпросния метод forEach (в клас Iterable):

// jdk8-src->java/lang/Iterable.java
package java.lang;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
public interface Iterable<T> {
 Iterator<T> iterator();
 default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
       action.accept(t);
    }
 }
 default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(iterator(), 0);
 }
}

Виждате, че в Java 7 имаше просто един метод iterator(), докато тук вече имаме три метода - стария абстрактен iterator(), както и два нови, които не са абстрактни - forEach() и spliterator(). За нашия пример се фокусираме върху метода forEach. Знаем, че интерфейс Collection наследява интерфейс Iterable - значи ще наследи и вече конкретно реализирания метод forEach. Това означава просто, че всички програми, които са писани на Java 7 или по-стара версия, които имат обекти работещи по интерфейс Collection, автоматично ще получат този нов метод наготово ако бъдат пуснати под Java 8. Така стария интерфейс не е нарушен (абстрактните методи в новия са останали същите), а е разширен.

Методите по подразбиране слагат край на класическия шаблон "интерфейс -> абстрактен клас", в който интерфейса просто дефинираше общия изглед на функционалностите, а абстрактния клас реализираше повечето от тях. В Java 8 интерфейсите вече започват да "изземат" от територията на абстрактните класове и сами си реализират методите, които са им нужни. Единственото, с което абстрактните класове могат да допринесат е добавянето на член променливи.

Забележете също така нещо друго, което е важно - ако един интерфейс има множество методи по подразбиране, но има само един единствен абстрактен метод, този интерфейс продължава да е функционален интерфейс! Правилото е, че ламбда израз по такъв интерфейс ще работи винаги с абстрактния метод.

Дотук всичко е логично и ясно, но на хоризонта се появяват нови проблеми. Ние добре знаем, че в Java няма множествено наследяване, но за сметка на това е възможно един клас да работи по множество различни интерфейси. Какво ще се случи ако един клас имплементира два интерфейса, които имат методи с едно и също име? Тоест следното:

interface Interface1{
  ...
  default void print(){
    System.out.println("Interface1 print");
  }
}

interface Interface2{
  ...
  default void print(){
    System.out.println("Interface2 print");
  }
}

class ExampleClass implements Interface1, Interface2{
  ...
}

Ако методите print() бяха абстрактни, щяхме да имаме ясен казус, познат от предишните версии на Java - ExampleClass тъй или иначе е задължен да ги предефинира, т.е. тук нямаме двусмислие за това какъв ще бъде. Да, но тук методите не са абстрактни - те са с конкретна реализация и реално ExampleClass ги е наследил от интерфейсите. Или тук се сблъскваме с реален проблем на множествено наследяване в Java. В езици като C++ има сложни правила за разрешаване на подобни конфликти. В Java разработчиците са приели най-простия възможен подход - разрешаването на този конфликт е проблем за програмиста, т.е. той трябва да предефинира задължително конфликтния метод! Или в горния пример - ExampleClass е длъжен да предефинира метод print(). Ако не го направи, програмата няма да се компилира, а ще даде следната грешка: "Error: class ExampleClass inherits unrelated defaults for print() from types Interface1 and Interface2".

Има и още един хипотетичен казус - един интерфейс наследява друг, като предефинира негов метод по подразбиране. В такъв случай правилото е, че клас, който работи по тези интерфейси, би "получил" (наследил) метода от дъщерния интерфейс. Нека го демонстрираме с възможно най-прост пример:

public class Example{
   public static void main(String [] args) {
      ExampleClass e = new ExampleClass();
      e.print();
   }
}

class ExampleClass implements SuperInterface,InheritedInterface{
}

interface SuperInterface{
  default void print(){
    System.out.println("SuperInterface print");
  }
}

interface InheritedInterface extends SuperInterface{
  default void print(){
    System.out.println("InheritedInterface print");
  }
}

Резултатът от изпълнението на тази програма ще е съобщение в конзолата "InheritedInterface print".

Казусите обаче не свършват дотук - какво ще стане ако един клас наследява друг клас и имплементира интерфейс, като метод от родителския клас съвпада по име и входни параметри с метод по подразбиране от интерфейса? Правилото тук е, че "родителския клас печели пред интерфейса". Ето пример:

public class Example{
   public static void main(String [] args) {
     ChildClass c = new ChildClass();
     c.print();
   }
}

class ParentClass{
  public void print(){
    System.out.println("ParentClass print");
  }
}

interface TheInterface{
  default void print(){
    System.out.println("Interface print");
  }
}

class ChildClass extends ParentClass implements TheInterface{
}

Отпечатаният резултат в конзолата ще е "ParentClass print", което спазва и горното правило - класът печели пред интерфейса.

Последното правило (класът печели) е много важно за обратната съвместимост на кода. Програми писани на по-стари версии на Java ще продължат да работят както преди и добавянето на default метод, който дублира името си с метод на даден клас няма да причини проблеми. Това обаче внася и малко ограничения върху интерфейсите - те не могат да дефинират методи, които се дублират с методите на клас Object. Например следното няма да се компилира:

interface TheInterface{
  default String toString(){
    return "AAA";
  }
}

Грешката, която компилатора ще върне е "Error: default method toString in interface TheInterface overrides a member of java.lang.Object". Забраната на предефиниране на методи от клас Object и тук е логична, защото подобни методи никога няма да могат да бъдат извикани - в Java в основата на всичко стои клас Object, а правилото (припомняме) е, че "класа печели пред интерфейса".

И последно, но не и по значение - няма проблем методите по подразбиране да са статични.

Обновено 18.10.2018 г. С версия 9 на Java се появява и още едно удобно нововъведение - интерфейсите вече могат да имат private методи. Чрез тях два метода по подразбиране могат да си споделят обща функционалност.

 



2 коментара


  1. Като заключение може да се отбележи това, което начекнахте в статията:
    Основната философия на Java и на JVM архитектите е стремеж и постигане към т. нар. "source code compatibility".
    Или казано направо - код написан през 1995г. да работи без проблеми и без допълнително компилиране през 2015г. и през 2035г.!!!

Добави коментар

Адресът на електронната поща няма да се публикува


*