Kategorie
java Junior Developer

Interfejs w Java – na rozmowę kwalifikacyjną

Dzisiaj temat idealny na rozmowę kwalifikacyjną, czyli Interfejs w Java. Dlaczego idealny? Ten temat po prostu bardzo często pojawia się w różnego rodzaju testach, jak również cieszy się dużym zainteresowaniem osób rekrutujących. Warto więc wiedzieć i pamiętać o paru rzeczach. A w artykule znajdziesz też fragment, którym możesz zaskoczyć nie jednego rekrutera.

Lecimy!

junior-hava-developer-handbook-what-to-know

Słowem wstępu, kiedyś było prościej

Przed Java 8 temat interfejsów był w zasadzie prosty. Interfejs nie miał implementacji metod. To była podstawa. To proste stwierdzenie właściwie pokazywało na dzień dobry, że dana osoba już coś chociaż kojarzy w temacie. Jeden mały plusik na rozmowie do przodu.

Wraz z przyjściem JDK 8, a konkretnie JEP 126: Lambda Expressions & Virtual Extension Methods, powyższa odpowiedź zmieniła zupełnie oblicze. Z poprawnej, stała się po prostu technicznie błędna.

I tutaj uwaga, bo często na rozmowach trafia się pytanie, czym różni się interfejs od klasy abstrakcyjnej. I już nie można powiedzieć, że jedno nie ma implementacji metod, a drugie może mieć. Bo patrząc wyżej to nie jest już prawda.

Przyjrzyjmy się zatem szczegółom.

Krótka definicja

Jeśli chodzi o jakąś ogólną definicję ze świata biznesu, czym jest interfejs, to najbardziej odpowiednim wydaje mi się stwierdzenie, że jest to pewien kontrakt. Można powiedzieć, że jest to pewna umowa gwarantująca zestaw zachowań (czyli deklaracje metod).

Technicznie, wg specyfikacji Java, interfejs jest po prostu typem referencyjnym, czyli takim, do którego można utworzyć referencję.

Kilka rzeczy, o których trzeba pamiętać

Interfejs nie zawiera zmiennych instancyjnych. Wynika to z tego, że reprezentuje on pewną abstrakcję, a zmienne w obiektach konkretnych klas są już czymś bardzo specyficznym.

Może jednak zawierać deklaracje metod, deklaracje stałych, deklaracje klas oraz deklaracje deklaracje innych interfejsów.

Skupmy się tutaj na chwilę na deklaracji metod. To one m. in. spowodowały, że temat, interfejs a klasa abstrakcyjna w Java, ma nieco bardziej subtelny wymiar.

Na początek to, co był dostępne jeszcze przed JDK 8. Czyli deklaracje metod abstrakcyjnych. Za przykład niech posłużą nam zwierzęta. Czyli:

package pl.itbrains.samples;

public interface Animal {
  String type();
}

Zakładamy, że każde zwierzę ma pewien tekstowy typ zwracany przez metodę type(). Co wiemy o metodzie type()?

Domyślnie jest to metoda abstrakcyjna metoda publiczna. Możemy dodać oczywiście przed nią public abstract, ale to nic nie zmieni. Większość IDE podpowie nam zresztą, że jest to zupełnie niepotrzebne. I tak z pragmatycznego punktu widzenia, dodawanie tego nic nie wnosi.

Dalej wiemy, że ostatecznie nieabstrakcyjna klasa, która implementuje ten interfejs musi zapewnić implementację tej metody. Tutaj będzie to klasa Dog:

package pl.itbrains.samples;

public class Dog implements Animal {
  @Override
  public String type() {
    return "DOG";
  }
}

Przeprowadzamy oczywiście test:

  @Test
  public void interfaceMethodImplementationTest() {
    Dog dog = new Dog();
    assertEquals("DOG", dog.type());
  }

I widzimy, że zgodnie z założeniem nasza metoda została poprawnie wywołana.

Przejdźmy do tych bardziej subtelnych zagadnień.

Od JDK 8 interfejsy w Javie mogą zawierać metody oznaczone modyfikatorem default, które zawierają domyślną implementację metody. Wygląda to następująco:

package pl.itbrains.samples;

public interface Car {
  default String name() {
    return "I'm a car!";
  }
}

Jeśli chcielibyśmy skompilować ten kod we wcześniejszej wersji, to dostalibyśmy błąd. Warto o tym pamiętać. Cały czas istnieją projekty, które chodzą wyłącznie na starszych wersjach Javy, a ich migracja do nowszych jest często zbyt kosztowna. To takie uroki obsługi mocno legacy systems.

W ramach testu utwórzmy sobie klasę anonimową implementującą interfejs Car i skorzystajmy z domyślnej metody. Przykład poniżej:

  @Test
  public void defaultMethodTest() {
    Car car = new Car() {};
    assertEquals("I'm a car!", car.name());
  }

