Interfejsy w Java
Standaryzacja interakcji
Interfejsy definiują i standaryzują sposoby, w jakie ludzie lub systemy porozumiewają się ze sobą. Na przykład przyciski sterujące radia to interfejs między użytkownikiem radia a komponentami wewnętrznymi radia. Przyciski umożliwiają wykonanie ograniczonej liczby operacji (wybór stacji, regulacja głośności, wybór pasma), a różne radia w różny sposób implementują poszczególne elementy sterujące (przyciski, pokrętła, dotyk, polecenia głosowe). Interfejs definiuje, jakie operacje na nim może wykonać użytkownik, ale nie określa, jak te operacje będą przeprowadzane.
Wróćmy do analogii z samochodem z podrozdziału 1.5. Interfejs „podstawowych możliwości sterowania” składa się z kierownicy, pedału gazu (przyspieszenia) i pedału hamulca. Dzięki nim kierowca informuje auto, co ma robić. Gdy wiemy, jak wykorzystać interfejs do skręcania, przyspieszania i hamowania, jesteśmy w stanie prowadzić różne rodzaje samochodów, choć ich producenci w różny sposób implementują szczegóły systemu. Istnieje na przykład wiele rodzajów systemów hamowania: hamulce tarczowe, hamulce bębnowe, hamulce ręczne, hamulce hydrauliczne, hamulce pneumatyczne itp. Naciskając pedał hamulca, nie zastanawiasz się nad stosowanym systemem hamowania — liczy się tylko to, że samochód po naciśnięciu pedału zmniejsza prędkość.
Obiekty programowe komunikują się poprzez interfejsy
Obiekty oprogramowanie również komunikują się poprzez interfejsy. Interfejs Javy opisuje zbiór metod, które mogą zostać wywołane dla obiektu, aby zwrócił pewne informacje lub wykonał określone zadanie. W następnym przykładzie wprowadzimy interfejs o nazwie Payable, aby opisać funkcjonalność obiektu, który musi być „zdolny do opłacenia”, czyli zaoferować metodę określającą kwotę płatności. Deklaracja interfejsu rozpoczyna się słowem kluczowym interface i zawiera jedynie stałe i metody abstrakcyjne. W odróżnieniu od klas składowe interfejsu muszą być publiczne i interfejs nie może określać szczegółów implementacyjnych takich jak konkretne deklaracje metod lub zmiennych instancji1. Wszystkie metody zadeklarowane w interfejsie są niejawnie publicznymi metodami abstrakcyjnymi, a wszystkie pola niejawnie publicznymi, statycznymi i finalnymi. Korzystanie z interfejsu Aby użyć interfejsu, klasa konkretna stosuje słowo kluczowe implements do wskazania implementowanego interfejsu, a następnie deklaruje wszystkie metody interfejsu o dokładnie takiej samej sygnaturze jak podana w interfejsie. Słowo kluczowe implements wraz z nazwą interfejsu pojawia się na końcu pierwszego wiersza deklaracji klasy w postaci:
lub
W tym zapisie NazwaInterfejsu to lista oddzielonych przecinkami nazw implementowanych interfejsów. Implementacja interfejsu to jak podpisanie z kompilatorem kontraktu o treści „zadeklaruję wszystkie metody abstrakcyjne wskazane w interfejsie lub zadeklaruję
klasę jako abstrakcyjną”.
Typowy błąd programistyczny
W konkretnej klasie implementującej interfejs, jeśli zabraknie implementacji choćby jednej z metod interfejsu, spowoduje to błąd kompilacji wskazujący, że taka klasa musi być zadeklarowana jako abstrakcyjna.
Powiązanie różnorodnych typów
Interfejs stosuje się często do powiązania różnych klas — czyli klas niepowiązanych żadną hierarchią — które współdzielą te same metody lub stałe. Dzięki temu obiekty niepowiązanych klas mogą być przetwarzane polimorficzne — obiekty klas implementują ten sam interfejs, więc reagują na te same wywołania metod (dotyczy to metod interfejsu). Można zdefiniować interfejs opisujący pożądane funkcjonalności, a następnie zaimplementować go w każdej klasie wymagającej takiej funkcjonalności. Na przykład w aplikacji do obsługi płatności zaimplementujemy interfejs Payable we wszystkich klasach związanych z wyliczaniem
kwoty do zapłaty (czyli w klasach Employee i Invoice).
Interfejsy kontra klasy abstrakcyjne
Interfejs powinien być stosowany w miejscu klasy abstrakcyjnej, gdy nie istnieje domyślna implementacja do odziedziczenia — nie istnieją pola ani implementacja konkretnych metod. Podobnie jak publiczne klasy abstrakcyjne, także interfejsy są typami publicznymi. Interfejs publiczny, tak jak klasa publiczna, musi się znajdować w pliku o tej samej nazwie co interfejs i rozszerzeniu .java.
Obserwacja z poziomu inżynierii oprogramowania
Wielu programistów twierdzi, że interfejsy są znacznie istotniejszą technologią modelowania niż klasy, w szczególności po wprowadzeniu przez Javę SE 8 kilku usprawnień.
Interfejsy znacznikowe
Interfejsy znacznikowe to puste interfejsy, które nie zawierają żadnych metod ani stałych. Służą jedynie dodaniu związku „jest”. Na przykład Java obsługuje mechanizm nazywany serializacją obiektów, który konwertuje obiekty do ich reprezentacji bajtowej, a następnie pozwala przywrócić z tej reprezentacji oryginalny obiekt dzięki klasom ObjectOutputStream i ObjectInputStream. Aby umożliwić działanie tego mechanizmu z własnymi obiektami, wystarczy oznaczyć je jako Serializable, dodając implements Serializable na końcu pierwszego wiersza deklarującego klasę. W ten sposób wszystkie obiekty klasy otrzymają związek „jest”
z Serializable — to wszystko, czego wymaga się od prostej implementacji serializacji obiektów.
Tworzenie hierarchii Payable
Aby zbudować aplikację, która potrafi określić płatności dla pracowników i faktur, utwórzmy najpierw interfejs Payable zawierający metodę getPaymentAmount zwracającą kwotę typu double, którą obiekt musi zapłacić obiektowi implementującemu ten interfejs. Metoda getPaymentAmount to uogólniona wersja metody earnings z hierarchii Employee. Metoda earnings dotyczyła płatności na rzecz pracowników, natomiast metoda getPaymentAmount jest bardziej ogólna i może dotyczyć dowolnych płatności. Po zadeklarowaniu interfejsu Payable wprowadzimy klasę Invoice, która go implementuje. Dodatkowo zmodyfikujemy klasę Employee, aby również implementowała ten interfejs.
Klasy Invoice i Employee reprezentują elementy, dla których firma musi wyliczać kwotę płatności. Obie klasy implementują interfejs Payable, więc program może wywoływać metodę getPaymentAmount niezależnie od tego, czy jest to obiekt Invoice czy obiekt Employee. Jak się wkrótce przekonasz, dzięki temu możliwe jest polimorficzne przetwarzanie przez ten sam kod aplikacji obiektów Invoice i Employee.
Dobra praktyka programistyczna
Deklarując metodę interfejsu, wybierz nazwę metody, która opisuje cele metody w możliwie ogólny sposób, ponieważ metodę tę może implementować wiele niepowiązanych ze sobą klas.
Diagram UML zawierający interfejs
Diagram klas UML z rysunku 10.10 przedstawia interfejs i hierarchię klas stosowaną przez nową wersję aplikacji. Hierarchia rozpoczyna się od interfejsu Payable. UML rozróżnia interfejsy i klasy, umieszczając słowo „interface” wraz z cudzysłowami («i») powyżej nazwy interfejsu. UML wyraża związek między klasą i interfejsem poprzez związek typu realizacja. Mówi się, że klasa realizuje (implementuje) metody interfejsu. Diagram klas przedstawia realizację jako strzałkę z linią przerywaną z grotem skierowanym w stronę interfejsu, który klasa implementuje. Diagram z rysunku 10.10 pokazuje, że klasy Invoice i Employee realizują interfejs Payable. Podobnie jak w diagramie klas z rysunku 10.2, klasa Employee jest zapisana kursywą, co wskazuje, że jest to klasa abstrakcyjna. Konkretna klasa SalariedEmployee rozszerza klasę Employee, dziedzicząc również jej związek realizacyjny z interfejsem Payable.
Rysunek 10.10. Diagram klas UML dla hierarchii Payable
2 // Deklaracja interfejsu Payable
3
4 public interface Payable {
5 public abstract double getPaymentAmount(); // Brak implementacji
6 }
Rysunek 10.11. Deklaracja interfejsu Payable
Interfejs Payable
Deklaracja interfejsu Payable rozpoczyna się w wierszu 4. z rysunku 10.11 od słowa kluczowego interface. Interfejs zawiera publiczną metodę abstrakcyjną o nazwie getPaymentAmount. Metody interfejsu są domyślnie publiczne i abstrakcyjne, więc te modyfikatory są tak naprawdę zbędne. Interfejs Payable posiada tylko jedną metodę, ale interfejsy mogą mieć ich dowolną liczbę. Metoda getPaymentAmount nie przyjmuje parametrów, ale metody interfejsu mogą mieć parametry. Interfejsy mogą również zawierać stałe typu final static.
Dobra praktyka programistyczna
Używaj modyfikatorów public i abstract w metodach interfejsu w sposób jawny, aby wskazać intencję. Java SE 8 i Java SE 9 dopuszczają w interfejsie także inne rodzaje metod.
Klasa Invoice
Klasa Invoice (rysunek 10.12) reprezentuje pojedynczą fakturę, która zawiera informacje rozliczeniowe dotyczące tylko jednego rodzaju części. Klasa deklaruje prywatne zmienne instancji partNumber, partDescription, quantity i pricePerItem (wiersze od 5. do 8.), które dotyczą numeru części, opisu części, liczby sztuk i ceny za sztukę. Klasa zawiera również konstruktor (wiersze od 11. do
26.), metody pobierające (wiersze od 29. do 38.) oraz metodę toString (wiersze od 41. do 46.), która zwraca tekstową reprezentację obiektu Invoice.
2 // Klasa Invoice implementująca interfejs Payable
3
4 public class Invoice implements Payable {
5 private final String partNumber;
6 private final String partDescription;
7 private final int quantity;
8 private final double pricePerItem;
9
10 // Konstruktor
11 public Invoice(String partNumber, String partDescription, int quantity,
12 double pricePerItem) {
13 if (quantity < 0) { // Walidacja liczby sztuk
14 throw new IllegalArgumentException("Sztuki muszą być >= 0");
15 }
16
17 if (pricePerItem < 0.0) { // Walidacja ceny za sztukę
18 throw new IllegalArgumentException(
19 "Cena za sztukę musi być >= 0");
20 }
21
22 this.quantity = quantity;
23 this.partNumber = partNumber;
24 this.partDescription = partDescription;
25 this.pricePerItem = pricePerItem;
26 }
27
28 // Zwróć numer części
29 public String getPartNumber() {return partNumber;}
30
31 // Zwróć opis
32 public String getPartDescription() {return partDescription;}
33
34 // Zwróć liczbę sztuk
35 public int getQuantity() {return quantity;}
36
37 // Zwróć cenę za sztukę
38 public double getPricePerItem() {return pricePerItem;}
39
40 // Zwróć tekstową reprezentację obiektu Invoice
41 @Override
42 public String toString() {
43 return String.format("%s: %n%s: %s (%s) %n%s: %d %n%s: %,.2f zł",
44 "faktura", "numer części", getPartNumber(), getPartDescription(),
45 "liczba sztuk", getQuantity(), "cena za sztukę", getPricePerItem());
46 }
47
48 // Metoda wymagana, aby spełnić kontrakt dotyczący interfejsu Payable
49 @Override
50 public double getPaymentAmount() {
51 return getQuantity() * getPricePerItem(); // Oblicz łączną kwotę
52 }
53 }
Rysunek 10.12. Klasa Invoice implementująca interfejs Payable
Wiersz 4. wskazuje, że klasa Invoice implementuje interfejs Payable. Podobnie jak inne klasy, także klasa Invoice niejawnie rozszerza klasę Object. Klasa Invoice implementuje jedyną abstrakcyjną metodę interfejsu Payable — metodę getPaymentAmount zadeklarowaną w wierszach od 49. do 52. Metoda ta wylicza i zwraca łączną kwotę do zapłaty wynikającą z faktury, mnożąc wartości zmiennych quantity i pricePerItem (pobrane dzięki metodom pobierającym). Metoda spełnia wymagania implementacyjne interfejsu Payable — wypełniliśmy kontrakt zawarty z kompilatorem.
Klasa może rozszerzać tylko jedną inną klasę, ale może implementować dowolną liczbę interfejsów
Java nie dopuszcza do dziedziczenia po więcej niż jednej klasie przez podklasę, ale umożliwia dziedziczenie po jednej klasie nadrzędnej i implementację tylu interfejsów, ile to konieczne. Aby zaimplementować więcej niż jeden interfejs, należy umieścić listę oddzielonych przecinkami interfejsów tuż po słowie kluczowym implements. Oto przykład:
DrugiInterfejs, ...
Obserwacja z poziomu inżynierii oprogramowania
Wszystkie obiekty klasy implementujące wiele interfejsów mają związek „jest” z każdym implementowanym interfejsem. Klasa ArrayList to jedna z licznych klas Javy, które implementują wiele interfejsów. Przykładowo klasa ArrayList implementuje interfejs
Iterable, który pozwala stosować dla elementów tablicy rozszerzoną wersję instrukcji for. Klasa ta implementuje również interfejs List deklarujący podstawowe metody (takie jak add, remove i contains), które może wywoływać na dowolnym obiekcie reprezentującym listę elementów.
Modyfikacja klasy Employee w celu implementacji interfejsu Payable
Zmodyfikujmy teraz klasę Employee (rysunek 10.13), aby implementowała interfejs Payable. Deklaracja klasy jest w zasadzie taka sama jak klasy z rysunku 10.4, poza dwoma wyjątkami:
- wiersz 4. z rysunku 10.13 wskazuje, że klasa Employee implementuje interfejs Payable;
- wiersz 38. implementuje metodę getPaymentAmount interfejsu Payable.
2 // Abstrakcyjna klasa Employee implementująca interfejs Payable
3
4 public abstract class Employee implements Payable {
5 private final String firstName;
6 private final String lastName;
7 private final String socialSecurityNumber;
8
9 // Konstruktor
10 public Employee(String firstName, String lastName,
11 String socialSecurityNumber) {
12 this.firstName = firstName;
13 this.lastName = lastName;
14 this.socialSecurityNumber = socialSecurityNumber;
15 }
16
17 // Zwróć imię
18 public String getFirstName() {return firstName;}
19
20 // Zwróć nazwisko
21 public String getLastName() {return lastName;}
22
23 // Zwróć numer ubezpieczenia
24 public String getSocialSecurityNumber() {return socialSecurityNumber;}
25
26 // Zwróć tekstową reprezentację obiektu Employee
27 @Override
28 public String toString() {
29 return String.format("%s %s%nnumer ubezpieczenia: %s",
30 getFirstName(), getLastName(), getSocialSecurityNumber());
31 }
32
33 // Metoda abstrakcyjna musi być skonkretyzowana w podklasie
34 public abstract double earnings(); // Nie ma implementacji
35
36 // Implementacja getPaymentAmount w tym miejscu umożliwia stosowanie całej
37 // hierarchii klas Employee w aplikacji przetwarzającej Payable
38 public double getPaymentAmount() {return earnings();}
39 }
Rysunek 10.13. Abstrakcyjna klasa Employee implementująca interfejs Payable
Zwróć uwagę, że metoda getPaymentAmount po prostu wywołuje abstrakcyjną metodę earnings klasy Employee. W momencie wykonywania programu, gdy zostanie uruchomiona metoda getPaymentAmount, wywoła ona konkretną metodę earnings podklasy, która „wie”, jak obliczyć kwotę do wypłaty dla danego pracownika.
Podklasy klasy Employee i interfejs Payable
Gdy klasa implementuje interfejs, dochodzi do takiego samego związku „jest” jak w przypadku dziedziczenia. Klasa Employee implementuje Payable, dlatego możemy powiedzieć, że Employee jest Payable, więc każda podklasa Employee również jest Payable. Jeśli zatem uaktualnimy hierarchię klas z podrozdziału 10.5 nową klasą nadrzędną Employee z rysunku 10.13, obiekty SalariedEmployee, HourlyEmployee, CommissionEmployee i BasePlusCommissionEmployee także będą obiektami Payable. Podobnie jak przypisywaliśmy referencję do podklasy SalariedEmployee do zmiennej typu Employee, tak możemy przypisać obiekt SalariedEmployee (lub dowolny inny obiekt dziedziczący po Employee) do zmiennej typu Payable. Klasa Invoice również implementuje interfejs Payable, więc jest Payable, czyli obiekt Invoice też możemy umieścić w zmiennej typu Payable.
Obserwacja z poziomu inżynierii oprogramowania
Dziedziczenie i interfejsy są bardzo podobne w kwestii implementacji związku „jest”. Obiekt klasy implementujący dowolny interfejs może być traktowany jako obiekt tego interfejsu. Obiekt dowolnej podklasy klasy implementującej interfejs również może być traktowany jako obiekt tego interfejsu.
Obserwacja z poziomu inżynierii oprogramowania
Związek „jest” istniejący między podklasami i klasami nadrzędnymi oraz między interfejsami i klasami je implementującymi dotyczy również przekazywania obiektów do metod. Gdy parametr metody dotyczy klasy nadrzędnej lub interfejsu, metoda w sposób polimorficzny użyje wersji odpowiedniej dla obiektu przekazanego jako argument.
Obserwacja z poziomu inżynierii oprogramowania
Wykorzystując referencję klasy nadrzędnej, możemy polimorficznie wywołać dowolną metodę zadeklarowaną w klasie nadrzędnej i jej klasach nadrzędnych (np. klasie Object). Korzystając z referencji do interfejsu, możemy polimorficznie wywołać dowolną metodę zadeklarowaną w interfejsie, jego interfejsach nadrzędnych (interfejs może rozszerzać inny interfejs) lub klasie Object — każda klasa dziedziczy po klasie Object, więc jej metody są zawsze dostępne do wywołania w referencjach do interfejsu.
Użycie interfejsu Payable do polimorficznego przetwarzania klas Invoice i Employee
Program PayableInterfaceTest (rysunek 10.14) pokazuje, że interfejs Payable może posłużyć do jednoczesnego przetwarzania polimorficznego obiektów klas Invoice i Employee. Wiersze od 7. do 12. deklarują i inicjalizują czteroelementową tablicę
payableObjects. Wiersze 8. i 9. umieszczają w dwóch pierwszych elementach tablicy obiekty Invoice. Następnie wiersze 10. i 11. umieszczają w dwóch ostatnich elementach tablicy obiekty SalariedEmployee. Wszystko działa bez błędów po inicjalizacji obiektów Invoice i SalariedEmployee, ponieważ Invoice jest Payable, a SalariedEmployee jest Employee, który jest Payable.
2 // Program testujący interfejs Payable umożliwia polimorficzne przetwarzanie obiektów
3 // Invoice i Employee
4 public class PayableInterfaceTest {
5 public static void main(String[] args) {
6 // Utwórz czteroelementową tablicę obiektów Payable
7 Payable[] payableObjects = new Payable[] {
8 new Invoice("01234", "siedzenie", 2, 375.00),
9 new Invoice("56789", "opona", 4, 79.95),
10 new SalariedEmployee("Anna", "Nowak", "111-11-1111", 800.00),
11 new SalariedEmployee("Jan", "Kowalski", "888-88-8888", 1200.00)
12 };
13
14 System.out.println(
15 "Obiekty Invoice i Employee przetwarzane polimorficznie:");
16
17 // W ogólny sposób przetwórz każdy element tablicy payableObjects
18 for (Payable currentPayable : payableObjects) {
19 // Wyświetl dane currentPayable i kwotę płatności
20 System.out.printf("%n%s %nkwota płatności: %,.2f zł%n",
21 currentPayable.toString(), // Można użyć wywołania niejawnego.
22 currentPayable.getPaymentAmount());
23 }
24 }
25 }
Obiekty Invoice i Employee przetwarzane polimorficznie:
faktura:
numer części: 01234 (siedzenie)
liczba sztuk: 2
cena za sztukę: 375,00 zł
kwota płatności: 750,00 zł
faktura:
numer części: 56789 (opona)
liczba sztuk: 4
cena za sztukę: 79,95 zł
kwota płatności: 319,80 zł
pracownik ze stałym wynagrodzeniem: Anna Nowak
numer ubezpieczenia: 111-11-1111
wynagrodzenie tygodniowe: 800,00 zł
kwota płatności: 800,00 zł
pracownik ze stałym wynagrodzeniem: Jan Kowalski
numer ubezpieczenia: 888-88-8888
wynagrodzenie tygodniowe: 1 200,00 zł
kwota płatności: 1 200,00 zł
Rysunek 10.14. Program testujący interfejs Payable umożliwia polimorficzne przetwarzanie obiektów Invoice i Employee
Wiersze od 18. do 23. przetwarzają polimorficznie każdy obiekt Payable z payableObjects, wyświetlając tekstową reprezentację obiektu i kwotę do zapłaty. Wiersz 21. wywołuje metodę toString poprzez interfejs Payable, choć toString nie jest tam zadeklarowana — wszystkie referencje (również te dotyczące interfejsów) odnoszą się do obiektów, które muszą dziedziczyć po Object, więc zawierają metodę toString. (Metodę toString można także w tej sytuacji wywołać niejawnie). Wiersz 22. wywołuje metodę getPaymentAmount interfejsu Payable, aby otrzymać kwotę płatności dla każdego obiektu w payableObjects niezależnie od
jego faktycznego typu. Wynik działania aplikacji wyraźnie pokazuje, że wywołania metod z wierszy 21. i 22. wywołują metody toString i getPaymentAmount właściwych klas.
Programowanie w Javie. Solidna wiedza w praktyce. Wydanie XI, Autorzy: Paul Deitel, Harvey Deitel, Wydawnictwo: Helion