artykuł logo typy proste
Typy proste w języku Java
29 listopada 2015
artykuł logo
Choinka
25 grudnia 2015

W artykule przeczytasz o interfejsach. Poznasz interfejs ze standardowej biblioteki Java. Dowiesz się czym różni się interfejs od jego implementacji. Przeczytasz też o tym dlaczego używanie interfejsów uważane jest w większości przypadków za dobrą praktykę. Jak zwykle na koniec będziesz miał także proste zadanie do wykonania. Do kodu! 🙂

To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

Interfejs

Wyobraź sobie kuchenkę mikrofalową. Kuchenka ma zestaw przycisków, parę pokręteł możliwe, że dodatkowy wyświetlacz. Ten zestaw to nic innego jak właśnie interfejs (ang. interface). Interfejs to  zestaw „mechanizmów” służących do interakcji, w tym przypadku z kuchenką mikrofalową.

Pojęcie interfejsu można także przenieść do świata programowania. Mówimy wówczas o tak zwanym API (ang. Application Programming Interface).

Interfejs w kontekście programowania w języku Java to zestaw metod bez ich implementacji (bez kodu definiującego zachowanie metody)1 . Właściwa implementacja metod danego interfejsu znajduje się w klasie implementującej dany interfejs.

W języku Java do definiowani interfejsów używamy słowa kluczowego interface. Interfejsy, podobnie jak klasy, definiujemy w osobnych plikach. Nazwa pliku musi odpowiadać nazwie interfejsu.

Powyżej mamy przykład interfejsu o nazwie Clock, który ma jedną metodę secondsElapsedSince, która przyjmuje argument typu Date2 i zwraca wynik typu long mówiący o liczbie sekund, która minęła od czasu przekazanego w argumencie.

Wszystkie metody zawarte w interfejsie zawsze są publiczne więc w tym przypadku można ominąć słowo kluczowe public, nie jest potrzebne.

Poza zwykłymi metodami w interfejsie mogą się znajdować

  • metody domyślne,
  • metody statyczne,
  • stałe.

Więcej o metodach statycznych możesz przeczytać w artykule opisującym pierwszy program w języku Java. Nie jest to dla Ciebie nic nowego. Metody domyślne i stałe wymagają dodatkowego wyjaśnienia.

Metody domyślne

Istnieje możliwość zdefiniowania tak zwanych metod domyślnych. Metody te mogą mieć właściwą implementacje w ciele interfejsu. Metody takie poprzedzone są słowem kluczowym default jak w przykładzie poniżej

Klasy, które implementują interfejs mogą nadpisać metodę domyślną.

Wartości niezmienne i stałe

counter to zmienna. Do zmiennej counter możemy przypisać nową wartość:

Wartości niezmienne w odróżnieniu od zmiennych poprzedzamy słowem kluczowym final. Poniżej możesz zobaczyć przykład klasy z atrybutem, którego wartości nie możemy przypisać na nowo. Atrybuty tego typu możemy inicjalizować jak w przykładze poniżej: bezpośrednio bądź w ciele konstruktora.

Wartości niezmienne, podobnie jak metody, mogą być przypisane do instancji bądź klasy. Jeśli taka wartość przypisana jest do klasy mówimy wówczas o stałej. Jeśli chcemy aby stała była przypisana do klasy poprzedzamy ją słowem kluczowym static.

Do stałych wartość możemy przypisać wyłącznie raz – podczas inicjalizacji klasy. Zgodnie z konwencją nazewniczą stałe piszemy wielkimi literami.

W interfejsie powyżej mamy stałą, która pokazuje ile łap ma kot. Domyślnie wszystkie atrybuty interfejsu są stałymi publicznymi przypisanymi do interfejsu więc słowa kluczowe public static final mogą zostać pominięte.

Implementacja interfejsu

Sam interfejs nie jest zbyt wiele warty bez jego implementacji. Poniżej możesz zobaczyć przykładową, prostą implementację.

Klasa BrokenClock implementuje interfejs Clock. Zwróć uwagę na słowo kluczowe implements. Używamy go żeby pokazać że klasa BrockenClock implementuje interfejs Clock.

W języku Java jedna klasa może implementować wiele interfejsów. W takim przypadku klasa implementująca musi definiować metody wszystkich interfejsów, które implementuje 3.

Dziedziczenie interfejsów

