Interfejsy w Javie - Podstawy
Do 25-letniego już języka Java dodawało się i dodaje się wciąż (obecnie JDK-15) wiele możliwości. Wciąż jednak króluje sześcioletnia Java 8 i z tego względu polecam przyswajanie sobie Javy właśnie od tej wersji. Zrozumienie pojęć interfejsów, wyrażeń lambda to podstawa i potężne narzędzie Javy. Zakładam, że nieobce są wśród czytelników pojęcia m.in. klas abstrakcyjnych, hierarchii, dziedziczenia, rozszerzania klas, przesłaniania metod, polimorfizm, parametryzacji typów, gdyż rozumienie powyższych znacznie zwiększy komfort czytania i zrozumienia poniższych treści. Zastosowane przykłady nie wymagają IDE. Zachęcam do eksperymentowania z linii poleceń.
Używając słowa kluczowego interface
, można w pełni oddzielić interfejs klasy od jej implementacji. Interfejs określa, co klasa musi robić, ale nie wskazuje, w jaki sposób ma te zadania wykonać. Interfejsy są składniowo podobne do klas, ale nie mogą w nich pojawiać się zmienne składowe, a ich metody zazwyczaj deklaruje się bez podawania jakiegokolwiek kodu. W ten sposób określa się metody, które klasa musi zaimplementować, ale nie wskazuje się w żaden sposób jak tego dokonać. Dowolna ilość klas może implementować dany interfejs. Jedna klasa może implementować wiele różnych interfejsów.
Przykład:
interface Callback {
void callback(int param);
}
Raz zdefiniowany interfejs może być implementowany przez wiele klas np.:
class Client implements Callback {
//implementuje interfejs Callback
public void callback(int p) {
System.out.println("wywołanie calback() z wartością " + p);
}
}
W klasie implementowanej można umieścić własne np. metody:
class Client implements Callback {
//implementuje interfejs Callback
public void callback(int p) {
System.out.println("wywołanie calback() z wartością " + p);
}
void nonInterfaceMethod() {
System.out.println("Klasa implementująca interfejs " +
"może zawierać także własne metody.");
}
}
Zmienne reprezentujące referencje do obiektów można deklarować przy użyciu interfejsów (zamiast odpowiednich typów klasowych). Takiej zmiennej można przypisać dowolny egzemplarz dowolnej klasy implementującej zadeklarowany interfejs. Wywołanie metody dla takiej referencji spowoduje wywołanie odpowiedniej wersji metody należącej do właściwego egzemplarza klasy. Metoda do wykonania jest wyszukiwana dynamicznie w trakcie działania programu, co oznacza, iż klasy można tworzyć później niż wywołujący je kod. Kod korzystający z metod interfejsu nie musi dysponować wiedzą o obiekcie, którego będzie dotyczyło to wywołanie. Cała idea wywołań metod interfejsu jest bardzo podobna do wywołań metod przesłoniętych w klasach pochodnych klasy bazowej.
Class TestInterface {
public static void main(String args[]) {
Callback c = new Client();
c.callback(44);
}
}
Wynik działania to:
wywołanie callback() z wartością 44
Zmienna c
jest typu Callback
, a mimo to został jej przypisany obiekt typu Client
. Zmienna referencyjna „wie” tylko o metodach, które pojawiły się w deklaracji interfejsu, dlatego też nie uda się następujące wywołanie:
c.nonInterfaceMethod();
Stosując kolejną implementację interfejsu Callback
oraz używając polimorfizmu, można zauważyć, że wywoływana wersja metody callback()
jest identyfikowana dynamicznie w zależności od typu obiektu wskazywanego przez zmienną c w czasie wykonywania:
//inna implementacja interfejsu Callback
class AnotherClient implements Callback {
//implementuje interfejs Callback
public void callback(int p) {
System.out.println("Inna wersja metody callback()");
System.out.println("p podniesione do kwadratu to: " + (p * p));
}
}
oraz
class TestIface2 {
public static void main(String args[]) {
Callback c = new Client();
AnotherClient ob = new AnotherClient();
c.callback(44);
c = ob; //teraz c odnosi się do obiektu AnotherClient
c.callback(44);
}
}
Program wygeneruje wyniki:
wywołanie callback() z wartością 44
Inna wersja metody callback()
p podniesione do kwadratu to: 1936
Implementacja częściowa
Klasa, która dołącza interfejs, ale nie implementuje wszystkich wymaganych przez niego metod, musi być zadeklarowana jako abstrakcyjna.
abstract class Incomplete implements Callback {
int a, b;
void show() {
System.out.println(a + b);
}
//...
}
Klasy dziedziczące po Incomplete
muszą albo zaimplementować metodę callback()
, albo same zostać zadeklarowane jako abstrakcyjne.
Interfejsy zagnieżdżone
Interfejs można zadeklarować w formie składowej klasy lub innego interfejsu. Taki interfejs określa się mianem interfejsu składowego lub interfejsu zagnieżdżonego.
Zmienne w interfejsach
Można użyć interfejsów do wprowadzenia w wielu klasach tych samych stałych. Wystarczy w interfejsie zadeklarować i zainicjalizować zmienne. Klasa implementująca taki interfejs będzie w swoim zasięgu „widziała” wszystkie te zmienne jako stałe. Jeśli interfejs nie zawiera żadnych metod, wtedy implementująca je klasa nie musi deklarować żadnych dodatkowych metod. Zmienne występujące w interfejsie są widziane przez klasę korzystającą z interfejsu jako zmienne typu final
.
Rozszerzanie interfejsów
Jeden interfejs może dziedziczyć po drugim interfejsie, wystarczy do tego celu użyć słowa kluczowego extends
. Składnia jest taka sama jak w przypadku dziedziczenia po klasie. Gdy klasa implementuje interfejs dziedziczący po innym interfejsie, musi zapewnić implementację wszystkich metod zdefiniowanych w całym łańcuszku dziedziczenia.
Metody domyślne
We wcześniejszych niż JDK-8 wersjach Javy interfejsy nie mogły definiować żadnych implementacji, oznaczało to, że metody umieszczane w interfejsach musiały być abstrakcyjne – nie mogły zawierać żadnego kodu.
Wprowadzona opcja tzw. metod domyślnych (ang. default methods) – zwanych również metodami rozszerzenia (ang. extension method) - pozwalała na domyślne określenie implementacji metod. Zasadnicze cele wprowadzenia metod domyślnych to:
- Zapewnienie możliwości rozszerzenia interfejsów bez konieczności wprowadzania jakichkolwiek zmian w już istniejącym kodzie. Metody domyślne pozwalają dostarczyć implementacje, które mogą zostać użyte w przypadku, gdy nie zostaną jawnie podane inne implementacje metod.
- Zapewnienie możliwości tworzenia w interfejsach metod, które w zasadzie, w zależności od sposobu użycia interfejsu byłyby metodami opcjonalnymi.
Dodanie metod domyślnych nie zmienia cechy interfejsów: braku możliwości przechowywania stanu. Metody domyślne interfejsów definiuje się podobnie jak metody w klasach, poprzedzając jednak jej deklarację słowem kluczowym default
.
public interface MyIF {
//nie definiuje implementacji domyślnej
int getNumber();
//metoda domyślna definiuje implementację:
default String getString() {
return "Łańcuch domyślny";
}
}
Metoda getString()
zawiera domyślną implementację, zatem klasa implementująca interfejs MyIF nie musi jej przesłaniać.
//implementacja interfejsu MyIF
class MyIFImpl implements MyIF {
//konieczne jest zaimplementowanie metody getNumber()
public int getNumber() {
return 100;
}
}
Poniższy przykład tworzy instancję klasy MyIFImpl i używa jej do wywołania obu metod getNumber()
oraz getString()
.
Class DefaultMethodDemo {
public static void main(String args[]);
MyIFImpl obj = new MyIFImpl();
//wywołanie metody jawnie zaimplementowanej w klasie MyIFImpl
System.out.println(obj.getNumber());
//wywołanie metody domyślnej interfejsu MyIF
System.out.println(obj.getString());
}
Program wygeneruje wyniki
100
Łańcuch domyślny
Nic nie stoi na przeszkodzie by klasa implementująca MyIFImpl udostępniała własną implementację metody domyślnej getString()
.
Stosowanie metod domyślnych w pewnym stopniu udostępnia możliwości, które normalnie można powiązać z pojęciem wielokrotnego dziedziczenia. Jeśli zostanie zdefiniowana klasa implementująca wiele interfejsów zawierających metody domyślne, to klasa implementująca dziedziczyłaby te zachowania po wielu klasach. Jednakże w takiej sytuacji mogą pojawić się konflikty nazw. Metody domyślne zapewniają możliwość łagodnego modyfikowania interfejsów oraz udostępniania opcjonalnych możliwości funkcjonalnych bez zmuszania klasy do definiowania implementacji zastępczej w przypadku, gdy dana funkcjonalność nie jest potrzebna.
W przypadku przesłonięcia metod domyślnych interfejsów przez metodę klasy implementującej interfejsy pierwszeństwo ma metoda klasy implementującej. Odwołanie do niej wygląda standardowo:
nazwaMetody();
W przypadku gdy jeden interfejs dziedziczy po drugim i oba będą definiować metodę domyślną, wyższy priorytet będzie miała implementacja w interfejsie dziedziczącym. Istnieje jednak możliwość jawnego odwołania się do implementacji domyślnej dziedziczonego interfejsu. Ogólna postać to:
NazwaInterfejsu.super.nazwaMetody();
Metody statyczne w interfejsach
W JDK 8 do interfejsów została dodana możliwość definiowania jednej lub kilku metod statycznych, które podobnie jak w te definiowane w klasach mogą być wywoływane niezależnie od wszelkich obiektów. A zatem do wywołania metody statycznej zdefiniowanej w interfejsie nie trzeba go implementować w żadnej klasie ani tworzyć jakichkolwiek instancji. Wywołuje się następująco:
NazwaInterfejsu.nazwaMetodyStatycznej;
Przykład:
public interface MyIF {
//nie definiuje implementacji domyślnej
int getNumber();
//metoda domyślna definiuje implementację
default String getString() {
return "Łańcuch domyślny";
}
// to jest metoda statyczna zdefiniowana w interfejsie
static int getDefaultNumber() {
return 0;
}
}
Metodę getDefaultNumber()
można wywołać nastęująco:
int defNum=MyIF.getDefaultNumber();
Metody statyczne definiowane w interfejsach nie są dziedziczone, ani przez implementujące je klasy ani podinterfejsy.
Stosowanie metod prywatnych w interfejsach
Począwszy od JKD 9, interfejsy mogą zawierać także metody prywatne. Takie metody mogą być wykonywane wyłącznie przez metody domyślne bądź inne metody prywatne tego samego interfejsu. Są one oznaczone modyfikatorem private i nie mogą być wywoływane przez żaden kod spoza interfejsu, w którym zostały zdefiniowane.
To koniec pierwszej części cyklu artykułów o interfejsach Javy. Kolejne części będą traktowały m.in. o wyrażeniach lambda oraz interfesjach funkcyjnych.