从 lambda 内部修改局部变量

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

正常

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

和 Lambda 一起

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

有办法解决吗?

197296 次浏览

由于来自 lamda 外部的已使用变量必须(隐式地)是 final,因此必须使用类似于 AtomicInteger的东西或编写自己的数据结构。

Https://docs.oracle.com/javase/tutorial/java/javaoo/lambdaexpressions.html#accessing-local-variables.

用包装纸

任何形式的包装都是好的。

使用 Java10 + ,使用这个结构,因为它非常容易设置:

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

对于 Java8 + ,可以使用 AtomicInteger:

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

或者数组:

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

注意: 使用并行流时要非常小心。您可能不会得到预期的结果。像 斯图尔特的这样的其他解决方案可能更适合这些情况。

对于 int以外的类型

当然,这对于 int以外的类型仍然有效。

例如,使用 Java10 + :

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

或者如果你被 爪哇8或者 9卡住了,使用和上面一样的结构,但是使用 AtomicReference..。

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

或者数组:

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

这非常接近 XY 问题。也就是说,被问到的问题本质上是如何从 lambda 变换捕获的局部变量。但是手头的实际任务是如何对列表中的元素进行编号。

根据我的经验,超过80% 的时间都存在一个问题,那就是如何从一个 lambda 内部变异一个被捕获的本地人,有一个更好的方法来继续。通常这涉及到减少,但是在这种情况下,在列表索引上运行流的技术应用得很好:

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

如果只需要从外部将值传递给 lambda,而不需要传递出去,那么可以使用常规的匿名类而不是 lambda:

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

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

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

但是,看看 斯图尔特的回答: 可能有一个更好的方法来处理你的情况。

您可以将它包装起来以解决编译器的问题,但是请记住,lambdas 中的副作用是不被鼓励的。

引用 Javadoc的话

一般来说,流操作的行为参数中的副作用是不被鼓励的,因为它们常常会在不知情的情况下导致违反无状态要求 少量的流操作,如 forEach ()和 Peak () ,只能通过副作用进行操作; 这些操作应该谨慎使用

我遇到了一个稍微不同的问题。我需要将一个对象赋给局部变量,而不是在 forEach 中递增一个局部变量。

我通过定义一个私有的内部域类来解决这个问题,该类包装了我想要遍历的列表(country List)和我希望从该列表获得的输出(found Country)。然后使用 Java8“ 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 ()”并使字段“ country List”成为 final,但是我没有得到编译错误,因此这些细节保持不变。

如果你使用 Java10,你可以使用 var:

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

要获得更通用的解决方案,可以编写一个通用 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++;
});

(这是阿尔米尔坎波斯给出的解决方案的一个变体)。

在特定的情况下,这不是一个好的解决方案,因为 Integerint更不适合您的目的,无论如何,这个解决方案是更一般的我认为。

是的,您可以从 lambdas 内部修改局部变量(按照其他答案显示的方式) ,但是您不应该这样做。Lambdas 是为函数式编程风格而生的,这意味着: 没有副作用。你想做的事情被认为是不好的风格。在平行流的情况下也是危险的。

您要么找到一个没有副作用的解决方案,要么使用传统的 for 循环。