Java8のラムダ式
最近ぼちぼちJavaをやり始めたところタイミング良くJava8がリリースされたので新機能であるラムダ式について少し見ていこうと思います.
まず,Java8におけるラムダ式は一体何なのかというと,それはずばり抽象メソッドを一つだけ持つインタフェースのインスタンスです.例えば以下のようなインタフェースがあったとして,
public interface IntFunc{ public int func(int x); }
従来では
IntFunc f1 = new IntFunc(){ @Override public int func(int x){ return x*x; } };
と書いていたのがラムダ式によって
IntFunc f2 = (int x) -> {return x*x;};
と書けるようになります.実際には型および引数が一つの場合は括弧,return文のみの場合はreturnが省略できるので以下のように書けます.
// 型の省略 IntFunc f3 = (x) -> {return x*x;}; // 括弧 & returnの省略 IntFunc f4 = x -> x*x;
ラムダ式によって抽象メソッドが実際に定義されたわけで,関数を呼ぶには以下のようにします.
// 以下どれも同じ f1.func(3); // => 9 f2.func(3); f3.func(3); f4.func(3); ((IntFunc)x->x*x).func(3);
ラムダ式はキャストすることも出来るので最後の例のようにも書けます.
インタフェースがラムダ式を受け付けることを明示的に@FunctionalInterfaceをつけることで宣言できます.また,Java8で追加されたインタフェースのデフォルトメソッドは抽象メソッドとはカウントされません(それプラスtoString()とかのObjectのメソッドも).したがって以下のインタフェースは有効です.
@FunctionalInterface public interface IntFunc{ public int func(int x); } @FunctionalInterface public interface IntFunc2 extends IntFunc{ default public int doubleFunc(int x){ return func(x)+func(x); } } IntFunc2 f = x -> x*x; f.doubleFunc(3); // => 18
@FunctionalIntefaceがついているものとして代表的なものにはRunnableがあります.なので,以下のようにできます.
Runnable r = () -> System.out.println("Yeah"); Thread t = new Thread(r); t.start(); new Thread(()->{System.out.println("YYY");}).start(); ((Runnable)()->{System.out.println("ZZZ");}).run();
自分で使いたいラムダ式用にインタフェースを用意してもいいですが,よく使うと思われるものに関してはjava.util.function以下にいろいろと定義されています.
// 引数の型T, 戻り値の型Rの関数 public interface Function<T, R>{ R apply(T t); ... } // 戻り値がbooleanな関数 public interface Predicate<T> { boolean test(T t); ... } // 型Tの引数を受け取って何かする関数 public interface Consumer<T> { void accept(T t); ... } ...
デフォルトメソッドとしてFunctionには関数合成のためのcompose()であったり,Predicateにはand()やor()が用意されています.PredicateとConsumerの使用例:
public class Person { private String name; private int age; public Person(String name, int age){ this.name = name; this.age = age; } public String getName(){ return name; } public int getAge(){ return age; } public void check(Predicate<Person> predicate, Consumer<Person> consumer){ if(predicate.test(this)){ consumer.accept(this); } } public static void main(String[] args){ Person j = new Person("John",15); Predicate<Person> greaterThan12 = x -> x.getAge() > 12; Predicate<Person> lessThan20 = x -> x.getAge() < 20; Predicate<Person> isTeenage = greaterThan12.and(lessThan20); Consumer<Person> printName = x -> System.out.println(x.getName()); j.check(isTeenage,printName); // => John } }
いまいちあれな例ですが,check()は引数としてPredicateとConsumerを受け取り,Predicateの結果が真ならConsumerを実行します.
java.util.functionには他にも多くの関数があり,特にプリミティブ型向けに定義されているものが多いです.例えばIntFunction,LongBinaryOperator,DoubleUnaryFunction等々.この辺りはjavadocを読むより直接ソースをみて定義を確認した方が分かりやすいと思います.なぜプリミティブ型用のインタフェースが定義されているかというと,よく使うからという理由の他にジェネリクスではプリミティブ型が直接扱えないためラップクラスを使う必要があり,無駄なオブジェクトが生成されてしまうからです.具体的に以下の例を考えてみます.
Function<Integer,Integer> f1 = (x) -> x*x; IntUnaryOperator f2 = (x) -> x*x; long t1 = System.currentTimeMillis(); for(int i = 0;i < 1000000000;i++){ f1.apply(i); } long t2 = System.currentTimeMillis(); System.out.println(t2-t1); t1 = System.currentTimeMillis(); for(int i = 0;i < 1000000000;i++){ f2.applyAsInt(i); } t2 = System.currentTimeMillis(); System.out.println(t2-t1);
ここで,最初はFunction
Function<Integer,Integer> : 3420ms IntUnaryOperator : 4ms
2番目の方は早すぎてちゃんと計測できているか若干疑問ですが,とりあえず圧倒的にFunction
ラムダ式のスコープはラムダ式が宣言されているクラスになります.また,ラムダ式ではローカル変数の値を変更することはできません.ということでよくあるような以下のようなカウンタは作れません.
// NG public static IntSupplier makeCount(){ int count = 0; return ((IntSupplier)()-> { count += 1; return count;}); }
まぁこういうことをしたければ素直にクラスを作れということでしょうか.
さて,ここまでいろいろとラムダ式について見てきましたが,実際一番よく使うことになるのは以下のようにstream()と合わせた時な気がします.
List<Integer> xs = Arrays.asList(-3,-2,-1,0,1,2,3); xs.forEach((x)->{System.out.println(x);});
forEach()でリストの中身を順に処理できます.Java8から以下のようにしてメソッドを引数として渡すことができるようになりました.
xs.forEach(System.out::println);
stream()で返ってくるStreamクラスには他にもfilter()やmap(),reduce()といった関数が用意されています.
xs.stream().filter(x -> x > 0).forEach(x -> System.out.println(x*2)); // xsの総和 System.out.println(xs.stream().reduce(0,(x,y) -> x + y)); // xsの総和 (Integer::sumを使用) System.out.println(xs.stream().reduce(0,Integer::sum)); // xsの二乗和 System.out.println(xs.stream().map(x -> x*x).reduce(0,Integer::sum)); // xsの最大値 System.out.println(xs.stream().reduce(Integer.MIN_VALUE,Integer::max));
streamはparalellStreamを使うと並列になるようです.
xs.parallelStream().filter(x -> x > 0).forEach(x -> System.out.println(x*2));
この場合並列に処理されるので出力順は実行する毎に異なります.
また,Streamにはgenerator()というものがあります.以下のようにlimit()と合わせて使うようです.
Supplier<Double> random = Math::random; Stream.generate(random).limit(10).forEach(System.out::println); //10個の乱数を生成
generator()に似た物としてiterator()もあります.
Stream.iterate(1, y -> y*2).limit(10).forEach(System.out::println); // => 1,2,4,8,16,32,64,128,256,512
以下のようにすればカウンタも作れます.
Stream<Long> count = Stream.iterate(0L,y -> y+1); count.limit(10).forEach(System.out::println); // => 0,1,2,3,4,5,6,7,8,9
Stream.iterate()の実装を真似て以下のようにフィボナッチ数列のストリームを作ってみました.
public static Iterator<Long> getFibIterator(){ return new Iterator<Long>(){ private Long a = 1L; private Long b = 1L; private boolean first = true; private boolean second = true; @Override public boolean hasNext(){ return true; } @Override public Long next(){ if(first){ first = false; return 1L; }else if(second){ second = false; return 1L; } Long c = a + b; b = a; a = c; return c; } }; } public static Stream<Long> fibGen(){ return StreamSupport.stream(Spliterators.spliteratorUnknownSize( getFibIterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE), false); }
以下のように使えます.
Stream<Long> fibStream = fibGen(); fibStream.limit(10).forEach(System.out::println); // => 1,1,2,3,5,8,13,21,34,55
再び同じfibStreamを読み込むと今度は89から値が得られます.
ということでJava8のラムダ式について少し見てきました.java.util.functionにいろいろあるのでそこのソースを見るのが一番いいんじゃないかなぁと思います.JavaではEnumはクラスですが,ラムダ式も実際にはインタフェースのインスタンスな訳で,そういう発想がJavaらしいし面白いなぁと個人的には思いました(それが嫌なら他の言語使うよね).
参考
Maurice Naftalin's Lambda FAQ
Trying Out Lambda Expressions in the Eclipse IDE
Java 8 Tutorial: Streams Part 2 -- map, reduce, and specialized numbe…