Cześć! Dzisiaj porozmawiamy o ważnym pojęciu w Javie: interfejsach. To słowo jest ci prawdopodobnie znane. Na przykład większość programów komputerowych i gier ma interfejsy. W szerokim znaczeniu interfejs jest rodzajem „pilota”, który łączy dwie strony interakcji. Prostym przykładem interfejsu w życiu codziennym jest pilot do telewizora. Łączy dwa obiekty — osobę i telewizor — i wykonuje różne zadania: zwiększa lub zmniejsza głośność, przełącza kanały, włącza lub wyłącza telewizor. Jedna strona (osoba) musi uzyskać dostęp do interfejsu (nacisnąć przycisk na pilocie), aby druga strona wykonała akcję. Na przykład, aby telewizor przełączył się na następny kanał. Co więcej, użytkownik nie Nie musisz wiedzieć, jak zorganizowany jest telewizor lub jak wewnętrznie realizowany jest proces zmiany kanałów. Jedyną rzeczą, do której użytkownik ma dostęp, jest interfejs. Głównym celem jest uzyskanie pożądanego rezultatu. Co to ma wspólnego z programowaniem i Javą? Wszystko :) Tworzenie interfejsu jest bardzo podobne do tworzenia zwykłej klasy, ale zamiast tego używa się słowaclass , wskazujemy słowo interfejs . Przyjrzyjmy się najprostszemu interfejsowi Java, zobaczmy, jak działa i dlaczego jest nam potrzebny:

public interface CanSwim {

     public void swim();
}
Stworzyliśmy interfejs CanSwim . To trochę jak nasz pilot, ale z jednym „przyciskiem”: metodą swim() . Ale jak używamy tego pilota? Aby to zrobić, musimy zaimplementować metodę, czyli nasz przycisk pilota. Aby użyć interfejsu, niektóre klasy w naszym programie muszą implementować jego metody. Wymyślmy klasę, której obiekty „mogą pływać”. Na przykład klasa Duck pasuje do:

public class Duck implements CanSwim {

    public void swim() {
        System.out.println("Duck, swim!");
    }

    public static void main(String[] args) {

        Duck duck = new Duck();
        duck.swim();
    }
}
„Co tu widzimy? Klasa Duck jest „powiązana” z interfejsem CanSwim za pomocą słowa kluczowego implements. Być może pamiętasz, że użyliśmy podobnego mechanizmu do powiązania dwóch klas poprzez dziedziczenie, ale w tym przypadku użyliśmy słowa „rozciąga” . dla pełnej jasności, możemy przetłumaczyć „ publiczną klasę Duck implementuje CanSwim ” dosłownie jako: „Klasa publiczna Duck implementuje interfejs CanSwim ”. Oznacza to, że klasa powiązana z interfejsem musi implementować wszystkie swoje metody. Uwaga: nasza Duckklasa, podobnie jak interfejs CanSwimma swim()metodę i zawiera pewną logikę.To jest obowiązkowy wymóg.Jeśli tylko napiszemypublic class Duck implements CanSwimbez utworzenia swim()metody w Duckklasie kompilator zwróci nam błąd: Duck nie jest abstrakcyjny i nie zastępuje metody abstrakcyjnej swim() w CanSwim Dlaczego? Dlaczego to się dzieje? Jeśli wyjaśnimy błąd na przykładzie telewizora, będzie to tak, jakbyśmy wręczyli komuś pilota do telewizora z przyciskiem „zmień kanał”, który nie może zmieniać kanałów. Możesz nacisnąć przycisk tyle, ile chcesz, ale to nie zadziała. Pilot sam nie zmienia kanałów, tylko wysyła sygnał do telewizora, który realizuje złożony proces zmiany kanałów. I tak jest z naszą kaczką: musi umieć pływać, żeby można było ją wywołać za pomocą CanSwiminterfejsu. Jeśli nie wie jak, npCanSwiminterfejs nie łączy dwóch stron — człowieka i programu. Osoba nie będzie mogła użyć tej swim()metody do pływania Duckw programie. Teraz lepiej rozumiesz, do czego służą interfejsy. Interfejs opisuje zachowanie, jakie muszą mieć klasy implementujące interfejs. „Zachowanie” to zbiór metod. Jeśli chcemy stworzyć kilka komunikatorów, najprościej jest stworzyć interfejs Messenger. Czego potrzebuje każdy posłaniec? Na poziomie podstawowym muszą mieć możliwość odbierania i wysyłania wiadomości.

public interface Messenger{

     public void sendMessage();

     public void getMessage();
}
Teraz możemy po prostu stworzyć nasze klasy komunikatorów, które implementują odpowiedni interfejs. Sam kompilator „zmusi” nas do zaimplementowania ich w naszych klasach. Telegram:

public class Telegram implements Messenger {

    public void sendMessage() {

        System.out.println("Sending a Telegram message!");
    }

     public void getMessage() {
         System.out.println("Receiving a Telegram message!");
     }
}
WhatsApp:

public class WhatsApp implements Messenger {

    public void sendMessage() {

        System.out.println("Sending a WhatsApp message!");
    }

     public void getMessage() {
         System.out.println("Reading a WhatsApp message!");
     }
}
Viber:

public class Viber implements Messenger {

    public void sendMessage() {

        System.out.println("Sending a Viber message!");
    }

