poniedziałek, 10 listopada 2008

Java Killers #009

Jakiś czas temu odezwał się do mnie dobry znajomy Paweł Badeński, który uhahany rozpoczął rozmowę na gadu z tekstem "Patrz na to: ..." po czym wkleił kawałek kodu. Kod przerzuciłem na szybko do ulubionego IDE i już wtedy wiedziałem, że te kilka lini ma predyspozycje na kolejny odcinek killersów. Problem pozostawał jednak wciąż, gdyż za diabła nie wiedziałem, czemu kod zachowywał się właśnie tak a nie inaczej. Szybkie przestudiowania Java Language Specification nie dało odpowiedzi, google na szybko też nie były takie cwane, jak zwykle. Dopiero po dłuższej chwili grzebania udało mi się dotrzeć do buga na bugs.sun.com, gdzie po dogłebnej lekturze i ponownym otworzeniu Java Language Specification, wszystko stało się jasne. Jestem ciekaw czy i dla Was dziewiąta odsłona Java Killers będzie taką samą łamigłówką jak była dla mnie. A oto ona:

Mając poniższy kod, jakiego spodziewamy się outputu:


public class Foo
{
public int loo(List<String> a)
{
return 1;
}

public double loo(List<Integer> a)
{
return -1;
}

public static void main(String[] args)
{
List list = new ArrayList<Integer>();
System.out.println("output: " + new Foo().loo(list));
}

}
Odpowiedź poprawna tylko jedna, a kilka do wyboru:


A. Klasa nie skompiluje się, javac poinformuje nas, że "name clash: loo(java.util.List) and loo(java.util.List) have the same signature and erasure"
B. Klasa nie skompiluje się, javac poinformuje nas, że "reference to loo is ambiguous"
C. Skompiluje się, ale wyrzucony zostanie RuntimeException po uruchomieniu programu
D. Program skompiluje się i uruchomi się bez wyjątków i zostanie wypisane na ekranie 'output: -1'
E. Program skompiluje się i uruchomi się bez wyjątków i zostanie wypisane na ekranie 'output: 1'



I uwaga uwaga, poprawna odpowiedź to B!

Osoby, które przekonane były, że oby dwie metody loo miały taką samą sygnaturę i przez to kod się nie kompilował, nie martwcie się, nie byliście jedyni, którzy zaznaczyli odpowiedź A. Otóż okazuje się, że jednak sygnatury obu metod się różnią i występuje najprostszy w świecie method overloading. Później w czasie wywołania metody loo w ciele metody main, następuje konsternacja, gdyż kompilator widzi zmienną niegenneryczną List list i nie ma pojęcia do której metody loo ma się odwołać, stąd jego krzyk
"reference to loo is ambiguous".
Ok, ktoś się spyta, jak to metody loo mają taką samą sygnaturę? Patrząc na Java Language Specification punkt 8.4.9, widzimy, że:

* obie metody mają taką samą nazwę
* obie metody mają taką samą liczbę formalnych parametrów oraz parametrów typów prostych
* ALE typy tych parametrów się różnią, List<String> to co innego niż List<Integer>! Mimo, że przed samą kompilacją wszystkie generyki zostają zrzucone i nie są brane pod uwagę podczas kompilacji (pisałem o tym w poprzednich killersach) o tyle są uwzględniane podczas operacji zwanej "type erasure" - dlatego właśnie List<String> to co innego niż List<Integer>.

No i to tyle na dziś. Kto znał poprawną odpowiedź?

PS. Wspomniana strona do której dotarłem (i musiałem się nieźle nagooglać) znajduje się tutaj: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6182950

5 komentarzy:

swiety pisze...

To czemu w takim razie kod przestaje sie kompilowac jesli obie metody loo zwracaja ten sam typ (obie int lub obie double np)?
Po dekompilacji Foo.class za pomoca javap widac:

public int loo(java.util.List);
Code:
0: iconst_1
1: ireturn

public double loo(java.util.List);
Code:
0: ldc2_w #24; //double -1.0d
3: dreturn

public static void main(java.lang.String[]);
Code:
0: new #29; //class java/util/ArrayList
[...]
28: invokevirtual #46; //Method loo:(Ljava/util/List;)I

Widac ze obie metody maja te same sygnatury, ale w wolaniu w metodzie main() odwolujemy sie do metody loo zwracajacej typ int

Paweł Szulc pisze...

Sygnatury mają te same, ale inny jest tzw. erasure, w którym brany jest również typ zwraacany przez metodę.

Polecam przeczytac sobie evaluaton tego buga, do którego link podałem na samym dole posta

koziołek pisze...

Paweł nie olewaj mocy genericsów! Stosujemy zasadę wszystko albo nic. Mój kod:
public class Foo {
public int loo(List< String> a) {
return 1;
}

public double loo(List< Integer> a) {
return -1;
}

public static void main(String[] args) {
List< Integer> list = new ArrayList< Integer>();
System.out.println("output: " + new Foo().loo(list));
}

}

W tym przypadku zmienna list w twoim kodzie nie ma jednoznacznego typu. Jest listą po prostu listą. W moim kodzie jest powiedziane jakiego typu listą jest ta zmienna. Trochę to przypomina problem z poprzednich killersów o wywoływaniu metod z null, ale tu mamy do czynienia z sytuacją w której kompilator nie potrafi zdecydować co wywołać więc się wykrzacza.

Paweł Szulc pisze...

@koziolek: oj oczywiscie ze dodanie genryka do definicji zmiennej list by zmienilo calkowicie output, ale po to sa JK zeby wychwytywac przerozne sytuacje. I nie jest to znow nic nietypowego.
Przeciez nie raz wykorzystujemy w naszej pracy biblioteki jakis projektow opensourocwych czy moze nawet biblioteki komercyjne, ktore pisane byly jeszcze w 1.4. I wtedy nastepuje mieszanie sie kolekcji generycznych i niegenerycznych i moze byc bardzo bardzo wesolo.

Pozdrawiam

kretes pisze...

również założyłem, że będzie A ;-)

Generyki na początku są bardzo super.

Ale ich ostateczna złożoność razem z wildcardami jest całkiem niezła i chyba mało kto powie, że nie da się go na generykach zagiąć.
Jak ktoś tak uważa, niech sprawdzi najlepszy java generics FAQ

Racjonalny Developer