Kurs Java

Interfejs jako typ danych

Pamiętacie nasz rozdział Klasy - Dziedziczenie? Pisaliśmy tam o między innymi o tym, że obiekt klasy rozszerzającej superklasę jest tego samego typu co superklasa. Pora teraz wrócić do tego zagadanienia. Faktem jest, że interfejsy w Javie są również rozumiane jako typ, a więc jeśli tworzymy obiekt implementujący dany interfejs to obiekt ten staje się obiektem jego typu.

Typ obiektu implementującego interfejs

Wróćmy do naszej klasy DocumentItem. Miała ona już do tej pory okazję dziedziczyć z regularnej klasy Item, z abstrakcyjnej klasy Item, a nawet implementowała interfejs Item. W ten sposób pokazaliśmy jak różnie można przedstawić taką relację, w której jeden byt wywodzi się z innego, dziedzicząc jego cechy i dodając swoje. Poniżej wracamy do przykładu, w którym nasza klasa implementowała dwa interfejsy. Przypominamy przy okazji, że w Javie możliwe jest rozszerzanie tylko jednej klasy, ale implementować można wiele interfejsów.
public class DocumentItem implements Item, Image {

    String name;       
    
    public void setName(String name) {
        this.name = name;
    }    
    
    public String getName() {
        return name;
    }

    public String getDescription() {
        return "This is description for DocumentItem";
    }
    
    public String getImageName() {
        return "DefaultImage.png";
    }
} 
Co możemy powiedzieć o naszej klasie, a właściwie co możemy powiedzieć o obiekcie stworzonym na bazie takiej klasy? Czy możemy powiedzieć, że obiekt tej klasy będzie obiektem typu Item, czy też raczej obiektem typu Image? Jak się zapewne domyślacie obiekt ten będzie obiektem typu Item oraz Image. W takiej sytuacji mówimy, że obiekt jest wielotypowy. Zobaczmy teraz co nam to daje.

Wielotypowy obiekt - przykład

Spróbujmy teraz stworzyć obiekt wielotypowy w klasie Start i przeanalizujmy co się tam po kolei dzieje:
public class Start {

    public static void main(String[] args) {
        
    	DocumentItem documentItem = new DocumentItem();
    	documentItem.setName("DocumentItem type");
    	System.out.println(documentItem.getName());
    	System.out.println(documentItem.getDescription());
        System.out.println(documentItem.getImageName());
    	
    	Item item = new DocumentItem();
    	item.setName("Other type");
    	System.out.println(item.getName());
    	System.out.println(item.getDescription());
    	
    	Image image = new DocumentItem();
    	System.out.println(image.getImageName());
    }
    
}
Najpierw tworzymy obiekt klasy DocumentItem i przypisujemy go do typu DocumentItem. W kilku kolejnych linijkach (aż do przerwy) korzystamy z pełnego zestawu metod klasy. Pamiętamy, że część z nich została zaimplementowa, ponieważ wymusił to na nas interfejs Item, a część - a dokładnie jedna metoda getImageName - została wymuszona przez interfejs Image.

Zaraz po przerwie tworzymy kolejny obiekt klasy DocumentItem, ale tym razem przypisujemy go do typu Item. Tutaj możemy korzystać już jedynie z metod, które są dostępne w ramach interfejsu Item. Z tego powodu nie możemy wydrukować nazwy obrazka. Nazwa ta jest dostępna tylko z interfejsu Image, bądź też z klasy implementującej ten interfejs.

Na końcu, po kolejnej przerwie tworzymy trzeci obiekt klasy DocumentItem, ale w tym przypadku przypisujemy go do typu Image. Spośród wszystkich zdefiniowanych przez nas metod mamy tutaj możliwość odwołania się tylko do metody getImageName, gdyż tylko taka metoda jest zdefiniowana w interfejsie Image.

Podsumowując, uzyskaliśmy dowód na to, iż obiekt klasy DocumentItem jest wielotypowy. Najszerszym z typów jest DocumentItem sam w sobie, który implementuje wszystkie interfejsy, a więc zawiera też wszystkie metody tych interfejsów. Później mamy kolejne dwa typy Item i Image, które pozwalają na uzyskanie dostępu do metod związanych tylko bezpośrednio z nimi.

Co nam daje wielotypowość

Po przeczytaniu poprzedniego paragrafu można się zastanawiać, po co właściwie rozbijać metody na różne interfejsy i tworzyć tyle typów. Musimy pamiętać, że interfejsy w Javie są stworzone po to, by definiować co powinno być wykonane w ramach danego interfejsu, a nie jak ma to być wykonane. Dlatego też należy tworzyć interfejsy, które będą specjalizowały się w jednej konkretnej dziedzinie (definiującej jakie operacje mają być wykonane) i nie będą przeładowane metodami. Lepiej jest posiadać więcej mniejszych interfejsów niż mniej dużych. Jeśli jest odwrotnie, to zwykle oznacza to, że źle przemyśleliśmy temat i nieodpowiednio podzieliliśmy interfejsy.

Mając interfejsy z odpowiednio wydzielonymi odpowiedzialnościami, możemy bardzo łatwo tworzyć w Javie program w oparciu o to co ma robić, a implementacja jest tak naprawdę tylko uzupełnieniem. Jeśli mamy wiele interfejsów, możemy dowolnie sterować aplikacją. Na przykład możemy utworzyć metodę, która będzie oczekiwała obiektu danego interfejsu i będzie zajmowała się zadaniami związanymi tylko z tym interfejsem. W takiej sytuacji jeśli mamy klasę, której obiekty powinny być przetwarzane w tej metodzie, a wcześniej nie implementowała ona takiego interfejsu, może w bardzo łatwy sposób zostać do tego przygotowana. Wystarczy, że zaimplementuje dany interfejs.

