JavaRush /Blog Java /Random-PL /Generyki dla kotów
Viacheslav
Poziom 3

Generyki dla kotów

Opublikowano w grupie Random-PL
Generyki dla kotów - 1

Wstęp

Dziś jest świetny dzień, aby przypomnieć sobie, co wiemy o Javie. Według najważniejszego dokumentu, tj. Specyfikacja języka Java (JLS - Java Language Specifiaction), Java jest językiem silnie typowanym, jak opisano w rozdziale „ Rozdział 4. Typy, wartości i zmienne ”. Co to znaczy? Powiedzmy, że mamy metodę główną:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
Silne typowanie gwarantuje, że podczas kompilacji tego kodu kompilator sprawdzi, czy jeśli określiliśmy typ zmiennej tekstowej jako String, to nie próbujemy jej nigdzie użyć jako zmiennej innego typu (na przykład jako Integer) . Na przykład, jeśli spróbujemy zapisać wartość zamiast tekstu 2L(tj. wartość długą zamiast ciągu), podczas kompilacji pojawi się błąd:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Te. Silne pisanie pozwala mieć pewność, że operacje na obiektach będą wykonywane tylko wtedy, gdy te operacje są dla nich dozwolone. Nazywa się to również bezpieczeństwem typu. Jak stwierdzono w JLS, w Javie istnieją dwie kategorie typów: typy pierwotne i typy referencyjne. O typach prymitywnych pamiętacie z artykułu przeglądowego: „ Typy prymitywne w Javie: nie są takie prymitywne ”. Typy referencyjne mogą być reprezentowane przez klasę, interfejs lub tablicę. A dzisiaj będziemy zainteresowani typami referencyjnymi. Zacznijmy od tablic:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Ten kod działa bez błędów. Jak wiemy (na przykład z „ Samouczek Java Oracle: Tablice ”), tablica to kontener przechowujący dane tylko jednego typu. W tym przypadku - tylko linie. Spróbujmy dodać long do tablicy zamiast String:
text[1] = 4L;
Uruchommy ten kod (na przykład w Repl.it Online Java Compiler ) i otrzymamy błąd:
error: incompatible types: long cannot be converted to String
Tablica i bezpieczeństwo typów języka nie pozwalały na zapisanie w tablicy tego, co nie pasowało do typu. Jest to przejaw bezpieczeństwa typu. Powiedziano nam: „Napraw błąd, ale do tego czasu nie będę kompilował kodu”. A najważniejsze jest to, że dzieje się to w momencie kompilacji, a nie podczas uruchamiania programu. Oznacza to, że błędy dostrzegamy natychmiast, a nie „kiedyś”. A skoro już wspomnieliśmy o tablicach, pamiętajmy też o Java Collections Framework . Mieliśmy tam różne struktury. Na przykład listy. Przepiszmy przykład:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Podczas kompilacji otrzymamy testbłąd w linii inicjującej zmienną:
incompatible types: Object cannot be converted to String
W naszym przypadku List może przechowywać dowolny obiekt (czyli obiekt typu Object). Dlatego kompilator twierdzi, że nie może wziąć na siebie takiego ciężaru odpowiedzialności. Musimy więc jawnie określić typ, który otrzymamy z listy:
String test = (String) text.get(0);
To wskazanie nazywa się konwersją typu lub rzutowaniem typu. I wszystko będzie teraz działać dobrze, dopóki nie spróbujemy uzyskać elementu o indeksie 1, ponieważ jest typu Long. I otrzymamy uczciwy błąd, ale już podczas działania programu (w czasie wykonywania):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Jak widzimy, jest tu kilka istotnych wad. Po pierwsze, zmuszeni jesteśmy „rzucić” wartość uzyskaną z listy na klasę String. Zgadzam się, to jest brzydkie. Po drugie, w przypadku błędu zobaczymy go dopiero podczas wykonywania programu. Gdyby nasz kod był bardziej złożony, moglibyśmy nie wykryć takiego błędu od razu. A programiści zaczęli myśleć o tym, jak ułatwić pracę w takich sytuacjach i uczynić kod bardziej przejrzystym. I tak się urodzili – Generics.
Generyki dla kotów - 2

Genetyki

