Kurs Java

Klasa z parametrem typu

Na początek wyjaśnijmy tytuł tego rozdziału. Parametr typu to taki parametr, który pozwala podstawić pod niego dowolny typ, na przykład String, Integer, Item, List etc. Skoro mówimy o parametrze dla klasy, to musi istnieć miejsce, gdzie ten parametr będziemy umieszczać. Należy zapamiętać, że miejsce to znajduje się zaraz za nazwą klasy i jest ograniczone nawiasami ostrymi.

W Javie mamy całe mnóstwo przykładów jak taka konstrukcja wygląda. Weźmy na przykład klasę ArrayList:
public class ArrayList<E> ... {

    ...        
}
Jej definicja jest bardzo rozbudowana, ale nas interesuje parametr typu. Zgodnie z tym, co napisaliśmy, jest on zdefiniowany między nawiasami ostrymi. Pytanie, które się Wam zapewne nasuwa, dotyczy tego, dlaczego jest to akurat litera E i to w dodatku duże E. Odpowiedź tkwi w pewnych umownych zasadach wprowadzonych przez programistów Javy. Przyjęli oni, że aby odróżnić parametr typu od innych parametrów lub zmiennych, będzie on zawsze pisany dużą literą. Nie znaczy to, że mała litera się nam nie skompiluje, ale jest to zasada odgórnie narzucona, w dodatku powszechnie stosowana i dlatego powinniśmy jej używać. Parametr typu ma się wyróżniać.

Sam wybór litery E oznacza, że typ, który tutaj zastosujemy, będzie traktowany jako typ elementu listy, a lista może zawierać wiele takich elementów. Od razu podkreślmy, że nie jest to sztywna reguła i w praktyce często stosujemy inne konwencje. Dopuszcza się nawet zastosowanie całego słowa (również złożonego z wielkich liter). Czyli w przypadku listy mogłoby być napisane na przykład ELEMENT.

Własna klasa z parametrem typu

No dobrze, przejdźmy teraz do konkretów. Stwórzmy prostą klasę, której zadaniem będzie przechowywanie danych oraz udostępnianie informacji o tych danych. Nazwiemy ją SmartContainer i w pierwszej wersji nie będziemy definiować własnego parametru typu:
import java.util.ArrayList;
import java.util.List;

public class SmartContainer {

    private List<String> elements = new ArrayList<>();

    public void add(String element) {
        System.out.print("Element add: ");
        System.out.println(element);
        elements.add(element);
    }

    public String get(int index) {
        String element = elements.get(index);
        System.out.print("Element get: ");
        System.out.println(element);
        return element;
    }

    public String update(int index, String element) {
        System.out.print("Element to be updated: ");
        System.out.println(elements.get(index));
        return elements.set(index, element);
    }

    public void printAll() {
        System.out.println("All elements:");
        for (String element : elements) {
            System.out.println(element);
        }
    }
}
Nasz kontener na dane (nazwijmy je elementami) opiera się na liście, w której te dane są przechowywane. Zgodnie z tym, co pokazaliśmy w poprzednim rozdziale Typy generyczne w Javie, lista ta powinna mieć określony typ, stąd zakładamy od razu, że będziemy przechowywać w niej obiekty typu String.

Tradycyjnie napiszemy od razu kawałek kodu klasy Start, tak aby można było uruchomić cały przykład:
public class Start {

    public static void main(String[] args) {

        SmartContainer smartContainer = new SmartContainer();
        smartContainer.add("Appa Item 1");
        smartContainer.add("Appa Item 2");
        smartContainer.add("Appa Item 3");

        String element = "Appa Item 3 modified";
        smartContainer.update(2, element);

        smartContainer.printAll();
    }
}

W ten sposób uzyskaliśmy całkiem zgrabny, choć bardzo prosty, kontener na dane. Na zewnątrz mamy wystawione metody takie jak add, update czy printAll i to właśnie z nich korzystamy, uruchamiając kod. W środku, oprócz tego, że nasze metody operują na liście, to jeszcze drukują informacje na konsolę o tym, co się dzieje w trakcie uruchamiania programu.

Taki kontenerek to całkiem przyjemny kawałek kodu, ale jest z nim jeden problem. Otóż mimo tego, że nie robimy tam niczego specyficznego dla obiektów typu String, to on tak naprawdę będzie działał tylko dla obiektów tego typu. Trochę szkoda, bo klasa SmartContainer ma potencjał do tego, by być używana częściej i to dla obiektów różnych typów. Możemy na przykład chcieć wykorzystać taką klasę do przechowywania liczb albo nawet obiektów stworzonej przez nas klasy.

To, co powinniśmy teraz zrobić, to doprowadzić do przebudowania klasy na bardziej ogólną. Nie chcemy oczywiście w ogóle zmieniać logiki działania klasy. Powinniśmy zapewnić tę samą funkcjonalność, ale działającą na obiektach dowolnego typu. Musimy określić parametr, pod który będziemy mogli podstawić dowolny typ obiektu. Innymi słowy, musimy dodać do naszej klasy parametr typu. Zmieniamy co trzeba i teraz nasza klasa wygląda tak:
import java.util.ArrayList;
import java.util.List;

public class SmartContainer<E> {

    private List<E> elements = new ArrayList<>();

    public void add(E element) {
        System.out.print("Element add: ");
        System.out.println(element);
        elements.add(element);
    }

    public E get(int index) {
        E element = elements.get(index);
        System.out.print("Element get: ");
        System.out.println(element);
        return element;
    }

    public E update(int index, E element) {
        System.out.print("Element to be updated: ");
        System.out.println(elements.get(index));
        return elements.set(index, element);
    }

    public void printAll() {
        System.out.println("All elements:");
        for (E element : elements) {
            System.out.println(element);
        }
    }
}
Co zmieniliśmy? Dodaliśmy parametr, a później użyliśmy go zamiast konkretnego typu we wszystkich wymaganych miejscach w kodzie. Zamieniliśmy wszystkie wystąpienia typu String na zadeklarowany przez nas parametr T. Dzięki temu, możemy definiować typ podczas tworzenia obiektu klasy, a więc każdy tworzony obiekt klasy SmartContainer może dla nas pracować z innym typem. Najpierw zobaczmy, jak to będzie wyglądało dla klasy String:
public class Start {

    public static void main(String[] args) {

        SmartContainer<String> smartContainer = new SmartContainer<>();
        smartContainer.add("Appa Item 1");
        smartContainer.add("Appa Item 2");
        smartContainer.add("Appa Item 3");

        String element = "Appa Item 3 modified";
        smartContainer.update(2, element);

        smartContainer.printAll();
    }
}

A teraz przygotujmy kawałek programu, który będzie przechowywał w naszym kontenerze liczby typu Integer (i to bez żadnej zmiany w kodzie kontenera!):
public class Start {

    public static void main(String[] args) {

        SmartContainer<Integer> smartContainer = new SmartContainer<>();
        smartContainer.add(1);
        smartContainer.add(2);
        smartContainer.add(3);

        Integer element = 33;
        smartContainer.update(2, element);

        smartContainer.printAll();
    }
}

Trzeba przyznać, że takie programowanie jest bardzo efektowne, a także bardzo praktyczne. Tak naprawdę na parametrach typu opiera się cała obecna Java, szczególnie Java w wersji 8. Bez tej wiedzy ani rusz.

Inny przykład z klasą SmartContainer pokazujemy w naszym Kursie Javy 8 do 14 (w rozdziale dotyczącym metody map i flatMap).

Interfejs z parametrem typu

Co prawda tytuł tego rozdziału brzmi "Klasa z parametrem typu", ale naturalnym pytaniem, które się nasuwa w tym momencie, jest to, czy interfejsy też można definiować generycznie za pomocą parametru typu? Oczywiście można i nawet bardzo często trzeba. Możemy na przykład stworzyć interfejs, który będzie deklarował metody dla naszej klasy SmartContainer:
public interface ContainerOperation<E> {

    void add(E element);

    E get(int index);

    E update(int index, E element);

    void printAll();
}
Przygotowanie parametru typu wygląda analogicznie, jak w przypadku klas. Pewna zmiana następuje w momencie implementacji interfejsu:
import java.util.ArrayList;
import java.util.List;

public class SmartContainer<E> implements ContainerOperation<E> {

    private List<E> elements = new ArrayList<>();

    public void add(E element) {
        System.out.print("Element add: ");
        System.out.println(element);
        elements.add(element);
    }

    public E get(int index) {
        E element = elements.get(index);
        System.out.print("Element get: ");
        System.out.println(element);
        return element;
    }

    public E update(int index, E element) {
        System.out.print("Element to be updated: ");
        System.out.println(elements.get(index));
        return elements.set(index, element);
    }

    public void printAll() {
        System.out.println("All elements:");
        for (E element : elements) {
            System.out.println(element);
        }
    }
}
Podczas implementowania interfejsu powinniśmy przekazać do niego wartość parametru (podobnie jak przekazujemy do interfejsu List). Nie możemy tutaj podać oczywiście ani typu String, ani Integer, ponieważ nasz kontener działa dla różnych typów. To, co powinniśmy zrobić, to przekazać E w nawiasach ostrych. Wtedy typ określony przy tworzeniu obiektu SmartContainer będzie przekazany przez parametr E do klasy, a następnie dalej jako wartość do interfejsu implementowanego przez tę klasę (a także do interfejsu listy). Wówczas kompilator wie, że klasa i interfejs działają na tym samym typie i wszystko między nimi się zgadza.

Podobnie wygląda to zagadnienie w przypadku dziedziczenia klas, ale to już jest temat na inny rozdział, w innym czasie. Poruszymy wtedy jeszcze kilka dodatkowych, bardziej złożonych kwestii.

Różne formy parametrów typu

Na koniec jeszcze krótka uwaga na temat form stosowanych podczas definiowania parametrów typu. O jednej konwencji już wspomnieliśmy. Dotyczyła ona elementu listy, zbioru itp. Takie byty określamy zwykle literą E. Znanych jest jeszcze kilka innych konwencji:
  • K - od słowa Key, czyli klucz, stosowane na przykład dla określenia typu dla kluczy w mapie
  • V - od słowa Value, czyli wartość, stosowane na przykład dla określenia typu dla wartości w mapie
  • N - od słowa Number, dla określenia typu liczbowego
  • T - od słowa Type, dla określenia dowolnego typu, który nie jest żadnym z pozostałych, bardzo często stosowana litera
  • S,U,V itd. - kolejne litery oznaczające kolejne typy stosowane w danym obszarze kodu (tak zgadza się, w nawiasach ostrych możemy określić więcej niż jeden parametr)
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