Tại sao thay đổi biến trả về trong khối cuối cùng không thay đổi giá trị trả về?


146

Tôi có một lớp Java đơn giản như dưới đây:

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());
    }
}

Và đầu ra của mã này là:

Entry in finally Block
dev  

Tại sao skhông bị ghi đè trong finallykhối, nhưng vẫn kiểm soát đầu ra được in?


11
bạn nên đặt câu lệnh hoàn trả vào khối cuối cùng, lặp lại với tôi, cuối cùng khối luôn được thực thi
Juan Antonio Gomez Moriano

1
trong thử khối, nó trả về s
Devendra

6
Thứ tự của các báo cáo quan trọng. Bạn đang trở lại strước khi bạn thay đổi giá trị của nó.
Peter Lawrey

6
Nhưng, bạn có thể trả về giá trị mới trong finallykhối , không giống như trong C # (mà bạn không thể)
Alvin Wong

Câu trả lời:


167

Các tryHoàn thành khối với việc thực hiện các returntuyên bố và giá trị của stại thời điểm returntuyên bố thực hiện là giá trị trả về bởi phương pháp này. Thực tế là finallymệnh đề sau thay đổi giá trị của s(sau khi returncâu lệnh hoàn thành) không (tại thời điểm đó) thay đổi giá trị trả về.

Lưu ý rằng các thỏa thuận trên liên quan đến các thay đổi đối với giá trị của schính nó trong finallykhối, không phải đối tượng stham chiếu. Nếu slà một tham chiếu đến một đối tượng có thể thay đổi ( Stringkhông phải) và nội dung của đối tượng đã được thay đổi trong finallykhối, thì những thay đổi đó sẽ được nhìn thấy trong giá trị được trả về.

Các quy tắc chi tiết về cách tất cả các hoạt động này có thể được tìm thấy trong Phần 14.20.2 của Đặc tả ngôn ngữ Java . Lưu ý rằng việc thực thi một returncâu lệnh được tính là chấm dứt đột ngột trykhối (phần bắt đầu " Nếu việc thực thi khối thử hoàn thành đột ngột vì bất kỳ lý do nào khác R .... " được áp dụng). Xem Phần 14,17 của JLS để biết lý do tại sao một returntuyên bố là chấm dứt đột ngột của một khối.

Bằng cách chi tiết hơn: nếu cả trykhối và finallykhối của try-finallycâu lệnh chấm dứt đột ngột vì các returncâu lệnh, thì các quy tắc sau từ §14.20.2 được áp dụng:

Nếu việc thực thi trykhối hoàn thành đột ngột vì bất kỳ lý do nào khác R [ngoài việc ném ngoại lệ], thì finallykhối đó được thực thi, và sau đó có một lựa chọn:

  • Nếu finallykhối hoàn thành bình thường, thì trycâu lệnh hoàn thành đột ngột vì lý do R.
  • Nếu finallykhối hoàn thành đột ngột vì lý do S, thì trycâu lệnh hoàn thành đột ngột vì lý do S (và lý do R bị loại bỏ).

Kết quả là returncâu lệnh trong finallykhối xác định giá trị trả về của toàn bộ try-finallycâu lệnh và giá trị được trả về từ trykhối bị loại bỏ. Một điều tương tự xảy ra trong một try-catch-finallycâu lệnh nếu trykhối ném một ngoại lệ, nó bị bắt bởi một catchkhối và cả catchkhối và finallykhối đều có returncâu lệnh.


Nếu tôi sử dụng lớp StringBuilder thay vì String thay vì nối một số giá trị trong khối cuối cùng, nó sẽ thay đổi giá trị trả về. Tại sao?
Devendra

6
@dev - Tôi thảo luận điều đó trong đoạn thứ hai của câu trả lời của tôi. Trong tình huống bạn mô tả, finallykhối không thay đổi đối tượng nào được trả về (the StringBuilder) nhưng nó có thể thay đổi phần bên trong của đối tượng. Các finallythực thi khối trước khi phương pháp này thực sự trở lại (mặc dù returntuyên bố đã hoàn tất), do đó, những thay đổi xảy ra trước khi mã gọi thấy giá trị trả về.
Ted Hopp

Hãy thử với Danh sách, bạn có được hành vi tương tự như StringBuilder.
Yogesh Prajapati

1
@yogeshprajapati - Đúng. Điều này cũng đúng với bất kỳ giá trị trả về có thể thay đổi ( StringBuilder, List, Set, quảng cáo nauseum): nếu bạn thay đổi nội dung trong finallykhối, sau đó những thay đổi đó được nhìn thấy trong đoạn code gọi khi phương pháp này cuối cùng cũng thoát.
Ted Hopp

Điều này nói lên những gì xảy ra, nó không nói lý do tại sao (sẽ đặc biệt hữu ích nếu nó chỉ ra nơi điều này được đề cập trong JLS).
TJ Crowder

65

Bởi vì giá trị trả về được đặt trên ngăn xếp trước khi gọi đến cuối cùng.


3
Điều đó đúng, nhưng nó không giải quyết được câu hỏi của OP về lý do tại sao chuỗi trả về không thay đổi. Điều đó có liên quan đến tính bất biến của chuỗi và các tham chiếu so với các đối tượng nhiều hơn là đẩy giá trị trả về trên ngăn xếp.
templatetypedef

Cả hai vấn đề đều liên quan.
Tordek

2
@templatetypedef ngay cả khi String có thể thay đổi, sử dụng =sẽ không làm thay đổi nó.
Owen

1
@Saul - Quan điểm của Owen (đúng) là tính bất biến không liên quan gì đến lý do tại sao finallykhối của OP không ảnh hưởng đến giá trị trả về. Tôi nghĩ rằng những gì templatetypedef có thể nhận được (mặc dù điều này không rõ ràng) là bởi vì giá trị được trả về là một tham chiếu đến một đối tượng bất biến, thậm chí thay đổi mã trong finallykhối (ngoài việc sử dụng một returncâu lệnh khác ) không thể ảnh hưởng đến giá trị trả về từ phương thức.
Ted Hopp

1
@templatetypedef Không, không. Không có gì để làm với bất biến String nào. Điều tương tự sẽ xảy ra với bất kỳ loại khác. Nó phải làm với việc đánh giá biểu thức trả về trước khi vào khối cuối cùng, nói cách khác là exacfly 'đẩy giá trị trả về trên ngăn xếp'.
Hầu tước Lorne

32

Nếu chúng ta nhìn vào bên trong mã byte, chúng ta sẽ nhận thấy rằng JDK đã thực hiện một tối ưu hóa đáng kể và phương thức foo () trông giống như:

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

Và mã byte:

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 bảo tồn chuỗi "dev" khỏi bị thay đổi trước khi quay trở lại. Trong thực tế ở đây không có khối cuối cùng.


Đây không phải là một tối ưu hóa. Đây chỉ đơn giản là một triển khai của ngữ nghĩa cần thiết.
Hầu tước Lorne

22

Có 2 điều đáng chú ý ở đây:

  • Dây là bất biến. Khi bạn đặt s thành "ghi đè biến s", bạn đặt s để tham chiếu Chuỗi nội tuyến, không thay đổi bộ đệm char vốn có của đối tượng s để thay đổi thành "ghi đè biến s".
  • Bạn đặt một tham chiếu đến s trên ngăn xếp để trở về mã gọi. Sau đó (khi khối cuối cùng chạy), việc thay đổi tham chiếu sẽ không làm gì cho giá trị trả về đã có trên ngăn xếp.

1
Vì vậy, nếu tôi dùng Stringbuffer hơn sting sẽ bị ghi đè ??
Devendra

10
@dev - Nếu bạn thay đổi nội dung của bộ đệm trong finallymệnh đề, điều đó sẽ được nhìn thấy trong mã gọi. Tuy nhiên, nếu bạn đã gán một bộ đệm chuỗi mới s, thì hành vi sẽ giống như bây giờ.
Ted Hopp

có nếu tôi nối vào bộ đệm chuỗi trong cuối cùng thay đổi khối phản ánh trên đầu ra.
Devendra

@ 0xCAFEBABE bạn cũng đưa ra câu trả lời và khái niệm rất tốt, tôi rất cảm ơn đầy đủ cho bạn.
Devendra

13

Tôi thay đổi mã của bạn một chút để chứng minh quan điểm của Ted.

Như bạn có thể thấy trong đầu ra sthực sự thay đổi nhưng sau khi trả lại.

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);
}
}

Đầu ra:

Entry in finally Block 
dev 
override variable s

Tại sao ghi đè chuỗi s không trả về.
Devendra

2
Như Ted và Tordek đã nói "giá trị trả lại được đặt vào ngăn xếp trước khi cuối cùng được thực thi"
Frank

1
Mặc dù đây là thông tin bổ sung tốt, tôi miễn cưỡng nâng cấp nó, bởi vì nó không (tự nó) trả lời câu hỏi.
Joachim Sauer

5

Về mặt kỹ thuật, returnkhối thử trong sẽ không bị bỏ qua nếu một finallykhối được xác định, chỉ khi khối cuối cùng đó cũng bao gồm a return.

Đó là một quyết định thiết kế đáng ngờ có lẽ là một sai lầm khi nhìn lại (giống như các tài liệu tham khảo là nullable / mutable theo mặc định, và theo một số trường hợp ngoại lệ được kiểm tra). Theo nhiều cách, hành vi này hoàn toàn phù hợp với sự hiểu biết thông tục về ý finallynghĩa của nó - "bất kể điều gì xảy ra trước trong trykhối, luôn luôn chạy mã này." Do đó, nếu bạn trả về true từ một finallykhối, hiệu ứng tổng thể phải luôn luôn là return s, không?

Nói chung, điều này hiếm khi là một thành ngữ tốt và bạn nên sử dụng finallycác khối một cách tự do để dọn dẹp / đóng tài nguyên nhưng hiếm khi trả lại giá trị từ chúng.


Đó là ngờ vực tại sao? Nó phù hợp với thứ tự đánh giá trong tất cả các bối cảnh khác.
Hầu tước Lorne

0

Hãy thử điều này: Nếu bạn muốn in giá trị ghi đè của s.

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

1
nó đưa ra cảnh báo .. cuối cùng khối không hoàn thành bình thường
Devendra
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.