Co widać powyżej. W linii 3 utworzyliśmy sobie obiekt klasy anonimowej implementującej interfejs Car. Widzimy, że nasza klasa anonimowa nie dostarcza własnej implementacji metody name(), a mimo wszystko test przechodzi z wynikiem pozytywnym. Metoda domyślna zadziałała.

Właściwie dlaczego wprowadzono metody default?

Wróćmy na chwilę do JDK 8 i JEP 126: Lambda Expressions & Virtual Extension Methods. Skąd wziął się pomysł na metody default?

Popatrzmy na przykład na interfejs java.lang.Iterable, który implementowany jest przez interfejs kolekcji, czyli java.util.Collection, dalej listy w postaci java.util.List.

W ramach prac nad lambdą dodano do java.lang.Iterable metodę forEach(). Co by to oznaczało przed JDK 8. Ot tyle, że każda klasa implementująca interfejs java.util.List musiałaby implementować samodzielnie metodę forEach(). I oczywiście nie chodzi tu tylko o klasy z JDK, a również o całą rzeszę klas w różnego rodzaju bibliotekach i różnych projektach.

Byłaby to w rzeczy samej duża niekompatybilność wsteczna między wersjami Javy. Stąd powstał koncept metod domyślnych w interfejsach, żeby tę kompatybilność zapewnić.

W tym momencie interfejs java.lang.Iterable dostarcza domyślną implementację tej metody. Dzięki temu nie musimy się martwić własną implementacją.

I tak zupełnie szczerze, jeśli na rozmowie kwalifikacyjnej będziesz miał pytania o interfejsy i różne kruczki z nimi związane, to właśnie takim opisem możesz zabłysnąć i wykazać się ponadprzeciętną wiedzą w temacie. Z dużym prawdopodobieństwem Twoja wiedza zaskoczy samego rekrutera. 👍

Co jeszcze można umieścić w interfejsie?

Dodajmy więc do naszego interfejsu Car kilka rzeczy:

  • stałą CONSTANT,
  • metodę prywatną sayConstant().
  • drugą metodę domyślną sayANumber().
  • metodę statyczną sayHello(),
  • metodę statyczną sayAnotherHell(),
  • prywatną metodę statyczną sayLoudHello().

Tym samym wyczerpiemy elementy, które mogą znaleźć się w interfejsie w Java. Wygląda to tak:

package pl.itbrains.samples;

public interface Car {
  int CONSTANT = 17;

  default String name() {
    return "I'm a car!";
  }

  default int sayANumber() {
    return sayConstant();
  }

  static String sayHello() {
    return "HELLO!";
  }

  static String sayAnotherHello() {
    return sayLoudHello();
  }

  private static String sayLoudHello() {
    return "LOUD HELLO!";
  }

  private int sayConstant() {
    return CONSTANT;
  }
}

Stała CONSTANT domyślnie jest public static final. Jest to niezmienny kontrakt dla tej wartości.

Metoda prywatna sayConstant() dla przykładu zwraca wartość tej stałej. I jak widzimy jest ona wywoływana z domyślnej implementacji metody sayANumber().

Z kolei sayHello(), jako metoda statyczna, jest domyślnie public. Podobnie sayAnotherHello(), która wywołuje prywatną metodę statyczną sayLoudHello().

Jaki jest w ogóle sens tworzenia metod prywatnych w interfejsie? Wiemy przecież, że metoda prywatna może być wywołana tylko w tym interfejsie, a co za tym idzie jedynym miejscem jej wywołania może być implementacja domyślna innej metody (a w przypadku prywatnej metody statycznej inna metoda statyczna). Właściwie jedynym sensownym zastosowaniem metod prywatnych wprowadzonych w JDK 9 jest odseparowanie fragmentów logiki, której nie chcemy pokazywać na zewnątrz. Czyli tak po ludzku, pokazujemy metodę domyślną, która wywołuje pod spodem np. jedną z metod prywatnych. Tak robi właśnie sayANumber() w przykładzie.

I sprawdźmy działanie tych elementów w nieidealnym teście:

  @Test
  public void otherStuffTest() {
    assertEquals(17, Car.CONSTANT);
    assertEquals("HELLO!", Car.sayHello());
    assertEquals("LOUD HELLO!", Car.sayAnotherHello());
    Car car = new Car() {};
    assertEquals(17, car.sayANumber());
  }

Trochę o hierarchiach

Interfejsy mogą same rozszerzać inne interfejsy. I nie są ograniczone do rozszerzania tylko jednego interfejsu, mogą rozszerzać ich wiele.

Ostatnio oglądałem Le Mans ’66, także przykład będzie inspirowany filmem. Zobaczmy kilka interfejsów:

