Typy generyczne

Nadszedł czas na zrozumienie pewnego zagadnienia. Już parę tygodnii temu pokazałem Ci listę – LinkedList oraz ArrayList, w których mogłeś bez problemu przechowywać obiekty.

Pamiętasz ten zapis?

List<String> names = new LinkedList<String>();

Wtedy Ci nie tłumaczyłem od czego są ostre nawiasy przy typie zmiennej, jednak dziś w końcu nadszedł czas… 😉

Ostre nawiasy

Ostre nawiasy właśnie np. w takiej linii kodu:

List<String> names = new LinkedList<String>();

Oznaczają właśnie wykorzystanie typów generycznych.

Co to jest…

Typy generyczne pozwalają na stworzenie jeszcze bardziej uniwersalnego szablonu z klasy, aniżeli z samej klasy.

Zwykła definicja klasy może wyglądać np. tak:

public class AnyClass {}

Jednak, gdy użyjemy typów generycznych wygląda ona tak:

public class AnyClass<T> {}

I znowu te nawiasy ostre…

Co nam to daje?

Typy generyczne dają nam możliwość tworzenia z klas szablonu dla obojętnie jakich typów danych lub konkretnych “rodzin” – czyli obiektów, które np. implementują dany interfejs. No ale o tym już kiedy indziej. 😉

Pamiętasz, jak tworzyłeś listę Stringów? Robiłeś to właśnie tak:

List<String> names = new LinkedList<String>();

Podczas tworzenia obiektu definiowałeś jaki typ będzie używany w obrębie listy. I jedyne co mogłeś wrzucać do tej listy to były własnie obiekty typu String.

Aby przejść jak najszybciej do przykładu (ponieważ w tej lekcji jest to konieczne) stwórzmy sobie trzy proste klasy:

Klasę Fruit

package pl.maniaq;

public class Fruit {

    public Fruit() {

    }

    public String getName() {
        return "Owoc";
    }
}

Klasę Apple dziedziczącą po Fruit 

package pl.maniaq;

public class Apple extends Fruit {
    public Apple() {
        super();
    }

    @Override
    public String getName() {
        return "Jabłko";
    }
}

Oraz klasę Strawberry dziedziczącą również po Fruit

package pl.maniaq;

public class Strawberry extends Fruit {

    public Strawberry() {
        super();
    }

    @Override
    public String getName() {
        return "Truskawka";
    }
}

Startujmy!

Mając już te trzy klasy bez problemu możemy użyć typów generycznych.

Stworzymy sobie kontener na owoce – gdzie przez konstruktor będziemy przekazywać jakiś owoc i będzie on zapisywany w prywatnym polu. Możemy zrobić to np. w taki sposób:

package pl.maniaq;

public class FruitContainer {

    private Fruit fruit;

    public FruitContainer(Fruit fruit) {
        this.fruit=fruit;
    }

    public Fruit getFruit() {
        return fruit;
    }

}

Wtedy z góry będziemy przyjmować wszystkie typy owoców, a my tego nie chcemy…

Musimy, więc użyć wcześniej wspomnianych typów generycznych!

package pl.maniaq;

public class FruitContainer<T> {

    private T fruit;

    public FruitContainer(T fruit) {
        this.fruit=fruit;
    }

    public T getFruit() {
        return fruit;
    }

}

Co oznacza litera T? Oznacza, że typ, który został przekazany do klasy podczas jego tworzenia jest podstawiony pod literę T.

W takim razie zamiast używania wszędzie gdzie tam to konieczne klasy Fruit, użyjemy litery T.

Oczywiście może to być jakaś dowolna inna litera lub slowo – jednak zazwyczaj jest to litera T.

Stwórzmy w klasie main sobie teraz trzy owoce i wrzućmy je wszystkie do naszego kontenera.

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      Fruit fruit = new Fruit();
        Apple apple = new Apple();
      Strawberry strawberry = new Strawberry();

      FruitContainer fruitContainer = new FruitContainer(fruit);
      System.out.println("Fruit container 1: " + ((Fruit) fruitContainer.getFruit()).getName());

      FruitContainer<Fruit> fruitContainer1 = new FruitContainer<>(fruit);
        System.out.println("Fruit container 2: " + fruitContainer1.getFruit().getName());

      FruitContainer<Apple> appleFruitContainer = new FruitContainer<>(apple);
        System.out.println("Apple container: " + appleFruitContainer.getFruit().getName());

      FruitContainer<Strawberry> strawberryFruitContainer = new FruitContainer<>(strawberry);
        System.out.println("Strawberry container: " + strawberryFruitContainer.getFruit().getName());
    }
}

