Dlaczego zmiana zwracanej zmiennej w ostatnim bloku nie zmienia zwracanej wartości?

146

Mam prostą klasę Java, jak pokazano poniżej:

public class Test {

    private String s;

    public String foo() {
        try {
            s = "dev";
            return s;
        } 
        finally {
            s = "override variable s";
            System.out.println("Entry in finally Block");  
        }
    }

    public static void main(String[] xyz) {
        Test obj = new Test();
        System.out.println(obj.foo());
    }
}

Wynik tego kodu jest następujący:

Entry in finally Block
dev  

Dlaczego snie jest nadpisywany w finallybloku, ale kontroluje wydruk?

Dev
źródło
11
powinieneś umieścić instrukcję powrotu na ostatnim bloku, powtórz ze mną, ostatecznie blok jest zawsze wykonywany
Juan Antonio Gomez Moriano
1
w try block it zwraca s
Devendra
6
Kolejność wypowiedzi ma znaczenie. Wracasz, szanim zmienisz jego wartość.
Peter Lawrey,
6
Ale ty może zwracać nową wartość w finallybloku , w przeciwieństwie do C # (co nie można)
Alvin Wong

Odpowiedzi:

167

W tryfinalizuje bloku z realizacją returnoświadczenia i wartości sw momencie returnoświadczenie wykonuje to wartość zwracana przez metodę. Fakt, że finallyklauzula później zmienia wartość s(po zakończeniu returninstrukcji) nie zmienia (w tym momencie) wartości zwracanej.

Zwróć uwagę, że powyższe dotyczy zmian wartości ssamego siebie w finallybloku, a nie obiektu, do którego się sodnosi. Gdyby sbyło odniesieniem do zmiennego obiektu (a Stringnie jest), a zawartość obiektu została zmieniona w finallybloku, to te zmiany byłyby widoczne w zwracanej wartości.

Szczegółowe zasady działania tego wszystkiego można znaleźć w sekcji 14.20.2 specyfikacji języka Java . Należy zauważyć, że wykonanie returninstrukcji liczy się jako nagłe zakończenie trybloku (obowiązuje sekcja rozpoczynająca się od „ Jeśli wykonanie bloku try nagle kończy się z innego powodu R… ”). Zobacz sekcję 14.17 JLS, aby dowiedzieć się, dlaczego returnoświadczenie jest nagłym zakończeniem bloku.

W celu uzyskania dalszych szczegółów: jeśli zarówno tryblokada, jak i finallyblokada try-finallyoświadczenia returnulegają nagłemu przerwaniu z powodu oświadczeń, wówczas obowiązują następujące zasady z §14.20.2:

Jeśli wykonanie trybloku kończy się nagle z innego powodu R [oprócz rzucenia wyjątku], wtedy finallyblok jest wykonywany, a następnie jest wybór:

  • Jeśli finallyblok kończy się normalnie, tryinstrukcja kończy się nagle z powodu R.
  • Jeśli finallyblok kończy się nagle z powodu S, to tryinstrukcja kończy się nagle z powodu S (i powód R jest odrzucany).

W rezultacie returninstrukcja w finallybloku określa zwracaną wartość całej try-finallyinstrukcji, a wartość zwracana z trybloku jest odrzucana. Podobnie dzieje się w try-catch-finallyinstrukcji, jeśli tryblok zgłasza wyjątek, zostaje przechwycony przez catchblok i zarówno blok, jak catchi finallyblok mają returninstrukcje.

Ted Hopp
źródło
Jeśli zamiast klasy String użyję klasy StringBuilder, a następnie dołączę jakąś wartość do bloku Final, zmienia się zwracana wartość. Dlaczego?
Devendra
6
@dev - omawiam to w drugim akapicie mojej odpowiedzi. W opisywanej przez Ciebie sytuacji finallyblok nie zmienia zwracanego obiektu (the StringBuilder), ale może zmienić elementy wewnętrzne obiektu. W finallyWykonuje bloku przed metody faktycznie zwraca (mimo że returnoświadczenie zostało zakończone), więc te zmiany nastąpić przed wywołaniem kodu widzi zwracanej wartości.
Ted Hopp,
spróbuj z listą, uzyskasz takie samo zachowanie jak StringBuilder.
Yogesh Prajapati
1
@yogeshprajapati - Tak. Tak samo jest z każdym zmienny wartości zwracanej ( StringBuilder, List, Set, reklama nauseum): jeśli zmienisz zawartość w finallybloku, to te zmiany są widoczne w wywołującego kodu gdy metoda wreszcie wyjść.
Ted Hopp,
To mówi, co się dzieje, ale nie mówi dlaczego (byłoby to szczególnie przydatne, gdyby wskazywało, gdzie zostało to uwzględnione w JLS).
TJ Crowder
65

Ponieważ wartość zwracana jest umieszczana na stosie przed wywołaniem last.

Tordek
źródło
3
To prawda, ale nie odpowiada na pytanie OP, dlaczego zwrócony ciąg nie zmienił się. Ma to związek z niezmiennością ciągów znaków i odwołaniami do obiektów, a nie tylko umieszczaniem wartości zwracanej na stosie.
templatetypedef
Obie kwestie są ze sobą powiązane.
Tordek
2
@templatetypedef, nawet jeśli String =byłby zmienny, użycie nie spowodowałoby jego mutacji.
Owen
1
@Saul - Owen zauważył (co jest poprawne), że niezmienność nie ma nic wspólnego z tym, dlaczego finallyblok OP nie wpływa na zwracaną wartość. Myślę, że to, do czego mógł dojść z templatetypedef (chociaż nie jest to jasne), polega na tym, że ponieważ zwracana wartość jest odniesieniem do niezmiennego obiektu, nawet zmiana kodu w finallybloku (inna niż użycie innej returninstrukcji) nie może wpłynąć na wartość zwracana z metody.
Ted Hopp
1
@templatetypedef Nie, nie ma. Nie ma to nic wspólnego z niezmiennością String. To samo stanie się z każdym innym typem. Ma to związek z oceną wyrażenia zwracającego przed wejściem do bloku last, innymi słowy exacfly 'umieszczenie wartości zwracanej na stosie'.
Markiz Lorne
32

Jeśli zajrzymy do kodu bajtowego, zauważymy, że JDK dokonał znaczącej optymalizacji, a metoda foo () wygląda następująco:

String tmp = null;
try {
    s = "dev"
    tmp = s;
    s = "override variable s";
    return tmp;
} catch (RuntimeException e){
    s = "override variable s";
    throw e;
}

I kod bajtowy:

0:  ldc #7;         //loading String "dev"
2:  putstatic   #8; //storing it to a static variable
5:  getstatic   #8; //loading "dev" from a static variable
8:  astore_0        //storing "dev" to a temp variable
9:  ldc #9;         //loading String "override variable s"
11: putstatic   #8; //setting a static variable
14: aload_0         //loading a temp avariable
15: areturn         //returning it
16: astore_1
17: ldc #9;         //loading String "override variable s"
19: putstatic   #8; //setting a static variable
22: aload_1
23: athrow

java zachował ciąg znaków „dev” przed zmianą przed zwróceniem. W rzeczywistości nie ma tu żadnego ostatecznego bloku.

Michaił
źródło
To nie jest optymalizacja. To jest po prostu implementatiom wymaganej semantyki.
Markiz Lorne
22

Na uwagę zasługują 2 rzeczy:

  • Ciągi znaków są niezmienne. Kiedy ustawiasz s na „przesłonięcie zmiennej s”, ustawiasz s tak, aby odwoływało się do wstawionego ciągu znaków, nie zmieniając wewnętrznego bufora znaków obiektu s na „przesłonięcie zmiennej s”.
  • Umieszczasz odniesienie do s na stosie, aby powrócić do kodu wywołującego. Później (gdy ostatni blok zostanie uruchomiony), zmiana odwołania nie powinna robić nic dla wartości zwracanej już na stosie.
0xCAFEBABE
źródło
1
więc jeśli wezmę stringbuffer, to żądło zostanie zastąpione?
Devendra
10
@dev - jeśli miałbyś zmienić zawartość bufora w finallyklauzuli, byłoby to widoczne w kodzie wywołującym. Jeśli jednak przypiszesz nowy bufor łańcuchowy do s, zachowanie będzie takie samo jak teraz.
Ted Hopp,
tak, jeśli dołączę do bufora ciągów w końcu zmiany bloku odzwierciedlają wynik.
Devendra
@ 0xCAFEBABE również dajesz bardzo dobrą odpowiedź i koncepcje, bardzo ci dziękuję.
Devendra
13

Zmieniam trochę twój kod, żeby udowodnić cel Teda.

Jak widać na wyjściu sjest rzeczywiście zmieniony ale po powrocie.

public class Test {

public String s;

public String foo() {

    try {
        s = "dev";
        return s;
    } finally {
        s = "override variable s";
        System.out.println("Entry in finally Block");

    }
}

public static void main(String[] xyz) {
    Test obj = new Test();
    System.out.println(obj.foo());
    System.out.println(obj.s);
}
}

Wynik:

Entry in finally Block 
dev 
override variable s
Szczery
źródło
dlaczego nadpisane ciągi nie zwracają.
Devendra
2
Jak już powiedzieli Ted i Tordek "wartość zwracana jest umieszczana na stosie przed ostatecznym wykonaniem"
Frank
1
Chociaż jest to dobra dodatkowa informacja, niechętnie ją głosuję, ponieważ (sama w sobie) nie odpowiada na pytanie.
Joachim Sauer
5

Technicznie rzecz biorąc, returnblok w try nie będzie ignorowany, jeśli finallyblok jest zdefiniowany, tylko jeśli ten ostatni blok zawiera również return.

Jest to wątpliwa decyzja projektowa, która prawdopodobnie była błędem z perspektywy czasu (podobnie jak odniesienia domyślnie dopuszczają wartość null / mutable i, według niektórych, sprawdzane wyjątki). Pod wieloma względami to zachowanie jest dokładnie zgodne z potocznym rozumieniem tego, co finallyoznacza - „bez względu na to, co wydarzy się wcześniej w trybloku, zawsze uruchamiaj ten kod”. Dlatego jeśli zwrócisz prawdę z finallybloku, ogólny efekt zawsze musi być do return s, nie?

Ogólnie rzecz biorąc, rzadko jest to dobry idiom i powinieneś finallyswobodnie używać bloków do czyszczenia / zamykania zasobów, ale rzadko, jeśli w ogóle, zwracaj z nich wartość.

Kiran Jujare
źródło
Wątpliwe, dlaczego? Jest zgodny z kolejnością ocen we wszystkich innych kontekstach.
Markiz Lorne
0

Spróbuj tego: Jeśli chcesz wydrukować wartość przesłonięcia s.

finally {
    s = "override variable s";    
    System.out.println("Entry in finally Block");
    return s;
}
Achintya Jha
źródło
1
daje ostrzeżenie ... ostatecznie blok nie kończy się normalnie
Devendra