Metoda Java z typem powrotu kompiluje się bez instrukcji return

228

Pytanie 1:

Dlaczego poniższy kod kompiluje się bez instrukcji return?

public int a() {
    while(true);
}

Uwaga: jeśli dodam zwrot po pewnym czasie, otrzymam Unreachable Code Error.

Pytanie 2:

Z drugiej strony, dlaczego kompiluje następujący kod,

public int a() {
    while(0 == 0);
}

nawet jeśli poniższe nie.

public int a(int b) {
    while(b == b);
}
Willi Mentzel
źródło
2
Nie jest duplikatem stackoverflow.com/questions/16789832/... , dzięki drugiej połowie drugiego pytania.
TJ Crowder,

Odpowiedzi:

274

Pytanie 1:

Dlaczego poniższy kod kompiluje się bez instrukcji return?

public int a() 
{
    while(true);
}

Jest to objęte JLS§8.4.7 :

Jeśli metoda zostanie zadeklarowana jako typ zwracany (§ 8.4.5), wówczas błąd kompilacji wystąpi, jeśli treść metody może zakończyć się normalnie (§ 14.1).

Innymi słowy, metoda z typem zwracanym musi zwracać tylko za pomocą instrukcji return, która zapewnia zwrot wartości; metoda nie może „spaść z końca ciała”. Dokładne zasady dotyczące instrukcji return w treści metody znajdują się w § 14.17.

Metoda może mieć typ zwracany, ale nie zawiera instrukcji zwrotnych. Oto jeden przykład:

class DizzyDean {
    int pitch() { throw new RuntimeException("90 mph?!"); }
}

Ponieważ kompilator wie, że pętla nigdy się nie kończy ( trueoczywiście zawsze jest to prawda), wie, że funkcja nie może „powrócić normalnie” (zrzucić koniec swojego ciała), a zatem jest w porządku, że jej nie ma return.

Pytanie 2:

Z drugiej strony, dlaczego kompiluje następujący kod,

public int a() 
{
    while(0 == 0);
}

nawet jeśli poniższe nie.

public int a(int b)
{
    while(b == b);
}

W takim 0 == 0przypadku kompilator wie, że pętla nigdy się nie zakończy ( 0 == 0zawsze będzie to prawdą). Ale nie wie o tym b == b.

Dlaczego nie?

Kompilator rozumie wyrażenia stałe (§15.28) . Cytując § 15.2 - Formy wyrażeń (ponieważ dziwnie tego zdania nie ma w §15.28) :

Niektóre wyrażenia mają wartość, którą można określić podczas kompilacji. Są to wyrażenia stałe (§ 15,28).

W twoim b == bprzykładzie, ponieważ w grę wchodzi zmienna, nie jest to stałe wyrażenie i nie jest określone, aby miało być określone w czasie kompilacji. Widzimy , że zawsze będzie to prawdą w tym przypadku (chociaż gdyby bbyło double, jak wskazał QBrute , moglibyśmy się łatwo oszukać Double.NaN, co nie== jest samo w sobie ), ale JLS określa tylko, że wyrażenia stałe są określane w czasie kompilacji , nie pozwala kompilatorowi próbować oceniać wyrażeń niestałych. bayou.io podniósł dobrą rację, dlaczego nie: Jeśli zaczniesz iść drogą próbowania określenia wyrażeń obejmujących zmienne w czasie kompilacji, gdzie się zatrzymujesz? b == bjest oczywiste (er, dlaNaNwartości), ale co z tym a + b == b + a? Czy (a + b) * 2 == a * 2 + b * 2? Rysowanie linii na stałych ma sens.

Ponieważ więc nie „określa” wyrażenia, kompilator nie wie, że pętla nigdy się nie zakończy, więc uważa, że ​​metoda może powrócić normalnie - czego nie wolno, ponieważ jest wymagana return. Więc narzeka na brak return.

TJ Crowder
źródło
34

Interesujące może być pomyślenie o typie zwracanej metody nie jako obietnicy zwrócenia wartości określonego typu, ale jako obietnicy, że nie zwróci wartości, która nie jest określonego typu. Zatem jeśli nigdy niczego nie zwrócisz, nie złamiesz obietnicy, a zatem którykolwiek z poniższych elementów jest zgodny z prawem:

  1. Zapętlanie na zawsze:

    X foo() {
        for (;;);
    }
    
  2. Recursing na zawsze:

    X foo() {
        return foo();
    }
    
  3. Zgłaszając wyjątek:

    X foo() {
        throw new Error();
    }
    

(Uważam, że rekursja jest przyjemna do myślenia: kompilator uważa, że ​​metoda zwróci wartość typu X(cokolwiek to jest), ale to nieprawda, ponieważ nie ma kodu, który miałby pojęcie, jak stworzyć lub zdobyć X.)

Boann
źródło
8

Patrząc na kod bajtu, jeśli zwracany kod nie odpowiada definicji, pojawi się błąd kompilacji.

Przykład:

for(;;) pokaże kody bajtowe:

L0
    LINENUMBER 6 L0
    FRAME SAME
    GOTO L0

Zwróć uwagę na brak kodu zwrotnego

To nigdy nie trafia w zwrot, a zatem nie zwraca niewłaściwego typu.

Dla porównania metoda taka jak:

public String getBar() { 
    return bar; 
}

Zwróci następujące kody bajtowe:

public java.lang.String getBar();
    Code:
      0:   aload_0
      1:   getfield        #2; //Field bar:Ljava/lang/String;
      4:   areturn

Zwróć uwagę na „areturn”, co oznacza „zwróć referencję”

Teraz, jeśli wykonamy następujące czynności:

public String getBar() { 
    return 1; 
}

Zwróci następujące kody bajtowe:

public String getBar();
  Code:
   0:   iconst_1
   1:   ireturn

Teraz widzimy, że typ w definicji nie pasuje do zwracanego typu ireturn, co oznacza return int.

Tak naprawdę sprowadza się to do tego, że jeśli metoda ma ścieżkę zwrotną, ścieżka ta musi pasować do typu zwracanego. Są jednak przypadki w kodzie bajtowym, w których nie jest generowana żadna ścieżka zwrotna, a zatem nie dochodzi do złamania reguły.

Philip Devine
źródło