Tại sao chỉ có các biến cuối cùng có thể truy cập trong lớp ẩn danh?


354
  1. achỉ có thể là cuối cùng ở đây. Tại sao? Làm thế nào tôi có thể gán lại atrong onClick()phương thức mà không giữ nó là thành viên tư nhân?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. Làm thế nào tôi có thể trả lại 5 * akhi nó nhấp? Ý tôi là,

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }

1
Tôi không nghĩ rằng các lớp ẩn danh Java cung cấp kiểu đóng lambda mà bạn mong đợi, nhưng ai đó làm ơn sửa cho tôi nếu tôi sai ...
user541686

4
Bạn đang cố gắng để đạt được điều gì? Nhấp vào xử lý có thể được thực thi khi "f" kết thúc.
Ivan Dubrov

@Lambert nếu bạn muốn sử dụng một trong các phương pháp onClick nó phải @Ivan thức bao lon f () phương pháp cư xử như onClick () phương thức hoàn trả int khi nhấp

2
Ý tôi là vậy - nó không hỗ trợ đóng hoàn toàn vì nó không cho phép truy cập vào các biến không phải là cuối cùng.
dùng541686

4
Lưu ý: kể từ Java 8, biến của bạn chỉ cần có hiệu lực cuối cùng
Peter Lawrey 24/2/2016

Câu trả lời:


489

Như đã lưu ý trong các bình luận, một số điều này trở nên không liên quan trong Java 8, nơi finalcó thể ẩn. Chỉ có một biến cuối cùng có hiệu quả có thể được sử dụng trong một lớp bên trong ẩn danh hoặc biểu thức lambda.


Về cơ bản, đó là do cách Java quản lý các bao đóng .

Khi bạn tạo một thể hiện của một lớp bên trong ẩn danh, bất kỳ biến nào được sử dụng trong lớp đó đều có các giá trị của chúng được sao chép thông qua hàm tạo được tạo tự động. Điều này tránh trình biên dịch phải tự động tạo ra nhiều loại bổ sung khác nhau để giữ trạng thái logic của "biến cục bộ", ví dụ như trình biên dịch C # thực hiện ... (Khi C # bắt một biến trong hàm ẩn danh, nó thực sự nắm bắt biến - đóng có thể cập nhật biến theo cách được nhìn thấy bởi phần chính của phương thức và ngược lại.)

Vì giá trị đã được sao chép vào thể hiện của lớp bên trong ẩn danh, sẽ có vẻ kỳ lạ nếu biến có thể được sửa đổi bởi phần còn lại của phương thức - bạn có thể có mã dường như đang làm việc với biến đã lỗi thời ( bởi vì đó thực sự là những gì sẽ xảy ra ... bạn sẽ làm việc với một bản sao được chụp vào một thời điểm khác). Tương tự như vậy nếu bạn có thể thực hiện các thay đổi trong lớp bên trong ẩn danh, các nhà phát triển có thể mong đợi những thay đổi đó sẽ hiển thị trong phần thân của phương thức kèm theo.

Làm cho biến cuối cùng loại bỏ tất cả các khả năng này - vì giá trị hoàn toàn không thể thay đổi, bạn không cần phải lo lắng về việc liệu những thay đổi đó có thể nhìn thấy hay không. Các cách duy nhất để cho phép phương thức và lớp bên trong ẩn danh nhìn thấy những thay đổi của nhau là sử dụng một loại mô tả có thể thay đổi. Đây có thể là lớp kèm theo, một mảng, một kiểu trình bao bọc có thể thay đổi ... bất cứ thứ gì như thế. Về cơ bản, nó giống như giao tiếp giữa một phương thức này và một phương thức khác: những thay đổi được thực hiện cho các tham số của một phương thức mà người gọi của nó không nhìn thấy, nhưng những thay đổi được thực hiện cho các đối tượng được tham chiếu bởi các tham số được nhìn thấy.

Nếu bạn quan tâm đến việc so sánh chi tiết hơn giữa các lần đóng Java và C #, tôi có một bài viết tiếp theo. Tôi muốn tập trung vào phía Java trong câu trả lời này :)


4
@Ivan: Giống như C #, về cơ bản. Mặc dù vậy, nó đi kèm với một mức độ phức tạp khá cao, nếu bạn muốn có cùng loại chức năng như C # trong đó các biến từ các phạm vi khác nhau có thể được "khởi tạo" số lần khác nhau.
Jon Skeet


11
Điều này hoàn toàn đúng với Java 7, hãy nhớ rằng với Java 8, các bao đóng đã được giới thiệu và bây giờ thực sự có thể truy cập vào một trường không phải là cuối cùng của một lớp từ lớp bên trong của nó.
Mathias Bader

22
@MathiasBader: Thật sao? Tôi nghĩ rằng về cơ bản nó vẫn là cùng một cơ chế, trình biên dịch bây giờ chỉ đủ thông minh để suy luận final(nhưng nó vẫn cần phải có hiệu quả cuối cùng).
Thilo

3
@Mathias Bader: bạn có thể luôn luôn truy cập không chính thức các lĩnh vực , đó là không nên nhầm lẫn với địa phương các biến, mà phải là cuối cùng và vẫn phải có hiệu quả chính thức, vì vậy Java 8 không thay đổi ngữ nghĩa.
Holger

41

Có một mẹo cho phép lớp ẩn danh cập nhật dữ liệu trong phạm vi bên ngoài.

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

Tuy nhiên, thủ thuật này không tốt lắm do các vấn đề đồng bộ hóa. Nếu trình xử lý được gọi sau, bạn cần 1) đồng bộ hóa quyền truy cập vào res nếu trình xử lý được gọi từ luồng khác 2) cần có một số loại cờ hoặc dấu hiệu cho thấy res đã được cập nhật

Thủ thuật này hoạt động tốt, tuy nhiên, nếu lớp ẩn danh được gọi trong cùng một luồng ngay lập tức. Giống:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...

2
cảm ơn câu trả lời của bạn. Tôi biết tất cả những điều này và giải pháp của tôi tốt hơn thế này. Câu hỏi của tôi là "tại sao chỉ cuối cùng"?

6
Câu trả lời sau đó là cách chúng được thực hiện :)
Ivan Dubrov

1
Cảm ơn. Tôi đã sử dụng thủ thuật trên một mình. Tôi đã không chắc chắn nếu đó là một ý tưởng tốt. Nếu Java không cho phép, có thể có một lý do chính đáng. Câu trả lời của bạn làm rõ rằng List.forEachmã của tôi là an toàn.
RuntimeException

Đọc stackoverflow.com/q/12830611/2073130 để có một cuộc thảo luận tốt về lý do đằng sau "tại sao chỉ là cuối cùng".
lcn

có một số cách giải quyết. Của tôi là: Final int resf = res; Ban đầu tôi sử dụng cách tiếp cận mảng, nhưng tôi thấy nó có cú pháp quá rườm rà. AtomicReference có thể chậm hơn một chút (phân bổ một đối tượng).
zakmck

17

Một lớp ẩn danh là một lớp bên trong và quy tắc nghiêm ngặt áp dụng cho các lớp bên trong (JLS 8.1.3) :

Bất kỳ biến cục bộ, tham số phương thức chính thức hoặc tham số xử lý ngoại lệ được sử dụng nhưng không được khai báo trong lớp bên trong phải được khai báo cuối cùng . Bất kỳ biến cục bộ nào, được sử dụng nhưng không được khai báo trong một lớp bên trong phải được gán chắc chắn trước phần thân của lớp bên trong .

Tôi chưa tìm thấy lý do hoặc giải thích về jls hoặc jvms, nhưng chúng tôi biết rằng trình biên dịch tạo một tệp lớp riêng cho mỗi lớp bên trong và phải đảm bảo rằng các phương thức được khai báo trên tệp lớp này ( ở mức mã byte) ít nhất có quyền truy cập vào các giá trị của các biến cục bộ.

( Jon có câu trả lời hoàn chỉnh - Tôi giữ câu hỏi này không bị xóa vì người ta có thể quan tâm đến quy tắc JLS)


11

Bạn có thể tạo một biến cấp độ lớp để nhận giá trị trả về. Ý tôi là

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

bây giờ bạn có thể nhận giá trị của K và sử dụng nó ở nơi bạn muốn.

Trả lời của bạn tại sao là:

Một thể hiện của lớp bên trong cục bộ được gắn với lớp Main và có thể truy cập các biến cục bộ cuối cùng của phương thức chứa nó. Khi cá thể sử dụng một phương thức cục bộ cuối cùng của phương thức chứa của nó, biến đó sẽ giữ lại giá trị mà nó giữ tại thời điểm tạo cá thể, ngay cả khi biến đó nằm ngoài phạm vi (đây thực sự là phiên bản đóng, giới hạn của Java).

Bởi vì một lớp bên trong cục bộ không phải là thành viên của một lớp hoặc gói, nó không được khai báo với mức truy cập. (Tuy nhiên, hãy rõ ràng rằng các thành viên của chính nó có các cấp truy cập như trong một lớp học bình thường.)


