2016-05-04 37 views
11

Grałem około z rodzajowych i stwierdził, ku mojemu zaskoczeniu, następujący kod kompiluje:Dlaczego przy korzystaniu z typów tablic nie jest bezpieczna dla java?

class A {} 
class B extends A {} 

class Generic<T> { 
    private T instance; 
    public Generic(T instance) { 
     this.instance = instance; 
    } 
    public T get(){ return instance; } 
} 

public class Main { 
    public static void main(String[] args) { 
     fArray(new B[1], new Generic<A>(new A())); // <-- No error here 
    } 

    public static <T> void fArray(T[] a, Generic<? extends T> b) { 
     a[0] = b.get(); 
    } 
} 

spodziewałbym T należy wnioskować do B. A nie obejmuje B. Dlaczego więc kompilator nie narzeka na to? Wydaje się, że

T jest wywnioskowane na Object, ponieważ mogę również przekazać Generic<Object>.

Ponadto kiedy uruchomiony kod, to rzuca ArrayStoreException na linii a[0] = b.get();.

Nie używam żadnych surowych typów ogólnych. Wydaje mi się, że wyjątkowi temu można było uniknąć z powodu błędu czasu kompilacji lub co najmniej ostrzeżenia, jeśli w rzeczywistości został on wywnioskowany jako B.


Podczas dalszych badań, z równoważnikiem List<...>:

public static void main(String[] args) { 
    fList(new ArrayList<B>(), new Generic<A>(new A())); // <-- Error, as expected 
} 

public static <T> void fList(List<T> a, Generic<? extends T> b) { 
    a.add(b.get()); 
} 

to jest produkcja i błąd:

The method fList(List<T>, Generic<? extends T>) in the type Main is not applicable for the arguments (ArrayList<B>, Generic<A>) 

jak czyni bardziej ogólny przypadek:

public static <T> void fList(List<? extends T> a, Generic<? extends T> b) { 
    a.add(b.get()); // <-- Error here 
} 

kompilator poprawnie rozpoznaje, że pierwszy numer ? może znajdować się dalej w hierarchii dziedziczenia niż drugi ?.

np. Jeśli pierwsze ? były B i drugie ? były A, to nie jest bezpieczne.


Dlaczego więc pierwszy przykład nie powoduje podobnego błędu kompilatora? Czy to tylko niedopatrzenie? Czy istnieje techniczne ograniczenie?

Jedynym sposobem mogę produkować błąd jest wyraźnie zapewniając typ:

Main.<B>fArray(new B[1], new Generic<A>(new A())); // <-- Not applicable 

naprawdę nie znaleźć niczego, poprzez własne badania, z wyjątkiem this article z 2005 roku (przed rodzajowych) , która mówi o niebezpieczeństwach kowariancji macierzy.

Tablica kowariancji wydaje się wskazywać na wyjaśnienie, ale nie mogę o tym myśleć.


Aktualny JDK jest 1.8.0.0_91

+0

Zastanawiam się, czy możesz wpaść na wymazanie typu Java w generycznych? Po prostu myśl, nie mam czasu, by spojrzeć na bliżej ATM. – Ukko

Odpowiedz

4

Rozważmy następujący przykład:

class A {} 
class B extends A {} 

class Generic<T> { 
    private T instance; 
    public Generic(T instance) { 
     this.instance = instance; 
    } 
    public T get(){ return instance; } 
} 

public class Main { 
    public static void main(String[] args) { 
     fArray(new B[1], new Generic<A>(new A())); // <-- No error here 
    } 

    public static <T> void fArray(T[] a, Generic<? extends T> b) { 
     List<T> list = new ArrayList<>(); 
     list.add(a[0]); 
     list.add(b.get()); 
     System.out.println(list); 
    } 
} 

Jak widać, podpisy używane wywnioskować parametry typu są identyczne, jedyną rzeczą, że jest inaczej jest to, że fArray() czyta tylko elementy tablicy zamiast pisać je, czyniąc T -> A wnioskowanie doskonale uzasadnione w czasie wykonywania.

I nie ma sposobu, aby kompilator mógł stwierdzić, do czego będzie używane odwołanie do tablicy w implementacji metody.

+0

@JornVernee Tak, przesadziłem, ale masz ogólny pomysł. Nie ma powodu, aby kompilator wybrał 'T -> B', gdy' T -> A' spełnia wszystkie kryteria. – biziclop

+0

Tak, to całkiem miłe. Chodzi o to, że chcesz dodać dodatkowe bezpieczeństwo, mówiąc, że '' '' '' 'musi zawsze rozszerzać' '' T''' (tzn. Typ tablicy). Tak jak robisz '' 'a = = nowy A(); A1 = a; '' 'zamiast mieć' 'Object a = new A(); A a2 = (A) a; '' 'Czasami druga gra się dobrze (podczas rzucania), ale dla pewności używamy 1-szej drogi, ponieważ ma ona typ bezpieczeństwa. –

+0

@JornVernee Fair wystarczająco. A teraz? :) – biziclop

2

I would expect T to be inferred to B. A does not extend B. So why doesn't the compiler complain about it?

T nie wnioskuje się B, to wywnioskować być A.Od B rozszerza A, B[] jest podtypem A[], a zatem wywołanie metody jest prawidłowe.

W przeciwieństwie do generycznych, typ elementu tablicy jest dostępny w czasie wykonywania (są to reifikowane). kiedy więc spróbować zrobić

a[0] = b.get(); 

środowisko wykonawcze wie, że a jest rzeczywiście B tablicą i nie może posiadać A.

Problem polega na tym, że Java rozszerza się dynamicznie. Tablice są tam od pierwszej wersji Java, podczas gdy generyczne zostały dodane tylko w Javie 1.5. Ogólnie rzecz biorąc, Oracle stara się, aby nowe wersje Java były kompatybilne wstecz, a zatem błędy wprowadzone we wcześniejszych wersjach (na przykład kowariancja tablic) nie zostały poprawione w nowszych wersjach.

+0

Tak jak powiedziałem: _ "' '' T''' wydaje się być wywnioskowane na '' 'Object''', ponieważ mogę przekazać' '' Generic '' 'również." _ Ale czy możesz mi powiedzieć _why_ '' 'T''' wywodzi się z' '' Object'''? Czy możesz pokazać mi przypadek niepowodzenia, jeśli '' 'T''' został hipotetycznie wywnioskowany jako' '' B'''? –

+0

'T' nie jest wnioskowane jako' Obiekt', ponieważ wtedy nie byłoby możliwe przekazanie 'Generic ' do twojej metody, ponieważ 'Generic ' nie jest podtypem 'Generic '. Oczywiście, jeśli faktycznie przekazujesz 'Generic ' zamiast 'Generic ', to 'T' jest wnioskowane jako' Object'. – Hoopje

+0

Jeśli '' 'T''' jest' '' Object''''' '' Generic '' 'spełnia' 'Generic ' ''. Jest to dalej udowodnione przez wezwanie '' 'Główne. fArray (nowy B [1], nowy Generic (nowy A())); '' ', który kompiluje bez błędów. –