Dziedziczenie to temat na osobny, obszerny artykuł. Jednak już teraz wspomnę, że interfejsy mogą dziedziczyć po innych interfejsach. Dziedziczenie oznaczane jest słowem kluczowym extends. Interfejs, który dziedziczy po innych interfejsach zawiera wszystkie metody z tych interfejsów.

W przykładzie powyżej klasa implementująca interfejs FatCat, musi zaimplementować 3 metody:

  • String getName(),
  • String getLasagnaRecipe(),
  • duble getWeight().

Interfejs znacznikowy

A czy możliwa jest sytuacja kiedy interfejs nie ma żadnej metody? Oczywiście, że tak. Mówimy wówczas o interfejsie znacznikowym. Jak sama nazwa wskazuje służy on do oznaczenia, danej klasy. Dzięki temu możesz przekazać zestaw dodatkowych informacji. Przykładem takiego interfejsu jest java.io.Serializable, którego używamy aby dać znać kompilatorowi, że dana klasa jest serializowalna (o serializacji przeczytasz w innym artykule).

Interfejs a typ obiektu

Każdy obiekt w jęzku Java może być przypisany do zmiennej określonego typu. W najprostrzym przypadku jest to jego klasa.

Interfejsy pozwalają na przypisane obiektu do zmiennej typu interfejsu. Wydaje się to trochę skomplikowane jednak mam nadzieję, że przykład poniżej pomoże w zrozumieniu tego tematu.

 

dziedziczenie

Instancję klasy Garfield możemy przypisać zarówno do zmiennej klasy Garfield jak i każdego z interfejsów, który ta klasa implementuje (bezpośrednio lub pośrednio). Chociaż w trakcie wykonania programu każdy z obiektów jest tego samego typu (instancja klasy Garfield), to w trakcie kompilacji sprawa wygląda trochę inaczej:

  • na obiekcie garfield możemy wykonać wszystkie metody udostępnione w klasie Garfield i interfejsach, które ta klasa implementuje:
    • getWeight(),
    • getName(),
    • getLasagnaReceipe().
  • na obiekcie fatCat możemy wykonać wyszstkie metody udostępnione w interfejsie FatCat i interfejsach po których dziedziczy:
    • getWeight(),
    • getName(),
    • getLasagnaReceipe().
  • na obiekcie cat możemy wykonać wyłącznie metody z interfejsu Cat:
    • getName().
  • na obiekcie lasagnaEater możemy wykonać wyłącznie metody z intefejsu LasagnaEater:
    • getLasagnaReceipe().

Zastosowania interfejsów

Do czego właściwie potrzebne są nam interfejsy? Czy nie jest to po prostu zestaw dodatkowych linijek kodu, które trzeba napisać i nic one nie wnoszą? Otóż nie.

Interfejsy w bardzo prosty sposób ułatwiają różnego rodzaju integrację różnych fragmentów kodu. Wyobraź sobie sytuację, w której Piotrek pisze program obliczający średnią temperaturę w każdym z województw. Współpracuje on z Kasią, która pisze program udostępniający aktualną temperaturę w danej miejscowości.

Aby Piotrek mógł napisać swój program musi skorzystać z programu Kasi. Musi się z nim zintegrować. Taką integrację ułatwiają właśnie interfejsy.

Piotrek z Kasią uzgadniają, że będą używali następującego interfejsu

Dzięki niemu Piotrek może pisać swój program równolegle z Kasią.

Co więcej może się okazać, że implementacja Kasi nie jest zbyt dokładna. Ania implementuje ten sam interfejs ale temperatury przez nią zwracane są dokładniejsze. Wówczas Piotrek w ogóle nie musi zmieniać swojego programu. Wystarczy, ze użyje innej implementacji interfejsu Thermometer dostarczonej przez Anię.

To właśnie jest kolejna zaleta interfejsów. Dzięki nim możemy pisać programy, które możemy w łatwiejszy sposób modyfikować. Interfejsy jasno oddzielają komponenty programu. Dzięki takiem podejściu komponenty można z łatwością wymieniać.

Zadanie

Napisz dwie klasy implementujące interfejs Computation. Niech jedna z implementacji przeprowadza operację dodawania, druga mnożenia.

Użyj obu implementacji do uzupełnienia programu poniżej

