Java8 Lambdaの文法拡張まとめ

デフォルトメソッドは前に解説しました。
Java8で最もインパクトのある構文拡張、デフォルトメソッド

ここでは、それ以外の構文拡張についてまとめておきます。

ラムダ式

実装すべきメソッドがひとつだけのインタフェースを関数型インタフェースといいます。
UnaryOperatorインタフェースは実装すべきメソッドがapplyメソッドひとつだけなので、関数型インタフェースになります。
たとえば、UnaryOperatorインタフェースを実装したクラスを定義すると次のようになります。

class MyOp implements UnaryOperator<String>{
    @Override
    public String apply(String t) {
        return "** " + t + " **";
    }
}

このUnaryOperatorインタフェースを使う、List#replaceAllを使ってみると次のようになります。

public static void main(String... args){
    List<String> strs = Arrays.asList("hoge", "foo", "yeah");
    System.out.println(strs.toString());
    
    strs.replaceAll(new MyOp());
    System.out.println(strs.toString());        
}

ただし、このMyOpクラスは、このreplaceAllメソッドへの引数にしか使わないので、こういった場合は匿名クラスが使えます。

public static void main(String... args){
    List<String> strs = Arrays.asList("hoge", "foo", "yeah");
    System.out.println(strs.toString());
    
    strs.replaceAll(new UnaryOperator<String>(){
	    @Override
	    public String apply(String t) {
	        return "** " + t + " **";
	    }
	});
    System.out.println(strs.toString()); 
}

これで、クラスが別の場所で定義されているよりは見やすくなりました。けれども、やりたいことに比べて書く必要があるものが多すぎますね。
そこでJava SE 8からはラムダ式が導入されて、次のように書けるようになりました。

public static void main(String... args){
    List<String> strs = Arrays.asList("hoge", "foo", "yeah");
    System.out.println(strs.toString());
    
    strs.replaceAll((String t) -> {
        return "** " + t + " **";
    });
    System.out.println(strs.toString()); 
}

次のようになっています。メソッドの引数定義と処理だけを抜き出して「->」で結んだ感じです。

(String t) -> {
    return "** " + t + " **";
}

UnaryOperatorというインタフェース名やapplyというメソッド名、戻り値の型は推論を行ってくれるので記述する必要はありません。ラムダ式は次のようになります。

引数リスト -> ラムダ本体

また、実行する処理が一文だけなら、中括弧「{〜}」は省略できます。このときの行がreturn文だけであれば、returnも省略する必要があります。
そこで、次のようになります。

strs.replaceAll((String t) -> "** " + t + " **");

引数の型も推論されるので省略することができます。

strs.replaceAll((t) -> "** " + t + " **");

さらに、引数がひとつのときにはカッコも省略できるので、次のようになります。

strs.replaceAll(t -> "** " + t + " **");

最終的には次のようになりました。

public static void main(String... args){
    List<String> strs = Arrays.asList("hoge", "foo", "yeah");
    System.out.println(strs.toString());
    
    strs.replaceAll(t -> "** " + t + " **");
    System.out.println(strs.toString());        
}

複数の引数があるときに、次のように片方だけ型を省略するということはできません。

BiFunction<String, String, String> join = (s, String t) -> s + t;

両方の型を記述するか、両方の型を省略するか、ということになります。

また、ラムダ式の引数として「_」を使うことはできません。これは、将来的に特別な意味をもつ可能性があるための予約語とされています。

ラムダ式の使える場所

ラムダ式は、型を関数インタフェースとする変数の初期化や割り当てでの右辺、メソッド呼び出しの引数、キャスト演算子の対象として使うことができます。
変数の初期化の場合は、次のようになります。

BiFunction<String, String, String> join = (s, t) -> s + t;

引数としては、次のような形で例をあげていました。

strs.replaceAll(t -> "** " + t + " **");

キャスト演算子は、変数への割り当てや引数以外の場所でラムダ式の型を確定させるのに使うことができます。
次のようにすると、ラムダ式をRunnableオブジェクトに明示的に変換してrunメソッドを呼び出すことができます。

((Runnable)() -> System.out.println("やあ")).run();

しかし、このような使い方でラムダ式に対してキャスト演算子を使うことはほとんどないと思います。
変数への割り当てにしろ引数にしろ型推論が行われるため、Java7までのキャスト演算子の範囲では、ラムダ式に対してキャストを行う必要性は薄いです。
ラムダ式へのキャスト演算子を使うのは、次で紹介するJava8で拡張されたキャスト演算子を使う場合になるでしょう。

ラムダ式のために拡張されたキャスト演算子

