Java8 の Developer preview が公開されたので、そろそろ新機能についてまとめておきます。
機能一覧はこちらです。
インターフェースにstaticなメソッド定義が可能になった
例えば、java.util.Comparator には以下の static なメソッドが追加されています。
@FunctionalInterface public interface Comparator<T> { public static <T, U> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator) { Objects.requireNonNull(keyExtractor); Objects.requireNonNull(keyComparator); return (Comparator<T> & Serializable) (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1), keyExtractor.apply(c2)); } }
以下の Student クラスがあったとして
public class Student { private String name; private Integer gradYear; private Integer score; public Student(String name, Integer gradYear, Integer score) { this.name = name; this.gradYear = gradYear; this.score = score; } // getter/setter
名前順でソートしたい場合には以下のように書けます。
List<Student> students = Arrays.asList( new Student("mick", 2014, 76), new Student("thome", 2013, 89), new Student("king", 2014, 82)); students.sort(Comparator.comparing( Student::getName, String.CASE_INSENSITIVE_ORDER));
インターフェースの実装を返すファクトリを定義するのに便利です。
インターフェースにデフォルトメソッドが定義できるようになった
噂のデフォルトメソッドです。良く出てくる forEach() が java.lang.Iterable インターフェースに追加されています。
@FunctionalInterface public interface Iterable<T> { default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } }
default キーワード付きでメソッドが定義されています。
使い方はそのままですね。
List<String> list = Arrays.asList("a", "b", "c", "d"); list.forEach(System.out::println);
デフォルトメソッドにより振る舞いの多重継承が可能となります。
デフォルトメソッドの処理を変更するには、@Override で定義します。ArrayList ではループの途中で内容に変更があった場合に ConcurrentModificationException をスローするようにオーバーライドされています。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { @Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
同じシグネチャのメソッドを多重継承する場合は、@Override して明示的にどちらのデフォルトメソッドを呼び出すかを指定する必要があります。
ラムダ(Lambda)がようやく使えます
少し冗長ですが、2014年度の最高スコアは以下のようにして抽出できます。
List<Student> students = Arrays.asList( new Student("mick", 2014, 76), new Student("thome", 2013, 89), new Student("king", 2014, 82)); Integer max = students.stream() .filter(s -> s.getGradYear() == 2014) .map(s -> s.getScore()) .reduce(0, Math::max);
Scala の「=>」ではなく、「->」を使います。
上の例のfilter、map、reduce はいずれも Stream インターフェースで以下のような定義となっています。
public interface Stream<T> extends BaseStream<T, Stream<T>> { Stream<T> filter(Predicate<? super T> predicate); <R> Stream<R> map(Function<? super T, ? extends R> mapper); T reduce(T identity, BinaryOperator<T> accumulator); }
まず、filter は Predicate を引数に取り、Stream を返すメソッドになっています。
Predicate はこんなインターフェースになっています。
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
なので、「s -> s.getGradYear() == 2014」の箇所は Java8 以前の書き方だと以下のようになります。
students.stream().filter(new Predicate<Student>(){ @Override public boolean test(Student s) { return s.getGradYear() == 2014; } })・・・
「s -> s.getGradYear() == 2014」は(型推論によりStudentと判断された) s を引数に取り、(Studentである) s の getGradYear() を判定して boolean を返すというラムダ式になります。
同じように map は以下の Function を引数に取ります。
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
Function は、あるある引数に関数(ここでは s -> s.getScore()となる)を適用してその結果を返すので、Student は その点数(score)の Stream に変換されることになります。
で、reduce は ゼロを初期値として 順番に渡された大きいもを判定して畳み込みを行います(説明がめんどくさくなった)。
関数インターフェースの印となる@FunctionalInterface アノテーションが追加された
関数として扱うクラスに付与するアノテーションには @FunctionalInterface が付与されています。
@FunctionalInterface public interface Function<T, R> { R apply(T x); }
このアノテーションがある場合に関数として扱われるのではなく、このアノテーションがついていた場合に、適用するメソッドが唯一かどうかがチェックされるだけのようです。
ラムダ式のルールを簡単に
大体 Scala などと似たようなものです。
int 型の引数 x と y を { } の中の処理で変換して返却する関数。
(int x, int y) -> { return x + y; }
以下のようなインターフェースのシグネチャに該当します。
@FunctionalInterface public interface BiFunction<T, U, R> { R apply(T x, U y); }
型推論できて、適用する処理が1文で表現できれば以下の省略形が使えます。
(x, y) -> x + y
引数が1つの場合
x -> x + x
前述の map の引数と同じです。以下のようなインターフェースのシグネチャに該当します。
@FunctionalInterface public interface Function<T, R> { R apply(T x); }
結果がbooleanになる場合には Predicate が該当します。
引数が無い場合
() -> x
以下のようなインターフェースのシグネチャに該当します。
@FunctionalInterface public interface Supplier<T> { T get(); }
引数を1つ取り、戻り値 void の場合
x -> { System.out.println(x); }
以下のようなインターフェースのシグネチャに該当します。
@FunctionalInterface public interface Consumer<T> { void accept(T x); }
ラムダ式は、メソッドの引数として必要となる関数インターフェースの匿名クラスを定義する構文糖です。
メソッド参照
すでに何度かでている「::」でつなげるアレがメソッド参照です。
.reduce(0, Math::max);
list.forEach(System.out::println);
以下のようなメソッドを参照してラムダとして扱えます。
メソッド参照のタイプ | メソッド参照の例 | 対応するラムダ式 |
---|---|---|
スタティックメソッド | String::valueOf | x -> String.valueOf(x) |
非スタティックメソッド | Object::toString | x -> x.toString() |
メソッド参照 | x::toString | () -> x.toString() |
コンストラクタ参照 | ArrayList::new | () -> new ArrayList<>() |