Wydrukuj

Cykl: Podstawy Javy. Epizod 5: 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.


Michał Karmelita

Z wykształcenia prawnik i informatyk. Zawodowo i z zamiłowania programista Java. W wolnym czasie stara się oddawać przyjemnościom i unikać przykrości. Zapalony podróżnik.


Wydrukuj