仕事ではC#を使っているんだけど、Stream APIとラムダ式を勉強してみようと思う!


言いたいこと

普段使っているプログラミング言語と違うプログラミング言語を勉強することで、いつも使っている言語の特徴や良い点・悪い点に気づけます。特に、普段使っているプログラミング言語と違うパラダイムのプログラミング言語を学ぶことで、普段考えもしなかった考え方・書き方を学べるので、とても大きな成長に繋がる(と言われているのをよく聞きます。)

さて、普段何かと比較されることが多いJavaとC#。言語として違う部分も多いですが、似ている部分もたくさんあります。(すいません、偉そうに言いましたが、どちらもまだまだ勉強中です。似ている部分ありますよね?) さて、Javaをやっている人はC#を、C#をやっている人はJavaを勉強することはどのようなメリットがあるでしょうか?

私は、全く異なるパラダイムの言語と違う言語を学ぶこととは、また違ったメリットがあるのではないかなと思っています。

似ている部分のある2つの言語をそれぞれ勉強することで、それぞれの理念や優先することの違い、歴史的経緯などに気づき、それぞれの言語の理解や知識を深めることができるのではないか、そう思っています。むしろ「似ているからこそ、強く感じることもあるのでは」とも思っています。

(あ、とても偉そうに書いていますが私はどちらもまだまだ勉強中です。)

それぞれの言語の全体を勉強し比較するのは非常に大変です。そこで、ひとまずは比較的最近Java8で登場したStream API、そしてそれとC#のLINQを比較し、それぞれの良いところや違い、それぞれのAPIの考え方・理念の違いを理解したいと私は思っています。

C#3.0でLINQが導入された際、多くの言語機能がC#に追加されました。同様にJava8でSteram APIと同時に、そしてStream APIのためにJavaにいくつもの言語機能が追加されました。Stream APIを勉強することで、Javaが何を大切にしていて、どんな互換性を守っているのか、ということを理解できるとも思います。

きっととてもレベルアップできると思います。みなさんもいかがですか?

さて、言いたいことはここまでで全て言いました。
本投稿の残りでは、そう思ったきっかけを紹介します。

(ちなみに、私はC#のLINQという言語機能が大好きです。最近、「お前LINQの投稿しかしないな」と言われたり、LINQの利用に問題があったUnityゲームエンジン+iOSでも、LINQが使える(ことを目標とした)ライブラリを作ったりしました。)

C#のLINQが好きなんです

エピソードの前に、私のこととLINQのことを述べさせてください。

私の仕事は、Unityゲームエンジンを使ってのゲーム開発です。Unityはプログラミング言語C#が使われることが多く、私もC#を使っています。

JavaのStream APIのメリットの一つに、コレクションやマップを扱うコードが書きやすく、そしてコードが読みやすくなる、ということがあると思います。C#にはLINQという言語機能があり、Stream APIと同じように、LINQを使うことでコレクションのコードが書きやすく・読みやすくなります。

「文字列のリストの要素の中から、文字列の長さが5より大きい、最初の要素を取得する」コードをStream APIで書いてみます。

JavaのStreamAPIの例
List<String> nameList = getNameList ();
Optional<String> name = nameList.stream().filter(name -> name.length() > 5).findFirst();

こうですよね?(あってますよね?)

これをC#のLINQを使って書いてみます。

C#のLINQの例(その1)
List<string> nameList = GetNameList ();
string name = nameList.Where (name => name.Length > 5).First ();

Java8のStream APIを使っている方ならば、なんとなく何をやっているのか想像がつくと思います。Wherefilterの用に要素を選択します。FirstfindFirstの用に先頭要素を取得します。ちなみにFirstは要素が無かった場合、例外が発生します。

実はあまり上記のよなコードは書きません。次のようにWhereを無くすことができます。Firstにはオーバーロードがあり、それを使うと

C#のLINQの例(その2)
List<string> nameList = GetNameList ();
string name = nameList.First (name => name.Length > 5);

このように、「文字列のリストの要素の中から、文字列の長さが5より大きい、最初の要素を取得する」ことができます。Firstメソッド1つで条件指定と先頭要素取得の両方ができるのです。引数にとるラムダ式で条件を指定しています。

余談ですが、私が仕事で最初に使った言語はJavaでした。Androidアプリを1年間くらい作ってました。その後Unityに出会い、C#に出会い、LINQに出会いました。Unityで仕事をするようになってからも、たまにAndroidアプリのためにJavaを書くことがありました。AndroidですからStream APIは使えません。その時は、リストやマップを扱うコードが書くのが非常につらかったです。「あぁ、LINQを使えたらいいのに」と強く思いました。一度LINQを知ってしまったら、for文・if文でずらずら書くのがつらくて。「LINQやStream APIが使えれば1行なのに!」というイライラが募りました。

Stream APIには条件を満たした最初の要素を取得するやつがない?

だいぶ前ですが、Java 8でStream APIとラムダ式が出ましたね。残念ながらAndroidでは使うことはできないようですが、コレクションを扱うコードが非常に書きやすくなると、すこし嬉しくなりました。(Androidで使えたら良いのに!)

さて、ここからやっとC#を使っている私が、Stream APIを勉強したほうがいいと思ったきっかけのエピソードです。

時間は2014年の4月まで戻ります。
私はどんなメソッドがあるのかな?どういうことができるのかな?とStream APIをぼんやりと眺めていました。

その途中、「C#のFirstに相当するメソッドが見当たらないな?」と疑問に思いました。単体で条件指定とその条件を満たした先頭要素取得ができる方です。下記に再掲します。

C#のLINQの例(その2)再掲
List<string> nameList = GetNameList ();
string name = nameList.First (name => name.Length > 5);

これに相当するStream APIのメソッドを見つけることができません。Stream<T>型にfindFirstというメソッドはありましたが、このメソッドは引数をとらず、ただ先頭要素取得するだけのようです(Optional<T>で)。要素の選択はfilterというメソッドでできるようですが、これは結果がStream<T>ですね。

C#のLINQのFirstメソッドのように単体で条件指定とそれを満たす先頭要素取得ができるStream APIのメソッドはみつかりません。

stackoverflowで聞いてみた

「C#のFirstの用にラムダ式(デリゲート)を引数にとって、1つのメソッドで条件指定とその条件を満たした先頭要素を取得するメソッドはないのかな?」という疑問をstackoverflowで聞いてみました。
英語が得意でないので、ところどころお見苦しいところがあるかもしれません。こちらです。

タイトルは、

Is There a method that finds first matched element with a condition In List In Java 8 like C# Enumerable#First and Scala List#find?

質問文は、

Like C# LINQ Enumerable#First with delegate method and Scala List#find method, In Java8, is there a method that finds first matched element with a condition in list?

です。

この質問への回答はすぐに来ました。

条件を指定し、条件を満たした最初の要素を取得するコードはStream APIだとこのように書くと教えてもらいました。(実際のコードとは違います。)

JavaのStreamAPIで書き直す
List<String> nameList = getNameList ();
Optional<String> name = nameList.stream().filter(name -> name.length() > 5).findFirst();

ですが、それは私が求めている答えとは違いました。

質問する前から選択をするfilterメソッドと先頭要素取得をするfindFirstメソッドは知っていました。私が知りたかったのは、「filterした後、findFirstすればいい」というこのような回答ではなかったのです。

C#のLINQのFirstメソッドは1つのメソッドで、条件指定とその条件を満たした先頭要素取得をすることができます。私はJavaのStream APIでFirstの用に、1つのメソッドで条件指定と先頭要素取得ができるものが存在するかを知りたかったのです。

その答えはNoだとstackoverflowで回答者達が教えてくてました。そのような選択と先頭要素取得を同時に行うメソッドはStream APIには無いそうです。

大事なのはココからです。
そのstackoverflowで回答してくれた人達が、JavaのAPIにおける非常に大切なことを教えてくれました。

JavaのAPIで大切なこと。小さなものを組み合わせて

特にこのコメントがとても参考になりました。

それはJavaのAPIは「小さな再利用性のいいAPIを組み合わせて使うよう設計されている」ということです。

もちろん他の言語でもいろいろなAPIを組み合わせてコードを書くと思います。しかしJavaはより顕著にAPIの小ささ再利用性を意識していると思います。

個人の好みを言ってしまえば、C#の1つのメソッドで条件指定と先頭要素を取得できるFirstメソッドが好きです。ですが、JavaのStream APIの場合、「Javaは小さなものを組み合わせて」ということに元づき、条件指定とその条件を満たした先頭要素取得を行う1つのメソッッドは無い、ということはこのコメントを読んだら、とても納得しました。

さて、さらっと流しましたがJavaのStream<T>のfindFirstメソッドは、Optional<T>型が返り値ですね。

一方で、C#のLINQのFirstメソッドはもし要素が無かった場合、例外が発生します。またLINQにはFirstOrDefaultというメソッドも存在します。このメソッドは要素が無かった場合は、その型の既定値を返します。

ここもまたJavaのStream APIとC#のLINQの面白い違いですね。こちらは個人的にはJavaのOptional<T>を返す方が好きです。

このようにJavaのStream APIとC#のLINQは、それを使うことで配列、リスト、マップ(ディクショナリ)などを扱うコードが書きやすく、また読みやすくなるという共通点はありますが、APIの設計はかなり違いますね。また、関連の言語要素に違いも多くあるようです。

まとめ

もともとの言語としての、C#とJavaの違いも大きいと思います。Javaは、プリミティブ型はジェネリクスに指定できません。また、C#にはデリゲートという概念がもともとありました。

このように元々持っている言語の機能の差違や制約の違いも、JavaのStream APIとC#のLINQの設計や理念の違いに大きく関わってくると思います。それだけでなく言語が大切にしていること、それぞれのAPIが大切にしていること、もあるでしょう。Java8やC#3.0で追加されたそれぞれも、それぞれの考えがあっての仕様でしょう。

私が普段使っていないJavaのStream APIを勉強することで、仕事で使っているC#のLINQのメリットやデメリット、特徴に気づけると思います。

よく似ているが違うものと比較することで、それが何なのかというのがはっきりと見えてくると思います。

どうでしょう?みなさんも、C#やっている人はJavaやったり、Javaやっている人はC#やっては?