Tôi đã đề cập rằng "không giữ nó là thành viên riêng"

6

Chà, trong Java, một biến có thể là cuối cùng không chỉ là một tham số, mà là một trường cấp độ lớp, như

public class Test
{
 public final int a = 3;

hoặc như một biến cục bộ, như

public static void main(String[] args)
{
 final int a = 3;

Nếu bạn muốn truy cập và sửa đổi một biến từ một lớp ẩn danh, bạn có thể muốn biến biến đó thành một biến cấp độ lớp trong lớp kèm theo .

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

Bạn không thể có một biến là cuối cùng cung cấp cho nó một giá trị mới. finalcó nghĩa là: giá trị là không thể thay đổi và cuối cùng.

Và vì nó là bản cuối cùng, Java có thể sao chép nó một cách an toàn vào các lớp ẩn danh cục bộ. Bạn không nhận được một số tham chiếu đến int (đặc biệt là vì bạn không thể có các tham chiếu đến các nguyên hàm như int trong Java, chỉ tham chiếu đến các Đối tượng ).

Nó chỉ sao chép giá trị của a vào một int ẩn được gọi là trong lớp ẩn danh của bạn.


3
Tôi liên kết "biến cấp độ lớp" với static. Có lẽ nó rõ ràng hơn nếu bạn sử dụng "biến thể" thay thế.
eljenso

1
tốt, tôi đã sử dụng cấp độ lớp vì kỹ thuật này sẽ hoạt động với cả biến thể hiện và biến tĩnh.
Zach L

chúng ta đã biết rằng trận chung kết có thể truy cập được nhưng chúng ta muốn biết tại sao? bạn có thể vui lòng thêm một số giải thích về lý do tại sao bên?
Saurabh Oza

6

Lý do tại sao quyền truy cập chỉ bị giới hạn ở các biến cuối cùng cục bộ là vì nếu tất cả các biến cục bộ sẽ được truy cập thì trước tiên chúng sẽ được sao chép vào một phần riêng biệt nơi các lớp bên trong có thể có quyền truy cập vào chúng và duy trì nhiều bản sao của biến cục bộ có thể thay đổi có thể dẫn đến dữ liệu không nhất quán. Trong khi đó các biến cuối cùng là bất biến và do đó, bất kỳ số lượng bản sao nào đối với chúng sẽ không có bất kỳ tác động nào đến tính nhất quán của dữ liệu.


Đây không phải là cách nó được triển khai trong các ngôn ngữ như C # hỗ trợ tính năng này. Trong thực tế, trình biên dịch thay đổi biến từ một biến cục bộ thành một biến thể hiện hoặc nó tạo ra một cấu trúc dữ liệu bổ sung cho các biến này có thể vượt quá phạm vi của lớp bên ngoài. Tuy nhiên, không có "nhiều bản sao của biến cục bộ"
Mike76

Mike76 Tôi chưa từng xem qua triển khai của C #, nhưng Scala thực hiện điều thứ hai mà bạn đề cập Tôi nghĩ: Nếu một người Intđược gán lại vào bên trong một bao đóng, hãy thay đổi biến đó thành một thể hiện của IntRef(về cơ bản là một Integertrình bao bọc có thể thay đổi ). Mỗi truy cập biến sau đó được viết lại cho phù hợp.
Adowrath

3

Để hiểu lý do căn bản cho hạn chế này, hãy xem xét chương trình sau:

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

Các interfaceInstance vẫn còn trong bộ nhớ sau khi khởi tạo trở về phương pháp, nhưng các tham số val không. JVM không thể truy cập một biến cục bộ ngoài phạm vi của nó, do đó Java thực hiện cuộc gọi tiếp theo tới printInteger hoạt động bằng cách sao chép giá trị của val vào một trường ẩn có cùng tên trong interfaceInstance . Các interfaceInstance được cho là đã bị bắt giá trị của tham số địa phương. Nếu tham số không phải là cuối cùng (hoặc cuối cùng có hiệu lực) thì giá trị của nó có thể thay đổi, trở nên không đồng bộ với giá trị được chụp, có khả năng gây ra hành vi không trực quan.


2

Các phương thức trong một lớp bên trong anonomyous có thể được gọi tốt sau khi luồng sinh ra nó đã kết thúc. Trong ví dụ của bạn, lớp bên trong sẽ được gọi trên luồng gửi sự kiện và không nằm trong cùng luồng với luồng đã tạo ra nó. Do đó, phạm vi của các biến sẽ khác nhau. Vì vậy, để bảo vệ các vấn đề phạm vi gán biến như vậy, bạn phải khai báo chúng cuối cùng.


2

Khi một lớp bên trong ẩn danh được định nghĩa trong phần thân của một phương thức, tất cả các biến được khai báo cuối cùng trong phạm vi của phương thức đó có thể truy cập được từ bên trong lớp bên trong. Đối với các giá trị vô hướng, một khi nó đã được gán, giá trị của biến cuối cùng không thể thay đổi. Đối với các giá trị đối tượng, tham chiếu không thể thay đổi. Điều này cho phép trình biên dịch Java "nắm bắt" giá trị của biến trong thời gian chạy và lưu trữ một bản sao dưới dạng một trường trong lớp bên trong. Khi phương thức bên ngoài đã kết thúc và khung ngăn xếp của nó đã bị xóa, biến ban đầu sẽ biến mất nhưng bản sao riêng của lớp bên trong vẫn tồn tại trong bộ nhớ riêng của lớp.

( http://en.wikipedia.org/wiki/Final_%28Java%29 )


1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}

0

Như Jon có các chi tiết triển khai trả lời một câu trả lời khả dĩ khác là JVM không muốn xử lý ghi trong bản ghi đã kết thúc kích hoạt của anh ta.

Xem xét trường hợp sử dụng trong đó lambdas của bạn thay vì được áp dụng, được lưu trữ ở một số nơi và chạy sau đó.

Tôi nhớ rằng ở Smalltalk bạn sẽ có một cửa hàng bất hợp pháp được nâng lên khi bạn thực hiện sửa đổi như vậy.


0

Hãy thử mã này,

Tạo Danh sách mảng và đặt giá trị bên trong đó và trả về:

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}

OP đang hỏi lý do là tại sao một cái gì đó được yêu cầu. Do đó, bạn nên chỉ ra cách mã của bạn xử lý nó
NitinSingh

0

Lớp ẩn danh Java rất giống với đóng Javascript, nhưng Java thực hiện điều đó theo cách khác. (kiểm tra câu trả lời của Andersen)

Vì vậy, để không nhầm lẫn Nhà phát triển Java với hành vi lạ có thể xảy ra đối với những người đến từ nền Javascript. Tôi đoán đó là lý do tại sao họ buộc chúng tôi sử dụngfinal , đây không phải là giới hạn của JVM.

Hãy xem ví dụ Javascript dưới đây:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

Trong Javascript, countergiá trị sẽ là 100, vì chỉ có mộtcounter biến từ đầu đến cuối.

Nhưng trong Java, nếu không có final, nó sẽ in ra 0, bởi vì trong khi đối tượng bên trong đang được tạo, 0giá trị được sao chép vào các thuộc tính ẩn của đối tượng lớp bên trong. (có hai biến số nguyên ở đây, một biến trong phương thức cục bộ, một biến khác trong các thuộc tính ẩn của lớp bên trong)

Vì vậy, bất kỳ thay đổi nào sau khi tạo đối tượng bên trong (như dòng 1), nó sẽ không ảnh hưởng đến đối tượng bên trong. Vì vậy, nó sẽ làm cho sự nhầm lẫn giữa hai kết quả và hành vi khác nhau (giữa Java và Javascript).

Tôi tin rằng đó là lý do tại sao, Java quyết định buộc nó là cuối cùng, vì vậy dữ liệu là 'nhất quán' từ đầu đến cuối.


0

Biến cuối cùng của Java bên trong một lớp bên trong

lớp bên trong chỉ có thể sử dụng

  1. tài liệu tham khảo từ lớp ngoài
  2. biến cục bộ cuối cùng từ ngoài phạm vi là loại tham chiếu (ví dụ: Object ...)
  3. loại giá trị (nguyên thủy) (ví dụ int...) có thể được bọc bởi một loại tham chiếu cuối cùng. IntelliJ IDEAcó thể giúp bạn chuyển đổi nó thành một mảng phần tử

Khi một non static nested( inner class) [Giới thiệu] được tạo bởi trình biên dịch - một lớp mới - <OuterClass>$<InnerClass>.classđược tạo và các tham số ràng buộc được truyền vào hàm tạo [Biến cục bộ trên ngăn xếp] . Nó tương tự như đóng cửa

biến cuối cùng là một biến không thể gán lại. biến tham chiếu cuối cùng vẫn có thể được thay đổi bằng cách sửa đổi trạng thái

Có thể nó sẽ kỳ lạ bởi vì là một lập trình viên, bạn có thể làm như thế này

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}

-2

Có lẽ thủ thuật này cho bạn một ý tưởng

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
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.