質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Q&A

解決済

4回答

11874閲覧

「ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります」というエラー

mosa

総合スコア218

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

0グッド

0クリップ

投稿2016/12/22 02:46

下記質問の続きです。中途半端してしまったので、改めて質問します。
https://teratail.com/questions/42539

Javaでラムダ式からローカル変数を参照すると、タイトルのようなエラーとなりますが、finalな配列(オブジェクト)として参照させれば、エラーとはなりません。
実際IDEなどで、ラムダ式からローカル変数を参照ような実装をすると「B」のようなサジェストをされます。
ですが、決して見栄えの良い書き方とはいえません。
であれば、コンパイラが内部的にこのように変更してくれればよいような気もするのですが、どうでしょうか。
また、将来的にそんな変更がありえるものでしょうか。


A.ラムダを使わない記述

java

1List<String> list = Arrays.asList("hoge", "fuga"); 2int a = 0; 3for(String elem : list){ 4 if(elem.startsWith("h")){ 5 a++; 6 } 7}

B.ラムダを使って配列(オブジェクト)経由で参照させる記述

java

1List<String> list = Arrays.asList("hoge", "fuga"); 2final int[] a = {0}; 3list.stream().filter(elem -> elem.startsWith("h")).forEach(elem -> a[0]++);

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

例に出されてるコードですとcount()使えば良いじゃんで終わってしまうかと思いますので、課題を次のように変更しました。

  • 下記の要件を満たすメソッドを作成する。
  • 文字列(String)のリスト(List)を引数として受け取る。
  • リストの各要素で"h"から始まる文字列の数をカウントして、その数(Long)を返す。
  • リストの各要素で"h"から始める文字列は、先頭の"h"を削除した文字列に置き換える(リスト自身を変更)。

で、作ったのが下記です。

Java

1import java.util.AbstractMap.SimpleImmutableEntry; 2import java.util.Collections; 3import java.util.List; 4import java.util.Arrays; 5import java.util.Iterator; 6import java.util.function.BiFunction; 7import java.util.function.Consumer; 8import java.util.function.Function; 9import java.util.stream.Collectors; 10import java.util.stream.Stream; 11public class LambdaTest { 12 public static void main(String[] args) { 13 final Consumer<Function<List<String>, Long>> cons = (func) -> { 14 final List<String> list = Arrays.asList("hoge", "fuga"); 15 final Long count = func.apply(list); 16 System.out.println(count); 17 list.forEach((str) -> System.out.println(str)); 18 }; 19 cons.accept(LambdaTest::checkAndDelFor); 20 cons.accept(LambdaTest::checkAndDelSeparate); 21 cons.accept(LambdaTest::checkAndDelOnce); 22 } 23 24 public static Long checkAndDelFor(List<String> list) { 25 long count = 0; 26 for (int i = 0; i < list.size(); i++) { 27 if (list.get(i).startsWith("h")) { 28 count++; 29 list.set(i, list.get(i).substring(1)); 30 } 31 } 32 33 return count; 34 } 35 36 public static Long checkAndDelSeparate(List<String> list) { 37 final long count = 38 list.stream().filter((str) -> str.startsWith("h")).count(); 39 list.replaceAll((str) -> str.startsWith("h") ? str.substring(1) : str); 40 41 return count; 42 } 43 44 public static Long checkAndDelOnce(List<String> list) { 45 final SimpleImmutableEntry<Long, List<String>> result = 46 list.stream().reduce( 47 new SimpleImmutableEntry<Long, List<String>>( 48 new Long(0), Collections.emptyList()), 49 (acc, str) 50 -> { 51 final Long accCount = acc.getKey(); 52 final Stream<String> accStream = acc.getValue().stream(); 53 final BiFunction<Boolean, String, 54 SimpleImmutableEntry<Long, List<String>>> 55 createPair = (incr, s) 56 -> new SimpleImmutableEntry<Long, List<String>>( 57 incr ? accCount : accCount + 1, 58 Stream.concat(accStream, Stream.of(s)) 59 .collect(Collectors.toList())); 60 61 return str.startsWith("h") 62 ? createPair.apply(true, str.substring(1)) 63 : createPair.apply(false, str); 64 }, 65 (a, b) 66 -> new SimpleImmutableEntry<Long, List<String>>( 67 a.getKey() + b.getKey(), 68 Stream.concat(a.getValue().stream(), b.getValue().stream()) 69 .collect(Collectors.toList())) 70 71 ); 72 final Iterator<String> itr = result.getValue().iterator(); 73 list.replaceAll((_s) -> itr.next()); 74 75 return result.getKey(); 76 } 77}

checkAndDelForが愚直なforループです。説明は不要で良いですよね。

checkAndDelSeparateはカウントと置き換えを別々にしたものです。とても短く単純ですが、"h"から始まるというチェックが一つの要素に付き二回走るという無駄があり、なんともすっきりしたものとは言えません。

と言うことで、最後のcheckAndDelOnce は"h"から始まるというチェックをなんとうか一回にしようと頑張った物です。

そもそもの考えとして、Java8のラムダ式はStream等で使うことを想定しているかと思います。そして、ラムダ式を含むこれらの処理は関数型プログラミングの考えを基にしており、関数型プログラミングでは基本的にオブジェクトは全てimmutableとして扱うべき、副作用が無い純粋関数を使うべきとしています。また、Streamは並列化が容易にできることを想定している側面があり、各ラムダ式が純粋関数でない場合は、並列化がうまく動作するかが保証できなくなります。つまり、「オブジェクトを変更する等の副作用がある処理をすること自体がラムダ式の使い方として間違っている」と私は個人的に思っています。

その考えを基に最終的にできが上がったのがcheckAndDelOnce です。ソースは一番長いですが、考え方は単純です。reduceを使いながら、カウントと新しいリストを次々作って言っているだけで、最後に元々のリストの中身をできあがったリストに置き換えています。でも、何か複雑なことをしているように見えのも事実です。なぜそうなったのか…それはJavaの関数型プログラミング対応が中途半端だからです。

Java8からラムダ式やStream、Optional等関数型プログラミングの要素が追加されました。しかし、タプルが無い、zipが無い、Streamはmutable、ラムダ式内での検査例外がすごく邪魔、::の使い方が他の言語と違う、プリミティブ型Streamを作るしか無かったという事情、オブジェクト自体を変更させていくという命令型オブジェクト指向の典型的なやり方に基づいた言語設計、等などの理由により、関数型プログラミングしやすいとはとてもじゃないですが言えません。

ということで、オブジェクト自体を変更させていくような命令型プログラミングでコーディングする場合は、そもそもラムダ式は使わない方がいいと思います。かといって、ラムダ式を使って関数型プログラミングをするのであれば、そもそもJavaは捨てた方が良いと個人的には思っています。

投稿2016/12/22 17:22

編集2016/12/22 17:23
raccy

総合スコア21733

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mosa

2016/12/23 00:33

詳細にご説明いただきありがとうございます。 元々は、IntelliJ に final int[] a = {0}; というサジェストをされて、意味はわかるがなんという提案をしてくるんだ、と思い考えていることを質問させていただきました。 ちょっとまだ checkAndDelOnce は理解できていないですが、時間をかけて見てみます。 具体的に仕事上なにか困っている等の話ではないので、こういった考えをおきかせいただいて助かります。
guest

0

内部的に変更してくれれば~と言いますが、intとint[]ではまるで違います。全く構造の違うものに「内部的に変更」してしまったら様々な危険が伴います。
それに、そのようなコードならリダクションを使って対応できます。特にこのコードは数えるだけなので、long型になりますがcountメソッドが使えます。単純に数えるのでないなら、それ用のリダクションを記述するreduceメソッドもあります。

投稿2016/12/22 04:42

swordone

総合スコア20649

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mosa

2016/12/23 00:04

ありがとうございます。 「様々な危険」について具体的な例などを1つ2つなどいただけますか。 元々は、IntelliJ に final int[] a = {0}; というサジェストをされて、意味はわかるがなんという提案をしてくるんだ、と思い考えていることを質問させていただきました。
guest

0

ベストアンサー

将来的にそんな変更がありえるものでしょうか。

State of the Lambda

情報が古いですがopenjdkのlambda pojectのドキュメント(2013年)をみるとnon finalの変数が実質的にfinalかどうか推論するよという話の後に、以下のような記述があります。

However, it is not a goal of this project to address all the problems of inner classes. Neither arbitrary capture of mutable variables (4) nor nonlocal control flow (5) are within this project's scope (though such features may be revisited in a future iteration of the language.)

英語が得意じゃないので間違ってるかもですが「このプロジェクトでは変更可能な変数をlambdaの中で参照することをゴールとはしない。でも将来またこの点を検討することもあるかもね」というような意味と捉えました。3年前なので状況は変わっているかもしれませんが・・・可能性としてはなくはないといったところでしょうか。

C#では質問者さんがいったような考え方(配列ではなくてlambdaのためにコンパイラーが自動生成する無名クラスのフィールドとして)サポートしているようで、ローカル変数への代入が無名クラス(的なもの)のフィールドの代入に置き換わるように見えました。javaのlambdaの実装も似た雰囲気なので同様の方式はとれると思います。

この仕様は実装の手間もさりながらJavaのポリシーにあうのかどうかも争われる気がします。

投稿2016/12/22 15:49

編集2016/12/23 02:56
KSwordOfHaste

総合スコア18392

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mosa

2016/12/23 00:35

ありがとうございます。 元々は、IntelliJ に final int[] a = {0}; というサジェストをされて、意味はわかるがなんという提案をしてくるんだ、と思い考えていることを質問させていただきました。 頂いたドキュメントを元にまた情報を探してみます。 また、今は環境がないのですが、 C#でどういった記述になるのかもみてみたいと思います。
KSwordOfHaste

2016/12/23 00:48

「なんという提案をしてくるんだ」=>同感です。
guest

0

「自動でプリミティブ型の変数を置き換える」ことでは対応できないものが1つあります。引数です。引数は外部にインターフェースとして公開される以上、内部の論理だけで勝手に置き換えてしまうことはできません。

そして、あくまで個人的な感覚かもしれませんが、「同じint i;で宣言したものが状況によってプリミティブ型の変数になったり、配列やオブジェクトのフィールドになったりする」という一貫性のなさのほうが、Java的には「インナークラスやラムダの内部で直接プリミティブ型変数を書き換えられない」という利便性より問題があるのかなと思います。

Javaは利便性より厳密性に重きをおいた言語です。JVMで動く言語は他にもありますので、Javaの思想が気に入らないなら別なものに乗り換えるのもありです(Javaのエコシステムは、他のJVM言語でもほぼそのまま使えます)。

投稿2016/12/22 04:39

maisumakun

総合スコア145121

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mosa

2016/12/23 00:08

ありがとうございます。 確かに引数では無理ですね。 元々は、IntelliJ に final int[] a = {0}; というサジェストをされて、意味はわかるがなんという提案をしてくるんだ、と思い考えていることを質問させていただきました。 おっしゃるとおりだとすると、このサジェストは「あまり適切ではないけど、どうしてもこの場でローカル変数を書き換えたければこうするしかないよ」という程度のものという感じですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問