public interface Fast {
}
public interface EvenFaster {
}
public interface Ferrari extends Car, Fast {
}
public interface Ford extends Car, Fast, EvenFaster {
}

Widzimy tutaj dwa interfejsy Fast i EvenFaster, tzw. marker interface. Są to interfejsy, które nie mają żadnych deklaracji, są tylko znacznikami. Ferrari i Ford dziedziczą z kolei po interfejsie Car i dodatkowych interfejsach.

Dodajmy jeszcze konkretnego zwycięzcę rywalizacji w Le Mans ’66, czyli Forda GT40:

public class FordGT40 implements Ford {
  public String fordStuffs() {
    return "FORD STUFFS";
  }
}

Dzięki temu, że mamy pewną hierarchię, to możemy korzystać z naszego Forda GT40 w różnych kontekstach:

FordGT40 fordGT40 = new FordGT40();
Car car = new FordGT40();
Fast fast = new FordGT40();
EvenFaster evenFaster = new FordGT40();

W każdym kontekście będziemy mieli dostęp tylko do metod z danego kontekstu. Czyli na samym dole hierarchii mamy klasę FordGT40 i w niej możemy wywołać wszystkie dostępne metody z hierarchii interfejsów.

fordGT40.fordStuffs();
fordGT40.name();

Ale również tę klasę możemy potraktować nieco bardziej abstrakcyjnie. I korzystać z niej poprzez referencję do Car, Fast lub EvenFaster. I wówczas w tych kontekstach już nie będziemy mieli dostępu np. do metody fordStuffs().

Creme de la Creme, czyli podsumowanie dla Ciebie

Lista rzeczy do zapamiętania dla Ciebie:

  • Interfejs to kontrakt.
  • Od JDK 8 ten kontrakt może zawierać metody z implementacją domyślną oznaczone poprzez słowo default.
  • Interfejs może być implementowany przez dowolną klasę.
  • Domyślnie metody interfejsów są public i abstract.
  • Stałe w interfejsach są public, static i final.
  • W interfejsie może pojawić się metoda statyczna (również prywatna), jak również zwykła metoda prywatna. Tylko wtedy zadaj sobie pytanie – po co?
  • Interfejsu mogą dziedziczyć po wielu innych interfejsach, lecz pamiętaj, że co za dużo, to niezdrowo.
  • Klasa nieabstrakcyjna implementująca interfejs musi dostarczyć implementację jego metod.
  • Klasa abstrakcyjna może implementować interfejs, ale nie musi dostarczać implementacji metod (ale, jak wyżej, jej podklasa nieabstrakcyjna wówczas musi to zrobić).
  • Interfejsy pozwalają na spojrzenie na obiekt z różnych perspektyw.

Szczera rada na koniec: interfejsy najlepiej sprawdzają się jako kontrakty zachowań. Dlatego nie wstawiaj do nich na siłę implementacji domyślnych metod i innych elementów. Warto wiedzieć, że można. Ale to, że można, nie znaczy, że należy tak robić.

Dobry artykuł? Będę wdzięczny za komentarze i share 👍👍👍

Czytałeś już materiał o klasie String? Nie. No to łap tutaj: Klasa String dla początkujących w Java

Zainteresowany rozwojem kariery Junior Java Developera? Fajnie. Poniższa seria jest dla Ciebie.

W serii Junior Developer ukazały się następujące wpisy:

  1. Junior Developer w 2020 roku
  2. Top 10 umiejętności Junior Java Developera
  3. Junior Developer a Regular
  4. Co tak naprawdę sprawdza rozmowa kwalifikacyjna na stanowisko Junior Developer?
  5. Junior Developer 2020 – Podsumowanie

5 6 votes
Article Rating
Subscribe
Powiadom o
guest
6 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Adrian
Adrian
3 lat temu

Dzięki za artykuł 🙂 konkretna wiedza z cennymi przykładami. Najbardziej cenię w Twoim wpisie aktualność i kompletność informacji, bo zauważyłem, że czasami w Internecie jest miszmasz i niekiedy trudno dojść do ładu. Swoją drogą.. interfejsy strasznie się pokomplikowały od pierwotnej wersji. A jak to wygląda w praktyce w dużych projektach? Jak często umieszcza się implementacje i inne elemeny w interfejsach? Są one wgl używane czy raczej jest to ostateczność?

Przemek
Przemek
2 lat temu

Hej, najlepszy materiał o interfejsach jaki widziałem w internecie. Potrzebowałem przypomnienia wiedzy i nie dość, że właśnie to dostałem, to jeszcze rozszerzyło to moja wiedzę.

Krystian
Krystian
1 rok temu

Dzięki, bardzo dobry artykuł.

6
0
Jestem ciekawy, co myślisz. Dodaj komentarz na dole!x