Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Poznaj podstawy Javy - interfejsy

Poznaj zasady funkcjonowania interfejsów w Javie na przykładzie figur geometrycznych.

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 SquareCircle, 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 SquareCircle 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.

Zobacz kogo teraz szukają