Zwracanie null jako int dozwolone z operatorem trójskładnikowym, ale nie w przypadku instrukcji if

186

Spójrzmy na prosty kod Java w następującym fragmencie:

public class Main {

    private int temp() {
        return true ? null : 0;
        // No compiler error - the compiler allows a return value of null
        // in a method signature that returns an int.
    }

    private int same() {
        if (true) {
            return null;
            // The same is not possible with if,
            // and causes a compile-time error - incompatible types.
        } else {
            return 0;
        }
    }

    public static void main(String[] args) {
        Main m = new Main();
        System.out.println(m.temp());
        System.out.println(m.same());
    }
}

W tym najprostszym kodzie Java temp()metoda nie powoduje błędu kompilatora, mimo że zwracany jest typ funkcji int, i próbujemy zwrócić wartość null(poprzez instrukcję return true ? null : 0;). Po skompilowaniu powoduje to oczywiście wyjątek czasu wykonywania NullPointerException.

Wydaje się jednak, że to samo jest nie tak, jeśli reprezentujemy operatora trójskładnikowego za pomocą ifinstrukcji (jak w same()metodzie), która powoduje błąd podczas kompilacji! Czemu?

Lew
źródło
6
Również int foo = (true ? null : 0)i new Integer(null)zarówno kompilacji porządku, druga jest wyraźne forma autoboxing.
Izkata,
2
@Izkata problem polega na tym, że rozumiem, dlaczego kompilator próbuje wykonać autobox nulldo Integer... To mogłoby wyglądać jak „zgadywanie” lub „sprawianie, by działało” ...
Marsellus Wallace
1
... Huhm, myślałem, że mam tam odpowiedź, ponieważ konstruktor liczb całkowitych (to, co mówią dokumenty, które znalazłem, służy do autoboxowania) może przyjmować Łańcuch jako argument (który może być zerowy). Mówią jednak również, że konstruktor działa identycznie jak metoda parseInt (), która
wygenerowałaby wyjątek
3
@Izkata - argument String c'tor dla Integer nie jest wyrażeniem autoboksowania. Ciąg nie może być automatycznie powiązany z liczbą całkowitą. (Funkcja Integer foo() { return "1"; }nie będzie się kompilować.)
Ted Hopp,
5
Fajnie, nauczyłem się czegoś nowego o trójskładnikowym operatorze!
oksayt,

Odpowiedzi:

118

Kompilator interpretuje nulljako odwołanie zerowe do Integer, stosuje reguły autoboxowania / rozpakowywania dla operatora warunkowego (zgodnie z opisem w specyfikacji języka Java, 15.25 ) i szczęśliwie się porusza. Spowoduje to wygenerowanie NullPointerExceptionw czasie wykonywania, które można potwierdzić, wypróbowując.

Ted Hopp
źródło
Biorąc pod uwagę link do opublikowanej przez ciebie specyfikacji języka Java, który według Ciebie zostanie wykonany w przypadku powyższego pytania? Ostatni (ponieważ wciąż próbuję zrozumieć capture conversioni lub(T1,T2))? Ponadto, czy naprawdę można zastosować boks do wartości zerowej? Czy to nie byłoby jak „zgadywanie”?
Marsellus Wallace,
´ @ Gevorg Wskaźnik zerowy jest prawidłowym wskaźnikiem do każdego możliwego obiektu, więc nic złego się tam nie stanie. Kompilator po prostu zakłada, że ​​null jest liczbą całkowitą, którą może następnie autoboxować na int.
Voo
1
@Gevorg - Zobacz komentarz nowejq i moją odpowiedź na jego post. Myślę, że wybrał poprawną klauzulę. lub(T1,T2)jest najbardziej specyficznym typem odniesienia wspólnym w hierarchii typów T1 i T2. (Oba mają co najmniej obiekt, więc zawsze istnieje najbardziej specyficzny typ odniesienia.)
Ted Hopp
8
@Gevorg - nullnie jest zapakowane w liczbę całkowitą, jest interpretowane jako odwołanie do liczby całkowitej (odwołanie zerowe, ale to nie jest problem). Żaden obiekt liczb całkowitych nie jest konstruowany z wartości zerowej, więc nie ma powodu dla wyjątku NumberFormatException.
Ted Hopp,
1
@Gevorg - Jeśli spojrzysz na zasady konwersji boksu i zastosujesz je null(co nie jest prymitywnym typem liczbowym), odpowiednią klauzulą ​​jest: „Jeśli p jest wartością dowolnego innego typu, konwersja boksu jest równoważna konwersji tożsamości „. Bokserska konwersja nullna Integerplony null, bez wywoływania jakiegokolwiek Integerkonstruktora.
Ted Hopp,
40