Zatem leki generyczne. Co to jest? Rodzajowy to specjalny sposób opisywania używanych typów, którego kompilator kodu może używać w swojej pracy, aby zapewnić bezpieczeństwo typów. Wygląda to mniej więcej tak:
Generyki dla kotów - 3
Oto krótki przykład i wyjaśnienie:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
W tym przykładzie mówimy, że mamy nie tylko List, ale List, co działa TYLKO z obiektami typu String. I żadnych innych. To, co wskazano w nawiasach, możemy to zapisać. Takie „nawiasy” nazywane są „nawiasami kątowymi”, tj. nawiasy kątowe. Kompilator uprzejmie sprawdzi, czy nie popełniliśmy błędów podczas pracy z listą ciągów znaków (lista nazywa się tekstem). Kompilator zobaczy, że bezczelnie próbujemy umieścić Long na liście String. A w czasie kompilacji wyświetli się błąd:
error: no suitable method found for add(long)
Być może pamiętasz, że String jest potomkiem CharSequence. I zdecyduj się na coś takiego:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Ale nie jest to możliwe i otrzymamy błąd: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> Wydaje się to dziwne, ponieważ. linia CharSequence sec = "test";nie zawiera błędów. Rozwiążmy to. Mówią o tym zachowaniu: „Części rodzajowe są niezmienne”. Co to jest „niezmiennik”? Podoba mi się, jak napisano o tym na Wikipedii w artykule „ Kowariancja i kontrawariancja ”:
Generyki dla kotów - 4
Zatem niezmienność to brak dziedziczenia między typami pochodnymi. Jeśli Kot jest podtypem Animals, wówczas Set<Cats> nie jest podtypem Set<Animals>, a Set<Animals> nie jest podtypem Set<Cats>. Swoją drogą warto powiedzieć, że począwszy od Java SE 7 pojawił się tzw. „ operator diamentowy ”. Ponieważ dwa nawiasy ostrokątne <> są jak diament. Dzięki temu możemy używać typów generycznych w następujący sposób:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Na podstawie tego kodu kompilator rozumie, że jeśli po lewej stronie wskazaliśmy, że będzie Listzawierał obiekty typu String, to po prawej stronie mamy na myśli, że chcemy zapisać linesnową ArrayList do zmiennej, która również będzie przechowywać obiekt typu określonego po lewej stronie. Zatem kompilator z lewej strony rozumie lub wnioskuje typ dla prawej strony. Dlatego to zachowanie nazywa się wnioskowaniem o typie lub „wnioskowaniem o typie” w języku angielskim. Kolejną interesującą rzeczą wartą odnotowania są typy RAW lub „typy surowe”. Ponieważ Typy generyczne nie zawsze były dostępne, a Java stara się zachować kompatybilność wsteczną, gdy tylko jest to możliwe, wtedy generyczne są zmuszone w jakiś sposób pracować z kodem, w którym nie określono żadnego generycznego. Zobaczmy przykład:
List<CharSequence> lines = new ArrayList<String>();
Jak pamiętamy, taka linia nie zostanie skompilowana ze względu na niezmienność typów generycznych.
List<Object> lines = new ArrayList<String>();
Ten też się nie skompiluje, z tego samego powodu.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Takie linie zostaną skompilowane i będą działać. To właśnie w nich wykorzystywane są typy surowe, czyli tzw. nieokreślone typy. Jeszcze raz warto podkreślić, że typy surowe NIE POWINNY być używane we współczesnym kodzie.
Generyki dla kotów - 5

Wpisane klasy

