Cześć! Kontynuujemy naszą serię lekcji na temat leków generycznych. Wcześniej mieliśmy ogólne pojęcie o tym, czym one są i dlaczego są potrzebne. Dzisiaj dowiemy się więcej o niektórych funkcjach typów generycznych io pracy z nimi. Chodźmy! W ostatniej lekcji mówiliśmy o różnicy między typami ogólnymi a typami surowymi . Typ surowy to klasa ogólna, której typ został usunięty.
Dokumentacja mówi: „T - typ klasy modelowanej przez ten obiekt klasy”. Przekładając to z języka dokumentacji na zwykłą mowę, rozumiemy, że klasa obiektu
List list = new ArrayList();
Oto przykład. Tutaj nie wskazujemy jakiego typu obiekty będą umieszczane w naszym List
. Jeśli spróbujemy stworzyć taki List
i dodać do niego jakieś obiekty, w IDEA zobaczymy ostrzeżenie:
"Unchecked call to add(E) as a member of raw type of java.util.List".
Ale rozmawialiśmy też o tym, że generyki pojawiły się dopiero w Javie 5. Do czasu wydania tej wersji programiści napisali już sporo kodu przy użyciu surowych typów, więc ta funkcja języka nie mogła przestać działać, a możliwość tworzenie surowych typów w Javie zostało zachowane. Problem okazał się jednak bardziej powszechny. Jak wiesz, kod Java jest konwertowany do specjalnego skompilowanego formatu zwanego kodem bajtowym, który jest następnie wykonywany przez wirtualną maszynę Java. Ale gdybyśmy umieścili informacje o parametrach typu w kodzie bajtowym podczas procesu konwersji, zepsułoby to cały wcześniej napisany kod, ponieważ przed Javą 5 nie było parametrów typu! Podczas pracy z lekami generycznymi należy pamiętać o jednej bardzo ważnej koncepcji. Nazywa się to wymazywaniem czcionek. Oznacza to, że klasa nie zawiera informacji o parametrze typu. Informacje te są dostępne tylko podczas kompilacji i są usuwane (stają się niedostępne) przed uruchomieniem. Jeśli spróbujesz umieścić niewłaściwy typ obiektu w swoim pliku List<String>
, kompilator wygeneruje błąd. To jest dokładnie to, co twórcy języka chcą osiągnąć, tworząc generyczne: kontrole w czasie kompilacji. Ale kiedy cały twój kod Java zamienia się w kod bajtowy, nie zawiera już informacji o parametrach typu. W kodzie bajtowym twoja List<Cat>
lista kotów nie różni się od List<String>
łańcuchów. W kodzie bajtowym nic nie mówi, że cats
jest to lista Cat
obiektów. Takie informacje są usuwane podczas kompilacji — tylko fakt, że masz listę, List<Object> cats
znajdzie się w kodzie bajtowym programu. 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ą TestClass
klasę generyczną. To całkiem proste: w rzeczywistości jest to mała „kolekcja” 2 obiektów, które są przechowywane natychmiast po utworzeniu obiektu. Posiada 2 T
pola. Kiedy createAndAdd2Values()
metoda jest wykonywana, dwa przekazane obiekty ( Object a
i Object b
muszą być rzutowane na T
typ, a następnie dodane do TestClass
obiektu. W main()
metodzie tworzymy TestClass<Integer>
, tj. Integer
argument typu zastępuje Integer
parametr typu. Przekazujemy również a Double
i a String
do metoda createAndAdd2Values()
. Myślisz, że nasz program zadziała? W końcu podaliśmy Integer
jako argument typ, ale String
zdecydowanie nie można rzutować na an Integer
! Uruchommymain()
sposób i sprawdź. Wyjście konsoli:
22.111
Test String
To było niespodziewane! Dlaczego się to stało? To wynik wymazywania czcionek. Informacje o Integer
argumencie typu użytym do utworzenia instancji naszego TestClass<Integer> test
obiektu zostały usunięte podczas kompilacji kodu. Pole staje się TestClass<Object> test
. Argumenty nasze Double
i String
zostały łatwo przekonwertowane na Object
obiekty (nie są konwertowane na Integer
obiekty, jak się spodziewaliśmy!) i po cichu dodane do TestClass
. Oto kolejny prosty, ale bardzo wymowny przykład wymazywania czcionek:
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());
}
}
Wyjście konsoli:
true
true
Wygląda na to, że stworzyliśmy kolekcje z argumentami trzech różnych typów — String
, Integer
, i naszą własną Cat
klasą. Ale podczas konwersji na kod bajtowy wszystkie trzy listy stają się List<Object>
, więc po uruchomieniu programu informuje nas, że używamy tej samej klasy we wszystkich trzech przypadkach.
Wymazywanie tekstu podczas pracy z tablicami i rodzajami
Istnieje bardzo ważny punkt, który należy jasno zrozumieć podczas pracy z tablicami i klasami ogólnymi (takimi jakList
). Powinieneś również wziąć to pod uwagę przy wyborze struktur danych dla swojego programu. Generyki podlegają wymazywaniu czcionek. Informacje o parametrach typu nie są dostępne w czasie wykonywania. Natomiast tablice wiedzą o swoim typie danych i mogą z niego korzystać, gdy program jest uruchomiony. Próba umieszczenia nieprawidłowego typu w tablicy spowoduje zgłoszenie 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 rodzajami, mogą one mieć problemy ze zgodnością. Przede wszystkim nie można utworzyć tablicy obiektów ogólnych ani nawet tablicy sparametryzowanej. Czy to brzmi trochę myląco? Spójrzmy. 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<String>
obiektów, otrzymamy błąd kompilacji, który narzeka na ogólne tworzenie tablicy:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
Ale dlaczego to się robi? Dlaczego tworzenie takich tablic jest niedozwolone? Wszystko to ma na celu zapewnienie bezpieczeństwa typu. Gdyby kompilator pozwolił nam tworzyć takie tablice ogólnych obiektów, moglibyśmy zrobić sobie mnóstwo problemów. Oto prosty przykład z książki Joshua Blocha „Effective 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 utworzenie tablicy typu like List<String>[] stringLists
jest dozwolone i nie spowoduje błędu kompilacji. Gdyby to była prawda, oto kilka rzeczy, które moglibyśmy zrobić: W linii 1 tworzymy tablicę list: List<String>[] stringLists
. Nasza tablica zawiera jeden List<String>
. W linii 2 tworzymy listę liczb: List<Integer>
. W linii 3 przypisujemy nasze List<String>[]
do Object[] objects
zmiennej. Język Java na to pozwala: tablica X
obiektów może przechowywać X
obiekty i obiekty wszystkich podklas X
. W związku z tym w tablicy można umieścić wszystko Object
. W linii 4 zamieniamy jedyny element tablicy objects()
(a List<String>
) na List<Integer>
. W ten sposób umieściliśmy a List<Integer>
w tablicy, która była przeznaczona tylko do przechowywaniaList<String>
obiekty! Napotkamy błąd tylko wtedy, gdy wykonamy linię 5. A ClassCastException
zostanie wyrzucone w czasie wykonywania. W związku z tym do Javy dodano zakaz tworzenia takich tablic. Dzięki temu unikniemy takich sytuacji.
Jak obejść wymazywanie czcionek?
Cóż, dowiedzieliśmy się o wymazywaniu czcionek. Spróbujmy oszukać system! :) Zadanie: MamyTestClass<T>
klasę ogólną. Chcemy napisać createNewT()
metodę dla tej klasy, która utworzy i zwróci nowy T
obiekt. Ale to jest niemożliwe, prawda? Wszystkie informacje o T
typie są usuwane podczas kompilacji, aw czasie wykonywania nie możemy określić, jaki typ obiektu musimy utworzyć. Właściwie jest na to jeden trudny sposób. Zapewne pamiętasz, że Java ma Class
klasę. Możemy go użyć do określenia klasy 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 jeden aspekt, o którym nie mówiliśmy. W dokumentacji Oracle zobaczysz, że klasa Class jest ogólna!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
to nie tylko Class
, ale raczej Class<Integer>
. Typ obiektu String.class
to nie tylko Class
, ale raczej Class<String>
, itp. Jeśli nadal nie jest to jasne, spróbuj dodać parametr typu do poprzedniego przykładu:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
A teraz, korzystając z tej wiedzy, możemy ominąć wymazywanie czcionek i wykonać nasze zadanie! Spróbujmy uzyskać informacje o parametrze typu. Naszym argumentem typu będzie MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
A 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:
A MySecretClass object was created successfully!
Właśnie przekazaliśmy wymagany argument klasy do konstruktora naszej klasy ogólnej:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Pozwoliło nam to zachować informacje o argumencie typu, zapobiegając jego całkowitemu wymazaniu. Dzięki temu udało nam się stworzyć tzwT
obiekt! :) Na tym dzisiejsza lekcja dobiega końca. Podczas pracy z rodzajami należy zawsze pamiętać o wymazywaniu czcionek. To obejście nie wygląda na zbyt wygodne, ale powinieneś zrozumieć, że generyczne nie były częścią języka Java, kiedy został stworzony. Ta funkcja, która pomaga nam tworzyć sparametryzowane kolekcje i wychwytywać błędy podczas kompilacji, została dołączona później. W niektórych innych językach, które zawierały generyczne z pierwszej wersji, nie ma wymazywania typu (na przykład w języku C#). Nawiasem mówiąc, nie skończyliśmy studiować generyków! W następnej lekcji zapoznasz się z kilkoma dodatkowymi cechami generyków. Na razie dobrze byłoby rozwiązać kilka zadań! :)