Kurs Java

Typy generyczne w Javie

Typy generyczne, a inaczej generyki, już samą nazwą potrafią wzbudzać trwogę i tak naprawdę, mimo że jest to jedno z najważniejszych pojęć w programowaniu, ciężko zabrać się do jego nauki. Okazuje się, że zupełnie niepotrzebnie, bo to naprawdę całkiem przyjemne rozwiązanie, a stosowane z głową znacznie ułatwia proces tworzenia kodu.

Kod bez typów generycznych

Naukę generyków najlepiej zacząć od przykładu, który pokazuje, jak problematyczne jest tworzenie kodu bez ich użycia.
import java.util.ArrayList;
import java.util.List;

public class Start {

    public static void main(String[] args) {

        List someNumbers = new ArrayList();
        someNumbers.add(new Long(123));
        someNumbers.add(new String("21345"));

        printNumbers(someNumbers);
    }

    public static void printNumbers(List numbers) {

        Long firstNumber = (Long) numbers.get(0);
        System.out.println(firstNumber);
        Long lastNumber = (Long) numbers.get(1);
        System.out.println(lastNumber);
    }
}
Appa Notka. Drobna uwaga. Celowo wprowadziliśmy w kodzie tworzenie obiektu Long i String za pomocą new. Chcemy przez to podkreślić typy tworzonych obiektów. W zupełności wystarczyłoby użyć tutaj literału L w przypadku longa oraz samych cudzysłowów w przypadku stringa. Jest to nawet zalecane:
someNumbers.add(123L);
someNumbers.add("21345");   
Do rzeczy. W powyższym kodzie zadaniem metody printNumbers jest wydrukowanie dwóch liczb zapisanych wcześniej w liście someNumbers. Na pierwszy rzut oka wszystko wygląda elegancko. Kod nie dość, że się kompiluje, to jeszcze rozpoczyna się jego uruchamianie. Niestety program nie wykona się poprawnie do końca. Podczas operacji przypisania drugiego elementu listy do zmiennej lastNumber otrzymamy wyjątek:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
Zanim przejdziemy do wyjaśnienia konkretnej przyczyny wyjątku, omówmy sobie, co się dzieje w tej linii kodu.

Próbujemy przypisać obiekt do zmiennej typu Long. Ze względu na to, że nasza lista przechowuje obiekty typu Object (domyślnie), rzutujemy pobrany obiekt na typ Long. Możemy to zrobić, ponieważ Long dziedziczy z Object. Taka operacja wykona się poprawnie, jeśli lista przechowuje na danej pozycji faktycznie obiekt stworzony z typu Long, tak jak to ma miejsce w przypadku liczby 123. Naszym problemem jest to, że liczba 21345 została podana w formie stringa - "21345", a precyzyjniej mówiąc w postaci obiektu typu String. Nie jest to więc obiekt typu Long, stąd dostajemy wyjątek opisujący błędne rzutowanie - ClassCastException.
Appa Notka. Rzutowanie (z ang. cast) to przekształcanie obiektu jednego typu w obiekt innego typu w ramach hierarchii dziedziczenia. Tak więc, jeśli na przykład klasa MovieItem dziedziczy z klasy Item, wtedy po stworzeniu obiektu klasy MovieItem możemy go zrzutować do typu Item. Podobnie obiekt klasy Long może zostać zrzutowany do typu Number, ponieważ klasa Long dziedziczy z klasy Number:
Long longValue = new Long(332);
Number numberValue = (Number) longValue;
Takie coś działa, ale jednak nie jest najlepszym zwyczajem i powinniśmy tego unikać. Rzutowanie najczęściej świadczy o błędnie zaprojektowanym kodzie (często w innym miejscu programu). Zresztą sami przyznacie, że przy większej liczbie rzutowań można się zgubić co i gdzie jest jakim obiektem. Lepiej zainwestować w czytelność kodu i to jest właśnie ten moment, w którym z pomocą przychodzą nam typy generyczne.

Kod z typami generycznymi

Zacznijmy od razu od konkretów. Zmienimy nasz przykład, wprowadzając do niego typy generyczne. Innymi słowy, określimy, z jakimi typami obiektów chcemy pracować w danej części kodu. Typy będziemy określać, stosując nawiasy ostre: <, >:
Typy generyczne w Javie - Przykład 1
Na obrazku widzimy, że obok deklaracji interfejsu List pojawiła się informacja o typie obiektów (Long), jaki chcemy przechowywać w tej liście. To bardzo ważna zmiana. Dzięki niej już na etapie kompilacji jesteśmy informowani, że próbując wstawić obiekt stringowy do tej listy, popełniamy błąd (add in list cannot be applied to String). W ten sposób bardzo wcześnie, bo jeszcze przed uruchomieniem programu, dowiadujemy się, że mamy błąd w kodzie.

Odkąd deklarujemy "przywiązanie" naszej listy do typu Long, możemy pozbyć się rzutowania, o czym zresztą informuje nas IDE. Komunikat mówi nam, że rzutowanie jest nadmiarowe (Casting...to 'Long' is redundant).

Po zmianach nasz kod będzie wyglądał tak:
import java.util.ArrayList;
import java.util.List;

public class Start {

    public static void main(String[] args) {

        List<Long> someNumbers = new ArrayList<>();
        someNumbers.add(123L);
        someNumbers.add(21345L);

        printNumbers(someNumbers);
    }

    public static void printNumbers(List<Long> numbers) {

        Long firstNumber = numbers.get(0);
        System.out.println(firstNumber);
        Long lastNumber = numbers.get(1);
        System.out.println(lastNumber);
    }
}

Co to jest Operator Diamond?

W podanym przykładzie może Was zastanawiać jeszcze jedna rzecz. Mianowicie, co się dzieje po prawej stronie operatora przypisania, a więc w miejscu, w którym tworzymy instancję obiektu listy za pomocą słowa new. Tam również wprowadziliśmy nawiasy ostre, ale nie określiliśmy typu. Zrobiliśmy to, ponieważ od Javy 7 istnieje możliwość, by po prawej stronie użyć Operatora Diamond, czyli takiego właśnie oznaczenia bez ponownego określania typu. Został on wprowadzony, aby ograniczyć potrzebę wpisywania oczywistego kodu. Wiadomo, że deklarując listę konkretnym typem (tutaj <Long>), faktyczna instancja obiektu będzie mogła przechowywać tylko obiekty zgodne z tym typem. Nie ma potrzeby powielania tej informacji.

Podsumowanie

W ten sposób udało nam się rozpocząć wątek typów generycznych w Javie, choć tak naprawdę omówiliśmy jedynie typy w ramach kolekcji. Tematyka generyków jest nieco bardziej rozbudowana, dlatego będziemy ją kontynuować w kolejnym rozdziale. W przypadku kolekcji polecamy wrócić na chwilę do rozdziałów, w których opisywaliśmy listy (Kolekcje - Listy), sety (Kolekcje - Sety) i mapy (Kolekcje - Mapy). Wprowadziliśmy tam kilka generycznych deklaracji, którym z pewnością warto się przyjrzeć raz jeszcze.
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