ラムダ内からローカル変数を変更する


115

でローカル変数を変更するとforEach、コンパイルエラーが発生します。

正常

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

ラムダと

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

これを解決する方法はありますか?


8
ラムダは本質的に匿名の内部クラスの構文糖であると考えると、私の直感は、非最終的なローカル変数をキャプチャすることは不可能であるということです。私は間違っていると証明されたいです。
Sinkingpoint 2015年

2
ラムダ式で使用される変数は、事実上最終でなければなりません。これはやり過ぎですが、アトミック整数を使用することもできるため、ラムダ式はここでは実際には必要ありません。forループに固執するだけです。
Alexis C.

3
変数は事実上finalでなければなりません。これを参照してください:なぜローカル変数キャプチャの制限なのですか?
Jesper


2
@Quirliom匿名クラスの構文糖ではありません。ラムダは内部でメソッドハンドルを使用します
ダイオキシン

回答:


176

ラッパーを使用する

どんな種類のラッパーも良いです。

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

なぜあなたはのような配列を使用することが許可されていますint[] ordinal = { 0 };か?説明してくれますか?ありがとう
mrbela

@mrbelaあなたは正確に何を理解していませんか?多分私はその特定のビットを明確にすることができますか?
オリヴィエ・グレゴワール

1
@mrbela配列は参照渡しされます。配列を渡すと、実際にはその配列のメモリアドレスが渡されます。整数などのプリミティブ型は値によって送信されます。つまり、値のコピーが渡されます。値渡し元の値とメソッドに送信されるコピーの間に関係はありません。コピーを操作しても元の値には影響しません。参照渡しとは、元の値とメソッドに送信される値が同じであることを意味します。メソッドで操作すると、メソッドの外側で値が変更されます。
探偵ピカチュウ

@OlivierGrégoireこれは本当に私の皮を救った。「var」でJava 10ソリューションを使用しました。これがどのように機能するか説明できますか?私は至る所でグーグルで検索しましたが、これがこの特定の使用法で見つけた唯一の場所です。私の推測では、ラムダはスコープの外からの(事実上)最終オブジェクトしか許可しないため、「var」オブジェクトは基本的にコンパイラーをだまして、最終オブジェクトのみがスコープ外で参照されると想定しているため、最終オブジェクトであると推測します。よくわかりません。それが私の推測です。私のコードがなぜ機能するのかを知りたいので、説明を提供して
もらえれば幸い

1
@oakerこれは、JavaがクラスMyClass $ 1を次のように作成するため機能します。class MyClass$1 { int value; }通常のクラスでは、という名前のパッケージプライベート変数を使用してクラスを作成しますvalue。これは同じですが、クラスは実際には匿名であり、コンパイラやJVMではありません。Javaは、コードをあたかもコンパイルしたかのようにコンパイルしますMyClass$1 wrapper = new MyClass$1();。そして、匿名クラスは単なる別のクラスになります。最後に、読みやすくするためにその上に構文糖を追加しました。また、クラスはpackage-privateフィールドを持つ内部クラスです。そのフィールドは使用可能です。
OlivierGrégoire

14

これはXY問題にかなり近いですです。つまり、尋ねられる質問は基本的に、ラムダからキャプチャされたローカル変数をどのように変異させるかです。しかし、手元にある実際のタスクは、リストの要素に番号を付ける方法です。

私の経験では、キャプチャされたローカルをラムダ内からどのように変異させるかという質問がある場合の80%以上で、続行するためのより良い方法があります。通常、これには削減が含まれますが、この場合、リストインデックスに対してストリームを実行する手法が適切に適用されます。

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

値を外部からラムダに渡すだけで、それを取得する必要がない場合は、ラムダの代わりに通常の匿名クラスを使用できます。

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

1
...そして実際に結果を読む必要がある場合はどうなりますか?結果はトップレベルのコードからは見えません。
Luke Usherwood、

@LukeUsherwood:その通りです。これは、外部からラムダにデータを渡すだけで、取り出す必要がない場合にのみ使用します。それを取得する必要がある場合は、ラムダに可変オブジェクト(配列など)への参照、またはfinal以外のパブリックフィールドを持つオブジェクトをキャプチャさせ、それをオブジェクトに設定してデータを渡す必要があります。
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 ]++ ) );

しかし、スチュアートの答えを見てください。あなたのケースに対処するより良い方法があるかもしれません。


2

私はそれが古い質問であることを知っていますが、回避策に慣れている場合は、外部変数が最終である必要があるという事実を考慮して、これを単に行うことができます:

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

おそらく最もエレガントではないかもしれませんし、最も正確でもないかもしれませんが、実際はそうです。


1

コンパイラで回避するためにまとめることもできますが、ラムダでの副作用はお勧めしません。

javadocを引用するには

ストリーム操作への動作パラメーターの副作用は、ステートレス要件の無意識の違反につながることが多いため、一般に推奨されていません。 -効果; これらは注意して使用する必要があります


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によって提供されたソリューションの変形です)。

特定のケースでは、これは良い解決策ではなく、目的Integerよりも悪いのでint、とにかくこの解決策はより一般的です。


0

はい、ローカル変数をラムダ内から(他の回答で示されている方法で)変更できますが、変更しないでください。ラムダは関数型のプログラミング用であり、これは次のことを意味します。副作用なし。あなたがしたいことは悪いスタイルだと考えられています。並列ストリームの場合にも危険です。

副作用のない解決策を見つけるか、従来のforループを使用する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.