Program po uruchomieniu powinien zapytać użytkownika jaką operację chce wykonać, następnie pobrać dwa argumenty niezbędne do wykonania tej operacji. Ostatnią linijką powinien być wynik dodawania/mnożenia wyświetlony użytkownikowi. Przygotowałem też dla Ciebie przykładowe rozwiązanie zadania, pamiętaj jednak, że rozwiązując je samodzielnie nauczysz się najwięcej.

Materiały dodatkowe

Oczywiście nie wyczerpaliśmy tematu mimo sporej objętości artykułu. Zachęcam do samodzielnego pogłębiania wiedzy korzystając z materiałów dodatkowych. Specyfikacja Języka Java jest w języku angielskim.

Podsumowanie

Dzisiaj poruszyliśmy bardzo wiele zagadnień. Dowiedziałeś się o interfejsach, przeczytałeś o ich przeznaczeniu. Poznałeś też kilka nowych słów kluczowych w języku Java. Wystarczająca dawka wiedzy jak na jeden dzień 🙂

Mam nadzieję, że artykuł był dla Ciebie ciekawy, jeśli cokolwiek nie było zrozumiałe bądź wymaga dokładniejszego wyjaśnienia daj znać, na pewno pomogę.

Jak zwykle na koniec mam do Ciebie prośbę. Proszę podziel się artykułem ze swoimi znajomymi, zależy mi na dotarciu do jak największej liczby osób, które chcą nauczyć się programowania 🙂 Zapraszam także na SamouczekProgramisty na facebooku. Możesz też zapisać się do mojego newsletera.

Do następnego razu!

Newsletter

  Jeśli chcesz otrzymywać informacje o nowych artykułach na blogu prosto na Twój email, zapisz się 🙂

Zdjęcie dzięki uprzejmości FreeImages.com/Piotr Lewandowski.

  1. Wyjątkiem tutaj są tak zwane metody domyślne o których przeczytasz niżej.
  2. java.util.Date jest jednym z typów z bilblioteki standardowej służącym do przedstawiania czasu.
  3. Oczywiście jest od tego wyjątek, o klasach abstrakcyjnych przeczytasz w innym artykule.

