Cześć! Dzisiaj porozmawiamy o ważnym pojęciu w Javie – interfejsach. Słowo to prawdopodobnie jest Ci znane. Na przykład większość programów i gier komputerowych ma interfejsy. W szerokim znaczeniu interfejs to rodzaj „pilota”, który łączy dwie strony współdziałające ze sobą. Prostym przykładem interfejsu z życia codziennego jest pilot do telewizora. Łączy dwa obiekty, osobę i telewizor, i wykonuje różne zadania: zwiększa lub zmniejsza głośność, zmienia kanały, włącza i wyłącza telewizor. Jedna strona (osoba) musi uzyskać dostęp do interfejsu (nacisnąć przycisk pilota), aby druga strona mogła wykonać akcję. Na przykład, aby telewizor przełączył kanał na następny. W takim przypadku użytkownik nie musi znać urządzenia telewizora i sposobu, w jaki realizowany jest w nim proces zmiany kanału. Jedyne, do czego użytkownik ma dostęp, to interfejs . Głównym zadaniem jest uzyskanie pożądanego rezultatu. Co to ma wspólnego z programowaniem i Javą? Direct :) Tworzenie interfejsu jest bardzo podobne do tworzenia zwykłej klasy, tyle że zamiast słowa
class
podajemy słowo interface
. Przyjrzyjmy się najprostszemu interfejsowi Java i dowiedzmy się, jak działa i do czego jest potrzebny:
public interface Swimmable {
public void swim();
}
Stworzyliśmy interfejs Swimmable
, który potrafi pływać . To coś w rodzaju naszego pilota, który ma jeden „przycisk”: metodą swim()
jest „pływanie”. Jak możemy korzystać z tego „ pilota ”? W tym celu stosuje się metodę, tj. przycisk na naszym pilocie musi zostać zaimplementowany. Aby skorzystać z interfejsu, jego metody muszą być zaimplementowane przez niektóre klasy naszego programu. Wymyślmy klasę, której obiekty pasują do opisu „umie pływać”. Na przykład odpowiednia jest klasa kaczki Duck
:
public class Duck implements Swimmable {
public void swim() {
System.out.println(„Kaczka, płyń!”);
}
public static void main(String[] args) {
Duck duck = new Duck();
duck.swim();
}
}
Co tu widzimy? Klasa Duck
jest powiązana z interfejsem Swimmable
za pomocą słowa kluczowego implements
. Jeśli pamiętasz, podobny mechanizm zastosowaliśmy do łączenia dwóch klas w dziedziczeniu, tyle że było tam słowo „ rozszerza ”. „ public class Duck implements Swimmable
” dla przejrzystości można przetłumaczyć dosłownie: „klasa publiczna Duck
implementuje interfejs Swimmable
”. Oznacza to, że klasa powiązana z interfejsem musi implementować wszystkie swoje metody. Uwaga: w naszej klasie, Duck
podobnie jak w interfejsie , Swimmable
istnieje metoda swim()
, a wewnątrz niej kryje się pewnego rodzaju logika. Jest to wymóg obowiązkowy. Gdybyśmy po prostu napisali „ public class Duck implements Swimmable
” i nie utworzyli metody swim()
w klasie Duck
, kompilator dałby nam błąd: Duck nie jest abstrakcyjny i nie zastępuje abstrakcyjnej metody swim() w Swimmable. Dlaczego tak się dzieje? Jeśli wyjaśnimy błąd na przykładzie telewizora, okaże się, że dajemy osobie pilota z przyciskiem „zmień kanał” od telewizora, który nie potrafi zmieniać kanałów. W tym momencie naciskaj przycisk tyle, ile chcesz, nic nie będzie działać. Sam pilot nie zmienia kanałów: przekazuje jedynie sygnał do telewizora, w którym realizowany jest złożony proces zmiany kanału. Podobnie jest z naszą kaczką: musi umieć pływać, aby można było do niej dotrzeć za pomocą interfejsu Swimmable
. Jeśli nie wie jak to zrobić, interfejs Swimmable
nie połączy dwóch stron – osoby i programu. Osoba nie będzie w stanie użyć metody, swim()
która sprawi, że obiekt Duck
wewnątrz programu będzie pływał. Teraz widziałeś wyraźniej, do czego służą interfejsy. Interfejs opisuje zachowanie, jakie muszą mieć klasy implementujące ten interfejs. „Zachowanie” to zbiór metod. Jeśli chcemy stworzyć wielu komunikatorów, najłatwiej to zrobić tworząc interfejs Messenger
. Co powinien umieć każdy posłaniec? W uproszczonej formie odbieraj i wysyłaj wiadomości.
public interface Messenger{
public void sendMessage();
public void getMessage();
}
A teraz możemy po prostu stworzyć nasze klasy komunikatorów, implementując ten interfejs. Sam kompilator „zmusi” nas do zaimplementowania ich wewnątrz klas. Telegram:
public class Telegram implements Messenger {
public void sendMessage() {
System.out.println(„Wysyłanie wiadomości do Telegrama!”);
}
public void getMessage() {
System.out.println(„Czytanie wiadomości w telegramie!”);
}
}
WhatsApp:
public class WhatsApp implements Messenger {
public void sendMessage() {
System.out.println(„Wysyłanie wiadomości WhatsApp!”);
}
public void getMessage() {
System.out.println(„Czytanie wiadomości WhatsApp!”);
}
}
Vibera:
public class Viber implements Messenger {
public void sendMessage() {
System.out.println(„Wysyłanie wiadomości do Viber!”);
}
public void getMessage() {
System.out.println(„Czytanie wiadomości w Viber!”);
}
}
Jakie korzyści to zapewnia? Najważniejszym z nich jest luźne sprzęgło. Wyobraźmy sobie, że projektujemy program, w którym będziemy zbierać dane klientów. Klasa Client
musi posiadać pole wskazujące z jakiego komunikatora korzysta klient. Bez interfejsów wyglądałoby to dziwnie:
public class Client {
private WhatsApp whatsApp;
private Telegram telegram;
private Viber viber;
}
Stworzyliśmy trzy pola, ale klient może spokojnie mieć tylko jednego komunikatora. Nie wiemy tylko który. A żeby nie pozostać bez komunikacji z klientem, trzeba „wcisnąć” do klasy wszystkie możliwe opcje. Okazuje się, że jeden lub dwa z nich zawsze tam będą null
i wcale nie są potrzebne, aby program działał. Zamiast tego lepiej skorzystać z naszego interfejsu:
public class Client {
private Messenger messenger;
}
To jest przykład „luźnego połączenia”! Zamiast podawać w klasie konkretną klasę komunikatora Client
, po prostu wspominamy, że klient ma komunikatora. Który zostanie określony w trakcie programu. Ale po co nam do tego interfejsy? Po co w ogóle dodano je do języka? Pytanie jest dobre i trafne! Ten sam wynik można osiągnąć stosując zwykłe dziedziczenie, prawda? Klasa Messenger
jest klasą nadrzędną, a Viber
, Telegram
i WhatsApp
są spadkobiercami. Rzeczywiście, jest to możliwe. Ale jest jeden haczyk. Jak już wiesz, w Javie nie ma dziedziczenia wielokrotnego. Istnieje jednak wiele implementacji interfejsów. Klasa może implementować dowolną liczbę interfejsów. Wyobraźmy sobie, że mamy klasę Smartphone
, która posiada pole Application
– aplikację zainstalowaną na smartfonie.
public class Smartphone {
private Application application;
}
Aplikacja i komunikator są oczywiście podobne, ale jednak to różne rzeczy. Komunikator może być zarówno mobilny, jak i stacjonarny, natomiast Aplikacja jest aplikacją mobilną. Zatem gdybyśmy zastosowali dziedziczenie, nie moglibyśmy dodać obiektu Telegram
do klasy Smartphone
. W końcu klasa Telegram
nie może dziedziczyć po Application
i po Messenger
! I udało nam się już to odziedziczyć po Messenger
i dodać do klasy w tej formie Client
. Ale klasa Telegram
może z łatwością zaimplementować oba interfejsy! Dlatego w klasie Client
możemy zaimplementować obiekt Telegram
jako Messenger
, a w klasie Smartphone
jako Application
. Oto jak to się robi:
public class Telegram implements Application, Messenger {
//...metody
}
public class Client {
private Messenger messenger;
public Client() {
this.messenger = new Telegram();
}
}
public class Smartphone {
private Application application;
public Smartphone() {
this.application = new Telegram();
}
}
Teraz możemy korzystać z klasy Telegram
według własnego uznania. Gdzieś będzie występował w roli Application
, gdzieś w roli Messenger
. Zapewne zauważyłeś już, że metody w interfejsach są zawsze „puste”, czyli nie mają implementacji. Powód tego jest prosty: interfejs opisuje zachowanie, a nie je implementuje. „Wszystkie obiekty klas, które implementują interfejs, Swimmable
muszą mieć możliwość pływania”: to wszystko, co mówi nam interfejs. Jak dokładnie będzie pływać ryba, kaczka czy koń, to jest pytanie do klas Fish
, Duck
i Horse
, a nie do interfejsu. Podobnie jak zmiana kanału jest zadaniem telewizora. Na pilocie po prostu masz przycisk, który to umożliwia. Jednak Java8 ma ciekawy dodatek - metody domyślne. Na przykład twój interfejs ma 10 metod. 9 z nich jest zaimplementowanych różnie w różnych klasach, ale jeden jest zaimplementowany tak samo we wszystkich. Wcześniej, przed wydaniem Java8, metody wewnątrz interfejsów nie miały żadnej implementacji: kompilator natychmiast zgłaszał błąd. Teraz możesz to zrobić w ten sposób:
public interface Swimmable {
public default void swim() {
System.out.println("Pływać!");
}
public void eat();
public void run();
}
Za pomocą słowa kluczowego default
utworzyliśmy w interfejsie metodę z domyślną implementacją. Będziemy musieli zaimplementować dwie pozostałe metody eat()
i run()
sami we wszystkich klasach, które będą implementować metodę Swimmable
. Nie ma potrzeby robić tego w przypadku metody swim()
: implementacja będzie taka sama we wszystkich klasach. Swoją drogą, w poprzednich zadaniach natrafiłeś na interfejsy nie raz, choć sam tego nie zauważyłeś :) Oto oczywisty przykład: Pracowałeś z interfejsami List
i Set
! Dokładniej, z ich implementacjami - , ArrayList
i innymi. Ten sam diagram pokazuje przykład, gdy jedna klasa implementuje kilka interfejsów jednocześnie. Na przykład implementuje interfejsy i (kolejkę dwustronną). Znasz także interfejs , a raczej jego implementacje - . Nawiasem mówiąc, na tym schemacie widać jedną cechę: interfejsy można dziedziczyć od siebie. Interfejs jest dziedziczony z , i jest dziedziczony z kolejki . Jest to konieczne, jeśli chcesz pokazać połączenie między interfejsami, ale jeden interfejs jest rozszerzoną wersją drugiego. Spójrzmy na przykład z interfejsem - kolejką. Nie przeglądaliśmy jeszcze kolekcji , ale są one dość proste i ułożone jak zwykła linia w sklepie. Możesz dodawać elementy tylko na końcu kolejki i usuwać je tylko od początku. Na pewnym etapie twórcy potrzebowali rozszerzonej wersji kolejki, aby można było dodawać i odbierać elementy z obu stron. Tak powstał interfejs - kolejka dwukierunkowa. Zawiera wszystkie metody zwykłej kolejki, ponieważ jest „rodzicem” kolejki dwukierunkowej, ale dodano nowe metody. LinkedList
HashSet
LinkedList
List
Deque
Map
HashMap
SortedMap
Map
Deque
Queue
Queue
Queue
Deque
GO TO FULL VERSION