Przykładowo możemy mieć w aplikacji klasę odpowiedzialną za przetwarzanie obrazków:
public class ImageProcessor {

    public void processImage(Image image) {
        ... // algorytm zmieniający rozmiar obrazków
    }
    
}
Jeśli nasz DocumentItem nie implementowałby interfejsu Image, wówczas nie moglibyśmy przekazać obiektu typu DocumentItem do powyższej metody. Natomiast jako że implementujemy ten interfejs, możemy bez problemu przesłać obiekt do tej metody:
public class Start {

    public static void main(String[] args) {
        
        Image image = new DocumentItem();
        ImageProcessor itemProcessor = new ImageProcessor();
        itemProcessor.processImage(image);    
    }
    
}
Inaczej mówiąc mamy klasę specjalizującą się w obróbce obrazków i pracuje ona na obiektach typu Image, więc jeśli w naszym kodzie mamy obiekt innego typu, a checemy by był on przetwarzany przez tą klasę, wówczas musimy go dopasować do jej wymagań.

Oczywiście w razie potrzeby możemy dorzucać kolejne interfejsy do danej klasy, tak aby jej obiekty stawały się obiektami typów oczekiwanych przez kolejne metody specjalizujące się w jakiejś dziedzinie (tak jak processImage specjalizuje się w obróbce obrazków).

Interfejsy bez metod

Na koniec jeszcze jedna ważna sprawa. Interfejsy często mają mało metod, a zdarza się nawet, że nie posiadają metod wcale. Wtedy interfejsy takie nazywamy interfejsami znacznikowymi. Można się zapytać, po co nam interfejs, który nie daje nam żadnej dodatkowej metody? I tutaj właśnie znowu musimy pomyśleć o interfejsach jako o mechanizmie kategoryzowania działań na obiektach. W przypadku naszej klasy ImageProcessor nie wspomnieliśmy nawet o tym, że będziemy wykorzystywać metodę interfejsu Image. Wystarczyło nam jedynie to, że obiekt przekazywany do tej metody ma być typu Image. I to jest dosyć często spotykana sytuacja.

Przykłady interfejsów Javy

Przyjrzyjmy się teraz kilku interfejsom, które pochodzą bezpośrednio z samego języka. To jest zawsze dobry sposób na ugruntowanie zdobytej wiedzy.
  • Serializable - interfejs z pakietu java.io. Oznacza on, że obiekt implementujący ten interfejs jest gotowy do zserializowania, czyli gotowy do przekształcenia do postaci binarnej. To oznacza, że można go będzie wysłać przez sieć lub na przykład zapisać na dysk. Ten interfejs nie posiada żadnej metody - jest interfejsem znacznikowym.
  • Comparable - interfejs z pakietu java.lang. Zaimplementowanie go w klasie powoduje, że obiekty tej klasy mogą być porównywane ze sobą.
    public interface Comparable<T> {
    
        public int compareTo(T o);
    }
    
    To w jaki sposób będziemy porównywać obiekty, na przykład jakich pól będziemy używać i jak będzie wyglądał algorytm porównujący, pozostaje już w naszej gestii. Musimy dostarczyć implementację metody compareTo tegoż interfejsu. Litera T oznacza tutaj typ obiektu jaki będziemy porównywać. Co dokładnie oznacza użycie takiej litery wytłumaczymy, gdy zaczniemy mówić o typach generycznych. Na razie nie zaprzątajcie sobie tym głowy.
  • Collection - interfejs z pakietu java.util. Definiuje on nam zbiór metod możliwych do wykonania na kolekcjach w Javie (zbiory danych trochę podobne do tablic - już niedługo poświęcimy temu zagadnieniu co najmniej jeden rozdział kursu). Interfejs zawiera sporo metod, a niektóre z nich są bardziej złożone, dlatego na razie przedstawiamy tylko fragment. Możliwości kolekcji są naprawdę imponujące:
    public interface Collection<E> extends Iterable<E> {
    
        int size();
    
        boolean isEmpty();
    
        boolean contains(Object o);
    
        boolean add(E e);
    
        boolean remove(Object o);
    
        boolean containsAll(Collection<?> c);
    
        boolean addAll(Collection<? extends E> c);
    
        boolean removeAll(Collection<?> c);
    
        boolean retainAll(Collection<?> c);
    
        void clear();
    
        boolean equals(Object o);
    
        int hashCode();
     
    	...
    }
    
    

Przykład w programie

Na koniec zobaczmy jak kod naszego programu z wielotypowym obiektem wygląda w IDE:
package com.javappa.itemsappa;

public class Start {

    public static void main(String[] args) {
        
        DocumentItem documentItem = new DocumentItem();
        documentItem.setName("DocumentItem type");
        System.out.println("Name: " + documentItem.getName());
        System.out.println("Description: " + documentItem.getDescription());
        System.out.println("Image name: " + documentItem.getImageName());       
        
        Item item = new DocumentItem();
        item.setName("Other type");
        System.out.println("Name: " + item.getName());
        System.out.println("Description: " + item.getDescription());
        
        Image image = new DocumentItem();
        System.out.println("Image name: " + image.getImageName());
    }
    
}
Obiekt wielotypowy w Javie
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
kursjava@javappa.com

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE