Javaのラムダ式を使ってみよう

2018年3月29日(木)
広谷 嘉巳

ラムダ式とは

「ラムダ式」はメソッド定義を式として扱える機能のことです。2014年3月にリリースされたJava 8の新たな表記法として追加されました。それから4年が経過していますが、皆さんはラムダ式を扱えますか。

もちろん、Java 8は従来のコーディングでも問題なく動作するので、無理にラムダ式を覚える必要はないのですが、ラムダ式は記述がシンプルになるだけでなく、ラムダ式の使用を前提とした数々のAPIも追加されているため、プロジェクトによっては「従来の記述はNG、ラムダ式は必須」というところもあります。

筆者が参画していたプロジェクトも、ラムダ式でのコーディングが必須でした。

サンプルでラムダ式を見ていく

では、Javaのラムダ式はどのようなものなのか、サンプルソースを使って見ていきましょう。以下のような社員名のリストを用意します。

List<String> employees = Arrays.asList("鈴木", "佐藤", "高橋");

社員名を降順でソートする場合、従来の無名内部クラス(匿名クラス)を使ったコーディングは以下のようになります(無名内部クラス(匿名クラス)は、メソッドの中で一度しか使われない使い捨てクラスを作成したい場合に利用されます。自分の名前を持たないクラスなので、このように呼ばれます)。

Collections.sort(employees, new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return b.compareTo(a);
  }
});

これをラムダ式でコーディングすると、以下のようになります。

Collections.sort(employees, (a, b) -> b.compareTo(a));

いかがですか。従来のコーディングにはない矢印「->」が登場して違和感があるかも知れませんが、ラムダ式で記述した場合はこのように短く記述できます。

冒頭でも説明したように、ラムダ式とはメソッド定義を式として扱える機能のことです。つまり、ラムダ式は内部的には匿名クラスと同じであることが分かると思います。

ラムダ式の基本文法は以下の通りです。処理の実装を簡潔に記述できるようになります。

( 引数 ) -> { 処理 }

ちなみに、引数が1つの場合は括弧を省略できます。

a -> { 処理 }

また引数の型は推論されるので、これも省略できます(以下は両方OK)。

(String a, int b) -> { 処理 }
(a, b) -> { 処理 }

処理が1文の場合は、returnも波括弧も省略できます(以下は両方OK)。

( 引数 ) -> {return a + b;}
( 引数 ) -> a + b

したがって、前述のサンプルソース(1)は(2)と同一です。

(1) Collections.sort(employees, (a, b) -> b.compareTo(a));
(2) Collections.sort(employees, (String a, String b) -> {return b.compareTo(a);}); 

ラムダ式を使うと記述がシンプルになるだけでなく、パフォーマンスも向上できます。

Javaはバージョンアップされるに従って、CPUの能力をどんどん使いこなせるようになりました。しかし、それもお作法、つまりバージョンに見合った実装になっていなければほとんど意味がありません。Java 8でラムダ式を使わずに以前と変わらないコーディングをしていては、宝の持ち腐れになるというわけです。

上記以外にも、ラムダ式を使うメリットとしてJava 8がラムダ式の使用を前提とした数々の新機能を用意している点です。その新機能の一部を、サンプルソースを使って紹介します。このサンプルソースは、ListからMapへの変換でラムダ式を説明するものですが、その前にListとMapについて解説しましょう。

ListとMapはどちらも配列のようにたくさんの要素をまとめて格納できるコレクションクラスですが、違いがあります。Listはセットした順番にインデックス(添字)が付き、インデックスを指定して要素の取得や更新ができます。Listは重複する要素を持つことができます。

Mapはキーと値をワンセットにし、そのワンセットを1つの要素として保持します。値の取得や更新はキーを指定して行います。Mapはキーが重複する要素は持つことができません。それぞれ特徴があるので、順番にデータを処理したい場合はList、キーを指定して該当のデータを処理したい場合はMapを採用することになります。

実際の現場でも仕様変更などでListからMap、MapからListへの変換がよく行われます。まず、以下のようなidやcodeがあるValueObjectを用意します。

public class SampleVO {
  private String id;
  private String code;
  private String name;

  public String getId() {
    return id;
  }
 public void setId(String id) {
    this.id = id;
  }

 public String getCode() {
    return code;
  }

 public void setCode(String code) {
    this.code = code;
  }

 public String getName() {
    return name;
  }

 public void setName(String name) {
    this.name = name;
  }
}

上記のValueObjectをListとして以下に複数作成します。このListを使ってMapに変換していきましょう。

List<SampleVO> sampleVOs = Arrays.asList(new SampleVO("d1", "c1", "sample1"), ..

ListからMapに変換するには、従来では以下のようにコーディングします。まず、Map型の変数を用意し、次にListをfor文で回しながらMap変数に追加します(ちなみにサンプルソースではキーをSampleVOのid、値をSampleVOにしている)。

Map<String, SampleVO> sampleMap = new HashMap<String, SampleVO>();
for (SampleVO sampleVO : sampleVOs) {
  sampleMap.put(sampleVO.getId(), sampleVO);
}

これが、ラムダ式では次のように1行でできてしまいます。

Map<String, SampleVO> sampleMap = sampleVOs.stream().collect(Collectors.toMap(SampleVO::getId, d -> d));

ちなみに、2つのコロン(::)は「メソッド参照」と言い、呼び出したいメソッドを「{クラス名}::{メソッド名}」という表記で指定できます。これもJava 8の新機能です。

stream()やcollect()といった箇所がJava 8の新機能の1つ「Stream API」です。Stream APIは配列やCollectionなどの集合体を扱い、これまでCollectionに行っていた煩雑な処理を分かりやすいコードで記述できるようになります。ListからMapへの変換もfor文を使わずに簡単にできます。

もう1つ例を紹介しましょう。1から5までの数字で、奇数の数字の合計を計算するプログラムを書くとします。従来では、以下のようになるかと思います。

int total = 0;
for (int i = 1; i < 6; i++) {
    if (i % 2 != 0) {
        total += i;
    }
}

ラムダ式では、以下のようになります。

int total = IntStream.range(1, 6).filter(i -> i % 2 != 0).sum();

IntStream.rangeは指定した数値の範囲をStreamとして生成します(StreamとはJava 8から追加されたデータの読み書きを行う標準化された機能であり、データを使った値の集計処理などが出来る非常に便利なAPIです)。従来のfor文を使ったコーディングでは、for文のループにi++のようにインクリメント処理を記述する必要があります。

ラムダ式では劇的にシンプルになったとは言えませんが、それでもインクリメント処理のように不要な部分は省かれ、1行で書けるようになります。また、ラムダ式のほうがより言葉に近い記述になるため、どのような処理なのか分かりやすいです。

以下は、「1から6の範囲(range)で、条件(filter)は奇数(2で割れない)の合計(sum)をする」というコーディング例です。処理内容が直感的に分かるのではないでしょうか。

IntStream.range(1, 6).filter(i -> i % 2 != 0).sum();

ラムダ式を使わないと新機能が利用できない

ここまでいくつか例を見てきましたが、Java 8では便利な機能がたくさん追加されています。しかし、これらはラムダ式を使うことを前提に作られているため、ラムダ式を使わなければ新機能は利用できません。

冒頭でも述べたように、Java 8は従来のコーディングでも問題なく動作するので、ラムダ式を使わなくても大丈夫です。ただし、Java 8を使用しているプロジェクトに入ると、「ラムダ式を使っている他人のプログラムが読めない!」という事態に陥るのでご注意を。「ラムダ式を知らない」という人は、少しずつでも勉強することをお勧めします。

おわりに

いかがでしたか。今回はJava 8から追加されたラムダ式について解説しました。

次回はJavaの後継言語と言われるScala言語について解説します。

株式会社ビーサンク
Javaを使ったシステム開発に従事し、オブジェクト指向での設計から開発を得意としている。以前はデザインパターンを取り入れたクラス設計やリファクタリングでのコードの体質改善という作業も行っていた。自分の力がまだまだと自覚しており、JavaやUMLの資格も積極的に取得している。
仕事の傍らスキー1級やスノボ、テニス、また、野菜ソムリエ取得やフランス料理学校に通うなど、仕事よりも趣味の方に重きを置いているのが玉に瑕。
http://bexank.co.jp/

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています