Metody statyczne i domyślne w interfejsach w Javie

1. Przegląd

Java 8 przyniosła do tabeli kilka zupełnie nowych funkcji, w tym wyrażenia lambda, interfejsy funkcjonalne, odwołania do metod, strumienie, opcjonalne oraz statyczne i domyślne metody w interfejsach.

Niektóre z nich zostały już omówione w tym artykule. Niemniej jednak statyczne i domyślne metody w interfejsach zasługują na głębsze spojrzenie na siebie.

W tym artykule omówimy szczegółowo, jak używać metod statycznych i domyślnych w interfejsach oraz przejdziemy przez niektóre przypadki użycia, w których mogą być przydatne.

2. Dlaczego potrzebne są domyślne metody w interfejsach

Podobnie jak zwykłe metody interfejsu, metody domyślne są niejawnie publiczne - nie ma potrzeby określania modyfikatora public .

W przeciwieństwie do zwykłych metod interfejsu są one deklarowane za pomocą domyślnego słowa kluczowego na początku sygnatury metody i zapewniają implementację .

Zobaczmy prosty przykład:

public interface MyInterface { // regular interface methods default void defaultMethod() { // default method implementation } }

Powód, dla którego domyślne metody zostały uwzględnione w wydaniu Java 8, jest dość oczywisty.

W typowym projekcie opartym na abstrakcjach, w którym interfejs ma jedną lub wiele implementacji, jeśli do interfejsu zostanie dodana jedna lub więcej metod, wszystkie implementacje będą również zmuszone do ich implementacji. W przeciwnym razie projekt po prostu się zepsuje.

Domyślne metody interfejsu są skutecznym sposobem rozwiązania tego problemu. Pozwalają nam dodawać nowe metody do interfejsu, które są automatycznie dostępne w implementacjach . Dlatego nie ma potrzeby modyfikowania klas implementujących.

W ten sposób wsteczna kompatybilność jest starannie zachowana bez konieczności refaktoryzacji implementatorów.

3. Domyślne metody interfejsu w akcji

Aby lepiej zrozumieć funkcjonalność domyślnych metod interfejsu, stwórzmy prosty przykład.

Powiedzmy, że mamy naiwny interfejs pojazdu i tylko jedną implementację. Mogłoby być więcej, ale nie komplikujmy:

public interface Vehicle { String getBrand(); String speedUp(); String slowDown(); default String turnAlarmOn() { return "Turning the vehicle alarm on."; } default String turnAlarmOff() { return "Turning the vehicle alarm off."; } }

I napiszmy klasę implementującą:

public class Car implements Vehicle { private String brand; // constructors/getters @Override public String getBrand() { return brand; } @Override public String speedUp() { return "The car is speeding up."; } @Override public String slowDown() { return "The car is slowing down."; } } 

Na koniec zdefiniujmy typową klasę główną , która tworzy instancję Car i wywołuje jej metody:

public static void main(String[] args) { Vehicle car = new Car("BMW"); System.out.println(car.getBrand()); System.out.println(car.speedUp()); System.out.println(car.slowDown()); System.out.println(car.turnAlarmOn()); System.out.println(car.turnAlarmOff()); }

Zwróć uwagę, jak domyślne metody turnAlarmOn () i turnAlarmOff () z naszego interfejsu pojazduautomatycznie dostępne w klasie Car .

Ponadto, jeśli w którymś momencie zdecydujemy się dodać więcej metod domyślnych do interfejsu Vehicle , aplikacja będzie nadal działać i nie będziemy musieli zmuszać klasy do dostarczania implementacji dla nowych metod.

Najbardziej typowym zastosowaniem metod domyślnych w interfejsach jest przyrostowe zapewnianie dodatkowej funkcjonalności dla danego typu bez rozbijania klas implementujących.

Ponadto można ich użyć do zapewnienia dodatkowej funkcjonalności wokół istniejącej metody abstrakcyjnej :

public interface Vehicle { // additional interface methods double getSpeed(); default double getSpeedInKMH(double speed) { // conversion } }

4. Reguły dziedziczenia wielu interfejsów

Domyślne metody interfejsu są rzeczywiście całkiem fajną funkcją, ale warto wspomnieć o kilku zastrzeżeniach. Ponieważ Java umożliwia klasom implementację wielu interfejsów, ważne jest, aby wiedzieć, co się dzieje, gdy klasa implementuje kilka interfejsów, które definiują te same metody domyślne .

Aby lepiej zrozumieć ten scenariusz, zdefiniujmy nowy interfejs Alarm i zmienimy klasę samochodu :

public interface Alarm { default String turnAlarmOn() { return "Turning the alarm on."; } default String turnAlarmOff() { return "Turning the alarm off."; } }

Dzięki temu nowemu interfejsowi definiującemu własny zestaw domyślnych metod, klasa Car implementowałaby zarówno pojazd, jak i alarm :

public class Car implements Vehicle, Alarm { // ... }

W takim przypadku kod po prostu się nie skompiluje, ponieważ występuje konflikt spowodowany dziedziczeniem wielu interfejsów (inaczej Diamentowy Problem). Samochodów klasy odziedziczy oba zestawy domyślnych metod. Których więc należy nazwać?

Aby rozwiązać tę niejednoznaczność, musimy jawnie podać implementację dla metod:

@Override public String turnAlarmOn() { // custom implementation } @Override public String turnAlarmOff() { // custom implementation }

Możemy również kazać naszej klasie używać domyślnych metod jednego z interfejsów .

Zobaczmy przykład wykorzystujący domyślne metody z interfejsu pojazdu :

@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff(); } 

Podobnie możemy sprawić, by klasa używała domyślnych metod zdefiniowanych w interfejsie Alarm :

@Override public String turnAlarmOn() { return Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Alarm.super.turnAlarmOff(); } 

Co więcej, jest nawet możliwe, aby klasa Car używała obu zestawów domyślnych metod :

@Override public String turnAlarmOn() { return Vehicle.super.turnAlarmOn() + " " + Alarm.super.turnAlarmOn(); } @Override public String turnAlarmOff() { return Vehicle.super.turnAlarmOff() + " " + Alarm.super.turnAlarmOff(); } 

5. Metody interfejsu statycznego

Oprócz możliwości deklarowania domyślnych metod w interfejsach, Java 8 pozwala nam definiować i implementować statyczne metody w interfejsach .

Ponieważ metody statyczne nie należą do określonego obiektu, nie są częścią interfejsu API klas implementujących interfejs i muszą być wywoływane przy użyciu nazwy interfejsu poprzedzającej nazwę metody .

Aby zrozumieć, jak statyczne metody działają w interfejsach, przeprowadźmy refaktoryzację interfejsu Vehicle i dodajmy do niego statyczną metodę narzędzia:

public interface Vehicle { // regular / default interface methods static int getHorsePower(int rpm, int torque) { return (rpm * torque) / 5252; } } 

Definiowanie metody statycznej w interfejsie jest identyczne jak definiowanie metody w klasie. Ponadto metodę statyczną można wywołać w ramach innych metod statycznych i domyślnych .

Teraz powiedzmy, że chcemy obliczyć moc silnika danego pojazdu. Po prostu wywołujemy metodę getHorsePower () :

Vehicle.getHorsePower(2500, 480)); 

Ideą statycznych metod interfejsu jest zapewnienie prostego mechanizmu, który pozwala nam zwiększyć stopień spójności projektu poprzez połączenie powiązanych metod w jednym miejscu bez konieczności tworzenia obiektu.

Prawie to samo można zrobić z klasami abstrakcyjnymi. Główna różnica polega na tym, że klasy abstrakcyjne mogą mieć konstruktory, stan i zachowanie .

Ponadto metody statyczne w interfejsach umożliwiają grupowanie powiązanych metod narzędziowych bez konieczności tworzenia sztucznych klas narzędzi, które są po prostu obiektami zastępczymi dla metod statycznych.

6. Wniosek

W tym artykule dogłębnie zbadaliśmy użycie statycznych i domyślnych metod interfejsu w Javie 8. Na pierwszy rzut oka ta funkcja może wyglądać nieco niechlujnie, szczególnie z purystycznego punktu widzenia obiektowego. W idealnym przypadku interfejsy nie powinny hermetyzować zachowania i powinny być używane tylko do definiowania publicznego interfejsu API określonego typu.

Jeśli chodzi o zachowanie wstecznej kompatybilności z istniejącym kodem, statyczne i domyślne metody są dobrym kompromisem.

I jak zwykle wszystkie przykłady kodu przedstawione w tym artykule są dostępne na GitHub.