Podczas tworzenia obiektu FruitContainer w nawiasach ostrych podaliśmy typ obiektu, jaki będzie w nim przechowywany.

Z wyjątkiem jeden linii:

FruitContainer fruitContainer = new FruitContainer(fruit);

Jak widzisz w przypadku typów generycznych można utworzyć również obiekt bez nawiasów ostrych, jednak…

System.out.println("Fruit container 1: " + ((Fruit) fruitContainer.getFruit()).getName());

Jednak kompilator niestety nie ma pewności, czy przechowywany obiekt jest owocem i ma metodę getName.

I to jest duża przewaga typów generycznych – z góry wiadomo co będzie przechowywane w danej klasie. Dlatego w kolejnych przypadkach nie musimy robić tzw. rzutowania na obiekt Fruit.

FruitContainer<Fruit> fruitContainer1 = new FruitContainer<>(fruit);
System.out.println("Fruit container 2: " + fruitContainer1.getFruit().getName());

Ograniczanie

Stworzyliśmy FruitContainer, który nie do końca jest kontenerem na owoce…

FruitContainer<String> stringFruitContainer = new FruitContainer<>("String");

W końcu możemy tam wrzucić co tylko chcemy…

Wtedy z pomocą przychodzi nam ograniczenia typów – czyli słowo extends.

public class FruitContainer<T extends Fruit> {

Dzięki temu możemy zawęzić zbiór obiektów, które pasują do naszego kontenera – czyli dziedziczą po danej klasie lub implementują dany interfejs. W naszym przypadku są to wszystkie owoce. 😉

Wincy typów!

Oczywiście można wprowadzać też do klasy więcej parametrów – tak naprawdę tyle ile tylko chcemy.

Możemy, więc zrefaktorować naszą klasę, aby przechowywała dwa typy owoców:

public class FruitContainer<T extends Fruit, S extends Fruit> {

A cała klasa wygląda teraz tak:

package pl.maniaq;

public class FruitContainer<T extends Fruit, S extends Fruit> {

    private T firstFruit;
    private S secondFruit;


    public FruitContainer(T firstFruit, S secondFruit) {
        this.firstFruit = firstFruit;
        this.secondFruit = secondFruit;
    }


    public T getFirstFruit() {
        return firstFruit;
    }

    public S getSecondFruit() {
        return secondFruit;
    }
}

Jeszcze szybka zmiana main:

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      Fruit fruit = new Fruit();
        Apple apple = new Apple();
      Strawberry strawberry = new Strawberry();


      FruitContainer<Strawberry, Apple> fruitContainer = new FruitContainer<>(strawberry, apple);
        System.out.println("Fruit container: " + fruitContainer.getFirstFruit().getName() + ", " + fruitContainer.getSecondFruit().getName());

    }
}

I po uruchomieniu otrzymujemy:

Fruit container: Truskawka, Jabłko

Podsumowanie

Typy generyczne największe zastosowanie mają w przypadku tworzenia kolekcji – czyli klas odpowiedzialnych za przechowywanie i operację na obiektach. Dzięki typom generycznym, z góry możemy ustalić jakie obiekt mają być dostarczane do klasy przez co obiekty nie zostaną pomieszczane – żaden inny typ nie dostanie się do naszej klasy. 😉

Choć generyki są głównie wykorzystywane dopiero podczas używania Springa to i tak nie zaszkodzi przećwiczyć tej lekcji. W tym celu wykonaj jedno zadanie – mówią, że każdy programista powinien je umieć wykonać. Nie poddawaj się i do dzieła! 😉

  1. Napisz klasę Stos – Stack. Stos jest strukturą danych, czyli kolekcją (podobnie jak lista), na którą można wkładać obiekt i zdejmować tylko z góry. Wygląda to trochę jak stos talerzy – możemy brać tylko z samej góry i kłaść na samą górę.
  2. W ramach podpowiedzi: W klasie Stack musisz mieć zdefiniowaną tablicę o jakimś początkowym rozmiarze – np. 10.
  3. W przypadku, gdy będziesz chciał dodać 11 element metodą add musisz przekopiować całą tablicę do nowej – większe np. o rozmiarze 20.
  4. Musisz pamiętać indeks ostatniego elementu w swojej tablicy – metoda pop zwraca element z pod tego indeksu i wyrzuca go z tablicy. Podczas usuwania elementu wstawiaj null na miejsce usuwanego obiektu.
  5. Pamiętaj o typach generycznych – na poczatku polecam Ci stworzyć Stack bez typu generycznego, a na sam koniec go dodać.

Moje rozwiązanie możesz zobaczyć tutaj.

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x