Cześć! Kontynuujemy cykl wykładów na temat leków generycznych. Wcześniej ustaliliśmy ogólnie, co to jest i dlaczego jest potrzebne. Dzisiaj porozmawiamy o niektórych cechach leków generycznych i przyjrzymy się pewnym pułapkom podczas pracy z nimi. Iść! W ostatnim wykładzie mówiliśmy o różnicy pomiędzy typami ogólnymi i typami surowymi . Jeśli zapomniałeś, Raw Type jest klasą ogólną, z której usunięto jej typ.
List list = new ArrayList();
Oto przykład. Nie określamy tutaj, jaki rodzaj obiektów będzie umieszczany w naszym pliku List
. Jeśli spróbujemy go utworzyć List
i dodać do niego jakieś obiekty, w IDEa pojawi się ostrzeżenie:
“Unchecked call to add(E) as a member of raw type of java.util.List”.
Ale rozmawialiśmy też o tym, że typy generyczne pojawiły się tylko w wersji języka Java 5. Do czasu jej wydania programiści napisali dużo kodu przy użyciu typów surowych, aby nie przestał działać, możliwość tworzenie i praca z typami surowymi w Javie została zachowana. Problem ten okazał się jednak znacznie szerszy. Jak wiadomo, kod Java jest konwertowany na specjalny kod bajtowy, który jest następnie wykonywany przez wirtualną maszynę Java. A jeśli podczas procesu tłumaczenia umieścimy w kodzie bajtowym informacje o typach parametrów, zepsuje to cały wcześniej napisany kod, ponieważ przed Javą 5 nie istniały żadne typy parametrów! Pracując z lekami generycznymi, należy pamiętać o jednej bardzo ważnej funkcji. Nazywa się to wymazywaniem typu. Jej istota polega na tym, że wewnątrz klasy nie jest przechowywana żadna informacja o typie jej parametru. Informacje te są dostępne jedynie na etapie kompilacji i są usuwane (stają się niedostępne) w czasie wykonywania. Jeśli spróbujesz umieścić obiekt niewłaściwego typu w swoim List<String>
kompilator zgłosi błąd. To właśnie osiągnęli twórcy języka, tworząc generyki – sprawdzenia na etapie kompilacji. Ale kiedy cały napisany kod Java zamieni się w kod bajtowy, nie będzie żadnych informacji o typach parametrów. Wewnątrz kodu bajtowego lista List<Cat>
kotów nie będzie się różnić od List<String>
ciągów znaków. Nic w kodzie bajtowym nie będzie mówiło, że cats
jest to lista obiektów Cat
. Informacje na ten temat zostaną usunięte podczas kompilacji, a do kodu bajtowego trafi jedynie informacja, że masz w programie określoną listę List<Object> cats
. Zobaczmy jak to działa:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
Stworzyliśmy własną klasę generyczną TestClass
. Jest to dość proste: w zasadzie jest to mały „zbiór” 2 obiektów, które są tam umieszczane natychmiast po utworzeniu obiektu. Ma 2 obiekty jako pola T
. Po wykonaniu metody createAndAdd2Values()
dwa przekazane obiekty należy rzucić Object a
na Object b
nasz typ T
, po czym zostaną dodane do obiektu TestClass
. W metodzie , main()
którą tworzymy TestClass<Integer>
, to znaczy w jakości, T
którą będziemy mieli Integer
. Ale jednocześnie createAndAdd2Values()
przekazujemy do metody liczbę Double
i obiekt String
. Czy sądzisz, że nasz program się sprawdzi? W końcu określiliśmy jako typ parametru Integer
, ale String
z pewnością nie można go rzutować na Integer
! Uruchommy metodę main()
i sprawdźmy. Dane wyjściowe konsoli: 22.111 Ciąg testowy Nieoczekiwany wynik! Dlaczego się to stało? Właśnie z powodu usunięcia typu. Podczas kompilacji kodu została usunięta informacja o typie parametru Integer
naszego obiektu . TestClass<Integer> test
Zamienił się w TestClass<Object> test
. Nasze parametry zostały bez problemu przekształcone w Double
( a nie w , jak się spodziewaliśmy!) i po cichu dodane do . Oto kolejny prosty, ale bardzo ilustrujący przykład usuwania typów: String
Object
Integer
TestClass
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
Dane wyjściowe konsoli: true true Wygląda na to, że stworzyliśmy kolekcje z trzema różnymi typami parametrów - String
, Integer
i utworzoną przez nas klasą Cat
. Ale podczas konwersji na kod bajtowy wszystkie trzy listy zmieniły się w List<Object>
, więc po uruchomieniu program informuje nas, że we wszystkich trzech przypadkach używamy tej samej klasy.
Wpisz erasure podczas pracy z tablicami i typami rodzajowymi
Jest jeden bardzo ważny punkt, który należy jasno zrozumieć podczas pracy z tablicami i typami rodzajowymi (na przykładList
). Warto to również rozważyć przy wyborze struktury danych dla swojego programu. Typy generyczne podlegają wymazaniu. Informacja o typie parametru nie jest dostępna w trakcie wykonywania programu. Natomiast tablice znają i mogą wykorzystywać informacje o swoim typie danych podczas wykonywania programu. Próba umieszczenia wartości niewłaściwego typu w tablicy spowoduje wygenerowanie wyjątku:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Wyjście konsoli:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Ponieważ istnieje tak duża różnica między tablicami a tablicami rodzajowymi, mogą wystąpić problemy ze zgodnością. Po pierwsze, nie można utworzyć tablicy obiektów ogólnych ani nawet tablicy z określonym typem. Brzmi trochę mylące? Przyjrzyjmy się bliżej. Na przykład nie możesz tego zrobić w Javie:
new List<T>[]
new List<String>[]
new T[]
Jeśli spróbujemy utworzyć tablicę list List<String>
, otrzymamy ogólny błąd kompilacji tworzenia tablicy:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
//ошибка компиляции! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
Ale dlaczego to zrobiono? Dlaczego tworzenie takich tablic jest zabronione? Wszystko po to, aby zapewnić bezpieczeństwo typu. Gdyby kompilator pozwolił nam na utworzenie takich tablic z obiektów ogólnych, moglibyśmy mieć mnóstwo kłopotów. Oto prosty przykład z książki Joshuy Blocha „Efektywna Java”:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
Wyobraźmy sobie, że tworzenie tablic List<String>[] stringLists
byłoby dozwolone, a kompilator nie narzekałby. Oto, co moglibyśmy zrobić w tym przypadku: W linii 1 tworzymy tablicę arkuszy List<String>[] stringLists
. Nasza tablica zawiera jeden List<String>
. W linii 2 tworzymy listę liczb List<Integer>
. W linii 3 przypisujemy naszą tablicę List<String>[]
do zmiennej Object[] objects
. Język Java pozwala na to: X
możesz umieścić zarówno obiekty X
, jak i obiekty wszystkich klas podrzędnych w tablicy obiektów Х
. W związku z tym Objects
możesz umieścić w tablicy wszystko. W linii 4. zastępujemy pojedynczy element tablicy objects (List<String>)
listą List<Integer>
. W rezultacie umieściliśmy List<Integer>
w naszej tablicy, która była przeznaczona wyłącznie do przechowywania List<String>
! Błąd napotkamy dopiero, gdy kod dotrze do linii 5. Podczas wykonywania programu zostanie zgłoszony wyjątek ClassCastException
. Dlatego do języka Java wprowadzono zakaz tworzenia takich tablic – pozwala to uniknąć takich sytuacji.
Jak mogę ominąć usuwanie typu?
Cóż, dowiedzieliśmy się o wymazywaniu typów. Spróbujmy oszukać system! :) Zadanie: Mamy klasę ogólnąTestClass<T>
. Musimy stworzyć w nim metodę createNewT()
, która utworzy i zwróci nowy obiekt typu Т
. Ale tego nie da się zrobić, prawda? Wszystkie informacje o typie Т
zostaną usunięte podczas kompilacji, a podczas działania programu nie będziemy w stanie dowiedzieć się, jakiego rodzaju obiekt musimy utworzyć. W rzeczywistości jest jeden trudny sposób. Prawdopodobnie pamiętasz, że w Javie istnieje klasa Class
. Za jego pomocą możemy uzyskać klasę dowolnego z naszych obiektów:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
Wyjście konsoli:
class java.lang.Integer
class java.lang.String
Ale jest jedna funkcja, o której nie rozmawialiśmy. W dokumentacji Oracle zobaczysz, że Klasa jest klasą ogólną! Dokumentacja mówi: „T to typ klasy modelowany przez ten obiekt klasy”. Jeśli przetłumaczymy to z języka dokumentacji na język ludzki, oznacza to, że klasa obiektu Integer.class
to nie tylko Class
, ale Class<Integer>
. Typ obiektu string.class
to nie tylko Class
, Class<String>
, itp. Jeśli nadal nie jest jasne, spróbuj dodać parametr typu do poprzedniego przykładu:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
A teraz korzystając z tej wiedzy możemy ominąć kasowanie typów i rozwiązać nasz problem! Spróbujmy uzyskać informację o typie parametru. Jej rolę pełnić będzie klasa MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
Oto jak wykorzystujemy nasze rozwiązanie w praktyce:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
Wyjście konsoli:
Объект секретного класса успешно создан!
Po prostu przekazaliśmy wymagany parametr klasy do konstruktora naszej klasy ogólnej:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Dzięki temu zapisaliśmy informację o typie parametru i zabezpieczyliśmy ją przed usunięciem. W rezultacie udało nam się stworzyć obiekt T
! :) Na tym zakończymy dzisiejszy wykład. Podczas pracy z typami generycznymi zawsze należy pamiętać o wymazywaniu typów. Nie wygląda to zbyt wygodnie, ale musisz zrozumieć, że typy generyczne nie były częścią języka Java w momencie jego tworzenia. Jest to później dodana funkcja, która pomaga nam tworzyć kolekcje z typem i wychwytywać błędy na etapie kompilacji. Niektóre inne języki, w których języki generyczne istnieją od wersji 1, nie mają możliwości wymazywania typów (na przykład C#). Jednak nie skończyliśmy studiować leków generycznych! W następnym wykładzie poznasz jeszcze kilka możliwości pracy z nimi. W międzyczasie miło byłoby rozwiązać kilka problemów! :)
GO TO FULL VERSION