26 Komentarze

  1. Krzysztof napisał(a):

    Hej, mógłbyś rzucić okiem, co jest nie tak. Wynik wychodzi mi 0.0.

  2. Krzysztof napisał(a):

    Dzięki, lame mistake 😉

  3. Kuba napisał(a):

    Hej,
    pomożesz?:)
    http://paste.ofcode.org/KKp8WT38EEZWTDDgr8HRjJ (źródło usunięte)
    Czemu wynik mam 0,0?

    Nie zmieniałem pozostałych klas z Twojego przykładu.

    Dzieki za pomoc;)

    • Marcin Pietraszek napisał(a):

      Cześć Kuba!

      Przeczytaj proszę ten artykuł: http://www.samouczekprogramisty.pl/porownywanie-obiektow-metody-equals-i-hashcode-w-jezyku-java – nie powinieneś porównywać instancji String przy pomocy operatora == (linijka 29 w Twoim przykładzie).

      Jeśli chodzi o wynik 0.0. Zwróć uwagę co dzieje się z liczbą pobraną od użytkownika w linijce 38. Co zwracasz 2 linijki niżej?

      • Kuba napisał(a):

        a jakaś wskazówka… nie czaję tego, wszystko oprócz return 0; powoduję, że program się nie kompiluje. Jakieś dalsze wskazówki?
        Wg mnie program powinien zwracać wynik. Wpisywałem już w linijkę 38 (w kodzie poniżej 44) różne kombinacje, ale jest dalej bez sukcesów:
        http://paste.ofcode.org/MFi8KdJr2LpNWdFKEusNZM

        Dzięki za dalsze wskazówki:)

        • Marcin Pietraszek napisał(a):

          Błąd musisz mieć w innym miejscu. Taka metoda (kopia Twojej z jednym return) działa poprawnie.

          Błędy kompilacji w swojej treści zawsze mają dokładną informację o tym co jest nie tak, postaraj się tę treść dokładnie przeanalizować.

          • Kuba napisał(a):

            tak sprawa się wyjaśniła, return należało zmienić też w pozostałych klasach:) Dzięki za pomoc!

  4. lukas napisał(a):

    Czy zapis Computation computation; oznacza że computation implementuje interfejs Computation?

  5. charlesw napisał(a):

    Cześć Marcin,
    Dlaczego wywołujemy zmienną computation typu Computation bez wywołania nowego obiektu? (Computation computation = new Computation();)

    • Marcin Pietraszek napisał(a):

      Siemasz 🙂

      Szczerze mówiąc nie zrozumiałem do końca Twojego pytania. Biorąc pod uwagę tę linijkę i przykładowe rozwiązanie – linijka się nie skompiluje.

      Nie możemy stworzyć instancji interfejsu. Możemy jednak utworzyć klasę anonimową ( przy okazji jej jedyną instancję):

      Więcej o klasach anonimowych możesz przeczytać w osobnym artykule.

      • charlesw napisał(a):

        Nie rozumiem po prostu dlaczego jest wywołana zmienna computation typu Computation.
        Znaczy się widzę, że w dalszej części kodu zmienna ta przyjmuje, w zależności od if/elsa metodę compute, która jest wspólna dla dodawania i mnozenia, po to, aby podać prawidłowy, dla wybranego przez użytkownika wcześniej rodzaju operacji, wynik.
        Chodzi mi o to, jaką szczególną właściwość ma ta zmienna, że później jest niezbędna do wykonania operacji?
        No bo tak: interfejs Computation posiada jedną metodę, którą implementują 2 klasy. Wywołanie zmiennej computation, która jest referencją do tego interfejsu powoduje, że tylko ona może w późniejszym kodzie spowodować zadzianie się dodawania, lub mnożenia?

        • charlesw napisał(a):

          Jestem na etapie poznawania dopiero co w ogóle programowania obiektowego, więc mogę mieć głupie pytania, natomiast Twoje ćwiczenia robię od początkowych lekcji i po prostu nie było jeszcze czegoś takiego 🙂
          Zdaję sobie sprawę, że mogę mieć sporę braki w wiedzy 😛

        • Marcin Pietraszek napisał(a):

          Cześć 🙂

          Więc tak, nie mówimy o „wywoływaniu zmiennej” a wywoływaniu metody na obiekcie. W naszym przypadku zmienna jest typu Computation (typ interfejsu) w trakcie kompilacji programu.

          W trakcie uruchomienia (ang. runtime) zmienna ta ma typ Multiplication lub Addition – jak zauważyłeś z zależności od if.

          Napisałeś „Chodzi mi o to, jaką szczególną właściwość ma ta zmienna, że później jest niezbędna do wykonania operacji?”. Otóż nie ma żadnej, szczególnej właściwości. Musi po prostu zostać zdefiniowana przed blokiem if/else. Jeśli byłaby zdefiniowana wewnątrz nie byłaby dostępna poza nim.

          „Wywołanie zmiennej computation, która jest referencją do tego interfejsu powoduje, że tylko ona może w późniejszym kodzie spowodować zadzianie się dodawania, lub mnożenia?”

          Jak napisałem wcześniej, nie wywołujesz zmiennej, a metodę compute na obiekcie computation, który w trakcie uruchamiania programu będzie typu Addition lub Multiplication.

          Interfejsy tutaj pozwalają uwspólnić dalszą część programu, chociaż możesz tam mieć dwa różne typy traktujesz je jako jeden odwołując się do nich przez interfejs.

  6. charlesw napisał(a):

    Czy w takim razie 4.linijkę kodu moglibyśmy czymś zastąpić, zakładając, że nie tworzymy żadnego nowego interfejsu, który jest implementowany przez 2 klasy (Multiplication i Addition)?

    • Marcin Pietraszek napisał(a):

      To nic innego jak utworznie nowej zmiennej, której jeszcze nie nadaliśmy wartości. Wartość tę nadajemy w kolejnych linijkach.

      Możemy to zrobić w jenej linii używając operatora ?:. Jednak moim zdaniem jest to mniej czytelne i do mnie bardziej przemawia „dłuższa” wersja.

      Powtórzę jeszcze raz, w linijce 4 do której się odnosisz nie tworzymy nowego interfejsu a jedynie zmienną, która jest typu Computation.

  7. PiotrekB napisał(a):

    Hej. Niestety w tłumaczeniu/opisie „Stałych” jest błąd. Przedstawiłeś stałe jako „wartości finalne” czyt. final, jednakże nie jest to zgodne z prawdą. Aby wartość była stała musi być również statyczna.

    public final int value = 10; // to nie jest stała, oznacza to jedynie, że wartość pola może być przypisana tylko raz. Natomiast jej wartość może być INNA dla każdej kolejnej instancji obiektu, więc jest zależna od argumentu przekazanego do konstruktora.
    Przykład:

    public static final VALUE = 10; // to jest Stała, pole jest jedno, dostępne dla wszystkich obiektów, niezależne od tworzonej instancji oraz wartość nie może zostać zmieniona.

    Nie możemy przypisać wartości stałej w konstruktorze, jedynym wyjściem oprócz bezpośredniego przypisania jest inicjalizator statyczny(?)

    Inaczej będzie błąd podczas kompilacji.

    Ostatnią konkluzją jest również to, że stosowanie konwencji nazewniczej dla stałych do wartości jedynie finalnych (upper case oraz „_”) może być mylące oraz prowadzić do błędów. (Oracle również powyższą konwencję zaleca stosować jedynie do static final ).

    Poza tym, świetny Blog bardzo konkretny i praktyczny! Wielkie dzięki, bo cały kurs przerabiam w ramach powtórki 🙂

    • Marcin Pietraszek napisał(a):

      Cześć Piotrek!

      Wielkie dzięki za zwrócenie uwagi – zmieniłem treść artykułu. Wprowadziłem teraz pojęcie „wartości niezmiennych” – final i stałych static final. Mam nadzieję, że różnica teraz będzie jasna.

      Jeśli w jakimkolwiek innym miejscu zauważysz jakąś nieścisłość, proszę daj znać 🙂

  8. Mikołaj napisał(a):

    Mam takie pytanie odnośnie „Interfejs a typ obiektu”. Zrozumiałe jest dla mnie tylko pierwsze, że tworzymy obiekt typu Garfirld z konstruktora Garfield. Garfield garfield = new Garfield(); Natomiast nie rozumiem dlaczego można Instancję klasy Garfield możemy przypisać również do każdego z interfejsów, który ta klasa implementuje (bezpośrednio lub pośrednio). Dlaczego tak można i jakie ma to konsekwencjie/ różnice w porównaniu ze standardowym stworzeniem obiektu- Garfield garfield = new Garfield();??

    Interfejs a typ obiektu.

    • Marcin Pietraszek napisał(a):

      Dodałem dodatkowe wyjaśnienie w akapicie, który jest dla Ciebie niejasny. Proszę daj znać czy teraz jest ok – jeśli nie postaram się rozbudować go jeszcze bardziej.

  9. Mikołaj napisał(a):

    Czyli każdy ze stworzonych obiektów jest obiektem klasy Garfield ale różnią się ilością posiadanych / zadziedziczonych metod.
    Np. Cat cat = new Garfield(); obiekt cat jest klasy Garfield ale ma tylko do wywoływania jedną metode getName();
    Zaś Garfield garfield = new Garfield(); obiekt garfield jest również obiektem klasy Garfield ale można powiedzieć pełną wersją nie okrojoną tzn. ma wszystkie trzy metody jakie posiada klasa Garfield?

    Bardzo dziękuję Ci za dodatkowe wyjaśnienie! 🙂

    • Marcin Pietraszek napisał(a):

      Tak, każdy obiekt jest instancją klasy Garfield. Nie, nie różnią się liczbą metod. Typ zmiennej, do której przypiszemy tę instancję określa jakie metody możemy na niej wykonać. Innymi słowy jeśli new Garfield() przypiszemy do zmiennej typu Cat to tylko metody z tego intefejsu będą dostępne. Jeśli ten sam obiekt przypiszemy do zmiennej typu Garfield to wtedy będziemy mogli wywołać tylko część metod.

      Więc mimo tego, że cat i garfield pokazują na ten sam obiekt na stercie, to kompilator na cat pozwoli nam wywołąć tylko częśc z metod dostępnych dla garfield.

      • Mikołaj napisał(a):

        Złych słów użyłem. powinno być: każdy z obiektów nie dziedziczy metod tylko ma możliwość wywołania metod. cat Cat cat = new Garfield(); może wywołać tylko getName() a garfield może wywołać wszystkie trzy Garfield garfield = new Garfield(); .
        czy teraz dobrze myslę?

        • Marcin Pietraszek napisał(a):

          Tak 🙂 Chociaż, muszę, dodać, że w przyszłości poznasz też mechanizm refleksji. Dodaje ona trochę możliwości, ale to dopiero za jakiś czas 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *