从Lambda内部修改局部变量


114

修改中的局部变量forEach会产生编译错误:

正常

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

与Lambda

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

任何想法如何解决这个问题?


8
考虑到lambda本质上是匿名内部类的语法糖,我的直觉是不可能捕获非最终局部变量。我很想证明自己是错的。
Sinkingpoint,2015年

2
lambda表达式中使用的变量必须有效地为final。您可以使用原子整数,尽管它是过分的,所以这里实际上不需要lambda表达式。只是坚持for循环。
亚历克西斯·C。

3
该变量必须有效地为final。看到此:为什么限制局部变量捕获?
杰斯珀,2015年


2
@Quirliom对于匿名类,它们不是语法糖。Lambdas使用方法在内部进行处理
Dioxin

Answers:


175

使用包装纸

任何一种包装纸都是好的。

对于Java 8+,请使用AtomicInteger

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

...或数组:

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

使用Java 10+

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

注意:如果使用并行流,非常小心。您可能无法获得预期的结果。诸如Stuart的其他解决方案可能会更适合这些情况。

对于其他类型 int

当然,这对于之外的其他类型仍然有效int。您只需要将包装类型更改为AtomicReference或该类型的数组即可。例如,如果您使用String,只需执行以下操作:

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

使用数组:

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

或使用Java 10+:

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

为什么允许使用like之类的数组int[] ordinal = { 0 };?你能解释一下吗?谢谢
mrbela

@mrbela你到底不明白什么?也许我可以澄清一下具体内容?
奥利维尔·格雷戈尔

1
@mrbela数组通过引用传递。当您传递数组时,实际上是在传递该数组的内存地址。诸如整数之类的原始类型是按值发送的,这意味着将传递值的副本。按值传递在原始值和发送到您的方法中的副本之间没有关系-操作副本对原始对象没有任何作用。通过引用传递意味着原始值和发送到方法中的原始值是相同的-在您的方法中对其进行操作将在方法外更改值。
侦探

@OlivierGrégoire确实救了我的皮。我将Java 10解决方案与“ var”一起使用。您能解释一下这是如何工作的吗?我到处都有Google搜索,这是我找到的唯一使用此特殊用法的地方。我的猜测是,由于lambda仅允许(有效地)从其作用域之外的最终对象,因此“ var”对象从根本上愚弄了编译器以推断出它是最终的,因为它假定只有最终对象将在范围之外被引用。我不知道,这是我最好的猜测。如果您愿意提供一个解释,我将不胜感激,因为我想知道我的代码为什么起作用:)
oaker

1
@oaker这行得通,因为Java将按如下方式创建类MyClass $ 1:class MyClass$1 { int value; }在通常的类中,这意味着您将创建一个名为package的私有变量value。这是相同的,但是该类实际上对我们是匿名的,而不是编译器或JVM的匿名。Java将像编译代码一样进行编译MyClass$1 wrapper = new MyClass$1();。匿名类成为另一个类。最后,我们只是在其之上添加了语法糖以使其易于阅读。同样,该类是带有package-private字段的内部类。该字段是可用的。
OlivierGrégoire

14

这非常接近XY问题。也就是说,所要问的问题本质上是如何从lambda突变捕获的局部变量。但是,眼下的实际任务是如何为列表中的元素编号。

以我的经验,在高达80%的时间里,有一个问题是如何从lambda内突变捕获的局部基因,这是一种更好的方法。通常,这涉及减少,但是在这种情况下,在列表索引上运行流的技术非常适用:

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

3
良好的解决方案,但只有list一个RandomAccess列表
ZhekaKozlov

我很想知道是否可以在没有专用计数器的情况下实际上解决每k次迭代的进度消息问题,即这种情况:stream.forEach(e-> {doSomething(e); if(++ ctr%1000 = = 0)log.info(“我已经处理过{}个元素”,ctr);}我看不到更实际的方法(可以减少约简,但更冗长,尤其是在并行流中)
zakmck

14

如果您只需要从外部将值传递给lambda而不是将其取出,则可以使用常规的匿名类而不是lambda来实现:

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

1
...以及如果您需要实际读取结果怎么办?结果对于顶级代码不可见。
路加·乌瑟伍德

@LukeUsherwood:你是对的。这仅适用于仅需要将数据从外部传递到lambda而不是将其取出的情况。如果需要删除它,则需要让lambda捕获对可变对象(例如数组或具有非最终公共字段的对象)的引用,并通过将其设置为对象来传递数据。
newacct


3

如果您使用的是Java 10,则可以使用var

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

3

AtomicInteger(或任何其他能够存储值的对象)的替代方法是使用数组:

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

但是请参阅Stuart的回答:可能有更好的方式处理您的案件。


2

我知道这是一个老问题,但是如果您对解决方法感到满意,考虑到外部变量应该是最终变量,则可以执行以下操作:

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

也许不是最优雅的,甚至不是最正确的,但它会做到的。


1

您可以将其包装起来以解决编译器问题,但是请记住,不要鼓励使用lambda的副作用。

引用javadoc

通常不鼓励行为参数对流操作的副作用,因为它们经常会导致无意识地违反无状态要求。少量流操作(例如forEach()和peek())只能通过side操作-效果;这些应该小心使用


0

我有一个稍微不同的问题。我不需要在forEach中增加局部变量,而是需要为该局部变量分配一个对象。

我通过定义一个私有内部域类解决了这一问题,该类既包装了我要迭代的列表(countryList),又包装了希望从该列表中获得的输出(foundCountry)。然后使用Java 8“ forEach”,遍历列表字段,找到所需对象后,将该对象分配给输出字段。因此,这会为局部变量的字段分配一个值,而不更改局部变量本身。我相信,由于局部变量本身未更改,因此编译器不会抱怨。然后,我可以使用在列表之外的输出字段中捕获的值。

域对象:

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

包装对象:

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

迭代操作:

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

您可以删除包装器类方法“ setCountryList()”,并将字段“ countryList”定为final,但是我没有遇到编译错误,使这些详细信息保持原样。


0

要获得更通用的解决方案,可以编写一个通用的Wrapper类:

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

(这是Almir Campos提供的解决方案的变体)。

在特定情况下,这不是一个好的解决方案,因为Integerint您的目的还差,无论如何,我认为这种解决方案更为笼统。


0

是的,您可以从lambda内部修改局部变量(以其他答案所示的方式),但您不应该这样做。Lambda一直是功能性编程风格,这意味着:无副作用。您想做的事被认为是不好的风格。在并行流的情况下也很危险。

您应该找到没有副作用的解决方案,或者使用传统的for循环。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.