Kolekcje i typy generyczne są wdzięcznym tematem na egzaminie, zwłaszcza przy próbach integracji ich z kodem używającym typów niegenerycznych, a także wtedy, gdy do koła fortuny dodamy jeszcze zwykłe tablice.

Które z linii prawidłowo deklarują 5-elementową tablicę list?
List<?>[] x1 = new ArrayList<?>[5];
List<?>[] x2 = new ArrayList<Integer>[5];
List<Integer>[] x3 = new ArrayList<?>[5];
List<Integer>[] x4 = new ArrayList<Integer>[5];

Tylko linia pierwsza nie spowoduje błędu kompilacji. Jak to możliwe, skoro w większości tekstów o typach generycznych wbijają nam do główy, że wildcard (?) nie może być używany przy tworzeniu obiektów?

Przy tworzeniu obiektów faktycznie nie może być używany, lecz nie tworzymy tutaj żadnego konkretnego obiektu typu ArrayList, a jedynie tablicę referencji. Linia kodu

List<?> x1 = new ArrayList<?>();

faktycznie nie skompilowałaby się - zgodnie z powyższym, nie można tworzyć konkretnych obiektów z użyciem wildcardów.

Co jest zatem powodem, że jedynie pierwsza linia w przykładzie z tablicami prawidłowo się skompiluje, a niemożliwe jest stworzenie tablicy listy parametryzowanej dowolnym typem, tutaj - Integerem? Cały problem leży w istotnych cechach implementacji zarówno tablic jak i typów generycznych.

W Javie typy generyczne implementowane są poprzez wymazywanie typów (ang. type erasure). Informacje o parametryzacji dostępne są jedynie na etapie kompilacji, potem natomiast są usuwane i w skompilowanym kodzie nie ma śladu po parametryzacji.

Istotnie przeczy to silnemu typowaniu tablic, których dokładne informacje o typie zachowywane są po kompilacji i zgodność typów sprawdzana jest także w czasie wykonania. Ilustruje to przykład:

Integer[] x7 = new Integer[5];
Number[] x8 = x7;
x8[0] = 3.0;
System.out.println(x8[0]);

Tablica Integerów rzutowana jest do tablicy typu Number (co jest możliwe dzięki polimorfizmowi, który w przypadku tablic działa tak samo jak dla pojedynczych referencji). Po takim rzutowaniu możliwe jest przypisanie liczby typu double (dokładnie - obiektu typu Double, następuje autoboxing) do naszej tablicy. Kompilator nie wygeneruje błędu, nie wie bowiem, że pierwotnie tablica ta stworzona była do przechowywania liczb całkowitych (pozwalam sobie tutaj na pewną nieścisłość, żeby ciągle nie pisać o referencjach do wrapperów). Co się natomiast stanie po uruchomieniu tego kodu?

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double at Main.main(Main.java:19)

Pojawi się wyjątek ArrayStoreException - informacja o typie tablicy została zachowana, program w trakcie wykonania wie, że w tablicy mogą znajdować się jedynie Integery. Zachowanie informacji o typach w czasie wykonania w przypadku tablic zostało w Javie wymuszone poprzez ograniczenie tego, jakie typy mogą zostać użyte przy deklaracji tablicy.

Są to tzw. reifiable types, co w skrócie oznacza typy, które są kompletne i nie tracą żadnej części informacji o sobie po kompilacji. Do typów tych zaliczamy:

  • Prymitywy
  • Typy nieparametryzowane
  • Typy parametryzowane, w których wszystkie argumenty są wildcardami <?>.
  • "Gołe" typy - niegeneryczne kolekcje (np. List, ArrayList, Map)
  • Tablice zbudowane w oparciu o reifiable types

Użycie typów reifikowanych (znacie jakieś sensowne tłumaczenie na to słowo używane w tym kontekście?) pozwala nam zatem na stworzenie tablicy kolekcji. Tracimy jednak na tym kontrolę na etapie kompilacji tego, co robimy z naszymi kolekcjami - kompilator nie zablokuje już możliwości dodania elementu złego typu.

List<?>[] x1 = new List<?>[5];
x1[0] = Arrays.asList(3.0);
x1[1] = Arrays.asList(3);
System.out.println(x1[0].get(0));
System.out.println(x1[1].get(0));

Nie mając informacji o typach bez trudu do tablicy list dodamy listę liczb całkowitych oraz listę liczb zmiennoprzecinkowych. Kompilator nam w niczym nie pomoże, bo niby jak? Czy zatem jest mozliwość obejścia problemu i użycia w tablicach typów parametryzowanych?

Po części. Legalne jest zadeklarowanie referencji z typem parametryzowanym.

List<Integer>[] x1;

Jest jak najbardziej w porządku. Problemem jest inicjalizacja. Czy z pomocą może przyjść rzutowanie?

List<Integer>[] x1 = (List<Integer>[])new List[5];

Kod się prawidłowo skompiluje i uruchomi. Również IDE oraz kompilator pomogą nam w pilnowaniu prawidłowo dodawanych typów do tej tablicy.

List<Integer>[] x1 = (List<Integer>[])new List[5];
x1[0] = Arrays.asList(3);
x1[0] = Arrays.asList(3.0);

zaowocuje błędem kompilacji

Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - incompatible types required: java.util.List found: java.util.List at Main.main(Main.java:9)

musimy jednak pamiętać, że o ile kompilator pilnuje nas na etapie kompilacji, o tyle - mimo iż jest to tablica - nie mamy kontroli nad czasem wykonania. Z powodu wymazywania typów w czasie wykonania informacje o parametryzacji typem Integer zostają bezpowrotnie utracone. Nie uświadczymy wyjątku ArrayStoreException. Łatwo to udowodnić:

List<Integer>[] x1 = (List<Integer>[])new List[5];
List<? extends Number>[] x2 = x1;
x2[0] = Arrays.asList(3);
x2[1] = Arrays.asList(3.0);
System.out.println(x2[0].get(0));
System.out.println(x2[1].get(0));

Przedstawiony kod skompiluje się, i... zadziała. Z wynikiem:

3
3.0

I na to niestety lekarstwa już nie ma. Jedyne, co nam pozostaje, to trzymać się reguły, by w publicznym API nigdy nie wystawiać tablic opartych o typy niereifikowane (Principle of Indecent Exposure).

Z punktu widzenia SCJP, najważniejszy jest sam początek - która z czterech przedstawionych linii jest prawidłowa. Reszta to bardzo pobieżne potraktowanie tematu, o którym można mówić długo, ale moja widze mi na to nie pozwala ;)

Zainteresowanych tematem typów parametryzowanych, kolekcji, reifikacji a także innych sposobów na walczenie z takimi problemami (w tym z użyciem Reflection API) zachęcam do sięgnięcia po Java - Generics & Collections O'Reilliego. Wpis został zainspirowany i powstał w oparciu o idee przedstawione w rozdziale 6 tej książki.

Komentarze do wpisu "Tablice a kolekcje parametryzowane - reifiable types":

1. Anonim napisał(a):
09 lipca 2009, 21:14:35

Tak się patrze i sie zastanawiam to jest a to java ;-[

Dodaj komentarz: