Sửa đổi biến cục bộ từ bên trong lambda


115

Việc sửa đổi một biến cục bộ trong gây forEachra lỗi biên dịch:

Bình thường

    int ordinal = 0;
    for (Example s : list) {
        s.setOrdinal(ordinal);
        ordinal++;
    }

Với Lambda

    int ordinal = 0;
    list.forEach(s -> {
        s.setOrdinal(ordinal);
        ordinal++;
    });

Bất kỳ ý tưởng làm thế nào để giải quyết điều này?


8
Xem xét lambdas về cơ bản là đường cú pháp cho một lớp bên trong ẩn danh, trực giác của tôi là không thể nắm bắt một biến cục bộ, không cuối cùng. Tôi rất muốn được chứng minh là sai.
Sinkingpoint

2
Một biến được sử dụng trong biểu thức lambda phải có giá trị cuối cùng. Bạn có thể sử dụng một số nguyên nguyên tử mặc dù nó quá mức cần thiết, vì vậy biểu thức lambda không thực sự cần thiết ở đây. Chỉ cần gắn bó với vòng lặp for.
Alexis C.

3
Biến phải có hiệu lực cuối cùng . Xem phần này: Tại sao lại hạn chế việc nắm bắt biến cục bộ?
Jesper


2
@Quirliom Chúng không phải là đường cú pháp cho các lớp ẩn danh. Lambdas sử dụng phương pháp xử lý dưới mui xe
Dioxin

Câu trả lời:


176

Sử dụng một trình bao bọc

Bất kỳ loại giấy gói nào cũng tốt.

Với Java 8+ , hãy sử dụng AtomicInteger:

AtomicInteger ordinal = new AtomicInteger(0);
list.forEach(s -> {
  s.setOrdinal(ordinal.getAndIncrement());
});

... hoặc một mảng:

int[] ordinal = { 0 };
list.forEach(s -> {
  s.setOrdinal(ordinal[0]++);
});

Với Java 10+ :

var wrapper = new Object(){ int ordinal = 0; };
list.forEach(s -> {
  s.setOrdinal(wrapper.ordinal++);
});

Lưu ý: hãy rất cẩn thận nếu bạn sử dụng dòng song song. Bạn có thể không đạt được kết quả như mong đợi. Các giải pháp khác như Stuart's có thể phù hợp hơn cho những trường hợp đó.

Đối với các loại khác int

Tất nhiên, điều này vẫn có giá trị đối với các loại khác int. Bạn chỉ cần thay đổi kiểu gói thành một AtomicReferencehoặc một mảng của kiểu đó. Ví dụ: nếu bạn sử dụng a String, chỉ cần làm như sau:

AtomicReference<String> value = new AtomicReference<>();
list.forEach(s -> {
  value.set("blah");
});

Sử dụng một mảng:

String[] value = { null };
list.forEach(s-> {
  value[0] = "blah";
});

Hoặc với Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});

Tại sao bạn được phép sử dụng một mảng như thế int[] ordinal = { 0 };nào? Bạn có thể giải thích nó được không. Cảm ơn bạn
mrbela

@mrbela Bạn không hiểu chính xác điều gì? Có lẽ tôi có thể làm rõ rằng một chút cụ thể?
Olivier Grégoire

1
Mảng @mrbela được chuyển qua tham chiếu. Khi bạn truyền vào một mảng, bạn thực sự đang truyền một địa chỉ bộ nhớ cho mảng đó. Các kiểu nguyên thủy như số nguyên được gửi theo giá trị, có nghĩa là một bản sao của giá trị được chuyển vào. Giá trị chuyển theo giá trị không có mối quan hệ giữa giá trị gốc và bản sao được gửi vào các phương thức của bạn - thao tác sao chép không có tác dụng gì đối với bản gốc. Chuyển bằng tham chiếu có nghĩa là giá trị gốc và giá trị được gửi vào phương thức là một trong cùng một - thao tác với nó trong phương thức của bạn sẽ thay đổi giá trị bên ngoài phương thức.
DetectivePikachu,

@Olivier Grégoire điều này thực sự đã cứu tôi. Tôi đã sử dụng giải pháp Java 10 với "var". Bạn có thể giải thích nó hoạt động như thế nào không? Tôi đã lên Google ở ​​khắp mọi nơi và đây là nơi duy nhất tôi tìm thấy với cách sử dụng cụ thể này. Tôi đoán là vì lambda chỉ cho phép (hiệu quả) các đối tượng cuối cùng từ bên ngoài phạm vi của nó, đối tượng "var" về cơ bản đánh lừa trình biên dịch suy ra rằng nó là cuối cùng, vì nó giả định rằng chỉ các đối tượng cuối cùng mới được tham chiếu bên ngoài phạm vi. Tôi không biết, đó là dự đoán tốt nhất của tôi. Nếu bạn muốn cung cấp lời giải thích, tôi sẽ đánh giá cao điều đó, vì tôi muốn biết tại sao mã của mình hoạt động :)
oaker

1
@oaker Điều này hoạt động vì Java sẽ tạo lớp MyClass $ 1 như sau: class MyClass$1 { int value; }Trong một lớp thông thường, điều này có nghĩa là bạn tạo một lớp với một biến gói-riêng được đặt tên value. Điều này cũng giống như vậy, nhưng lớp thực sự là ẩn danh đối với chúng tôi, không phải đối với trình biên dịch hoặc JVM. Java sẽ biên dịch mã như thể nó vốn có MyClass$1 wrapper = new MyClass$1();. Và lớp ẩn danh chỉ trở thành một lớp khác. Cuối cùng, chúng tôi chỉ thêm đường cú pháp lên trên nó để làm cho nó có thể đọc được. Ngoài ra, lớp là một lớp bên trong với trường gói-riêng. Trường đó có thể sử dụng được.
Olivier Grégoire

14

Điều này khá gần với một vấn đề XY . Đó là, câu hỏi được đặt ra về cơ bản là làm thế nào để biến đổi một biến cục bộ đã được bắt từ lambda. Nhưng nhiệm vụ thực tế trước mắt là làm thế nào để đánh số các phần tử của một danh sách.

Theo kinh nghiệm của tôi, có đến 80% thời gian có câu hỏi làm thế nào để thay đổi một cục bộ đã bị bắt từ bên trong lambda, có một cách tốt hơn để tiếp tục. Thông thường điều này liên quan đến việc giảm bớt, nhưng trong trường hợp này, kỹ thuật chạy luồng qua các chỉ mục danh sách áp dụng tốt:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));

3
Giải pháp tốt nhưng chỉ khi listlà một RandomAccessdanh sách
ZhekaKozlov

Tôi rất tò mò muốn biết liệu vấn đề về thông báo lũy tiến mỗi k lần lặp có thể được giải quyết trên thực tế mà không có bộ đếm chuyên dụng hay không, tức là trong trường hợp này: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Tôi đã xử lý {} phần tử", ctr);} Tôi không thấy cách nào thực tế hơn (giảm có thể làm được, nhưng sẽ dài dòng hơn, đặc biệt là với các luồng song song).
zakmck

14

Nếu bạn chỉ cần truyền giá trị từ bên ngoài vào lambda và không lấy nó ra ngoài, bạn có thể thực hiện điều đó với một lớp ẩn danh thông thường thay vì lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});

1
... và nếu bạn thực sự cần đọc kết quả thì sao? Kết quả không hiển thị cho mã ở cấp cao nhất.
Luke Usherwood

@LukeUsherwood: Bạn nói đúng. Điều này chỉ dành cho trường hợp bạn chỉ cần truyền dữ liệu từ bên ngoài vào lambda, không phải lấy nó ra ngoài. Nếu bạn cần lấy nó ra, bạn sẽ cần có lambda nắm bắt một tham chiếu đến một đối tượng có thể thay đổi, ví dụ: một mảng hoặc một đối tượng có các trường công khai không phải cuối cùng và truyền dữ liệu bằng cách đặt nó vào đối tượng.
newacct


3

Nếu bạn đang sử dụng Java 10, bạn có thể sử dụng var:

var ordinal = new Object() { int value; };
list.forEach(s -> {
    s.setOrdinal(ordinal.value);
    ordinal.value++;
});

3

Một thay thế cho AtomicInteger(hoặc bất kỳ đối tượng nào khác có thể lưu trữ một giá trị) là sử dụng một mảng:

final int ordinal[] = new int[] { 0 };
list.forEach ( s -> s.setOrdinal ( ordinal[ 0 ]++ ) );

Nhưng hãy xem câu trả lời của Stuart : có thể có cách tốt hơn để giải quyết trường hợp của bạn.


2

Tôi biết đó là một câu hỏi cũ, nhưng nếu bạn cảm thấy thoải mái với cách giải quyết khác, xem xét thực tế là biến bên ngoài phải là cuối cùng, bạn có thể chỉ cần thực hiện điều này:

final int[] ordinal = new int[1];
list.forEach(s -> {
    s.setOrdinal(ordinal[0]);
    ordinal[0]++;
});

Có thể không phải là trang nhã nhất, hoặc thậm chí là chính xác nhất, nhưng nó sẽ làm được.


1

Bạn có thể kết thúc nó để giải quyết trình biên dịch nhưng hãy nhớ rằng không khuyến khích các tác dụng phụ trong lambdas.

Để trích dẫn javadoc

Nói chung, không khuyến khích các tác dụng phụ trong các tham số hành vi đối với hoạt động luồng vì chúng thường có thể dẫn đến việc vô tình vi phạm yêu cầu không trạng thái Một số lượng nhỏ các hoạt động luồng, chẳng hạn như forEach () và peek (), chỉ có thể hoạt động thông qua bên -Các hiệu ứng; những thứ này nên được sử dụng cẩn thận


0

Tôi đã có một vấn đề hơi khác. Thay vì tăng một biến cục bộ trong forEach, tôi cần gán một đối tượng cho biến cục bộ.

Tôi đã giải quyết vấn đề này bằng cách xác định một lớp miền bên trong riêng tư bao bọc cả danh sách tôi muốn lặp lại (countryList) và kết quả mà tôi hy vọng nhận được từ danh sách đó (foundCountry). Sau đó, sử dụng Java 8 "forEach", tôi lặp qua trường danh sách và khi đối tượng tôi muốn được tìm thấy, tôi gán đối tượng đó cho trường đầu ra. Vì vậy, điều này chỉ định một giá trị cho một trường của biến cục bộ, không thay đổi chính biến cục bộ. Tôi tin rằng vì bản thân biến cục bộ không bị thay đổi, trình biên dịch không phàn nàn. Sau đó, tôi có thể sử dụng giá trị mà tôi đã nắm bắt trong trường đầu ra, bên ngoài danh sách.

Đối tượng miền:

public class Country {

    private int id;
    private String countryName;

    public Country(int id, String countryName){
        this.id = id;
        this.countryName = countryName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountryName() {
        return countryName;
    }

    public void setCountryName(String countryName) {
        this.countryName = countryName;
    }
}

Đối tượng gói:

private class CountryFound{
    private final List<Country> countryList;
    private Country foundCountry;
    public CountryFound(List<Country> countryList, Country foundCountry){
        this.countryList = countryList;
        this.foundCountry = foundCountry;
    }
    public List<Country> getCountryList() {
        return countryList;
    }
    public void setCountryList(List<Country> countryList) {
        this.countryList = countryList;
    }
    public Country getFoundCountry() {
        return foundCountry;
    }
    public void setFoundCountry(Country foundCountry) {
        this.foundCountry = foundCountry;
    }
}

Lặp lại hoạt động:

int id = 5;
CountryFound countryFound = new CountryFound(countryList, null);
countryFound.getCountryList().forEach(c -> {
    if(c.getId() == id){
        countryFound.setFoundCountry(c);
    }
});
System.out.println("Country found: " + countryFound.getFoundCountry().getCountryName());

Bạn có thể xóa phương thức lớp wrapper "setCountryList ()" và đặt trường "countryList" là cuối cùng, nhưng tôi không gặp lỗi biên dịch khi để nguyên các chi tiết này.


0

Để có một giải pháp chung hơn, bạn có thể viết một lớp Wrapper chung:

public static class Wrapper<T> {
    public T obj;
    public Wrapper(T obj) { this.obj = obj; }
}
...
Wrapper<Integer> w = new Wrapper<>(0);
this.forEach(s -> {
    s.setOrdinal(w.obj);
    w.obj++;
});

(đây là một biến thể của giải pháp do Almir Campos đưa ra).

Trong trường hợp cụ thể, đây không phải là một giải pháp tốt, cũng như Integertệ hơn intcho mục đích của bạn, dù sao thì giải pháp này tôi nghĩ là tổng quát hơn.


0

Có, bạn có thể sửa đổi các biến cục bộ từ bên trong lambdas (theo cách được hiển thị bởi các câu trả lời khác), nhưng bạn không nên làm điều đó. Lambdas dành cho phong cách lập trình chức năng và điều này có nghĩa là: Không có tác dụng phụ. Những gì bạn muốn làm được coi là phong cách xấu. Nó cũng nguy hiểm trong trường hợp các dòng song song.

Bạn nên tìm một giải pháp không có tác dụng phụ hoặc sử dụng vòng lặp for truyền thống.

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.