Dlaczego javac dopuszcza niektóre niemożliwe rzuty, a inne nie?

52

Jeśli spróbuję rzucić Stringajava.util.Date , kompilator Java wyłapuje błąd. Dlaczego więc kompilator nie oznacza następującego błędu jako błędu?

List<String> strList = new ArrayList<>();                                                                      
Date d = (Date) strList;

Oczywiście JVM rzuca ClassCastException w czasie wykonywania, ale kompilator go nie oflaguje.

Zachowanie jest takie samo w przypadku javac 1.8.0_212 i 11.0.2.

Mike Woinoski
źródło
2
Nie ma Listtu nic specjalnego . Date d = (Date) new Object();
Elliott Frisch
1
Ostatnio gram z arduino. Uwielbiam kompilator, który z radością nie akceptuje żadnej obsady, a następnie zrobił to z całkowicie nieprzewidywalnymi rezultatami. Ciąg do liczby całkowitej? Jasne! Podwójna liczba całkowita? Tak jest! Ciąg do wartości logicznej? Przynajmniej ten w większości staje się fałszywy ...
Stian Yttervik
@ElliottFrisch: Istnieje oczywisty związek dziedziczenia między datą a obiektem, ale nie ma związku między datą a listą. Spodziewałem się więc, że kompilator oznaczy tę obsadę w taki sam sposób, jak oznaczy rzutowanie od String do Date. Ale jak wyjaśnia Zabuza w swojej doskonałej odpowiedzi, List jest interfejsem, więc obsada byłaby legalna, gdyby strListbyła instancją klasy, która implementuje List.
Mike Woinoski
To często powtarzające się pytanie i jestem pewien, że widziałem wiele jego duplikatów. Jest to w zasadzie odwrotna wersja mocno powiązanego: stackoverflow.com/questions/21812289/…
Hulk
1
@StianYttervik -fpermissive właśnie to robi. Włącz ostrzeżenia kompilatora.
bobsburner

Odpowiedzi:

86

Obsada jest technicznie możliwa. Javac nie może łatwo udowodnić, że tak nie jest w twoim przypadku, a JLS faktycznie definiuje to jako prawidłowy program Java, więc oznaczenie błędu byłoby nieprawidłowe.

To dlatego, że Listjest interfejsem. Więc możesz mieć podklasę, Datektóra faktycznie implementuje się w Listprzebraniu, tak jak Listtutaj - a następnie rzutowanie na Datebyłoby całkowicie w porządku. Na przykład:

public class SneakyListDate extends Date implements List<Foo> {
    ...
}

I wtedy:

List<Foo> list = new SneakyListDate();
Date date = (Date) list; // This one is valid, compiles and runs just fine

Wykrywanie takiego scenariusza może nie zawsze być możliwe, ponieważ wymagałoby to informacji o środowisku wykonawczym, jeśli instancja pochodzi na przykład z metody. I nawet jeśli kompilator wymagałby znacznie więcej wysiłku. Kompilator zapobiega tylko rzutowaniom, które są absolutnie niemożliwe z powodu braku możliwości dopasowania drzewa klas. Jak widać, w niniejszym przypadku tak nie jest.

Pamiętaj, że JLS wymaga, aby kod był prawidłowym programem Java. W 5.1.6.1. Dozwolona zawężająca się konwersja odniesienia mówi:

Konwersja referencyjny zwężenie istnieje od rodzaju odniesienia Sdo typu odniesienia T, jeśli wszystkie z poniższych są prawdziwe :

  • [...]
  • Obowiązuje jeden z następujących przypadków :
    • [...]
    • Sjest typem interfejsu, Tjest typem klasy i Tnie nazywa finalklasy.

Więc nawet jeśli kompilator może zorientować się, że twoja sprawa jest rzeczywiście niemożliwa do udowodnienia, nie jest dozwolone oznaczenie błędu, ponieważ JLS definiuje go jako prawidłowy program Java.

Dopuszczalne byłoby jedynie wyświetlenie ostrzeżenia.

Zabuzard
źródło
16
Warto zauważyć, że powodem, dla którego łapie on przypadek String, jest to, że String jest ostateczny, więc kompilator wie, że żadna klasa nie może go rozszerzyć.
MTilsted
5
Właściwie nie sądzę, że to „ostateczność” Stringa powoduje, że myDate = (Date) myStringzawodzi. Za pomocą terminologii JLS instrukcja próbuje przekonwertować z S(the String) na T(the Date). Tutaj Snie ma typu interfejsu, więc powyższy warunek JLS nie ma zastosowania. Jako przykład spróbuj rzucić kalendarz na datę, a otrzymasz błąd kompilatora, nawet jeśli żadna klasa nie jest ostateczna.
Mike Woinoski
1
Nie wiem, czy się rozczarować, kompilator nie może przeprowadzić wystarczającej analizy statycznej, aby udowodnić, że strList może być zawsze typu ArrayList.
Joshua
3
Kompilatorowi nie zabrania się sprawdzania. Ale zabronione jest nazywanie tego błędem. To spowodowałoby, że kompilator byłby niezgodny. (Zobacz moją odpowiedź ...)
Stephen C
3
Aby dodać trochę żargon, kompilator musiałby udowodnić, że typ Date & Listjest do zamieszkania , to nie wystarczy, aby udowodnić, że jest niezamieszkane obecnie (może to być w przyszłości).
Polygnome
15

Rozważmy uogólnienie twojego przykładu:

List<String> strList = someMethod();       
Date d = (Date) strList;

Są to główne powody, dla których Date d = (Date) strList;nie występuje błąd kompilacji.

  • Intuicyjny powodem jest to, że kompilator nie (w ogóle) zna dokładny typ obiektu zwróconego przez wywołanie tej metody. Możliwe, że oprócz tego List, że jest klasą, która implementuje , jest to również podklasa Date.

  • Przyczyną techniczną jest to, że specyfikacja języka Java „pozwala” na zawężenie konwersji referencji , który odpowiada na tego typu cast. Zgodnie z JLS 5.1.6.1 :

    „Istnieje zawężająca się konwersja referencji z typu Sreferencji na typ referencji, Tjeśli spełnione są wszystkie poniższe warunki:”

    ...

    5) Sjest typem interfejsu, Tjest typem klasy iT nie nazywa finalklasy.”

    ...

    W innym miejscu JLS mówi również, że wyjątek może zostać zgłoszony w czasie wykonywania ...

    Należy zauważyć, że określenie JLS 5.1.6.1 opiera się wyłącznie na zadeklarowanych typach zmiennych, a nie na rzeczywistych typach środowiska wykonawczego. W ogólnym przypadku kompilator nie zna i nie może znać rzeczywistych typów środowiska wykonawczego.


Dlaczego więc kompilator Java nie może się zorientować, że obsada nie działa?

  • W moim przykładzie someMethodwywołanie może zwrócić obiekty różnego rodzaju. Nawet jeśli kompilator był w stanie przeanalizować treść metody i określić dokładny zestaw typów, które mogą zostać zwrócone, nic nie stoi na przeszkodzie, aby ktoś zmienił go, aby zwracał różne typy ... po skompilowaniu kodu, który go wywołuje. Jest to podstawowy powód, dla którego JLS 5.1.6.1 mówi to, co mówi.

  • W twoim przykładzie inteligentny kompilator może stwierdzić, że rzutowanie nigdy się nie powiedzie. I wolno emitować ostrzeżenie podczas kompilacji, aby wskazać problem.

Dlaczego więc inteligentny kompilator nie może powiedzieć, że to błąd?

  • Ponieważ JLS mówi, że jest to poprawny program. Kropka. Kompilator, który nazwałby to błędem , nie byłby zgodny z Javą.

  • Ponadto każdy kompilator, który odrzuca programy Java, które zdaniem JLS i innych kompilatorów są poprawne, stanowi przeszkodę w przenoszeniu kodu źródłowego Java.

Stephen C.
źródło
4
Głosuj za fakt, że po skompilowaniu klasy wywołującej wywoływana funkcja może się zmienić , więc nawet jeśli jest to możliwe do udowodnienia w czasie kompilacji, przy obecnej implementacji odbiorcy, że rzutowanie jest niemożliwe, może nie być tak w późniejszych czasach wykonywania po zmianie lub wymianie strony odbierającej.
Peter - Przywróć Monikę
2
Uznanie za podkreślenie problemu przenośności, który zostałby wprowadzony, gdyby kompilator próbował być zbyt inteligentny.
Mike Woinoski
2

5.5.1 Typ odniesienia Casting:

Biorąc pod uwagę typ odwołania w czasie kompilacji S(źródło) i typ odwołania w czasie kompilacji T(cel), istnieje konwersja rzutowania z Sna, Tjeśli nie wystąpią błędy czasu kompilacji z powodu następujących reguł.

[...]

Jeśli Sjest typem interfejsu:

  • [...]

  • Jeśli Tjest to klasa lub interfejs typu, który nie jest ostateczny, a następnie, jeśli istnieje supertypem Xz Ti supertypem Ysię Stak, że zarówno Xa Ysą provably różne typy sparametryzowane i że wymazania z Xi Ysą takie same, to błąd kompilacji występuje.

    W przeciwnym razie rzutowanie jest zawsze legalne w czasie kompilacji (ponieważ nawet jeśli Tnie zostanie zaimplementowane S, podklasa Tpotęgi).

List<String>jest Si Datejest Tw twoim przypadku.

Oleksandr Pyrohov
źródło