ラムダ式を特定のインタフェースのインスタンスとして扱うために明示的にキャストすることができますが、2つ以上のインタフェースのインスタンスにしたい場合、キャストに&を使って複数のインタフェースを指定できるようになりました。

    @FunctionalInterface
    interface Doubler{
        void proc();
        default void doubleProc(){
            proc();proc();
        }
    }
    @FunctionalInterface
    interface Tripler{
        void proc();
        default void tripleProc(){
            proc();proc();proc();
        }
    }
    
    @FunctionalInterface
    interface DoublerAndTripler extends Doubler, Tripler{}
    
    public static void main(String... args){
        Date d = new Date();
        
        LocalDateTime dt = LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());
        System.out.println(dt.getClass());
        System.out.println(dt.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));
        System.out.println(Date.from(dt.atZone(ZoneId.systemDefault()).toInstant()));
        ((Runnable)() -> System.out.println("やあ")).run();
        
        Doubler dbl = () -> System.out.println("二回くりかえす");
        dbl.doubleProc();
        
        Tripler tpl = () -> System.out.println("3回くりかえす");
        tpl.tripleProc();
        
        DoublerAndTripler dbltpl = () -> System.out.println("何回かくりかえす");
        dbltpl.doubleProc();
        dbltpl.tripleProc();
        
        Object o = (Doubler & Tripler)() -> System.out.println("いろいろくりかえす");
        ((Doubler)o).doubleProc();
        ((Tripler)o).tripleProc();
    }

returnの扱い

ラムダブロックでのreturnは、ラムダブロックを持っているメソッドを抜けるのではなく、そのラムダブロックを抜けます。
breakやcontinueなども効かないので、ブロック中でループを制御するということはできません。

次のようなコードは、「end」の場合にそのループを抜けるのではなく、「end」の場合に以降の処理を飛ばします。

public static void main(String... args){
    List<String> strs = Arrays.asList("hoge", "foo", "end", "yeah");
    strs.forEach(s -> {
    if(s.equals("end")) return;
        System.out.println(s);
    });
}

結果はこのようになります。

hoge
foo
yeah

その意味ではcontinueに近いですが、あとに処理が続く場合はそうとも言えません。次の場合には、そのまま結果が続くforEachに渡されます。

public static void main(String... args){
	List<String> strs = Arrays.asList("hoge", "foo", "end", "yeah");
    strs.stream()
            .map(s -> {
                if(s.length() < 4){
                    return "短い";
                }
                return "長い";
            })
            .forEach(s -> {
                System.out.println(s);
            });
}


結果はこのようになります。

長い
短い
短い
長い

次のように、allMatchメソッドなどを使うと途中で処理を終わらせることができます。

public static void main(String... args){
	List<String> strs = Arrays.asList("hoge", "foo", "end", "yeah");
	names.stream().allMatch(s -> {
	    if(s.startsWith("e")) return false;
	    System.out.println(s);
	    return true;
	});
}

ただ、やはりこのようにループの途中で処理を終わらせるというような手続き的な処理を行う場合は、通常のfor文を使うほうがよいでしょう。
streamは、それぞれの値に対する処理を行う場合に使うと考えたほうがよいです。

thisの扱い

匿名クラス内でthisを使うと、その匿名クラスのインスタンスを指しましたが、ラムダ式ではそのラムダ式が含まれたメソッドやフィールドが属するクラスのインスタンスを指します。
これは、わかりやすさのためという理由もありますが、ラムダ式がクラスとして展開されずメソッドとして展開される実装を可能にするためでもあります。

次のコードは、匿名クラスはSample$1、ラムダ式はSampleを表示します。

public class Sample {
    public void hoge(){
        new Runnable(){
            @Override
            public void run() {
                System.out.println(this.getClass());
            }
        }.run();

        ((Runnable)() ->  System.out.println(this.getClass())).run();
        
    }
}

実質的なfinal

これまで次のような、ローカル変数や引数をインナークラスで使う場合にはfinalが必要でした。

public void hoge(final int a){
    new Runnable(){
        @Override
        public void run() {
            System.out.println(a);
        }
    }.run();
}

Java 8では、初期化のみでそれ以降書き換えられてないローカル変数はfinalとしてみなされます。
次のように書けます。

public void hoge(int a){
    new Runnable(){
        @Override
        public void run() {
            System.out.println(a);
        }
    }.run();
}

ただしここで次のように変数aを書き換えるとエラーになります。

public void hoge(final int a){
    new Runnable(){
        @Override
        public void run() {
            System.out.println(a);
        }
    }.run();
    a = 10;
}

メソッド参照

ラムダ式のかわりにメソッドを直接指定することもできます。

クラス名::staticメソッド名