Myślę, że kompilator Java interpretuje true ? null : 0jako Integerwyrażenie, które można niejawnie przekonwertować na int, być może podać NullPointerException.

W drugim przypadku wyrażenie nullma specjalny typ zerowy patrz , więc kod return nullpowoduje niedopasowanie typu.

Vlad
źródło
2
Zakładam, że ma to związek z automatycznym boksem? Prawdopodobnie pierwszy zwrot nie skompilowałby się przed Javą 5, prawda?
Michael McGowan
@Michael wydaje się, że tak jest, jeśli ustawisz poziom zgodności Eclipse na wcześniejszy niż 5.
Jonathon Faust
@Michael: to zdecydowanie wygląda jak auto-boxing (jestem całkiem nowy w Javie i nie mogę sformułować bardziej precyzyjnego stwierdzenia - przepraszam).
Vlad
1
@Vlad, jak kompilator skończyłby interpretować true ? null : 0jako Integer? 0Najpierw autoboxing ??
Marsellus Wallace
1
@Gevorg: Spójrz tutaj : W przeciwnym razie drugi i trzeci operand są odpowiednio typu S1 i S2. Niech T1 będzie typem wynikającym z zastosowania konwersji boksu do S1, i niech T2 będzie typem wynikającym z zastosowania konwersji boksu do S2. i następujący tekst.
Vlad
32

W rzeczywistości wszystko to wyjaśniono w specyfikacji języka Java .

Typ wyrażenia warunkowego określa się w następujący sposób:

  • Jeśli drugi i trzeci operand mają ten sam typ (który może być typem pustym), to jest to typ wyrażenia warunkowego.

Dlatego „null” w twoim (true ? null : 0)pobiera typ int, a następnie jest automatycznie przenoszony na Integer.

Spróbuj czegoś takiego, aby to zweryfikować, (true ? null : null)a pojawi się błąd kompilatora.

nowaq
źródło
3
Ale ta klauzula reguł nie ma zastosowania: drugi i trzeci operand nie mają tego samego typu.
Ted Hopp,
1
Wydaje się, że odpowiedź brzmi następująco:> W przeciwnym razie drugi i trzeci operand są odpowiednio typu S1 i S2. Niech T1 będzie typem wynikającym z zastosowania konwersji boksu do S1, i niech T2 będzie typem wynikającym z zastosowania konwersji boksu do S2. Rodzaj wyrażenia warunkowego wynika z zastosowania konwersji przechwytywania (§5.1.10) na lub (T1, T2) (§15.12.2.7).
nowaq
Myślę, że to jest odpowiednia klauzula. Następnie próbuje zastosować automatyczne rozpakowywanie w celu zwrócenia intwartości z funkcji, która powoduje NPE.
Ted Hopp,
@nowaq Też tak myślałem. Jednakże, jeśli starają się wyraźnie box nulldo Integerz new Integer(null);„Let T1 być typem, który wynika ze stosowania konwersji boks S1 ...” byś dostać NumberFormatExceptioni to nie jest przypadek ...
Marsellus Wallace
@Gevorg Myślę, że ponieważ zdarza się wyjątek podczas boksowania, nie otrzymujemy ŻADNEGO wyniku tutaj. Kompilator musi tylko wygenerować kod zgodny z definicją, którą robi - po prostu otrzymujemy wyjątek, zanim skończymy.
Voo
25

W przypadku ifinstrukcji nullreferencja nie jest traktowana jako Integerreferencja, ponieważ nie uczestniczy w wyrażeniu, które zmusza ją do takiej interpretacji. Dlatego błąd można łatwo wychwycić w czasie kompilacji, ponieważ jest to wyraźniejszy błąd typu .

Jeśli chodzi o operator warunkowy, specyfikacja języka Java § 15.25 „Operator warunkowy ? :” odpowiada na to ładnie w zasadach stosowania konwersji typu:

  • Jeśli drugi i trzeci operand mają ten sam typ (który może być typem pustym), to jest to typ wyrażenia warunkowego.

    Nie dotyczy, ponieważ nullnie jest int.

  • Jeśli jeden z drugiego i trzeciego operandu jest typu logicznego, a typ drugiego jest typu logicznego, wówczas typ wyrażenia warunkowego jest typu logicznego.

    Nie dotyczy, ponieważ ani nullani nie intjest, booleanani Boolean.

  • Jeśli jeden z drugiego i trzeciego operandu jest typu zerowego, a typ drugiego jest typem referencyjnym, wówczas typem wyrażenia warunkowego jest ten typ referencyjny.

    Nie ma zastosowania, ponieważ nulljest typu zerowego, ale intnie jest typem odniesienia.

  • W przeciwnym razie, jeśli drugi i trzeci operand mają typy, które są konwertowalne (§5.1.8) na typy numeryczne, wówczas istnieje kilka przypadków: […]

    Dotyczy: nulljest traktowany jako konwertowalny na typ numeryczny i jest zdefiniowany w §5.1. 8 „Konwersja rozpakowywania”, aby rzucić NullPointerException.
Jon Purdy
źródło
Jeśli 0zostanie automatycznie przeniesione do, Integerkompilator wykonuje ostatni przypadek „reguł operatora trójskładnikowego”, jak opisano w specyfikacji języka Java. Jeśli to prawda, trudno mi uwierzyć, że przeskoczyłbym wtedy do przypadku 3 tych samych reguł, które mają wartość zerową i typ odwołania, które sprawiają, że zwracana wartość operatora trójskładnikowego jest typem odniesienia (liczba całkowita). .
Marsellus Wallace
@Gevorg - Dlaczego trudno uwierzyć, że operator trójskładnikowy zwraca Integer? Właśnie tak się dzieje; NPE jest generowany przez próbę rozpakowania wartości wyrażenia w celu zwrócenia wartości intz funkcji. Zmień funkcję na return an, Integera ona wróci nullbez problemu.
Ted Hopp,
2
@TedHopp: Gevorg odpowiedział na wcześniejszą wersję mojej odpowiedzi, która była niepoprawna. Należy zignorować rozbieżność.
Jon Purdy,
@JonPurdy „Mówi się, że typ jest konwertowalny na typ numeryczny, jeśli jest to typ numeryczny lub jest to typ referencyjny, który można przekonwertować na typ numeryczny poprzez konwersję rozpakowywania” i nie sądzę, że nullnależy do tej kategorii . Następnie przejdziemy do kroku „W przeciwnym razie stosowana jest promocja binarna numeryczna (§5.6.2) ... Zauważ, że promocja binarna numeryczna wykonuje konwersję rozpakowywania (§5.1.8) ...” w celu ustalenia typu zwrotu. Ale konwersja rozpakowywania wygenerowałaby NPE, a dzieje się tak tylko w czasie wykonywania, a nie podczas próby określenia typu operatora trójskładnikowego. Nadal jestem zdezorientowany ..
Marsellus Wallace
@Gevorg: Rozpakowywanie odbywa się w czasie wykonywania. nullJest traktowany tak, jakby miał typ int, ale jest rzeczywiście równoważne throw new NullPointerException(), to wszystko.
Jon Purdy,
11

Pierwszą rzeczą, o której należy pamiętać, jest to, że trójskładnikowe operatory Java mają „typ”, i to właśnie kompilator określi i rozważy bez względu na rzeczywiste / rzeczywiste typy drugiego lub trzeciego parametru. W zależności od kilku czynników typ operatora trójskładnikowego określa się na różne sposoby, jak pokazano w specyfikacji języka Java 15.26

W powyższym pytaniu powinniśmy rozważyć ostatni przypadek:

W przeciwnym razie drugi i trzeci argument są odpowiednio typu S1 i S2 . Niech T1 będzie typem wynikającym z zastosowania konwersji boksu do S1 , i niech T2 będzie typem wynikającym z zastosowania konwersji boksu do S2 . Rodzaj wyrażenia warunkowego wynika z zastosowania konwersji przechwytywania (§5.1.10) na lub (T1, T2) (§15.12.2.7).

Jest to zdecydowanie najbardziej złożony przypadek, gdy przyjrzysz się zastosowaniu konwersji przechwytywania (§ 5.1.1), a przede wszystkim w lub (T1, T2) .

Prostym językiem angielskim i po skrajnym uproszczeniu możemy opisać ten proces jako obliczenie „najmniejszej wspólnej superklasy” (tak, pomyśl o LCM) drugiego i trzeciego parametru. To da nam trójskładnikowy „typ”. Ponownie, to, co właśnie powiedziałem, jest ekstremalnym uproszczeniem (rozważ klasy, które implementują wiele popularnych interfejsów).

Na przykład, jeśli spróbujesz:

long millis = System.currentTimeMillis();
return(true ? new java.sql.Timestamp(millis) : new java.sql.Time(millis));

Zauważysz, że wynikowym typem wyrażenia warunkowego jest java.util.Dateto, że jest to „Najmniejsza wspólna nadklasa” dla pary Timestamp/ Timepair.

Ponieważ nullmoże być autoboxowane do czegokolwiek, „Least Common Superclass” jest Integerklasą i będzie to zwracany typ wyrażenia warunkowego (operator trójskładnikowy) powyżej. Zwracana wartość będzie wtedy zerowym wskaźnikiem typu Integeri to, co zostanie zwrócone przez operator trójskładnikowy.

W czasie wykonywania, gdy wirtualna maszyna Java się rozpakowuje, wyrzucane jest Integera NullPointerException. Dzieje się tak, ponieważ JVM próbuje wywołać funkcję null.intValue(), gdzie nulljest wynikiem autoboxowania.

Moim zdaniem (a ponieważ mojej opinii nie ma w specyfikacji języka Java, wiele osób i tak uzna to za błędne), kompilator źle radzi sobie z oceną wyrażenia w pytaniu. Biorąc pod uwagę, że napisałeś, true ? param1 : param2kompilator powinien od razu ustalić, że pierwszy parametr - null- zostanie zwrócony i powinien wygenerować błąd kompilatora. Jest to nieco podobne do tego, kiedy piszesz, while(true){} etc...a kompilator narzeka na kod pod pętlą i oznacza go flagą Unreachable Statements.

Druga sprawa jest dość prosta i ta odpowiedź jest już za długa ...;)

KOREKTA:

Po kolejnej analizie uważam, że nie miałem racji, twierdząc, że nullwartość może być spakowana / autoboxed do dowolnego elementu. Mówiąc o klasie Integer, jawne boksowanie polega na wywołaniu new Integer(...)konstruktora, a może Integer.valueOf(int i);(znalazłem tę wersję gdzieś). Pierwsze rzuciłoby NumberFormatException(i tak się nie dzieje), a drugie nie miałoby sensu, ponieważ intnie można null...

Marsellus Wallace
źródło
1
nullW oryginalnym kodzie OP nie jest zapakowane. Działa to tak: kompilator zakłada, że nulljest to odwołanie do liczby całkowitej. Korzystając z reguł dla typów wyrażeń trójskładnikowych, decyduje, że całe wyrażenie jest wyrażeniem całkowitym. Następnie generuje kod do autokoxowania 1(w przypadku, gdy warunek zostanie oceniony false). Podczas wykonywania warunek jest oceniany truetak, aby wyrażenie wyrażało się jako null. Podczas próby zwrócenia an intz funkcji nullrozpakowuje się. To następnie rzuca NPE. (Kompilator może zoptymalizować większość tego.)
Ted Hopp,
4

W rzeczywistości w pierwszym przypadku wyrażenie może zostać ocenione, ponieważ kompilator wie, że należy je ocenić jako Integer, jednak w drugim przypadku nullnie można określić typu wartości zwracanej ( ), więc nie można go skompilować. Jeśli go rzucisz Integer, kod się skompiluje.

Dostać
źródło
2
private int temp() {

    if (true) {
        Integer x = null;
        return x;// since that is fine because of unboxing then the returned value could be null
        //in other words I can say x could be null or new Integer(intValue) or a intValue
    }

    return (true ? null : 0);  //this will be prefectly legal null would be refrence to Integer. The concept is one the returned
    //value can be Integer 
    // then null is accepted to be a variable (-refrence variable-) of Integer
}
Youans
źródło
0

Co powiesz na to:

public class ConditionalExpressionType {

    public static void main(String[] args) {

        String s = "";
        s += (true ? 1 : "") instanceof Integer;
        System.out.println(s);

        String t = "";
        t += (!true ? 1 : "") instanceof String;
        System.out.println(t);

    }

}

Dane wyjściowe są prawdziwe, prawdziwe.

Kolor Eclipse koduje 1 w wyrażeniu warunkowym jako autoboxed.

Domyślam się, że kompilator widzi zwracany typ wyrażenia jako Object.

Jon
źródło