※本記事は、Simon RobertsとMikalai Zaikinによる”Quiz Yourself: Using Core Functional Interfaces: Consumer (Advanced)“を翻訳したものです。
上級プログラマーにも難解なコンシューマ・インタフェース
著者:Simon Roberts、Mikalai Zaikin
2020年4月6日
その他の設問はこちらから
Predicate、Consumer、Function、Supplierなどのコア関数型インタフェースを使いたいと思います。この設問では、コンシューマに注目します。
次のコード部分について:
Stream.of(1).peek(
((Consumer<Integer>)(i1)->{i1 = i1 + 1;})
.andThen((i2)->{i2 = i2 + 2;}))
.forEach(System.out::print);
コンソールには何が出力されますか。1つ選んでください。
| A. 1 | |
| B. 2 | |
| C. 3 | |
| D. 4 |
解答:Stream.peek(…)メソッドのAPIドキュメントには、元のストリームの要素からなるストリームを返しつつ、それぞれの要素が結果ストリームで使われる際に、指定された操作が実行されると記載されています。
このメソッドでは、引数としてConsumerを受け取ります。その目的は、各要素をConsumerに渡す際に元のストリームを変更しないことです。このメソッドは、デバッグに役立てるためにストリーム内の項目を確認(出力など)することを目的として使用する場合が多いでしょう。
この設問の要点は、Consumerでの実行結果と、その結果がストリームのデータに影響を与えるかどうかです。ストリームのあらゆる操作において、一般的に、ストリーム内の要素の変更は避けることが最善です。好まれる動作は、変更を反映した新しい要素を作成し、その要素をストリームとともに次の操作に渡すことです。このような推奨事項があるにもかかわらず、通常のJava構文でこの推奨事項に違反し、ストリーム・データが変更されてしまう動作になる可能性があるという状況も数多く存在します。
java.util.function.Consumerインタフェースの抽象メソッドのシグネチャは次のとおりです。
void accept(T t);
このメソッドでは、戻りタイプがvoidと宣言されているため、データを変更できる方法は2つしかありません。1つ目として、メソッドの引数が変更可能な参照タイプ(StringBuilderは該当しますが、Stringは該当しません)の場合、引数で参照されているオブジェクトの内容を変更できます。2つ目として、変更可能なオブジェクトへの外部参照がこのメソッドで使われている場合、オブジェクトの内容を変更できます。
この設問のConsumerを実装しているコードに外部参照がないことは明らかであるため、おそらくメソッドの引数以外は変更できません。引数を変更できるのは、引数が変更可能なオブジェクトである場合に限られることから、次に確認する必要があるのは、引数の型です。引数はプリミティブのintのように見えるかもしれませんが、実際はオブジェクトであり、具体的に言えば、オートボクシングされたIntegerオブジェクトです。注意すべき点は、ストリームのソースがStream.ofファクトリ・メソッドであることです。このメソッドではオブジェクトのストリームが作成されるため、intはボクシングする必要があります。このストリームがIntStream.ofを使って作成されていたとすれば、プリミティブのストリームになっていました。
次にわかることは、Integerオブジェクトは不変であり、最初に作成されたオブジェクトが以下の形式のコードによって変更されることはない点です。新しいIntegerが作成され、i1が参照する値は、新しいIntegerを参照するように変更されます。
Integer i1 = 1; i1 = i1 + 1;
これは、String型を使っている次のコードとまったく同様です。
String s = "Hello"; s = s + " world! ";
ここから、この場合、peekが返すストリームは、元の変更されていない1つのオブジェクトで構成されなければならないことがわかります。したがって、出力は1であり、選択肢Aが正解となり、選択肢B、C、Dは誤りとなります。
Consumerのコードをもう少し詳しく見てみるのも興味深いでしょう。動作を説明しやすくするために、重要な要素を取り出して並べてみます。
final int ONE = 1;
final int TWO = 2;
Consumer<Integer> c1 = (i1)->{i1 = i1 + ONE;};
Consumer<Integer> c2 = (i2)->{i2 = i2 + TWO;};
Consumer<Integer> cFinal = c1.andThen(c2);
cFinal.accept(Integer.valueOf(1));
Consumerインタフェースには、defaultメソッドとしてandThen(Consumer c2)が存在します。このメソッドでは、元の2つのConsumerの動作を組み合わせた新しいConsumerが作成されます。cFinalで定義された動作が呼び出されると、c1にその呼出しの引数(ストリームをボクシングしたInteger(1))が渡されます。これによって新しくInteger(2)が作成されますが、voidメソッドが終了したときに即座にスコープ外になります。
その後、cFinalの操作ではもともとのInteger(1)を受け取り、それを引数としてc2を呼び出します。c2が呼び出されると、TWOが加算され、新しく別のInteger(3)が作成されますが、このオブジェクトもvoidメソッドが終了したときにスコープ外になります。その結果、前述のように、メソッドローカルなIntegerオブジェクトはいずれもストリームには含まれないため、オブジェクトを作成したメソッドが完了した途端に、両方ともガベージ・コレクションの候補になります。
この構文では、Consumerのラムダの本体で、リテラル定数1および2ではなく、final変数ONEおよびTWOを加算するように変更した点に注意してください。もともとのコードで使用されている形式では、同じであるように見える2か所のコードを見て、実際に同じものだと思ってしまうことが非常に起こりがちです。長い形式でコードを表現することで、その違いがわかりやすくなります。実際のコードで同じような状況になった場合は、このようにする方がよいかもしれません。
正解は選択肢Aです。
![]() |
Simon RobertsSimon Roberts:Sun Microsystemsがイギリスで初めてJavaの研修を行う少し前にSunに入社し、Sun認定Javaプログラマー試験とSun認定Java開発者試験の作成に携わる。複数のJava認定ガイドを執筆し、現在はフリーランスでPearson InformITにおいて録画やライブによるビデオ・トレーニングを行っている(直接またはO’Reilly Safari Books Onlineサービス経由で視聴可能)。OracleのJava認定プロジェクトにも継続的に関わっている。 |
![]() |
Mikalai ZaikinMikalai Zaikin:ベラルーシのミンスクを拠点とするIBA IT ParkのリードJava開発者。OracleによるJava認定試験の作成に携わるとともに、複数のJava認定教科書のテクニカル・レビューを行っている。Kathy Sierra氏とBert Bates氏による有名な学習ガイド『Sun Certified Programmer for Java』では、3版にわたってテクニカル・レビューを務めた。 |