Więc wpisałem klasy. Zobaczmy, jak możemy napisać własną klasę z typem. Na przykład mamy hierarchię klas:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Chcemy stworzyć klasę implementującą kontener na zwierzęta. Byłoby możliwe napisanie klasy, która zawierałaby dowolny plik Animal. To proste, zrozumiałe, ALE... mieszanie psów i kotów jest złe, one się nie przyjaźnią. Poza tym, jeśli ktoś otrzyma taki pojemnik, może omyłkowo wrzucić koty z pojemnika do stada psów… i to do niczego dobrego nie doprowadzi. I tutaj pomogą nam leki generyczne. Na przykład napiszmy implementację w ten sposób:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
Nasza klasa będzie działać z obiektami typu określonego przez rodzajnik o nazwie T. Jest to rodzaj aliasu. Ponieważ Rodzaj jest określony w nazwie klasy, wówczas otrzymamy go przy deklarowaniu klasy:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Jak widzimy, wskazaliśmy, że mamy Box, co działa tylko z Cat. Kompilator zdał sobie sprawę, że catBoxzamiast typu generycznego Tnależy zastąpić typ Catwszędzie tam, gdzie podana jest nazwa generyczna T:
Generyki dla kotów - 6
Te. to dzięki Box<Cat>kompilatorowi rozumie, co slotswłaściwie powinno być List<Cat>. Wewnątrz Box<Dog>będzie slots, zawierający List<Dog>. W deklaracji typu może znajdować się kilka typów ogólnych, na przykład:
public static class Box<T, V> {
Nazwa rodzajowa może być dowolna, chociaż zaleca się przestrzeganie kilku niepisanych zasad - „Konwencje nazewnictwa parametrów typu”: typ elementu - E, typ klucza - K, typ numeru - N, T - dla typu, V - dla typ wartości. Przy okazji, pamiętajmy, że powiedzieliśmy, że rodzaje rodzajowe są niezmienne, tj. nie zachowuj hierarchii dziedziczenia. Tak naprawdę mamy na to wpływ. Oznacza to, że mamy możliwość stworzenia generycznego COwariantu, tj. utrzymanie spadku w tej samej kolejności. To zachowanie nazywa się „typem ograniczonym”, tj. ograniczone typy. Na przykład nasza klasa Boxmoże zawierać wszystkie zwierzęta, wtedy zadeklarujemy rodzaj rodzajowy w następujący sposób:
public static class Box<T extends Animal> {
Oznacza to, że ustalamy górną granicę klasy Animal. Po słowie kluczowym możemy także określić kilka typów extends. Będzie to oznaczać, że typ, z którym będziemy pracować, musi być potomkiem jakiejś klasy i jednocześnie implementować jakiś interfejs. Na przykład:
public static class Box<T extends Animal & Comparable> {
W takim przypadku, jeśli spróbujemy wstawić Boxcoś takiego, co nie jest dziedzicem Animali nie implementuje Comparable, to podczas kompilacji otrzymamy błąd:
error: type argument Cat is not within bounds of type-variable T
Generyki dla kotów - 7

Wpisywanie metody

Generyki generyczne są używane nie tylko w typach, ale także w poszczególnych metodach. Zastosowanie metod można zobaczyć w oficjalnym poradniku: „ Metody generyczne ”.

Tło:

Generyki dla kotów - 8
Spójrzmy na ten obrazek. Jak widać, kompilator sprawdza sygnaturę metody i widzi, że jako dane wejściowe pobieramy niezdefiniowaną klasę. Nie oznacza to przez podpis, że zwracamy jakiś przedmiot, tj. Obiekt. Dlatego jeśli chcemy utworzyć, powiedzmy, ArrayList, musimy to zrobić:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Musisz wyraźnie napisać, że wynikiem będzie ArrayList, co jest brzydkie i zwiększa ryzyko popełnienia błędu. Możemy na przykład napisać takie bzdury i się skompiluje:
ArrayList object = (ArrayList) createObject(LinkedList.class);
Czy możemy pomóc kompilatorowi? Tak, leki generyczne pozwalają nam to zrobić. Spójrzmy na ten sam przykład:
Generyki dla kotów - 9
Następnie możemy stworzyć obiekt taki jak ten:
ArrayList<String> object = createObject(ArrayList.class);
Generyki dla kotów - 10

Dzika karta

Zgodnie z samouczkiem Oracle dotyczącym rodzajów ogólnych, a konkretnie z sekcją „ Znaki wieloznaczne ”, „nieznany typ” możemy opisać znakiem zapytania. Symbol wieloznaczny to przydatne narzędzie łagodzące niektóre ograniczenia typów generycznych. Na przykład, jak omówiliśmy wcześniej, typy generyczne są niezmienne. Oznacza to, że chociaż wszystkie klasy są potomkami (podtypami) typu Object, List<любой тип>nie jest on podtypem List<Object>. ALE List<любой тип>jest to podtyp List<?>. Możemy więc napisać następujący kod:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Podobnie jak zwykłe typy generyczne (tj. bez użycia symboli wieloznacznych), typy generyczne z symbolami wieloznacznymi mogą być ograniczone. Symbol wieloznaczny ograniczony górną granicą wygląda znajomo:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Ale możesz to również ograniczyć za pomocą symbolu wieloznacznego dolnej granicy:
public static void printCatList(List<? super Cat> list) {
W ten sposób metoda zacznie akceptować wszystkie koty, a także koty znajdujące się wyżej w hierarchii (aż do obiektu).
Generyki dla kotów - 11

Wpisz Usuń

Skoro już mowa o lekach generycznych, warto wiedzieć o „Wymazywaniu typu”. W rzeczywistości wymazywanie typów polega na tym, że typy generyczne stanowią informację dla kompilatora. Podczas wykonywania programu nie ma już więcej informacji o rodzajach, nazywa się to „kasowaniem”. To wymazanie powoduje zastąpienie typu ogólnego typem specyficznym. Jeśli rodzajowy nie miał granicy, wówczas zostanie zastąpiony typ Obiekt. Jeżeli określono granicę (np <T extends Comparable>. ), to zostanie ona zastąpiona. Oto przykład z samouczka Oracle: „ Usuwanie typów ogólnych ”:
Generyki dla kotów - 12
Jak powiedziano powyżej, w tym przykładzie rodzajnik Tjest wymazany do granicy, tj. zanim Comparable.
Generyki dla kotów - 13

Wniosek

Generyki to bardzo ciekawy temat. Mam nadzieję, że ten temat Cię zainteresuje. Podsumowując, typy generyczne są doskonałym narzędziem, które deweloperzy otrzymali w celu dostarczenia kompilatorowi dodatkowych informacji, aby zapewnić z jednej strony bezpieczeństwo typów, a z drugiej elastyczność. A jeśli jesteś zainteresowany, sugeruję sprawdzenie zasobów, które mi się podobały: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION