修改lambda内部的局部变量

时间:2015-05-04 09:32:57

标签: java lambda java-8

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

正常

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

使用Lambda

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

知道如何解决这个问题吗?

11 个答案:

答案 0 :(得分:97)

使用包装器

任何类型的包装都很好。

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

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

适用于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";
});

答案 1 :(得分:12)

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

根据我的经验,在80%的时间内存在如何在lambda中改变被捕获的本地的问题,这是一个更好的方法。通常这涉及减少,但在这种情况下,在列表索引上运行流的技术很适用:

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

答案 2 :(得分:9)

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

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

答案 3 :(得分:7)

由于lamda外部使用的变量必须(隐式)为final,因此必须使用类似AtomicInteger的内容或编写自己的数据结构。

请参阅 https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#accessing-local-variables

答案 4 :(得分:3)

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

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

但请参阅the Stuart's answer:可能有更好的方法来处理您的案件。

答案 5 :(得分:2)

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

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

答案 6 :(得分:2)

我知道这是一个老问题,但是如果您对变通方法感到满意,考虑到外部变量应该是最终变量,您可以简单地这样做:

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

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

答案 7 :(得分:1)

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

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

答案 8 :(得分:0)

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

引用javadoc

  

通常不鼓励行为参数对流操作的副作用,因为它们通常会导致无意中违反无国籍要求   少量的流操作,例如forEach()和peek(),只能通过副作用运行;这些应该小心使用

答案 9 :(得分:0)

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

我通过定义一个私有内部域类来解决这个问题,该类包含了我想迭代的列表(countryList)和我希望从该列表中获取的输出(foundCountry)。然后使用Java 8&#34; forEach&#34;,我迭代列表字段,当找到我想要的对象时,我将该对象分配给输出字段。因此,这会为局部变量的字段赋值,而不会更改局部变量本身。我相信由于局部变量本身没有改变,编译器不会抱怨。然后,我可以使用在列表外部的输出字段中捕获的值。

域对象:

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

你可以删除包装类方法&#34; setCountryList()&#34;并制作字段&#34; countryList&#34;最后,但我没有得到编译错误,这些细节保持原样。

答案 10 :(得分: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差,我认为这种解决方案更笼统。