     public void getMessage() {
         System.out.println("Receiving a Viber message!");
     }
}
Jakie to daje korzyści? Najważniejszym z nich jest luźne sprzęgło. Wyobraź sobie, że projektujemy program, który będzie zbierał dane klientów. Klasa Clientzdecydowanie potrzebuje pola, aby wskazać, jakiego konkretnego komunikatora używa 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 mieć tylko jednego komunikatora. Tylko nie wiemy, który. Musimy więc dodać do klasy każdą możliwość, aby móc komunikować się z klientem. Okazuje się, że jeden lub dwa z nich zawsze będą nullzupełnie niepotrzebne programowi. Zamiast tego lepiej użyć naszego interfejsu:

public class Client {

    private Messenger messenger;
}
To jest przykład luźnego sprzężenia! Zamiast określać w Clientklasie konkretną klasę posłańca, po prostu wskazujemy, że klient ma posłańca. Który dokładnie zostanie określony podczas działania programu. Ale po co nam do tego interfejsy? Dlaczego w ogóle zostały dodane do języka? To dobre pytanie — i to właściwe! Czy nie możemy osiągnąć tego samego wyniku za pomocą zwykłego dziedziczenia? Klasa Messengerjako rodzic i , Viber, Telegrami WhatsAppjako dzieci. Rzeczywiście, jest to możliwe. Ale jest jeden haczyk. Jak już wiesz, Java nie ma wielokrotnego dziedziczenia. Ale istnieje obsługa wielu interfejsów. Klasa może zaimplementować dowolną liczbę interfejsów. Wyobraź sobie, że mamy Smartphoneklasę, która go maApppole, które reprezentuje aplikację zainstalowaną na smartfonie.

public class Smartphone {

    private App app;
}
Oczywiście aplikacja i komunikator są podobne, ale to wciąż różne rzeczy. Mogą istnieć mobilne i stacjonarne wersje komunikatora, ale Aplikacja reprezentuje konkretnie aplikację mobilną. Oto sprawa — gdybyśmy używali dziedziczenia, nie bylibyśmy w stanie dodać obiektu Telegramdo Smartphoneklasy. W końcu Telegramklasa nie może jednocześnie dziedziczyć Appi Messenger! I już sprawiliśmy, że dziedziczył Messengeri dodaliśmy go do Clientklasy. Ale Telegramklasa może łatwo zaimplementować oba interfejsy! W związku z tym możemy nadać Clientklasie Telegramobiekt jako Messenger, i możemy dać go klasie Smartphonejako App. Oto jak to zrobić:

public class Telegram implements Application, Messenger {

    // ...methods
}

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 używamy Telegramklasy tak, jak chcemy. W niektórych miejscach działa jak App. W innych miejscach pełni funkcję Messenger. Na pewno już zauważyłeś, że metody interfejsu są zawsze „puste”, tzn. nie mają implementacji. Powód tego jest prosty: interfejs opisuje zachowanie, ale go nie implementuje. „Wszystkie obiekty implementujące CanSwiminterfejs muszą umieć pływać”: to wszystko, co mówi nam interfejs. Specyficzny sposób, w jaki pływają ryby, kaczki i konie, jest pytaniem dla Fish, Duck, iHorseklasy, a nie interfejs. Tak jak zmiana kanału to zadanie dla telewizora. Pilot po prostu daje ci do tego przycisk. Jednak w Javie 8 pojawił się ciekawy dodatek — metody domyślne. Na przykład twój interfejs ma 10 metod. 9 z nich ma różne implementacje w różnych klasach, ale jedna jest implementowana tak samo dla wszystkich. Wcześniej, przed Javą 8, metody interfejsu nie miały żadnej implementacji: kompilator natychmiast zgłaszał błąd. Teraz możesz zrobić coś takiego:

public interface CanSwim {

   public default void swim() {
       System.out.println("Swim!");
   }

   public void eat();

   public void run();
}
Używając defaultsłowa kluczowego, stworzyliśmy metodę interfejsu z domyślną implementacją. Musimy zapewnić własną implementację dwóch innych metod — eat()i run()— we wszystkich klasach, które implementują CanSwim. Nie musimy tego robić za pomocą swim()metody: implementacja będzie taka sama w każdej klasie. Nawiasem mówiąc, już natknąłeś się na interfejsy w poprzednich zadaniach, nawet jeśli tego nie zauważyłeś :) Oto żywy przykład: Dlaczego interfejsy są niezbędne w Javie - 2Pracowałeś z interfejsami Listi Set! Mówiąc dokładniej, pracowałeś z ich implementacjami — ArrayList, LinkedList, HashSet, itd. Ten sam diagram wyraźnie pokazuje przykład, w którym jedna klasa implementuje wiele interfejsów jednocześnie. Na przykład LinkedListimplementuje ListandDeque(dwustronna kolejka) interfejsy. Zapoznałeś się z Mapinterfejsem, a raczej z jego HashMapimplementacją. Nawiasem mówiąc, ten diagram ilustruje pewną cechę: interfejsy mogą dziedziczyć inne interfejsy. Interfejs SortedMapdziedziczy Map, podczas gdy Dequedziedziczy Queue. Jest to konieczne, jeśli chcesz pokazać relacje między interfejsami, gdzie jeden interfejs jest rozszerzoną wersją innego. Rozważmy przykład z Queueinterfejsem. Jeszcze nie recenzowaliśmyQueues, ale jest raczej prosta i działa jak zwykła kolejka w sklepie. Możesz dodawać przedmioty tylko na końcu kolejki i możesz je brać tylko od początku. W pewnym momencie programiści potrzebowali ulepszonej wersji kolejki, aby dodawać i pobierać elementy na obu końcach. Stworzyli więc Dequeinterfejs, który jest dwustronną kolejką. Posiada wszystkie metody zwykłej kolejki. W końcu jest rodzicem kolejki dwustronnej, ale dodaje też nowe metody.