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 definiowania interfejsów używamy słowa kluczowego interface. Interfejsy, podobnie jak klasy, definiujemy w osobnych plikach. Nazwa pliku musi odpowiadać nazwie interfejsu.

public interface Clock {
    long secondsElapsedSince(LocalDateTime date);
}

Powyżej mamy przykład interfejsu o nazwie Clock, który ma jedną metodę secondsElapsedSince, która przyjmuje argument typu LocalDateTime2 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 domyślnie 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 prywatne,
  • 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.

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

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

public interface MicrowaveOven {
    void start();

    void setDuration(int durationInSeconds);

    boolean isFinished();

    void setPower(int power);

    default String getName() {
        return "MicrovaweOwen";
    }
}

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

Metody prywatne

Metody prywatne poprzedzone są słowem kluczowym private3. Metody prywatne, w odróżnieniu od pozostałych, mogą być wywołane wyłącznie w definicji interfejsu.

Z racji tego ograniczenia, metody prywatne w interfejsach mają sens wyłącznie w połączeniu z metodami domyślnymi. Proszę spójrz na przykład poniżej, w którym modyfikuję interfejs MicrowaveOven:

public interface MicrowaveOven {
    // removed for brevity
    default Duration getRecommendedDefrostTime(double foodWeightInGrams) {
        double frostRate = 0.8;
        int power = 300;
        return getRecommendedTime(power, frostRate, foodWeightInGrams);
    }

    default Duration getRecommendedWarmingUpTime(double foodWeightInGrams) {
        double frostRate = 0.2;
        int power = 700;
        return getRecommendedTime(power, frostRate, foodWeightInGrams);
    }

    private Duration getRecommendedTime(int power, double frostRate, double foodWeightInGrams) {
        double durationInMinutes = foodWeightInGrams / ((1 - frostRate) * power);
        long durationInSeconds = (long) (durationInMinutes * 60);
        return Duration.ofSeconds(durationInSeconds);
    }
}

Metody prywatne w interfejsach pozwalają na usunięcie kodu, który powtarza się w wielu miejscach. Ten powtarzający się kod jest wówczas zawarty w ciele metody prywatnej.

Więcej o dobrych praktykach w programowaniu możesz przeczytać w osobnym artykule opisującym DRY, KISS i YAGNI. Kilka uwag zebrałem też w artykule opisującym najczęściej popełniane błędy.

W przykładzie powyżej dwie domyślne metody getRecommendedDefrostTime i getRecommendedWarmingUpTime używają metody prywatnej getRecommendedTime, która pozwala na użycie „magicznego” wzoru na obliczanie zalecanej długości czasu pracy mikrofalówki. Bez tej metody wzór musiałby znaleźć się w obu metodach co powodowałoby duplikację kodu4.

Wartości niezmienne i stałe

int counter = 123;

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

counter = counter + 1;

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ładzie poniżej: bezpośrednio bądź w ciele konstruktora.

public class Calculator {
    public final double PI = 3.14;
    public final double SQRT_2;

    public Calculator() {
        SQRT_2 = Math.sqrt(2);
    }
}

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.

public interface Cat {
    int NUMBER_OF_PAWS = 4;
}

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ę.

public interface Clock {
    long secondsElapsedSince(LocalDateTime date);
}

public class BrokenClock implements Clock {
    public long secondsElapsedSince(LocalDateTime date) {
        return 300;
    }
}

Klasa BrokenClock implementuje interfejs Clock. Zwróć uwagę na słowo kluczowe implements. Używamy go żeby pokazać że klasa BrokenClock 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 implementuje5.

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.

public interface Cat {
    int NUMBER_OF_PAWS = 4;

    String getName();
}

public interface LasagnaEater {
    String getLasagnaRecipe();
}

public interface FatCat extends Cat, LasagnaEater {
    double getWeight();
}

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ęzyku Java może być przypisany do zmiennej określonego typu. W najprostszym 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.

public class Garfield implements FatCat {
    // implementacja metod
}

Diagram poniżej to tak zwany diagram klas. Więcej o tej notacji przeczytasz we wprowadzeniu do UML.

Przykład hierarchii dziedziczenia
Garfield garfield = new Garfield();
FatCat fatCat = new Garfield();
Cat cat = new Garfield();
LasagnaEater lasagnaEater = new Garfield();

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ć wszystkie 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 interfejsu 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

public interface Thermometer {
    double getCurrentTemperatureFor(String city);
}

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 takiemu podejściu komponenty można z łatwością wymieniać.

Interfejs czyli widok na obiekt

Postaram się pokazać Ci kolejny przykład. Ważne jest żeby zrozumieć koncept interfejsów. Są one bardzo ważne i często używane w codziennym programowaniu. Wyobraź sobie piekarnik. Piekarnik to obiekt. W piekarniku możesz upiec chleb, zrobić dobrą pieczeń czy upiec ciasteczka. Każde z tych dań wymaga innych ustawień piekarnika.

Inna temperatura, inny czas pieczenia, inny tryb. W programowaniu często chcemy ukryć takie szczegóły przez innymi klasami. Na zewnątrz w formie interfejsu wystawiamy jedynie dobrze zdefiniowane metody. Każda z tych metod może być umieszczona w osobnym interfejsie, który będzie implementowany przez obiekt piekarnika:

public interface BakingOven {
    void bakeCookies();
    void bakeBread();
}
public interface RoastingOven {
    void roastChicken();
}
public class Oven implements BakingOven, RoastingOven {

    private int time;
    private int temperature;

    @Override
    public void bakeBread() {
        temperature = 200;
        time = 120;
        turnOn();
    }

    @Override
    public void bakeCookies() {
        temperature = 180;
        time = 90;
        turnOn();
    }

    @Override
    public void roastChicken() {
        temperature = 130;
        time = 240;
        turnOn();
    }

    private void turnOn() {
        System.out.println(String.format("Start. Heat up to %s and work for %d minutes.", temperature, time));
    }

    public static void main(String[] args) {
        Oven oven = new Oven();
        BakingOven bakingOven = oven;
        RoastingOven roastingOven = oven;

        bakingOven.bakeBread();
        bakingOven.bakeCookies();
        roastingOven.roastChicken();
    }
}

Po uruchomieniu tego fragmentu kodu na konsoli pokaże się:

Start. Heat up to 200 and work for 120 minutes
Start. Heat up to 180 and work for 90 minutes.
Start. Heat up to 130 and work for 240 minutes.

Użyłem tutaj mechanizmu formatowania łańcuchów znaków. Jeśli chcesz przeczytać o tym więcej zachęcam do przeczytania osobnego artykułu na temat formatowania łańcuchów znaków w języku Java.

Interfejsy opisują spójny zakres funkcjonalności udostępniony przez dany obiekt. Metody, które są w nim zawarte powinny być ze sobą powiązane. Możesz porównać interfejsy do “widoku” na obiekt/klasę. Widząc obiekt przez pryzmat interfejsu możesz widzieć tylko podzbiór jego możliwości.

Zadanie

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

public interface Computation {
    double compute(double argument1, double argument2);
}

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

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        Computation computation;

        if (main.shouldMultiply()) {
            computation = new Multiplication(); // zaimplementuj brakującą klasę
        }
        else {
            computation = new Addition(); // zaimplementuj brakującą klasę
        }

        double argument1 = main.getArgument();
        double argument2 = main.getArgument();

        double result = computation.compute(argument1, argument2);
        System.out.println("Wynik: " + result);
    }

    private boolean shouldMultiply() {
        return false; // tutaj zapytaj użytkownika co chce zrobić (mnożenie czy dodawanie)
    }

    private double getArgument() {
        return 0; // tutaj pobierz liczbę od użytkownika
    }
}

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 poruszyłem bardzo wiele zagadnień. Po lekturze artykułu wiesz prawie wszystko interfejsach i ich przeznaczeniu. Teraz znasz 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ć, postaram się pomóc.

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 Samouczek Programisty na Facebooku. Możesz też zapisać się do samouczkowego newslettera żeby nie pomiąć żadnego nowego artykułu.

Do następnego razu!

  1. Wyjątkiem tutaj są tak zwane metody domyślne, o których przeczytasz niżej. 

  2. LocalDateTime jest jednym z typów z biblioteki standardowej służącym do przedstawiania daty/czasu. 

  3. Jest to tak zwany modyfikator dostępu, w oddzielnym artykule przeczytasz więcej o modyfikatorach dostępu

  4. Albo musiałby znaleźć się w innej domyślnej metodzie. Takie rozwiązanie powodowałoby rozszerzenie dostępnego interfejsu, co nie zawsze jest dobrym rozwiązaniem. 

  5. Jest od tego wyjątek, o klasach abstrakcyjnych przeczytasz w innym artykule. 

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Kategorie:

Ostatnia aktualizacja:

Autor: Marcin Pietraszek


Nie popełnia błędów tylko ten, kto nic nie robi ;). Bardzo możliwe, że znajdziesz błąd, literówkę, coś co wymaga poprawy. Jeśli chcesz możesz samodzielnie poprawić tę stronę. Jeśli nie chcesz poprawiać błędu, który udało Ci się znaleźć będę wdzięczny jeśli go zgłosisz. Z góry dziękuję!

Zostaw komentarz