Java オブジェクト集合のソートとラムダ式の初歩 山本富士男 2016-4-23 この資料は Java での コレクション Coections と ジェネリクス Generics に関してさらに深く学ぶためのものです 以下の事項を学びます レポート課題が 5 ページの末尾にあります 名称のない内部クラスである 匿名クラス を使う 一般のオブジェクトの集合 (List や Map など ) を何らかの基準でソートする Java SE8 で新たに導入されたラムダ式 (Lambda Expression) を知る ( ラムダ式は ストリーム機能と連携して マルチコア CPU 時代の並列プログラミングに結びつく ) 1. オブジェクトのリストをソートする ( 従来方式で書く ) 簡単な例として K 大学の 5 つの学部名を (String オブジェクトとして )List に登録し その文字数の昇順に並べる 従来の Java で記述したプログラムリストを以下に示す (ListSort_Cassic.java) // import java.uti.*; pubic cass ListSort_Cassic{ pubic static void main(string... args){ List<String> ist = Arrays.asList("E: ", "C: ", "I: ","A: ","N: " ); //Comparator<T> cass MyComparator impements Comparator<String>{ pubic int compare(string o1, String o2) { return o1.ength() - o2.ength(); MyComparator mycpr = new MyComparator(); Coections.sort(ist, mycpr); System.out.printn(" "); for(string s : ist) System.out.printn(s); 実行結果は以下のようになる 学部名を文字数の昇順に列挙します : E: 工学部 I: 情報学部 N: 看護学部 C: 創造工学部 A: 応用バイオ科学部 Arrays.asList で 5 つの String オブジェクト ( 学部名 ) を List にしている この List オブジェクトの型は List<String> になっていることに注意してほしい この List には String 型
オブジェクトを格納するということを意味する (a) Comparator インタフェースの利用 上記プログラムでは List にある String オブジェクトをどのような基準でソートするのかを指定するために Comparator というインタフェースを使っている 正確には Comparator<T> というインタフェースである すなわち 型 T のオブジェクトを比較するためのものである この例では 型 T は String 型なので Comparator<String> を使う インタフェースは クラスではないので 上記では MyComparator という内部クラスを作っている このクラスは Comparator<String> を実装 (impements) している 実装するということは Comparator で宣言されているメソッドの処理内容を この MyComparator クラスで定義するということである この場合 実装すべきメソッドは一つだけであり それは compare メソッドである この compare メソッドの前に というのが付いているのは インタフェース Comparator で宣言されているメソッドの内容をここで定義する と言う意味である Compare メソッドは 引数としてオブジェクトを 2 つ取り それらを並べるための比較 すなわち < = > を判定する方法を与える この例では以下のようになっている //Comparator<T> cass MyComparator impements Comparator<String>{ pubic int compare(string o1, String o2) { return o1.ength() - o2.ength(); compare メソッドの返値 ( 負 ゼロ 正 ) によって 以下のような比較結果になる この例では 負か ゼロか 正かは 文字列の長さの比較で決まることに注意して下さい 負ならば o1 < o2 0 ならば o1 = o2 正ならば o1 > o2 (b) オブジェクトの集合をソートする上記の MyComparator オブジェクト mycpr を用いて List にあるオブジェクトをソートします 具体的には 下記のとおり Coections.sort メソッドの第 2 引数に mycpr を与えます すると 第 1 引数の List オブジェクトに含まれる学部名を mycpr の判定方法によってソートしてくれます MyComparator mycpr = new MyComparator(); Coections.sort(ist, mycpr);
2. オブジェクトのリストをソートする ( 匿名クラスを使う ) 上では Comparator インタフェースを実装するクラス MyComparator を定義し そのオブジェクトを生成して sort メソッドに渡していました この MyComparator というクラスは そのオブジェクトを sort へ渡すためだけに定義されたものです 実は そのような場合は 特に 明示的に MyComparator のようなクラスを作らずに そのオブジェクトを直接 sort メソッドへ渡してしまうのが通例です それが 匿名クラス のオブジェクトを生成して利用するということです すなわち 以下のようなソースリストになります 実際 Coections.sort の第 2 引数は インターフェース Comparator のメッソド compare を実装したクラスのオブジェクトを生成する という形になっています compare メソッドの定義内容が そっくりそのまま この第 2 引数の内部に入っています (ListSort_Anony.java) // import java.uti.*; pubic cass ListSort_Anony{ pubic static void main(string... args){ List<String> ist = Arrays.asList("E: ", "C: ", "I: ","A: ","N: " ); //Comparator<T> Coections.sort(ist, new Comparator<String>() { pubic int compare(string o1, String o2) { return o1.ength() - o2.ength(); ); System.out.printn(" "); for(string s : ist) System.out.printn(s); 上記の考え方は 余分なクラスを定義しないので 良い方向のように思えます 実際 この方法は多用されています しかし 上記のとおり sort メソッドの第 2 引数が 肥大化 して やや読み難い感は免れません さらにこのようなメソッドの個数や 引数の個数が増えた場合は かなり長い 理解しにくいソースプログラムリストになってしまうことでしょう Java の開発者は そのような悪い事態を避けて さらに近年の PC のマルチコアの性能を引き出すための言語仕様の改訂検討を続けていました それに10 年を要したと言われています そして それが Java SE8 として結実し 世の中へ出ました そのうちで 最も重要なものがラムダ式 (Lambda Expression) と呼ばれるものです 次の節では ここで取り上げたオブジェクトの sort が ラムダ式によって如何に簡潔 明瞭に書けるよ うになったかを示します
3. オブジェクトのリストをソートする ( ラムダ式を使う ) 結論を先に示します 上記と同じ例題を ラムダ式を用いて書いたソースリストを以下に示します (ListSort_Lambda.java) // import java.uti.*; pubic cass ListSort_Lambda{ pubic static void main(string... args){ List<String> ist = Arrays.asList("E: ", "C: ", "I: ","A: ","N: " ); //Java SE8 Lambda Expression Coections.sort(ist, (s1, s2)-> s1.ength() - s2.ength()); System.out.printn(" "); ist.foreach(s -> System.out.printn(s)); // ソートを行う Coections.sort の呼び出し状況を 先の 匿名クラスを使う 場合と 今回の ラムダ式を使う 場合をあらためて比較してみます : ( 匿名クラスを使う場合 ) Coections.sort(ist, new Comparator<String>() { pubic int compare(string o1, String o2) { return o1.ength() - o2.ength(); ); ( ラムダ式を使う場合 ) Coections.sort(ist, (s1, s2)-> s1.ength() - s2.ength()); 行数だけ比較しても 6 行がわずか 1 行になってしまいました! 驚異的です この後説明しますが これだけ簡潔に書けることは 処理内容が明確に理解でき 間違いも起こりにくく ソフトウェアの生産性に大きく寄与すると考えられます そのため 注目度が高いのだと思われます Coections.sort の第 2 引数を再度見ます 匿名クラス では new 演算子で明らかにオブジェクトを生成して与えています それに対して ラムダ式 の方は オブジェクトを生成しているようには見えません! 単に 関数のような式を与えています そのとおりです ラムダ式を使うことで オブジェクトをあたかも関数のように扱える のです この例でのラムダ式は 以下のような関数の形になっています ( 引数 1, 引数 2, ) -> 計算式 ( 処理手順のステートメント ) (s1, s2)-> s1.ength() - s2.ength() もしも 学部名の文字数ではなく 辞書順 ( 学部名の先頭が英字になっています ) にソート
するにはどうすればよいでしょうか? 答えは ラムダ式を以下のように変えるだけです String クラスの compareto メソッドを利用しています (s1, s2)-> s1.compareto(s2) 実行結果は以下のようになります A: 応用バイオ科学部 C: 創造工学部 E: 工学部 I: 情報学部 N: 看護学部 4. ラムダ式でなぜこんなに簡単に書けるのか 上記の例だけでも ラムダ式の威力が感じられます しかし なぜ こんなに簡潔に書けることになったのでしょうか それを簡単に説明します (1) 関数型インタフェース上記の Comparator<T> というインタフェースでは それを impements するクラスが実装すべきメソッドは唯一つしかありません そして この例では T 型は String 型でした 以下のメソッドでした このように 実装すべきメソッドを唯一つしか持たないインタフェースは 関数型インタフェースと呼ばれます 実装すべき唯一つのメソッド pubic int compare(string o1, String o2) (2) 関数型インタフェースからラムダ式へ関数型インタフェースならば 実装すべき関数名 ( メソッド名 ) 引数の型と個数は決まっていますから それらを明示的に書かなくても使えることになります したがって 以下のように ラムダ式では 実装すべき関数 ( メソッド ) の処理内容だけが分かるように書いても良いことになったのです 以下のとおりです ( 先の例を再掲します ) (s1, s2)-> s1.ength() - s2.ength() この場合の Coections.sort では 関数型インタフェース Comparator<String> なので 比較するオブジェクトは String 型である だから ラムダ式の s1, s2 という引数は String オブジェクトであることは分かっている ソートの基準は それらの String の長さ ength() の差で決める ということだけを指示すればよいのです 課題 ( レポート提出が必要です ) 学部名称の長さの昇順ではなく 降順にソートするにはどうしたらよいでしょうか? ラムダ式を使ったソースリストと実行結果を提出しなさい ( 実行結果は以下のようになるはず ) A: 応用バイオ科学部 C: 創造工学部 I: 情報学部 N: 看護学部 E: 工学部
5. 内部イテレータにも発想の転換 ラムダ式関係で もうひとつ重要なことがあります List の要素 (String オブジェクト ) を列挙して出力する場合に 上記に示した 従来方式の記述 と ラムダ式を使う場合 で大きな違いがあります 具体的にみてみます ( 従来型の記述 ) 外部イテレータ for(string s : ist) System.out.printn(s); ( ラムダ式を使う場合 ) 内部イテレータ ist.foreach(s -> System.out.printn(s)); 両者の違いは何でしょうか? まるで 主役が for ループから ist そのものへ入れ替わった雰囲気です 外部イテレータの場合 : どのように反復するのかという制御を主題として for ループで書いている List の要素を一つ取り出し それをプリントする これを繰り返す 反復制御処理と処理自体が連動 混在している 処理の内部で 外側の for 文のローカル変数へアクセスしている!( これは並列処理を考える上で重大な事項なのだ!) 要素に対して どのように howto 処理したいのかという観点で書いている 内部イテレータの場合 : 要素に対する処理自体は ラムダ式で書かれ 反復制御から分離されている 要素に対して 何を what したいのかという観点で書いている