Słowo „interfejs” kojarzy się zwykle np. z interfejsem graficznym albo tekstowym, czyli sposobem interakcji użytkownika z programem. W pewnym sensie podobną funkcję pełni interfejs w programowaniu obiektowym.
Interfejsy
W poprzedniej lekcji omawiałem pokrótce mechanizm dziedziczenia w programowaniu obiektowym a także klasy abstrakcyjne, które pozwalają na określenie czegoś w rodzaju zarysu klasy, który przyjmuje swoją ostateczną postać dopiero w klasie potomnej. Interfejsy z kolei są, mówiąc mało fachowym językiem, jeszcze bardziej abstrakcyjne od klas abstrakcyjnych. Struktura interfejsu przypomina strukturę klasy, ale nie zawiera zmiennych, które mogą zmieniać wartość (choć może zawierać stałe z przypisaną wartością) i zwykle nie posiada implementacji zadeklarowanych w niej metod.
Sens tworzenia interfejsów jest taki, że pozwalają one na opisanie tego jaką funkcjonalność powinne mieć klasy, ale nie jak ją mają realizować. Gdybyśmy chcieli odnieść tą technikę do organizmów, to interfejs opisywałby, że organizm ma: jeść, rozmnażać się, rosnąć itd. natomiast nie opisywałby co i jak ma jeść, jak się ma rozmnażać i jak przebiega jego wzrost. Te czynności realizują konkretne organizmy i inaczej wyglądają one u chorobotwórczej bakterii, inaczej u rośliny a jeszcze inaczej u drapieżnego zwierzęcia. Jednak w stosunku do każdego organizmu możemy oczekiwać tych opisanych w „interfejsie” czynności.
Spróbujmy zatem stworzyć w Javie interfejs deklarujących parę czynności życiowych organizmu a następnie stworzyć kilka klas implementujących ten interfejs.
Stwórz nowy projekt w NetBeans, nazwij go np. „Interfejsy” z klasą Interfejsy
zawierającą metodę main
. Następnie utwórz nowy interfejs, w ten sposób, że w miejscu gdzie zwykle podczas tworzenia nowego pliku (File -> New File) wybierasz „Java Class”, wybierz „Java Interface” i nazwij go Organizm
. Powinien się pojawić odpowiedni plik z kodem:
1 2 3 4 5 |
package interfejsy; public interface Organizm { } |
Jak widać, zamiast słowa kluczowego class
pojawiło się interface
.
Teraz dodamy kilka deklaracji metod:
1 2 3 4 5 6 7 |
package interfejsy; public interface Organizm { String jedz(); Object rozmnozSie(); void rosnij(double wzrostMasy); } |
Jak widać, są to same deklaracje określające nazwę metody, rodzaj wartości zwracanej przez metodę oraz argumenty, które przyjmuje. Nie ma nawet pary nawiasów klamrowych.
Teraz zaimplementujmy te metody w kilku klasach. Najpierw stwórzmy klasę Mysz
i zmodyfikujmy nieco jej nagłówek, dopisując po nazwie klasy implements Organizm
:
1 2 3 4 5 6 |
package interfejsy; public class Mysz implements Organizm { } |
Oznacza to, że klasa będzie implementowała interfejs o nazwie Organizm
. Od razu NetBeans podkreśla na czerwono nazwę klasy i informuje nas, że Mysz
nie jest klasą abstrakcyjną i nie nadpisaliśmy metody abstrakcyjnej rosnij(Double)
. Zgadza się, jeśli implementujemy jakiś interfejs to należy to zrobić względem wszystkich niezaimplementowanych metod.
Teraz stwórz kolejną klasę, Kot
, także implementującą interfejs Organizm
.
Następnie uzupełnij kodem klasy Mysz
i Kot
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package interfejsy; // klasa Mysz implementuje interfejs Organizm public class Mysz implements Organizm { double masa; // Konstruktor Mysz (int masa) { this.masa = masa; } // Implementacja metod zadeklarowanych w interfejsie Organizm public String jedz() { return "Zjadam smaczne ziarna"; } public Mysz rozmnozSie() { Mysz malaMysz = new Mysz(5); return malaMysz; } public void rosnij(double wzrostMasy) { masa = masa * wzrostMasy; } // Ta metoda nie jest zadeklarowana w interfejsie Organizm public void piszcz() { System.out.println("Pi, Pi, Pi..."); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package interfejsy; // Klasa Kot implementuje interfejs Organizm public class Kot implements Organizm{ double masaKota; // Implementacja metod zadeklarowanych w interfejsie Organizm public String jedz() { return "Mniam, mniam..."; } public Kot rozmnozSie() { Kot malyKot = new Kot(); malyKot.masaKota = 500; return malyKot; } public void rosnij(double wzrostMasy) { System.out.println("Rosnę o " + wzrostMasy + " gramów"); masaKota += wzrostMasy; } } |
Zauważ, że obie klasy implementują trzy metody zadeklarowane w interfejsie Organizm
. W klasie Mysz
dopisana została ponadto metoda piszcz()
.
Teraz uzupełnijmy metodę main
w klasie kodem, który pokaże parę aspektów wykorzystania interfejsów.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package interfejsy; public class Interfejsy { public static void main(String[] args) { // Obiekty myszka i kot deklarujemy jako typ Organizm, Organizm mysz = new Mysz(5); Organizm kot = new Kot(); // Wywołujemy metody, są one zaimplementowane w różny sposób // w klasie Mysz i Kot System.out.println("Myszka je: " + mysz.jedz()); System.out.println("Kot je: " + mysz.jedz()); mysz.rosnij(1.2); kot.rosnij(10.0); Organizm kotek = kot.rozmnozSie(); // To się nie uda, interfejs nie ma zadeklarowanej metody piszcz() // myszka.piszcz(); // Teraz deklarujemy obiekt jako typ Mysz... Mysz myszka = new Mysz(6); // ... więc widzi on metodę piszcz() myszka.piszcz(); // Stosujemy kasting z typu Organizm na Mysz Mysz mycha = (Mysz) mysz.rozmnozSie(); mycha.piszcz(); } } |
W zasadzie komentarze wyjaśniają kod. Zwróć uwagę na różnicę w sytuacji gdy deklarujemy obiekt jako typ Organizm
, który jest interfejsem i typ właściwy dla klasy implementującej ten interfejs. W pierwszy przypadku nie możemy wykorzystać metod, które nie są zadeklarowane w interfejsie.
Teraz dopisz w klasie Interfejsy
dodatkową metodę:
static public void nakarmOrganizm(Organizm stworzenie) {
stworzenie.jedz();
}
oraz uzupełnij metodę main
:
nakarmOrganizm(mysz);
nakarmOrganizm(kot);
Teraz widać sens używania interfejsów. Metoda nakarmOrganizm
przyjmuje jako argument każdy obiekt, który został utworzony na podstawie klasy implementującej interfejs Organizm
. Wykorzystuje metody, które są w tym interfejsie zadeklarowane i nie musi „wiedzieć” jakiego dokładnie typu są te obiekty, co więcej implementacja tych metod a także wynik ich działania może być zupełnie inny.
Implementacja wielu interfejsów na raz
Zapewne nie raz podczas lektury tej lekcji nasuwały Ci się porównania z klasami abstrakcyjnymi i ogólniej z mechanizmem dziedziczenia, który omówiłem poprzednim razem. Rzeczywiście podobieństw jest wiele. Do podstawowych różnic natomiast należy to, że o ile klasa pochodna może dziedziczyć bezpośrednio tylko po jednej klasie, to klasa może implementować na raz kilka interfejsów.
Stwórz kolejny interfejs Genetyczny
1 2 3 4 5 6 7 8 |
package interfejsy; public interface Genetyczny { String MATERIAL_GENETYCZNY = "DNA"; int RODZAJE_NUKLEOTYDOW = 4; void wyswietlSekwencje(String[] sekwencja); } |
Zauważ, że znajduje się w nim deklaracja jednej metody, ale także dopisałem dwie stałe. Nie widać na pierwszy rzut oka, że są to stałe, poza zgodnym z konwencją użyciem wielkich liter, ale zmienne znajdujące się w interfejsach domyślnie są statyczne (modyfikator static
) i stałe (modyfikator final
).
Teraz zmodyfikujmy nieco klasę Mysz
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package interfejsy; import java.util.Arrays; // klasa Mysz implementuje interfejs Organizm oraz Genetyczny public class Mysz implements Organizm, Genetyczny { double masa; // Konstruktor Mysz (int masa) { this.masa = masa; } // Implementacja metod zadeklarowanych w interfejsie Organizm public String jedz() { return "Zjadam smaczne ziarna"; } public Mysz rozmnozSie() { Mysz malaMysz = new Mysz(5); return malaMysz; } public void rosnij(double wzrostMasy) { masa = masa * wzrostMasy; } // Ta metoda nie jest zadeklarowana w interfejsie Organizm public void piszcz() { System.out.println("Pi, Pi, Pi..."); } // Metoda zadeklarowana w interfejsie Genetyczny public void wyswietlSekwencje(String[] sekwencja) { System.out.println("Sekwencja to: " + Arrays.toString(sekwencja)); } } |
Zmodyfikowany został nagłówek klasy. Teraz implementuje ona zarówno interfejs Organizm
jak i Genetyczny
. Dodana została także implementacja metody zadeklarowanej w interfejsie Genetyczny
oraz zaimportowana klasa java.util.Arrays
.
Czas uzupełnić metodę main
w klasie Interfejsy
o kilka poleceń:
System.out.println("Materiał genetyczny myszy to " +
myszka.MATERIAL_GENETYCZNY);
// w interfejsie pola są statyczne, nie musimy ich
// wywoływać z obiektów
System.out.println("Liczba rodzajów nukeotydów = " +
Genetyczny.RODZAJE_NUKLEOTYDOW);
String[] sekwencja = {"A","C","C","A","G","A","G","G"};
myszka.wyswietlSekwencje(sekwencja);
Metody domyślne w interfejsach
Jak wspomniałem wcześniej interfejsy zasadniczo zawierają jedynie deklarację metod, bez wykonywalnego kodu. Wcześniej było to całkiem zabronione, ale wraz z Javą w wersji 8 pojawiła się możliwość umieszczania w interfejsach także implementacji metod. W takich wypadkach są to tzw. „metody domyślne”. Skąd takie określenie? Odpowiedź na pytanie znajdziemy w powodach, dla których w ostatniej (dotychczas) wersji naszego ulubionego języka programowania dodano taką opcję.
Twórcy Javy bardzo dbają o wsteczną kompatybiność języka. Oznacza to, że program oparty np. na Javie 5 powinien działać także gdy mamy zainstalowaną Javę 7 czy 8. Ale takie podejście stwarza też pewne problemy. Powiedzmy, że istnieje jakiś interfejs, który poszerzymy o nowe metody. Jak już wiemy klasa implementująca interfejs, musi zaimplementować wszystkie zadeklarowane w nim metody. Zatem, jeśli jakiś czas temu ktoś napisał klasę wykorzystującą omawiany interfejs, to po dodaniu do niego nowych metod, klasa przestanie działać, ponieważ nie posiada implementacji metod, które zostały napisane później. Umieszczenie domyślnej implementacji nowych metod rozwiązuje ten problem, stare klasy już nie muszą ich implementować.
Drugą zaletą stosowania metod domyślnych jest to, że mogą one być tworzone wtedy, gdy przewidujemy, że implementujące interfejs metody mogą korzystać tylko z części funkcjonalności oferowanej przez interfejs. W takim przypadku programista nie jest zmuszony do implementowania metod, których i tak nie będzie wykorzystywać.
Metoda domyślna musi otrzymać modyfikator default
. Dopiszmy zatem do interfejsu Organizm
metodę:
default void kopNore() {
System.out.println("Kopię...");
}
Teraz dopiszemy jej implementację do klasy Mysz
:
public void kopNore() {
System.out.println("Kopię sobie norkę");
}
nie ma sensu jej implementować w klasie Kot
gdyż jak wiadomo koty raczej raczej nor nie kopią.
Oczywiście w metodzie main
klasy Interfejsy
możemy wywołać metodę na odpowiednim obiekcie:
mycha.kopNore();
Bardzo dobrze opisane Interfejsy – w końcu zrozumiałem po co one są :) Mam tylko prośbę o rozwinięcie zapisu z metody main w linii 15: Organizm kotek = kot.rozmnozSie();
Nie wiem czy dobrze rozumiem- następuje deklaracja zmiennej „kotek” typu „Organizm” (ze względu na implementacje do klasy „Kot” interfejsu „Organizm”) która wskazuje na metodę „rozmnozSie” obiektu „kot”, czyli zwraca obiekt „małykot” jednocześnie ustawiając wartość „maskota” na 500 ?
Gdybyśmy nie stosowali interfejsów, to zapis maiłby postać: kotek = kot.rozmnozSie(); ?
Ogólnie mam problem ze zwracaniem obiektów i używaniem ich jako argumentów, stąd moje wątpliwości.
> Bardzo dobrze opisane Interfejsy – w końcu zrozumiałem po co one są :)
Ciesze się :-)
>Mam tylko prośbę o rozwinięcie zapisu z metody main w linii 15: Organizm kotek = kot.rozmnozSie();
> Nie wiem czy dobrze rozumiem- następuje deklaracja zmiennej „kotek” typu „Organizm”
raczej: obiektu kotek typu Organizm.
>(ze względu na implementacje do klasy „Kot” interfejsu „Organizm”) która wskazuje na metodę
>„rozmnozSie” obiektu „kot”, czyli zwraca obiekt „małykot” jednocześnie ustawiając wartość
> „maskota” na 500 ?
O ile dobrze rozumiem pytanie to tak :-)
> Gdybyśmy nie stosowali interfejsów, to zapis maiłby postać: kotek = kot.rozmnozSie(); ?
Np.:
Kot kotek = kot.rozmnozSie();
Klasa „Kot” po prostu byłaby samodzielną klasą.
Pozdrawiam
G.
Mam pytanie odnośnie linii 26 z metody main:
// Stosujemy kasting z typu Organizm na Mysz
Mysz mycha = (Mysz) mysz.rozmnozSie();
mycha.piszcz();
Po co jest tutaj to rzutowanie, skoro można użyć bezpośrednio tego typu bez rzutowania z metodą piszcz. Coś źle zrozumiałem?
Nie zauważyłem, żeby wcześnie ta zmienna była typu Organizm.
Obiekt „mysz” jest typu „Organizm” (linia 7) a nie typu „Mysz”, zatem metoda „rozmnozSie” zwraca obiekt typu „Object”. Dlatego jeśli chcemy otrzymać obiekt typu „Mysz”, trzeba użyć kastingu.
W klasie Interfejsy w linii 12 jest:
System.out.println(„Kot je: ” + mysz.jedz());
a powinno być:
System.out.println(„Kot je: ” + kot.jedz());
z kolei w linii 15 (u mnie wyświetlało jako błąd):
Organizm kotek = kot.rozmnozSie();
metoda rozmnozSie() zwraca Object, który należałoby zrzutować na Organizm, czyli:
Organizm kotek = (Organizm) kot.rozmnozSie();
Czy mógłby ktoś wytłumaczyć dlaczego w klasie Mysz w linijce 16 po public jest typ Mysz zamiast Object jak było zadeklarowane w interfejsie Organizm (linia 5)? (Podobnie w klasie Kot w linii 13?)
” public Mysz rozmnozSie() { ”
Kolejne pytanie dotyczy klasy Interfejsy linia 15
„Organizm kotek = (Organizm)kot.rozmnozSie(); ” – proszę o łopatologiczne wyjaśnienie bo siedzę nad tym kolejny dzień i nie bardzo rozumiem…
„Czy mógłby ktoś wytłumaczyć dlaczego w klasie Mysz w linijce 16 po public jest typ Mysz zamiast Object jak było zadeklarowane w interfejsie Organizm (linia 5)? (Podobnie w klasie Kot w linii 13?)
” public Mysz rozmnozSie() { ””
Klasa Mysz implementuje interfejs o nazwie Organizm.
Obiekt typu Mysz „rozmnaża się” zwracając obiekt typu Mysz, który oprócz metod zadeklarowanych w interfejsie Organizm (i wszystkich obiektów, których charakterystyka jest zdefiniowana w Object) zawiera dodatkową metodę piszcz().
Analogicznie obiekt typu Kot zwraca „kota”.
Interfejs Organizm określa tylko, że metoda rozmnozSie() zwraca obiekt, w klasach Mysz i Kot „doprecyzowujemy”, jakiego rodzaju jest obiekt.
” Kolejne pytanie dotyczy klasy Interfejsy linia 15
„Organizm kotek = (Organizm)kot.rozmnozSie(); ” – proszę o łopatologiczne wyjaśnienie bo siedzę nad tym kolejny dzień i nie bardzo rozumiem…”
Jeśli utworzymy „kotka” w ten sposób:
Organizm kotek = kot.rozmnozSie();
w obiekcie kotek, możemy używać tylko metod zadeklarowanych w interfejsie „Organizm”.
Proszę zwrócić uwagę na dalszy fragment kodu, kiedy tworzymy „myszkę”:
Mysz myszka = new Mysz(6);
Teraz możemy wykorzystywać także metody charakterystyczne dla klasy Mysz.
Analogicznie, można zrobić z „kotkiem”
Mam nadzieję, że pomogłem.
Pozdrawiam
Grzegorz Góralski
Rozkładając na czynniki pierwsze jeszcze raz ten fragment kodu:
Organizm kotek = kot.rozmnozSie();
popraw mnie proszę czy dobrze rozumiem: tj utworzenie nowego obiektu o nazwie kotek typu Organizm i przypisaniu mu metody rozmnozSie(), która zwraca typ Kot. Tylko dlaczego nie można napisać po prostu kot.rozmnozSie();
przecież kot w linii 8 jest zadeklarowany jako typ Organizm? Po co ten kotek??
Pisząc tak:
Organizm kotek = kot.rozmnozSie();
Otrzymujemy obiekt kotek, który później można wykorzystać w programie.
Samo kot.rozmnozSie(); nie spowoduje utworzenia obiektu, nadającego się do użytku, obiekt kot nie przekaże go skutecznie do metody main();
Inaczej: skąd metoda main() miałaby „wiedzieć” jak nazywa się obiekt?
BTW oczywiście jeśli chcemy później używać metod charakterystycznych dla klasy Kot (a nie tylko zdefiniowanych w interfejsie Organizm), trzeba napisać:
Kot kotek = kot.rozmnozSie();