Operator trójargumentowy Java vs if / else w <JDK8 kompatybilności

113

Ostatnio czytam kod źródłowy Spring Framework. Coś, czego nie rozumiem, jest tutaj:

public Member getMember() {
    // NOTE: no ternary expression to retain JDK <8 compatibility even when using
    // the JDK 8 compiler (potentially selecting java.lang.reflect.Executable
    // as common type, with that new base class not available on older JDKs)
    if (this.method != null) {
        return this.method;
    }
    else {
        return this.constructor;
    }
}

Ta metoda jest członkiem klasy org.springframework.core.MethodParameter. Kod jest łatwy do zrozumienia, a komentarze trudne.

UWAGA: brak wyrażenia potrójnego, aby zachować zgodność z JDK <8, nawet podczas korzystania z kompilatora JDK 8 (potencjalnie wybrany java.lang.reflect.Executablejako typowy typ, z nową klasą bazową niedostępną na starszych JDK)

Jaka jest różnica między używaniem wyrażenia trójskładnikowego a używaniem if...else...konstrukcji w tym kontekście?

jddxf
źródło

Odpowiedzi:

103

Kiedy myślisz o typie operandów, problem staje się bardziej widoczny:

this.method != null ? this.method : this.constructor

ma jako typ najbardziej wyspecjalizowany wspólny typ obu operandów, tj. najbardziej wyspecjalizowany typ wspólny dla obu this.methodi this.constructor.

W Javie 7 jest to java.lang.reflect.Memberjednak biblioteka klas Java 8 wprowadza nowy typ, java.lang.reflect.Executablektóry jest bardziej wyspecjalizowany niż typ ogólny Member. W związku z tym w bibliotece klas Java 8 typ wyniku wyrażenia trójskładnikowego jest Executableraczej niż Member.

Wydaje się, że niektóre (przedpremierowe) wersje kompilatora Java 8 Executablepodczas kompilowania operatora trójargumentowego tworzyły wyraźne odniesienie do wygenerowanego wewnątrz kodu. Spowodowałoby to ładowanie klasy, a zatem z kolei wywołanie ClassNotFoundExceptionw czasie wykonywania podczas pracy z biblioteką klas <JDK 8, ponieważ Executableistnieje tylko dla JDK ≥ 8.

Jak zauważył Tagir Valeev w tej odpowiedzi , jest to w rzeczywistości błąd występujący w przedpremierowych wersjach JDK 8 i od tego czasu został naprawiony, więc zarówno if-elseobejście, jak i komentarz wyjaśniający są już nieaktualne.

Dodatkowa uwaga: można by dojść do wniosku, że ten błąd kompilatora występował przed Java 8. Jednak kod bajtowy generowany dla trójskładnika przez OpenJDK 7 jest taki sam jak kod bajtowy generowany przez OpenJDK 8. W rzeczywistości typ wyrażenie nie jest wspominane w czasie wykonywania, kod jest tak naprawdę tylko testem, rozgałęzianiem, ładowaniem, zwracaniem bez żadnych dodatkowych sprawdzeń. Możesz więc być pewien, że nie stanowi to (już) problemu i rzeczywiście wydaje się, że był to tymczasowy problem podczas opracowywania Java 8.

dhke
źródło
1
Zatem w jaki sposób kod skompilowany za pomocą JDK 1.8 może działać na JDK 1.7. Wiem, że kod skompilowany z niższą wersją JDK może bez problemu działać na wyższej wersji JDK.
jddxf
1
@jddxf Wszystko jest w porządku, o ile określono odpowiednią wersję pliku klasy i nie korzystasz z żadnej funkcji, która nie jest dostępna w późniejszych wersjach. Problem na pewno wystąpi, jeśli jednak takie użycie ma miejsce w sposób dorozumiany, jak w tym przypadku.
dhke
13
@jddxf, użyj opcji
-source
1
Dziękuję wszystkim, zwłaszcza Dhke i Tagirowi Valeevowi, którzy udzielili dokładnych wyjaśnień
jddxf
30

Zostało to wprowadzone w dość starym zatwierdzeniu 3 maja 2013 roku, prawie rok przed oficjalnym wydaniem JDK-8. Kompilator był wtedy intensywnie rozwijany, więc takie problemy ze zgodnością mogły wystąpić. Wydaje mi się, że zespół Spring właśnie przetestował kompilację JDK-8 i próbował naprawić problemy, mimo że w rzeczywistości są to problemy z kompilatorem. Po oficjalnym wydaniu JDK-8 stało się to nieistotne. Teraz operator trójskładnikowy w tym kodzie działa dobrze zgodnie z oczekiwaniami (nie ma odniesienia do Executableklasy w skompilowanym pliku .class).

Obecnie podobne rzeczy pojawiają się w JDK-9: część kodu, który można ładnie skompilować w JDK-8, nie działa z JDK-9 javac. Wydaje mi się, że większość takich problemów zostanie naprawiona do czasu premiery.

Tagir Valeev
źródło
2
+1. Czy to był błąd we wczesnym kompilatorze? Czy to zachowanie, o którym mowa Executable, naruszało jakiś aspekt specyfikacji? A może po prostu Oracle zdało sobie sprawę, że mogą zmienić to zachowanie w sposób, który nadal byłby zgodny ze specyfikacją i bez naruszania kompatybilności wstecznej?
ruakh,
2
@ruakh, myślę, że to był błąd. W kodzie bajtowym (w Javie-8 lub wcześniejszych wersjach) nie jest konieczne jawne rzutowanie na Executabletyp pośredni. W Javie-8 koncepcja wnioskowania o typie wyrażenia zmieniła się drastycznie i ta część została całkowicie przepisana, więc nie jest tak zaskakujące, że wczesne implementacje miały błędy.
Tagir Valeev
7

Główna różnica polega na tym, że if elseblok jest instrukcją, podczas gdy trójskładnik (częściej nazywany operatorem warunkowym w Javie) jest wyrażeniem .

Oświadczenie może robić takie rzeczy jak returndo rozmówcy na niektórych ścieżkach kontrolnych. W przypisaniu można użyć wyrażenia :

int n = condition ? 3 : 2;

Tak więc dwa wyrażenia w trójniku po warunku muszą być koercyjne do tego samego typu. Może to powodować dziwne efekty w Javie, szczególnie przy automatycznym boksowaniu i automatycznym rzutowaniu referencji - do tego odnosi się komentarz w opublikowanym kodzie. Wymuszenie wyrażeń w twoim przypadku dotyczyłoby java.lang.reflect.Executabletypu (ponieważ jest to typ najbardziej wyspecjalizowany ), który nie istnieje w starszych wersjach Javy.

Stylistycznie powinieneś używać if elsebloku, jeśli kod jest podobny do instrukcji, i trójskładnika, jeśli jest podobny do wyrażenia.

Oczywiście możesz sprawić, by if elseblok zachowywał się jak wyrażenie, jeśli używasz funkcji lambda.

Batszeba
źródło
6

Na typ wartości zwracanej w wyrażeniu trójargumentowym mają wpływ klasy nadrzędne, które zmieniły się zgodnie z opisem w języku Java 8.

Trudno zrozumieć, dlaczego nie można było napisać obsady.

Markiz Lorne
źródło