白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Java使いがC#を勉強する その⑪ LINQ

こんばんは。

桜の開花宣言がされたりと、春がのそのそと迫ってくる感じですが、
気温の変化に弱いので、いまいち体調がよろしくないのが困りものです。

さて、今回でメインの機能はだいたい終わりかなと思っています。
初期化子とかenumとかyeildとかnullableとか細かいことはまだいろいろありますが、(細かくない?)
今回はLINQ(統合言語クエリ)について見ていきます。

LINQ = Stream API ?


LINQJavaのStream APIの機能にあたると思って問題ないでしょう。

Stream APIはJava8(2014年3月18日)で導入されましたが、
LINQC#3.0(2007年12月)で導入されているので、だいぶ先行ですね。

データ集合に対してデータの問い合わせや変換をする機能ですね。
 

クエリ構文とメソッド構文

 
LINQにはクエリ構文と呼ばれる書き方とメソッド構文と呼ばれる書き方があります。
クエリ構文は糖衣構文なので、コンパイル時にはメソッド構文に変換されるようです。

例えばクエリ構文はこんな感じ。
SQLと同じような構文ですが、selectが後ろに行きます。

var ary = new[] { "January", "February", "March", "April",
                  "May", "June", "July", "August",
                  "September", "October", "November", "December"};

var data = from e in ary
           where !e.StartsWith("J") // 「J」で始まる以外
           orderby e                // 辞書順ソート
           select e.ToLower();      // 小文字に変換


同じことをメソッド構文で書くとこうなります。

var data = ary.Where(e => !e.StartsWith("J"))
              .OrderBy(e => e)
              .Select(e => e.ToLower());


シンプルな変換だったらクエリ式の方が簡単かもしれませんが、
メソッド式の方がいろいろとできる上にStream APIっぽいので、
クエリ式は参考程度ということにしておきます。

LINQとStream APIを比較


比較します。一気に行きます。

中間処理

選択

説明 Java C#
条件を満たす要素に絞り込み filter(Predicate<? Super T>) Where(Func<T, bool>)
指定件数をスキップ skip(long) Skip(int)
先頭から条件を満たさなくなるまでスキップ - SkipWhile(Func<T, bool>)
指定件数をスキップ limit(long) Take(int)
先頭から条件を満たさなくなるまで要素を返す - TakeWhile(Func<T, bool>)
重複するものを除いた要素を返す distinct() Distinct()

射影

説明 Java C#
射影(変換) map(Function<? super T, ? extends R>) Select(Func<T, R>)
平坦化して射影 flatMap(Function<? super T, ? extends Stream<? extends R>>) SelectMany(Func<T, IEnumerable<R>>)
グループ化 - ※1 GroupBy(Func<T, K>>)

※1 Javaの場合にはMapに変換されるので中間処理ではなく終端処理

ソート

説明 Java C#
昇順にソート sorted(Comparator<? super T>) ※2 OrderBy(Func<T, K>)
降順にソート sorted(Comparator.reverseOrder(Function<? super T, ? extends K>)) OrderByDescending(Func<T, K>)
ソートしたシーケンスのキーが同一の場合
さらに昇順にソート
- ThenBy(Func<T, K>) ※3
ソートしたシーケンスのキーが同一の場合
さらに降順にソート
- ThenByDescending(Func<T, K>)
シーケンスの順序を反転する - Reverse()

※2 引数なしだと自然順序でソートされる
※3 ThenByはOrderByの後でしか使えない

合成

説明 Java C#
連結 ※4 Stream.concat((Stream<? extends T> , Stream<? extends T>) ※5 Concat(IEnumerable<T>)
和集合 - Union(IEnumerable<T>)
差集合 - Except(IEnumerable<T>)
積集合 - Intersect(IEnumerable<T>)
内部結合 - Join(IEnumerable<U>, Func<T, K>, Func<U, K>, Func<T, U, R>)
外部結合を使ってグループ化 - GroupJoin(IEnumerable<U>, Func<T, K>, Func<U, K>, Func<T, IEnumerable<U>, R>)
2つのシーケンスをマージ - Zip(IEnumerable<U>, Func<T, U, R>)

※4 和集合との違いはdistinctがかからないこと、SQLのUNION ALLとUNIONみたいな感じ

終端処理

要素を取得

説明 Java C#
先頭の要素を取得 findFirst() ※5 First() ※6
FirstOrDefault()
末尾の要素を取得 - Last() ※6
LastOrDefault()
なんか取得 ※7 findAny() -
ただ一つの要素を取得(複数ある場合例外) - Single()
SingleOrDefault()
指定した位置の要素を取得 - ElementAt(int)
ElementAtOrDefault(int)

※5 orDefaultがないのは戻り値がOptinal型だから
※6 Funcを引数に条件にあてはまる先頭、末尾を取れる
※7 並列化した場合にfindFirstよりパフォーマンスが良いから用意されているのかしら

集計

説明 Java C#
畳み込み reduce(BinaryOperator<T>) Aggregate(Func<T, T, T>)
最大値 max(Comparator<? super T>) ※8 Max()
最小値 min(Comparator<? super T>) ※8 Min()
平均値 average() ※9 Average()
合計値 ※9 sum() Sum()
件数 count() Count()

※8 max、minはプリミティブストリームの場合には引数なし
※9 average、sumはプリミティブストリームのみ実装

判定

説明 Java C#
すべての要素が条件を満たすか allMatch(Predicate<? super T>) All(Func<T, bool>)
条件を満たす要素が存在するか anyMatch(Predicate<? super T>) Any(Func<T, bool>)
条件を満たす要素が存在しないか noneMatch(Predicate<? super T>) -

変換

説明 Java C#
配列を作成 toArray() ToArray()
リストを作成 collect(Collectors.toList()) ToList()
セットを作成 collect(Collectors.toSet()) -
マップ、ディクショナリを作成 collect(Collectors.toMap(Function<? super T, ? extends K> Function<? super T, ? extends U>)) ToDictionary(Func<T, K>>)
グループ化してマップ、ディクショナリを作成 collect(Collectors.groupingBy(Function<? super T,? extends K>)) GroupBy(Func<T, K>>).ToDictionary(Func<IGrouping<K, T>, K>, Func<IGrouping<K, T>, List<T>>) ※10
真偽にグループ化してマップ、ディクショナリを作成 collect(Collectors.partitioningBy(Predicate<? super T>)) -

※10 ToLookup(Func<T, K>>).ToDictionary(Func<IGrouping<K, T>, K>, Func<IGrouping<K, T>, List<T>>) でもできる

引数とジェネリクスまで書いたらなんだか読みづらくなってしまいましたね。
大体同じような処理が実装されているようですが、Javaは合成系が弱いのかもしれません。
Zipとかは普通に欲しい。

 

LINQ to あれこれ

 
LINQには実は種類があります。

LINQ to Object

配列やコレクションなどのデータ集合に対する処理は「LINQ to Object」と呼ばれます。

LINQ to Entities

Entity Frameworkを使ったDBアクセスのための処理は「LINQ to Entities」と呼ばれます。

LINQ to SQL

Entity Frameworkを使わないレガシーなDBアクセスは「LINQ to SQL」と呼ばれます。
が、最近ではあまり使う機会もないでしょう。

文法的には変わらないのですが、「LINQ to Entities」や「LINQ to SQL」はSQLに変換されるので制限が厳しいです。
詳しくは触れませんが、メソッドとか呼び出せなくてすぐエラーになるので「むきー」ってなります。

地道に覚えるべし

 

さて、ちょっとした考察。

Javaの設計思想としては細かい部品をたくさん作って、
部品の組み合わせでプログラムを組み立てる発想なのだと思います。

なので、Stream APIを使う場合には必ずStreamに変換して処理し、
終端処理でコレクションなり配列なりに再度変換しています。
そしていったん利用したStreamは文字通りに流れて行ってしまいます。

一方でLINQではSystem.LinqがひたすらにIEnumerableへのメソッド拡張をしていることからもわかるように、
コレクションなどとLINQ自体の仕組みをそんなに切り分ける気がないようです。

遅延実行の仕組みなんかを考えると、Stream APIの発想は分かりやすい気もしますが、
やっぱりどうしてもコード自体は長くなってしまいますね。

Eclipse Collectionsなんかはこの部分をスマートにしたかったのでしょう。

なんにせよ、LINQはとっても便利な機能ですよね。
しっかり覚えないと。