ラムダ式で使用される変数はfinalまたは事実上finalである必要があります


134

ラムダ式で使用される変数はfinalまたは事実上finalである必要があります

使用しようとするcalTzと、このエラーが表示されます。

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

5
calTzラムダから変更することはできません。
Elliott Frisch

2
これはJava 8に間に合わなかったものの1つだと思いました。しかし、Java 8は2014年でした。ScalaとKotlinはこれを何年も許可してきたので、それは明らかに可能です。Javaはこの奇妙な制限をなくすつもりですか?
GlenPeterson 2018

5
ここで @MSDoustiさんのコメントに更新されたリンクです。
geisterfurz007 2018年

私は、Completable Futuresを回避策として使用できると思います。
Kraulain

私が観察した重要なことの1つ-通常の変数の代わりに静的変数を使用できます(これにより、効果的に最終的に推測できます)
kaushalpranav

回答:


68

finalそれは一度だけインスタンス化することができるという変数を意味します。Javaでは、ラムダや匿名の内部クラスで非final変数を使用することはできません。

古いfor-eachループでコードをリファクタリングできます:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

このコードの一部の意味がわからなくても:

  • v.getTimeZoneId();戻り値を使用せずにを呼び出す
  • 割り当てでcalTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());は、最初に渡されたものを変更calTzせず、このメソッドで使用しません
  • あなたはいつもを返しますnull、なぜvoid戻り値の型として設定しないのですか?

また、これらのヒントが改善に役立つことを願っています。


finalではない静的変数を使用できます
Narendra Jaggi

92

他の回答は要件を証明します、要件が存在する理由を説明していません。

JLSは、§15.27.2でその理由に言及しています。

効果的に最終的な変数への制限は、動的に変化するローカル変数へのアクセスを禁止します。そのキャプチャは並行性の問題を引き起こす可能性があります。

バグのリスクを低減するために、キャプチャした変数が変更されないようにすることを決定しました。


10
良い答え+1、そして事実上ファイナルの理由がほとんどカバーされていないように思われることに驚いています。注目すべきは:それはされている場合は、ローカル変数は、ラムダによって捕捉することができ、また間違いなくラムダの本体の前に割り当てられています。どちらの要件でも、ローカル変数へのアクセスがスレッドセーフになることが保証されているようです。
Tim Biegeleisen 2018

2
クラスメンバーではなくローカル変数のみに制限される理由は何ですか?私は自分の変数をクラスメンバーとして宣言することで問題を回避していることがよくあります...
David Refaeli

4
@DavidRefaeliクラスメンバーは、メモリモデルによってカバー/影響を受けます。これにより、共有した場合に予測可能な結果が生成されます。で述べたようにローカル変数は、ありません§17.4.1
ダイオキシン

これはばかげたハックなので、削除する必要があります。コンパイラーは、クロススレッド変数へのアクセスの可能性について警告する必要がありますが、許可する必要があります。または、ラムダが同じスレッドで実行されているか、並列で実行されているかなどを知るのに十分スマートでなければなりません。これは愚かな制限であり、私は悲しくなります。また、他の人が述べたように、問題はC#などには存在しません。
Josh M.

@JoshM。C#では、変更可能な値の型を作成することもできます。これにより、問題を回避することを推奨します。そのような原則を持つ代わりに、Javaはそれを完全に防ぐことに決めました。柔軟性を犠牲にして、ユーザーエラーを減らします。私はこの制限に同意しませんが、それは正当化できます。並列処理を考慮すると、コンパイラー側で追加の作業が必要になるため、おそらく「クロススレッドアクセスの警告」のルートが採用されなかったのです。仕様に取り組んでいる開発者がおそらくこれに対する唯一の確認になるでしょう。
ダイオキシン

57

ラムダから、最終でないものへの参照を取得することはできません。変数を保持するには、ラムダの外側から最終ラッパーを宣言する必要があります。

最後の「参照」オブジェクトをこのラッパーとして追加しました。

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   

私は同じまたは同様のアプローチについて考えていました-しかし、この答えについての専門家のアドバイス/フィードバックを見てみたいですか?
YoYo

