下記質問の続きです。中途半端してしまったので、改めて質問します。
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ページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答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総合スコア21751
0
内部的に変更してくれれば~と言いますが、intとint[]ではまるで違います。全く構造の違うものに「内部的に変更」してしまったら様々な危険が伴います。
それに、そのようなコードならリダクションを使って対応できます。特にこのコードは数えるだけなので、long型になりますがcountメソッドが使えます。単純に数えるのでないなら、それ用のリダクションを記述するreduceメソッドもあります。
投稿2016/12/22 04:42
総合スコア20675
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
ベストアンサー
将来的にそんな変更がありえるものでしょうか。
情報が古いですが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総合スコア18404
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
「自動でプリミティブ型の変数を置き換える」ことでは対応できないものが1つあります。引数です。引数は外部にインターフェースとして公開される以上、内部の論理だけで勝手に置き換えてしまうことはできません。
そして、あくまで個人的な感覚かもしれませんが、「同じint i;
で宣言したものが状況によってプリミティブ型の変数になったり、配列やオブジェクトのフィールドになったりする」という一貫性のなさのほうが、Java的には「インナークラスやラムダの内部で直接プリミティブ型変数を書き換えられない」という利便性より問題があるのかなと思います。
Javaは利便性より厳密性に重きをおいた言語です。JVMで動く言語は他にもありますので、Javaの思想が気に入らないなら別なものに乗り換えるのもありです(Javaのエコシステムは、他のJVM言語でもほぼそのまま使えます)。
投稿2016/12/22 04:39
総合スコア146557
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/12/23 00:33