* Методи по подразбиране
Публикувано на 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 методи. Чрез тях два метода по подразбиране могат да си споделят обща функционалност.
Като заключение може да се отбележи това, което начекнахте в статията:
Основната философия на Java и на JVM архитектите е стремеж и постигане към т. нар. "source code compatibility".
Или казано направо - код написан през 1995г. да работи без проблеми и без допълнително компилиране през 2015г. и през 2035г.!!!
Справят се - програми, които съм писал като студент в момента си работят на Java 8...