4
このコードはイニシャルを逃すreference.set(calTz);か、を使用して参照を作成する必要がありますnew AtomicReference<>(calTz)。そうしないと、パラメーターとして指定されたnull以外のTimeZoneが失われます。
ジュリアンクロネッグ

7
これが最初の答えです。AtomicReference(または同様のAtomic___クラス)は、考えられるあらゆる状況でこの制限を安全に回避します。
GlenPeterson、2018

1
同意された、これは受け入れられた答えであるはずです。他の回答は、非関数型プログラミングモデルにフォールバックする方法と、なぜこれが行われたのかについての有用な情報を提供しますが、実際に問題を回避する方法を教えてはいけません!
ジョナサンベン

2
@GlenPetersonはひどい決断です。この方法の方がはるかに遅いだけでなく、ドキュメントが要求する副作用のプロパティも無視しています。
ユージーン

41

Java 8には、「実質的に最終的な」変数と呼ばれる新しい概念があります。これは、初期化後に値が変化しないfinal以外のローカル変数を「Effectively Final」と呼ぶことを意味します。

この概念が導入されたのは、Java 8より前のバージョンでは、匿名クラスでfinal以外のローカル変数を使用できなかったためです。匿名クラスのローカル変数にアクセスしたい場合は、それをfinalにする必要があります。

ラムダが導入されたとき、この制限は緩和されました。したがって、ラムダ自体は匿名クラスにすぎないため、初期化された後で変更されない場合はローカル変数をfinalにする必要があります。

Java 8は、開発者がラムダを使用するたびにローカル変数をfinalと宣言する手間を実感し、この概念を導入して、ローカル変数をfinalにする必要をなくしました。したがって、匿名クラスのルールが変更されていないfinal場合は、ラムダを使用するときに毎回キーワードを記述する必要がないだけです。

ここで良い説明を見つけました


コードのフォーマットは、一般的な技術用語ではなく、コードにのみ使用してください。effectively finalコードではなく、用語です。非コードテキストにコードフォーマットいつ使用する必要があるかを参照してください上のメタスタックオーバーフロー
Charles Duffy

(したがって、「finalキーワード」はコードの単語であり、そのようにフォーマットするのが正しいですが、「最終」をコードとしてではなく記述的に使用する場合は、代わりに用語を使用します)。
Charles Duffy

9

あなたの例ではforEach、lambbaを単純なforループに置き換えて、変数を自由に変更できます。または、おそらく、変数を変更する必要がないようにコードをリファクタリングします。ただし、完全を期すために、エラーの意味と回避策を説明します。

Java 8言語仕様、§15.27.2

使用されているがラムダ式で宣言されていないローカル変数、仮パラメーター、または例外パラメーターは、finalで宣言するか、事実上finalである必要があります(§4.12.4)。そうしないと、使用しようとするとコンパイル時エラーが発生します。

基本的にcalTz、ラムダ(またはローカル/匿名クラス)内からローカル変数(この場合)を変更することはできません。これをJavaで実現するには、可変オブジェクトを使用して、ラムダから(最終変数を介して)変更する必要があります。ここでの可変オブジェクトの1つの例は、1つの要素の配列です。

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}

別の方法は、オブジェクトのフィールドを使用することです。たとえば、MyObj result = new MyObj(); ...; result.timeZone = ...; ....; result.timezone;を返します。ただし、上記で説明したように、これによりスレッドセーフの問題が発生することに注意してください。stackoverflow.com/a/50341404/7092558を
Gibezynu Nu

0

この種の問題に対する一般的な回避策よりも変数を変更する必要がない場合は、 ラムダを使用し、メソッドパラメーターでfinalキーワードを使用するコードの一部抽出することになります。


0

ラムダ式で使用される変数は、最終または実質的に最終でなければなりませんが、最後の1要素配列に値を割り当てることができます。

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

これは、Alexander Udalovの提案と非常によく似ています。それとは別に、このアプローチは副作用に依存していると思います。
スクラッチ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.