Poznaj podstawy Javy - interfejsy

W jednym z ostatnich wpisów opisane zostały klasy abstrakcyjne, pozwalające wynieść logikę wspólną dla większej liczby klas na wyższy, bardziej abstrakcyjny poziom. W Javie do dyspozycji programisty dostarczany jest jeszcze bardziej abstrakcyjny typ danych, zawierający (co do zasady) jedynie definicję operacji możliwych do wykonania; cała konkretna logika działania zaimplementowana musi zostać w takim przypadku w klasach „podrzędnych”. Typem tym są interfejsy i to one staną się przedmiotem dzisiejszego wpisu.
W celu zaprezentowania szczegółów funkcjonowania interfejsów posłużymy się klasycznym przykładem figur geometrycznych. Wyobraźmy sobie aplikację obliczającą pole powierzchni takich figur. Klasa reprezentująca kwadrat wyglądać mogłaby następująco:
public class Square {
private double side;
public Square(double side) {
this.side = side;
}
public double countArea() {
return this.side * this.side;
}
}
Klasa stworzona dla koła przybrać mogłaby natomiast następujący kształt:
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double countArea() {
return Math.PI * this.radius * this.radius;
}
Obie powyższe klasy posiadają metodę countArea()
. Opisuje ona działanie wspólne dla kwadratu i koła – obliczenie pola, które jednak w każdym przypadku dokonane zostaje inaczej. Z wcześniejszego wpisu wiemy, że wygodne w takiej sytuacji byłoby stworzenie klasy abstrakcyjnej, którą rozszerzałby zarówno kwadrat, jak i koło.
Pojawia się tutaj jednak pewien problem. Każda klasa w Javie dziedziczyć może jedynie po jednej klasie. Nie moglibyśmy zatem stworzyć na pewno żadnej wspólnej klasy abstrakcyjnej ponad Square
i Circle
, jeżeli którakolwiek z nich dziedziczyłaby już po jakieś innej klasie. Jeżeli zaś żadna z nich nie dziedziczyłaby jeszcze po innej klasie, to można zastanowić się, czy warto wykorzystywać tutaj mechanizm dziedziczenia, skoro tak naprawdę z klasy nadrzędnej potrzebna byłaby nam jedynie definicja metody abstrakcyjnej. W takiej sytuacji przydatne okazują się interfejsy.
Dla klasy Square
i Circle
takim interfejsem mógłby być Shape
, który utworzyć należałoby w następujący sposób:
public interface Shape {
public double countArea();
}
Jak można zauważyć, słowo class
znane z deklaracji klasy zastępowane jest w przypadku interfejsu przez interface
. Pozostałe elementy wyglądają podobnie, jednak ich katalog dostępny dla interfejsu został w Javie mocno okrojony: interfejsy co do zasady zawierać mogą abstrakcyjne, publiczne metody oraz stałe pola.
Również „rozszerzanie” interfejsu odbywa się nieco inaczej, niż ma to miejsce w przypadku dziedziczenia po innej klasie. Słówko extends
zastąpić należy w takim przypadku słowem implements
. Aby nasze klasy implementowały interfejs Shape
, zmienić należy więc ich definicje w następujący sposób. Kwadrat:
public class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
@Override
public double countArea() {
return this.side * this.side;
}
}
I koło:
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double countArea() {
return Math.PI * this.radius * this.radius;
}
}
W tym momencie mamy do dyspozycji wszelkie korzyści związane z polimorfizmem (opisane wcześniej w artykule nt. klas abstrakcyjnych). Możemy zatem np. operować na tablicy, w której znajdują się obiekty przypisane do zmiennej typu interfejsu, np.:
Shape[] shapes = new Shape[2];
shapes[0] = new Square(3.3);
shapes[1] = new Circle(1.4);
for (Shape shape : shapes) {
System.out.println(shape.countArea());
}
Dodanie w przyszłości kolejnego nowego kształtu implementującego interfejs Shape
nie będzie wymagało od nas zmiany logiki biznesowej. Równocześnie nie jesteśmy ograniczeni możliwością rozszerzenia tylko jednej klasy nadrzędnej – w przypadku interfejsów możemy implementować równocześnie większą ich liczbę. Co więcej – dana klasa może zarówno dziedziczyć po innej klasie, jak i implementować interfejs bądź kilka. Jej deklaracja wyglądać powinna wówczas analogicznie jak na poniższym przykładzie (najpierw extends
, następnie implements
):
public class Circle extends Item implements Shape, Round, Serializable {
//some logic here
}
Warto dodać, że interfejsy także mogą rozszerzać inne interfejsy. W takim przypadku należy użyć słowa extends
.
Do Javy 8 różnica między klasą abstrakcyjną a interfejsem była dość wyraźna. O ile klasa abstrakcyjna mogła zawierać własną logikę, o tyle w przypadku interfejsu dozwolone były jedynie publiczne metody abstrakcyjne oraz stałe pola. Jako że wszystkie metody były abstrakcyjne, każda z nich musiała zostać zaimplementowana w klasach podrzędnych. Ograniczenia te rekompensowane były faktem, że implementować możemy więcej niż jeden interfejs.
W Javie 8 różnica ta uległa pewnemu zatarciu, wprowadzono w niej bowiem możliwość tworzenia metod domyślnych. Metody takie zawierają już własną logikę i nie muszą być implementowane przez klasy podrzędne. Tworzone są z wykorzystaniem słowa default
, np. w ten sposób:
public interface Shape {
String message = "Area: ";
double countArea();
//since Java 8
default void printArea(String area) {
System.out.println(message + area);
}
}
Kolejnym elementem zacierającym różnice między interfejsami a klasami abstrakcyjnymi było umożliwienie tworzenia w tych pierwszych metod statycznych. Od Javy 8 moglibyśmy sobie zatem zdefiniować taką przykładową metodę pomocniczą bezpośrednio w interfejsie:
public interface Shape {
double countArea();
//since Java 8
static void printShapes(Shape[] shapes) {
for (Shape shape : shapes) {
System.out.println(shape.countArea());
}
}
}
W Javie 8 pozwolono programistom tworzyć metody domyślne, które jednak musiały być publiczne. Nie wpływało to dobrze na jakość kodu, ponieważ złożonej logiki zawartej w publicznej metodzie nie można było wyodrębnić do większej liczby odpowiednio nazwanych metod prywatnych, co podniosłoby czytelność kodu. Problem ten został szybko dostrzeżony i poprawiony i od Javy 9 możliwe jest także umieszczanie w interfejsach metod prywatnych. Tym samym różnica dzieląca je od klas abstrakcyjnych uległa jeszcze większemu zmniejszeniu.
Na podstawie sformułowanych wyżej uwag można stwierdzić, że interfejsy stanowiły z założenia typ danych, który jedynie definiuje zachowanie, nie zawiera zaś żadnej logiki. Wraz z ewolucją Javy założenie to straciło na aktualności i obecnie funkcjonalność interfejsów zbliża je do klas abstrakcyjnych. Niewątpliwą przewagą interfejsów pozostał natomiast fakt, że klasy nie są ograniczone do implementowania tylko jednego z nich, tak jak ma to miejsce w przypadku dziedziczenia.