きしだのはてな このページをアンテナに追加 RSSフィード

2013-08-24(土) Java8 Lambdaの文法拡張まとめ

[]Java8 Lambdaの文法拡張まとめ 08:35 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)
   RunnableIntSupplierLongSupplierDoubleSupplierBooleanSupplierSupplier 
int  IntConsumerIntUnaryOperatorIntToLongFunctionIntToDoubleFunctionIntPredicateIntFunction 
int int IntBinaryOperator     
long  LongConsumerLongToIntFunctionLongUnaryOperatorLongToDoubleFunctionLongPredicateLongFunction 
long long  LongBinaryOperator    
double  DoubleConsumerDoubleToIntFunctionDoubleToLongFunctionDoubleUnaryOperatorDoublePredicateDoubleFunction 
double double   DoubleBinaryOperator   
ObjectT  ConsumerToIntFunctionToLongFunctionToDoubleFunctionPredicateFunctionUnaryOperator
ObjectT ObjectUBiConsumerToIntBiFunctionToLongBiFuncitonToDoubleBiFunctionBiPredicateBiFunctionBinaryOperator
ObjectT intObjIntConsumer      
ObjectT longObjLongConsumer      
ObjectT doubleObjDoubleConsumer      

bleis-tiftbleis-tift 2013/08/28 13:11 一部、分かってて書いてるだろうな、という部分もありそうですが、一応間違いの報告です。

誤「匿名メソッド」
正「無名クラス」

誤「実行する処理が一行」
正「ラムダ本体の文が1つ」

誤「引数の型も推論されるので」
正「関数型インターフェイスを引数に取るメソッドに渡す場合は、ラムダ式の引数の型も推論されるので」
例えば、Object o = t -> ""; なんてものは推論されない・・・はず。

誤「if(tooLong.test("rifregerator)){」
正「if(tooLong.test("rifregerator")){」

あと、気になるのは、
「ラムダ記法が導入されたとはいえ、関数が値として扱えるようになったわけではなく」
の部分です。
Java言語の範囲では、ラムダ式やメソッド参照を値として扱えるようになっている気がするのですが、そうじゃない部分があるのでしょうか?

nowokaynowokay 2013/08/28 15:38 ありがとうございます。

「無名クラス」はJava SEのJavaDocでは使われていないので、匿名クラスにしました。(英語でもAnonymous Classなので)
個人的にも、名前は実際にあるので「無名」ではないと思っています。

「実行する処理が一行」というところは「ラムダ本体」の説明なので、その説明の中に「ラムダ本体」という言葉はなるべく使いたくないです。「ラムダ本体」には中カッコを省略したものも含まれるので。
「一行」を「一文」に変更しておきました。

型推論は、 Object o = t -> "";というのはそもそも構文エラーで、ここでは型を明記してもlambda式が使えませんね。
「関数型インターフェイスを引数に取るメソッドに渡す場合」以外でも、変数の初期化・割り当て・キャストなど、ラムダ式を使えるところなら推論してくれるので、「ラムダ式を使える場合は・・・」となって、結局ラムダ式の記述のしかたとしては不要な文になると思います。

値として扱えるようになったわけではない、というのは、ラムダ式やメソッド参照で関数を値として記述はできるものの、結局扱いとしては関数型インタフェースのインスタンスとして保持されることになるということです。関数単体を引数として渡したり、関数の配列などが直接扱えるようになったわけではないという意味で書いています。