もしくは

インスタンス::インスタンスメソッド名

のように書きます。

次のような使い方ができます。

List<String> strs = Arrays.asList("apple", "berry", "orange");
strs.stream().forEach(System.out::println);

関数型インタフェース

ラムダ記法が導入されたとはいえ、関数が値として扱えるようになったわけではなく、関数型インタフェースというルールにあてはまるインタフェースをラムダ記法の対象として扱えるようになっています。
関数型インタフェースは、実装すべきメソッドがひとつだけであるインタフェースです。
既存のインタフェースでは、Runnableインタフェースが、メソッドがrunメソッドだけなので、関数型インタフェースになります。

関数型インタフェースをあらわすアノテーションとして@FunctionalInterfaceが導入されています。
このアノテーションをつけなくても、実装すべきメソッドがひとつだけのインタフェースは関数型インタフェースとして扱えますが、 @FunctionalInterface をつけるとインタフェースが関数型インタフェースの要件をみたさないときにコンパイルエラーが出るようになります。

@FunctionalInterface
interface MyFunction{
    int proc(int param);
}

用意されている関数型インタフェース

java.util.functionパッケージに、関数をあらわすためによく使いそうな関数型インタフェースが定義されています。
Functionインタフェースが基本になる関数型インタフェースで、引数ひとつで戻り値をもつ関数をあらわします。指定する型の最初が引数の型で、後が戻り値の型です。
次のようにすると、文字列を引数にとって整数を戻り値として返すような関数を扱えます。

Function<String, Integer> len = s -> s.length();

ここでは、文字列の文字数を返すようにしています。

Functionインタフェースとして与えられた処理を実行するときにはapplyメソッドを呼び出します。

System.out.prinltn( len.apply("abc"));

先ほどのUnaryOperatorインタフェースは、引数と戻り値の型が同じであるFunctionとして定義されています。

戻り値がbooleanであるような場合にはPredicationインタフェースを使います。主に条件式を与えて使います。

Predication<String> tooLong = s -> s.length() > 10;

Predicationインタフェースに与えた条件を判定するには、testメソッドを使います。

if(tooLong.test("rifregerator")){
  System.out.println("長いよ");
}

戻り値がない場合には、Consumerインタフェースを使います。

Consumer<String> print = s -> System.out.println( s );

実行にはacceptメソッドを呼び出します。

print.accept("hoge");

引数がなくて戻り値だけの場合にはSupplierインタフェースを使います。

Supplier<String> sup = () -> String.join("", Collections.nCopies(100, "a"));

実行して値をとりだすには、getメソッドを使います。

System.out.println( sup.get() );

今回は「a」を100個つなぎあわせた文字列を生成しています。
こういった時間がかかりそうな処理を行う必要があるときに、もしこの値を使わないとするとこの処理は無駄になってしまいます。そこで、必要になったときまで処理の実行を遅らせて無駄な処理を行わないようにするために使います。
このように、処理を必要なときまで引き伸ばすような手法を、遅延実行ということがあります。

これらのうちで、引数をとらないSupplier以外は、引数を2つとるBiXxxというインタフェースが用意されています。たとえば引数を2つとる関数なら、BiFunctionインタフェースです。
2つの引数と戻り値の型がすべて同じときには、BinaryOperatorインタフェースが使えます。
また、引数や戻り値がint/long/doubleの場合には、専用のインタフェースが用意されています。

以上をまとめると次のようになります。引数をとらず戻り値もない場合に使えるインタフェースはfunctionパッケージに用意されていないようなので、Runnableインタフェースを使うことになります。

第一引数 第二引数 void int long double boolean object R (T=U=R)
    Runnable IntSupplier LongSupplier DoubleSupplier BooleanSupplier Supplier  
int   IntConsumer IntUnaryOperator IntToLongFunction IntToDoubleFunction IntPredicate IntFunction  
int int   IntBinaryOperator          
long   LongConsumer LongToIntFunction LongUnaryOperator LongToDoubleFunction LongPredicate LongFunction  
long long     LongBinaryOperator        
double   DoubleConsumer DoubleToIntFunction DoubleToLongFunction DoubleUnaryOperator DoublePredicate DoubleFunction  
double double       DoubleBinaryOperator      
ObjectT   Consumer ToIntFunction ToLongFunction ToDoubleFunction Predicate Function UnaryOperator
ObjectT ObjectU BiConsumer ToIntBiFunction ToLongBiFunciton ToDoubleBiFunction BiPredicate BiFunction BinaryOperator
ObjectT int ObjIntConsumer            
ObjectT long ObjLongConsumer            
ObjectT double ObjDoubleConsumer