Historia pytania.
Java wprowadziła generiki w wersji 5, używając zatarcia typów, aby zapewnić wsteczną kompatybilność binarną ze starszym kodem. Tablice są jednak reifikowane – niosą swój typ komponentu (Class) w czasie wykonywania, aby wymusić kontrole ArrayStoreException podczas wstawiania elementów. Ponieważ generyczne parametry typów, takie jak T, są zatarte do swoich ograniczeń (zwykle Object) w kodzie bajtowym, JVM nie może rozwiązać T do konkretnej klasy w czasie wykonywania, co tworzy nieusuwalny rozłam między systemem typów w czasie kompilacji a weryfikacją tablic w czasie wykonywania.
Problem.
Gdyby kompilator zezwolił na new T[10], generowany kod bajtowy zainicjowałby Object[], podczas gdy zmienna referencyjna twierdziłaby, że to T[]. Ta niezgodność umożliwia zanieczyszczenie sterty: do tablicy typu String[] (która w rzeczywistości wskazuje na Object[]) można by wprowadzić Integer, omijając strażnika typów JVM. Korupcja pozostałaby ukryta, aż do momentu, gdy operacja odczytu wywołałaby ClassCastException daleko od pierwotnego punktu wstawienia, naruszając gwarancję statycznego bezpieczeństwa typów w Javie i utrudniając debugowanie.
Rozwiązanie.
Programiści muszą unikać bezpośredniej instancji na rzecz bezpiecznych typowo alternatyw. Metoda java.lang.reflect.Array.newInstance(Class<T>, int) tworzy tablicę z odpowiednim typem komponentu Class w czasie wykonywania. Alternatywnie, można używać Object[] z wyraźnym rzutowaniem przy pobieraniu (z tłumieniem ostrzeżeń za pomocą @SuppressWarnings("unchecked")), lub lepiej, zastąpić tablice ArrayList<T> lub innymi kolekcjami, które w pełni przyjmują system typów generycznych bez konieczności tworzenia tablic w czasie wykonywania.
Opis problemu.
Podczas projektowania biblioteki algebry liniowej o wysokiej wydajności, zespół potrzebował generycznej Macierzy<T>, aby obsługiwać Double, Complex i własne typy numeryczne bez narzutu związane z opakowaniem w ArrayList<T>. Wewnętrzne przechowywanie wymagało dwuwymiarowej tablicy T[][] dla lokalności pamięci i surowej szybkości. Wyzwanie polegało na zainicjowaniu T[][] w konstruktorze bez wywoływania błędów kompilacji lub wprowadzania subtelnych luk w bezpieczeństwie typów, które mogłyby zniszczyć wyniki numeryczne.
Rozwiązanie 1: Niebezpieczne rzutowanie tablicy Object[].
Jedna z propozycji polegała na rzutowaniu (T[][]) new Object[rows][cols] i tłumieniu ostrzeżenia o niebezpiecznym rzutowaniu za pomocą adnotacji. To podejście nie wprowadzało dodatkowego narzutu na wydajność i dawało kontrolę nad układem pamięci. Stworzyło to jednak kruchą umowę: jeśli Macierz udostępniłaby swoją wewnętrzną tablicę za pośrednictwem getter’a, zewnętrzny kod mógłby zanieczyścić stertę, wstawiając niekompatybilne typy, co prowadziłoby do awarii ClassCastException podczas mnożenia macierzy, które byłyby prawie niemożliwe do prześledzenia do pierwotnego punktu korupcji.
Rozwiązanie 2: Rzutowanie elementów z przechowaniem Object.
Inna opcja polegała na przechowywaniu danych jako Object[][] i rzutowaniu poszczególnych elementów na T przy każdym odczycie. To gwarantowało natychmiastowe wykrywanie niezgodności typów w miejscu pobrania, znacznie upraszczając debugowanie. Wadą było znaczące pogrubienie kodu i mierzalna strata wydajności o 5-10% w ciasnych pętlach obliczeniowych z powodu powtarzających się instrukcji checkcast w kodzie bajtowym, co zniweczyło główny cel biblioteki, jakim była zgodność z wydajnością natywnych tablic.
Rozwiązanie 3: Odbicie poprzez Array.newInstance().
Zespół ostatecznie wykorzystał Array.newInstance(componentType, rows, cols), wymagając, aby wywołujący dostarczył token Class<T>. To generowało tablicę z precyzyjnym typem w czasie wykonywania, całkowicie zapobiegając zanieczyszczeniu sterty, jednocześnie utrzymując surową szybkość natywnych tablic. Koszt jednorazowej instancji refleksyjnej podczas tworzenia macierzy był znikomy w porównaniu do obciążenia obliczeniowego O(n³) operacji macierzowych, a rozwiązanie zapewniało bezpieczeństwo typów w czasie kompilacji bez niebezpiecznych rzutowań lub narzutu przy dostępie.
Wynik.
Biblioteka została dostarczona z zerowymi zgłoszonymi błędami ArrayStoreException lub ClassCastException w ciągu trzech lat intensywnego użytkowania w aplikacjach finansowych ilościowych. Podejście refleksyjne pozwoliło na bezproblemowe wsparcie zarówno dla opakowań prymitywnych, jak i złożonych typów własnych, podczas gdy ścisła kontrola typów zapobiegła cichej korupcji danych w kluczowych obliczeniach finansowych. Testy wydajności potwierdziły, że jednorazowy narzut refleksyjny pozostał znikomy w porównaniu do kosztów obliczeniowych operacji macierzowych.
**Dlaczego tablica wildcard List<?>[]** unika pułapek bezpieczeństwa typów, które dotykają **List<String>[]**, mimo że obie są tablicami typów parametryzowanych?** **List<?>[] reprezentuje tablicę nieznanych list generycznych, co kompilator traktuje jako tablicę typów surowych, z krytycznym ograniczeniem, że nie możesz dodać żadnego elementu różnego od null (ponieważ nie może zweryfikować zgodności typów). List<String>[] sugerowałoby tablicę, w której każdy element jest gwarantowany jako List<String>, ale po zatarciu JVM widzi tylko List[]. Gdyby to było dozwolone, mógłbyś przypisać List<Integer> do elementu tablicy (ponieważ w czasie wykonywania jest to po prostu List), a następnie pobrać jako List<String> i napotkać ClassCastException podczas dostępu do elementów. Wariant z wildcardami zapobiega temu, całkowicie zabraniając zapisów, zachowując bezpieczeństwo typów poprzez ograniczenia niezmienności.
Jak wywołanie metody varargs cicho instancjuje generyczną tablicę w miejscu wywołania, a dlaczego @SafeVarargs maskuje, a nie rozwiązuje ryzyko zanieczyszczenia sterty?
Przy deklaracji void process(T... items) kompilator syntetyzuje tablicę T[] do przechowywania argumentów, która w rzeczywistości staje się Object[] po zatarciu. Adnotacja @SafeVarargs tłumi ostrzeżenie kompilatora, ale nie zmienia kodu bajtowego; metoda nadal otrzymuje Object[], które udaje T[]. Niebezpieczeństwo pozostaje: jeśli metoda przechowuje tablicę items w polu lub pozwala jej uciec, a ta tablica zawiera elementy, które nie są T (możliwe dzięki zanieczyszczeniu sterty z miejsca wywołania), kolejne odczyty wywołują ClassCastException. Prawdziwe bezpieczeństwo wymaga defensywnego skopiowania items do ArrayList<T> lub użycia Array.newInstance w ciele metody.
Dlaczego używając Arrays.copyOf lub System.arraycopy z tablicami generycznymi, ClassCastException może wystąpić, nawet gdy źródło i cel wydają się zgodne typowo, i jak Class.getComponentType() zapewnia rozwiązanie?
Arrays.copyOf wewnętrznie używa Array.newInstance z klasą czasu wykonania oryginalnej tablicy. Jeśli posiadasz T[], która została stworzona poprzez niebezpieczne rzutowanie z Object[], jej typ komponentu to Object, a nie T. Podczas kopiowania za pomocą Arrays.copyOf(original, newLength) otrzymujesz Object[], który nie może być rzutowany na T[], co natychmiast generuje ClassCastException. Rozwiązanie polega na oddzielnym śledzeniu tokena Class<T> i wywołaniu Array.newInstance(componentType, length) zamiast polegać na własnym obiekcie klasy tablicy, zapewniając, że nowa tablica odpowiada zamierzonym typom generycznym, a nie zatartej implementacji.