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

2013-06-13(木) WebSocketをネタにJava EE 7正式版を試してみる

[][]WebSocketをネタにJava EE 7正式版を試してみる 09:35 WebSocketをネタにJava EE 7正式版を試してみる - きしだのはてな を含むブックマーク

Java EE 7がリリースされて、それに対応したGlassFish 4もリリースされました。

ついでに、Java EE 7やGlassFish 4に対応したNetBeans 7.3.1もリリースされています。


ということで、NetBeansGlassFishを使ってJava EE 7を試してみようと思います。

今回は、Java EE 7の中でも簡単なコードで動きの派手なWebSocket対応を試してみます。


ダウンロード

まずNetBeansをダウンロードします。

https://netbeans.org/downloads/


NetBeansは、ちょっと試すにしてもずっと使うにしても、インストーラーではなくzipで落とすほうが楽なので、ここでは「プラットフォーム」に「OSに依存しないZIP」を選択します。

「サポートテクノロジー」が「Java EE」か「すべて」のものをダウンロードしてください。


インストーラ版をダウンロードした場合にはバンドルされているので不要ですが、今回はGlassFishもダウンロードします。

https://glassfish.java.net/download.html


ここでは、「Full Java EE platform」で、「Zip」の「Multilingual」版をダウンロードします。

f:id:nowokay:20130613084740p:image

ダウンロードしたら適当なフォルダに解凍してください。


NetBeansの起動

JDKを複数インストールしている人は、NetBeansを起動する前に「/etc/netbeans.conf」の「netbeans_jdkhome」でJDKのフォルダを設定しておくほうがいいと思います。


/binフォルダにある、環境にあわせた実行ファイルを起動すると、NetBeansが起動します。

f:id:nowokay:20130613084741p:image


NetBeansの起動後の画面です。

f:id:nowokay:20130613084742p:image


「最新情報」でなんかネタが拾われててこっぱずかしいですね。

f:id:nowokay:20130613084743p:image

このブログは、「すべてのブログ」からたどれるPlanet NetBeansに登録されたものが表示されます。

http://planetnetbeans.org/ja/

NetBeansに関するブログを書く人は登録しておくといいと思います。


NetBeansGlassFishの組み込み

インストーラ版でNetBeansを起動した場合にはすでにGlassFishが設定されているはずですが、ここでは先ほどダウンロードしたGlassFishを登録する必要があります。

メインメニューから「ウィンドウ > サービス」を選択します。

f:id:nowokay:20130613084744p:image


「サービス」ウィンドウが開くので、「サーバー」を右クリックしてメニューから「サーバーの追加」を選択します。

f:id:nowokay:20130613084745p:image


「サーバー・インスタンスの追加」ダイアログが開くので、サーバーで「GlassFish Server」を選択します。名前はそのままでもかまいませんが、ここでは「GlassFish Server 4.0」として「次へ」ボタンを押します。

f:id:nowokay:20130613084746p:image


NetBeansでは使わないプラグインは無効になっています。「Java WebおよびEE」の機能がここでアクティブ化します。

f:id:nowokay:20130613084747p:image


GlassFishを解凍したフォルダを「インストール場所」に入力します。GlassFishが検出されると、下部に検出メッセージが表示されるので、「次へ」ボタンを押します。

f:id:nowokay:20130613084749p:image


ドメインなどの設定ができますが、そのまま「終了」ボタンを押します。

f:id:nowokay:20130613084750p:image


サーバーとしてGlassFishが追加されました。ここからGlassFishを起動・停止することもできます。ここでは、確認だけ。

f:id:nowokay:20130613084751p:image


Webプロジェクトの作成

まずはWebアプリケーション用のプロジェクトを作成します。


メインメニューから「ファイル > 新規プロジェクト」を選択します。

f:id:nowokay:20130613084752p:image


Java Web」カテゴリから「Webアプリケーション」を選んで「次へ」ボタンを押します。

f:id:nowokay:20130613084753p:image


プロジェクト名やプロジェクトの場所を選択します。ここではプロジェクト名に「WebApplicationEE7」として「次へ」ボタンを押します。

f:id:nowokay:20130613084754p:image


この画面ではサーバーやコンテキストパスの設定ができます。「Java EEバージョン」が「Java EE 7 Web」になっていますね。

f:id:nowokay:20130613084755p:image

そのまま「次へ」ボタンを押します。


JSFなどのフレームワークを選ぶことができますが、ここではそのまま「終了」ボタンを押します。

f:id:nowokay:20130613084756p:image


WebSocketエントリポイントの作成

Java EE 7でWebSocketと通信するためのWebSocketエントリポイントを作成します。

「プロジェクト」ウィンドウの「ソース・パッケージ」で右クリックして、メニューから「新規 > その他」を選択します。

f:id:nowokay:20130613084757p:image


「Web」カテゴりから、「ファイル・タイプ」として「WebSocketエンドポイント」を選択して「次へ」ボタンを押します

f:id:nowokay:20130613084758p:image


ここでは、「クラス名」や「WebSocket URI」などはそのまま、パッケージ名だけ設定しておきます。

f:id:nowokay:20130613084759p:image

「終了」ボタンを押すと、WebSocketエンドポイントとなるクラスが生成されます。


処理を加えて、次のように編集します。

package wsock;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/**
 *
 * @author naoki
 */
@ServerEndpoint("/endpoint")
public class NewWSEndpoint {

    static Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    
    @OnMessage
    public void onMessage(String message) {
        for(Session s : sessions){
            s.getAsyncRemote().sendText(message);
        }
    }
    
    @OnOpen
    public void open(Session sess){
        sessions.add(sess);
    }
    @OnClose
    public void close(Session sess){
        sessions.remove(sess);
    }
}

表示ページの作成

表示ページを作ります。

ここでは、プロジェクト作成時に同時に生成されたindex.htmlファイルをそのまま編集します。

URLは、プロジェクト作成時に指定したコンテキストパスにあわせて修正してください。

<!DOCTYPE html>
<html>
    <head>
        <title>WebSocketテスト</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
        <script type="text/javascript">
            var socket;
            $(document).ready(function(){
                var host="ws://localhost:8080/WebApplicationEE7/endpoint";
                socket = new WebSocket(host);
                
                socket.onmessage = function(message){
                    $('#log').append(message.data + "<br/>");
                }

                $('#send').click(function(){
                    var text = $('#msg').val();
                    socket.send(text);
                    $('#msg').val('');
                })
    
            });
        </script>        
    </head>
    <body>
        <h1>WebSocketテスト</h1>
        <div id="log">
        </div>
        <input id="msg" type="text"/>
        <button id="send">送信</button>
    </body>
</html>

実行

それでは実行してみましょう。

プロジェクトを右クリックしてメニューから「実行」を選択します。

f:id:nowokay:20130613084800p:image


GlassFishが起動して、プロジェクトが配備されます。

f:id:nowokay:20130613084801p:image


テキストボックスになにか入力して「送信」ボタンを押すと、その文字列が表示されます。

f:id:nowokay:20130613084802p:image

複数のブラウザを開いておくと、すべてのブラウザで文字列が表示されます。

WebSocketがちゃんと動いてますね。


これで、簡単にチャットなどを作ることができそうです。

2013-06-10(月) Java8で最もインパクトのある構文拡張、デフォルトメソッド

[][]Java8で最もインパクトのある構文拡張、デフォルトメソッド 17:21 Java8で最もインパクトのある構文拡張、デフォルトメソッド - きしだのはてな を含むブックマーク

Java8でのラムダの使い方などを説明してきたのですが、構文拡張自体には触れていなかったので、改めてここで簡単に説明しておこうと思います。

まずは、Java8で実際に最もインパクトがある言語拡張、インタフェースのデフォルトメソッドです。


デフォルトメソッドとデフォルト実装

いままでインタフェースには実装をもつことができませんでしたが、Java 8からはインタフェースが実装をもてるようになります。

実装をもつメソッドを定義するときには、キーワードdefaultをメソッドの前につけます。

interface Foo{
    void print(String s);
    default void twice(String s){
        print(s);
        print(s);
    }
}

twiceメソッドが実装をもっています。この実装をデフォルト実装といいます。

デフォルトメソッドを実装するクラスで、デフォルトメソッドを実装していない場合は、デフォルト実装が使われます。

static class FooImpl implements Foo{
    @Override
    public void print(String s) {
        System.out.println(s);
    }
}

次のtwiceメソッドの呼び出しでは、Fooインタフェースでのデフォルト実装が使われます。

public static void main(String... args){
    Foo f = new FooImpl();
    f.twice("yeah!");
}

ただし、toStringなどObjectクラスがもつメソッドのデフォルト実装をもつことはできません。


デフォルトメソッドでの多重継承

デフォルトメソッドは、Javaに多重継承を持ち込みます。

インタフェースは複数のインタフェースを継承することができ、またクラスでは複数のインタフェースを実装することができます。そうすると、インタフェースが実装をもつことによって、複数のインタフェースで同じシグネチャのメソッドがあったときに衝突が発生します。


たとえば、次のようにtwiceというデフォルトメソッドをもったインタフェースがあるとします。

interface Bar{
    void put(String s);
    default void twice(String s){
        put(s + s);
    }
}

単体での実装は問題ありません。

static class BarImpl implements Bar{
    @Override
    public void put(String s) {
        System.out.println(s);
    }
}

次のように呼び出すことができます。

Bar b = new BarImpl();
b.twice("ゴゴ");

ところが、次のように、それぞれデフォルトメソッドとしてtwiceメソッドを持ったFoo、Barインタフェースを両方実装するクラスを定義しようとすると、コンパイルエラーになります。

static class FooBarImpl implements Foo, Bar{
    @Override
    public void print(String s) {
        System.out.println(s);
    }

    @Override
    public void put(String s) {
        System.out.println(s);
    }
}

次のようなコンパイルエラーになります。

f:id:nowokay:20130610172002p:image


この場合、改めてデフォルトメソッドを実装してしまえば、問題なくコンパイルすることができます。どちらかのインタフェースのデフォルト実装を利用したい場合は、キーワードsuperを使ってインタフェースを指定したデフォルト実装を呼び出すことができます。

static class FooBarImpl implements Foo, Bar{

    @Override
    public void print(String s) {
        System.out.println(s);
    }

    @Override
    public void put(String s) {
        System.out.println(s);
    }
    @Override
    public void twice(String s){
        Bar.super.twice(s);
    }
}

2013-05-24(金) Java8でのプログラムの構造を変えるOptional、ただしモナドではない

[][]Java8でのプログラムの構造を変えるOptional、ただしモナドではない 10:15 Java8でのプログラムの構造を変えるOptional、ただしモナドではない - きしだのはてな を含むブックマーク

※ 5/29 3:23 追記:なんかモナドになったかも。最下部参照


さて、Java8での拡張をいろいろ見てきたわけですが、ではアプリケーションプログラムでFunctionを受け取るメソッドをがんがん定義するかというとそういうことはあまりなく、フレームワーク的な部分で数個定義する感じになると思います。もちろん数個でも効果はでかいのですが。

また、おそらくStreamを受け取ったり返したりするメソッドを定義することは、めったにないのではないかと思います。

Mapでの拡張も、メソッド内部での処理記述がかわる話で、メソッドの引数や戻り値はMapのまま変わりありません。


Javaでのプログラムの構造というのは、メソッドの引数や戻り値の型がなんであるかで決まると言うことができます。その意味では、lambdaやStreamというのは処理の記述は変わるけどプログラムの構造は変わらないとなります。

けれどもOptionalは、おそらく戻り値としてアプリケーションプログラムの中でも頻出することになるのではないかと思います。その意味でOptionalは、Javaのプログラムの構造を変えると言えます。


Optionalの基本的な使い方

Optionalとはなにかというと、nullの可能性がある値をラップして、より安全なプログラムが書けるようにする仕組みです。

Optional<String> name = Optional.of("きしだ");

のように使います。

ここでofメソッドにnullを与えると例外NullPointerExceptionが発生するので、値をもたないOptionalオブジェクトを得るときには次のようにemptyメソッドを使います。

Optional<String> name = Optional.empty();

なので、この時点では、nullによる例外が発生しにくくなるというよりは、nullによる例外をより早く発生させる、フェイルファスト的な安全性といえます。


Optionalから値を取り出すときには、getメソッドが使えます。

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

ただし、emptyオブジェクトに対してgetメソッドを呼び出すと例外java.util.NoSuchElementExceptionが発生します。

そのため、getメソッドを使うときには、isPresentメソッドによるチェックが必須になります。

if(name.isPresent()){
  System.out.println( name.get() );
}

getメソッドを使う限りでは、nullチェックがisPresentチェックに、チェックを忘れたときの例外がNullPointerExceptionからNoSuchElementExceptionに変わっただけということになって、あまりメリットはないと言えます。


そこで、Optionalに対しては、値があるときだけ処理を行うという場合にはifPresentメソッドで処理を行います。

name.ifPresent(s -> {
  System.out.println(s);
});

記述量としてはifでisPresentを判定した場合とそう変わりませんが、記述を強制しやすくなると思います。


値を持たない場合のデフォルト値を使うという場合には、orElseメソッドが使えます。

System.out.println("名前:" + name.orElse("指定なし"));

デフォルト値として使うオブジェクトの生成コストが高い場合には、orElseGetメソッドを使います。

System.out.println(name.orElseGet(() -> 
    ResourceBundle.getBundle("messages").getString("name.default")));

また、値がない場合は例外を発生させるという場合には、orElseThrowメソッドを使います。

System.out.println(name.orElseThrow(() -> new Exception("名前がないよー")));

例外オブジェクトの生成はコストが高いので、最初からSupplier経由になっているのだと思います。


ここでorElseThrowのシグネチャ

<E> T orElseThrow(Supplier<E> sup) throws E

のような感じになっているので、検査例外でも投げることができます。あまり見ないシグネチャなのでおもしろいなと思いました。

実際は、extendsなどが入った型指定になっていますが、省略しています。


Optionalに用意されているメソッドはこれだけですが、nullの可能性がある値が少し扱いやすくなります。

ただ、そもそもOptionalを扱う変数にnullを割り当てることができて、その場合にはifPresentメソッドやorEleseメソッドを使ったとしてもそこでNullPointerExceptionが発生するので、あまり強い仕組みではありません。そこは言語としてOptionalを持っているわけではないJava言語の限界ではないかと思います。

Optional<String> name = null;
name.ifPresent(System.out::println); //ぬるぽ

nullの可能性があるというのをシグネチャとして示せるだけでもありがたいという程度ですが、それでもやはりより安全なコードを書くためには大切な仕組みだと思います。

同様の仕組みは、外部ライブラリで用意することも可能ですが、こうやって標準ライブラリに入ることで、ほかの標準ライブラリでもOptionalが使われるようになっていくと思います。その点では標準になったというのは大きいのではないでしょうか。

JPAやJAXBなどマッピング系のライブラリで使えるようになるとありがたいです。


Optionalはモナドではない

ところでOptionalというと、たとえばScalaのOptionやHaskellのMaybeなど他言語での同様の仕組みはモナドの例としてよく取り上げられています。

でも、JavaのOptionalは残念ながらモナドではありません。

Optionalがモナドであるための条件のひとつとして、次のflatMapメソッドのようなシグネチャで、値を別の値に変換してOptionalでくるんで返すメソッドが必要です。

<U> Optional<U> flatMap(Function<T, Optional<U>> mappingFunc);

このようなメソッドはOptionalクラスにはないので、Optionalはモナドではないということになります。


たとえば、StringがOptionalでくるまれたfirstNameとlastNameというオブジェクトがあって、両方が値をもつ場合だけ両方を連結した文字列をOptionalでくるんだfullNameを作りたいとします。どちらかが値をもたなければemptyになるとします。

その場合、いま予定されているJava8のOptionalでは次のようなコードになります。

Optional<String> fullName = (lastName.isPresent() && firstName.isPresent()) ? 
        Optional.of(String.join(" ", lastName.get(), firstName.get())) : 
        Optional.empty();

こうやって、isPresentで判定してgetで値をとるというのでは、Optionalを使わない場合とあまり変わりません。



もしOptionalクラスに上記のflatMapメソッドがあれば、次のように書けます。

Optional<String> fullName = 
  lastName.flatMap(ln -> 
  firstName.flatMap(fn -> 
    Optional.of(String.join(" ", ln, fn))));

変換した値をOptionalでくるんでくれる、次のようなmapメソッドがあれば、もっと記述がすっきりします。

<U> Optional<U> map(Function<T, U> mappingFunc);

そうすると、次のようになりますね。

Optional<String> fullName = 
  lastName.flatMap(ln -> 
  firstName.map(fn -> 
    String.join(" ", ln, fn)));

どうなっているかというと、Optionalの中ではOptionalを気にせずに処理が記述ができて、それでいて外部からはOptionalで包まれているという風になっていて、Optionalの外と中が分離されたコードになっています。モナドというのは、モナドでくるまれた中の世界ではモナドを気にせずに処理が記述でき、外からみるとモナドでくるまれているという、外と中を分離するための仕組みです。

Optionalの役割を考えると、そのように分離されているほうが都合がよく、そのためScalaHaskellでの同様の仕組みはモナドになっているわけです。

Java8でも、Streamはモナドになっているので、Streamの中の処理はStreamを意識せずに記述でき、外からみると常にStreamにくるまれているという風になっています。


でも、Java8のOptionalはそうなっていないので、Optionalにくるまれた値を処理するときには、一旦Optionalの外に値を持ってくる必要がでてきます。これはちょっと残念です。


Optionalの議論

さて、じゃあOptionalがモナドになっていないことを、だれも気にしてないんでしょうか?

もちろんそんなことはなくて、たとえばMario Fuscoという人が、次のようなブログを書いてJava8でのOptionモナドの実装について記述しています。

No more excuses to use null references in Java 8 | Javalobby


その数日後、次のようにメーリングリストに投稿して、Optionalをモナドにしてほしいと要望を出しています。

OpenJDK Lambda Development - Option in Java 8

それに対して、Javaの設計者のひとりBrian Goetzは「JavaのOptionalの目的はOptionモナドの実装ではない」と答えています。


そして、Brian Goetzは次の投稿で、Optionalの議論にこれ以上時間をとらせないでくれというようなことを言っていますね。

Optional require(s) NonNull

この投稿の中で、Optionalの狭い設計対象は、JDKの内部での利用であると述べています。また、Optionモナドの実装や、Optionモナドが解決しようとしている問題を解決することが目的でないと述べています。

flatmapの言語サポートやパターンマッチングなどもない状況では、Optinalをモナドにしても価値は非常に落ちる、と。


Mario Fuscoは、なぜOptionalをモナドにしないか、納得いってないようですけど。

java.util.Optional in Java 8 - Google グループ


まあ、今後のリリースで、Optionalクラスにぺろっとmapメソッドが実装されていたりすることはなさそうです。

※ 5/29 1:49 追記 なんか先ほどぺろっとmap/flatMapメソッドが実装されたっぽい

http://hg.openjdk.java.net/lambda/lambda/jdk/rev/fde3666e6394

2013-05-23(木) Java8で強化されたMapと、書きやすくなったメモ化再帰

[][]Java8で強化されたMapと、書きやすくなったメモ化再帰 16:40 Java8で強化されたMapと、書きやすくなったメモ化再帰 - きしだのはてな を含むブックマーク

Java8のlambda構文の話を書くと、旧来の書き方でいいというコメントがつくのですが、それでも便利になったMapの恩恵を受けることは多いんじゃないかと思います。


Mapには、lambda式を使ったメソッドが多く追加されていますが、たとえばgetOrDefaultメソッドのようなlambda式を使わないメソッドも追加されていて、これも便利です。

そして、このようなlambda式を使わないメソッドも、間接的にはlambda構文サポートでの言語拡張のおかげです。

Mapはインタフェースなので、Java7までの構文でメソッドを追加しようとすると、Mapを実装しているすべてのクラスに新しいメソッドの実装を追加する必要がありました。そしてそれは現実的に不可能なので、今までMapなどのインタフェースに手がいれられることはありませんでした。それが、lambda構文サポートの一環として入れられた、インタフェースのデフォルト実装のおかげで、インタフェースを拡張することができるようになったわけです。


forEachとgetOrDefault

では、今回のMap拡張で一番使うことになりそうな、forEachとgetOrDefaultを見てみます。

まず、文字列のリストを集計して、文字列ごとの出現回数をカウントして表示するコードをJava7構文で書いてみます。

List<String> strs = Arrays.asList("blue", "red", "black", "blue", "white", "black", "blue");

Map<String, Integer> counter = new HashMap<>();
for(String s : strs){
    Integer c = counter.get(s);
    if(c == null){
        c = 0;
    }
    counter.put(s, c + 1);
}
for(Map.Entry<String, Integer> me : counter.entrySet()){
    System.out.printf("%s:%d%n", me.getKey(), me.getValue());
}

先に、結果表示部分を見てみると、Map.Entryを取り出してループをまわしています。

実際にこのコードを書くとき、ここの型指定でいちいちMapが格納している型を確認するのが結構めんどくさかったりしました。Map.Entryって書くのも面倒です。

これが、forEachを使うと次のようになります。

counter.forEach((k, v) -> {
  System.out.printf("%s:%d%n", k, v)
});

すっきり。

型推論してくれるので、counterが保持している型を改めて書く必要もないし、Map.Entryのように新しい型をもってくる必要もありません。


次に集計部分を見てみます。

ここでは、次のようにして、Mapから値をとりだして、値がなければ0を使うようにしています。

Integer c = counter.get(s);
if(c == null){
    c = 0;
}

これが、getOrDefaultを使うと次のように書けます。

Integer c = counter.getOrDefault(s, 0);

条件文がなくなりました!


では、for文を使っているところもforEachを使って書き直すと、結局こんなコードになります。

Map<String, Integer> counter = new HashMap<>();
strs.forEach(s -> {
    Integer c = counter.getOrDefault(s, 0);
    counter.put(s, c + 1);
});

いい感じです。


ただ、Java8ではリストを集計するのにそもそもこんなコードは書く必要がなくて、コレクターを使って次のように書けます。

Map<String, Long> counter = strs.stream()
    .collect(Collectors.groupingBy(s -> s, Collectors.counting()));

ここまでの話はなんだったんだろう?って感じですね。


putIfAbsentとcomputeIfAbsent

文字列を、頭文字ごとにリストに入れるというコードをJava7までの構文で書いてみると次のようになります。

List<String> strs = Arrays.asList("blue", "red", "black", "blue", "white", "black", "blue");

Map<String, List<String>> words = new HashMap<>();
for(String str : strs){
    String initial = str.substring(0, 1);
    if(!words.containsKey(initial)){
        words.put(initial, new ArrayList<String>());
    }
    List<String> ls = words.get(initial);
    ls.add(str);
}
for(Map.Entry<String, List<String>> me : words.entrySet()){
    System.out.println(me.getKey());
    for(String s : me.getValue()){
        System.out.println("  " + s);
    }
}

ひとつ、Java8での目立たない改善点として、既存文法での型推論も強化されたということがあります。この部分、本当はArrayListに与える型は推論してほしいのですが、Java7ではメソッドの引数では型推論がきかないので、ダイヤモンド演算子ではなく型を明示する必要があります。

words.put(initial, new ArrayList<String>());

ここが、Java8ではちゃんと型推論が効いて、ダイヤモンド演算子を使うことができるようになっています。

words.put(initial, new ArrayList<>());

さて、putIfAbsentメソッド。これを使うと、Mapが値を保持していないときだけ値を設定するということが可能になります。

そうすると、次のように書けます。

strs.forEach(str -> {
    String initial = str.substring(0, 1);
    words.putIfAbsent(initial, new ArrayList<>());
    List<String> ls = words.get(initial);
    ls.add(str);
});

ついでにforEachを使っています。


ここで、ちょっと問題なのは、wordsがinitialに対応する値をもっている場合でも、ArrayListのオブジェクトが生成されてしまうことです。Javaは遅延評価ではないので、引数の値が実際には使われないとしても、メソッドを呼び出す前に評価されることになって、いちいちArrayListのオブジェクトが生成されるというわけです。


そこで、Mapの初期値を与えるというような場合には、computeIfAbsentメソッドを使うほうが適しています。

strs.forEach(str -> {
    String initial = str.substring(0, 1);
    List<String> ls = words.computeIfAbsent(initial, s -> new ArrayList<>());
    ls.add(str);
});

こうすると、wordsがinitialに対応する値を保持していないときだけlambda式で与えた関数が実行されて、必要なときにだけArrayListのオブジェクトが生成されます。


で、まあこういう風にリストを集計して個別のリストに振り分けるという場合も、コレクターが使えるので、そもそもこんなコードを書く必要はないのですね。

Map<String, List<String>> words = strs.stream()
        .collect(Collectors.groupingBy(str -> str.substring(0, 1)));

computeIfAbsentを使ったメモ化再帰

computeIfAbsentメソッドのドキュメントにも、よくある利用法のひとつとして「memoized result」つまりメモ化があげられてます。

で、まあメモ化というとメモ化再帰、メモ化再帰というとフィボナッチということで、フィボナッチ書いてみます。

public static void main(String... args){
    IntStream.range(1, 101).forEach(i -> {
        System.out.printf("%d:%d%n", i, fib(i))
    });
}
public static long fib(int n){
    if(n == 0){
        return 0;
    }else if(n == 1){
        return 1;
    }else{
        return fib(n - 2) + fib(n - 1);
    }
}

こうすると、40あたりで実行が遅くなって、50まではたどりつかないくらい重くなります。

これは、同じ値を何回も計算してるからなので、一度計算した値はキャッシュすることにして、次のように書き換えます。

private static Map<Integer, Long> memo;

public static void main(String... args){
    memo = new HashMap<>();
    memo.put(0, 0L);
    memo.put(1, 1L);
    IntStream.range(1, 101).forEach(i -> {
        System.out.printf("%d:%d%n", i, fib(i))
    });
}

public static long fib(int n){
    Long result = memo.get(n);
    if(result != null){
        return result;
    }
    result = fib(n - 2) + fib(n - 1);
    memo.put(n, result);
    return result;
}

このfibメソッドを、computeIfAbsentメソッドを使って書き換えると次のようになります。

public static long fib(int n){
    return memo.computeIfAbsent(n, i -> fib(i - 2) + fib(i - 1));
}

いちぎょうだ!

computeIfAbsentメソッドのおかげで、メモ化がやりやすくなったことがわかります。


ところで、このフィボナッチ数列、93あたりで負の値がでてきて、なんだか怪しい感じです。longが桁あふれしてますね。

・・・

90:2880067194370816120

91:4660046610375530309

92:7540113804746346429

93:-6246583658587674878

94:1293530146158671551

・・・


Java8では、桁あふれで例外がでるような演算メソッドがMathクラスに用意されているので、これを使えば桁あふれが検出できるようになっています。

public static long fib(int n){
    return memo.computeIfAbsent(n, i -> Math.addExact(fib(i - 2),fib(i - 1)));
}

こうすると、93を計算するときには、次のようにoverflowの例外が発生します。

Exception in thread "main" java.lang.ArithmeticException: long overflow


ほかにもJava8ではintやlongを符号なし整数として扱うメソッドもIntegerクラスやLongクラスにそれぞれ用意されています。

大きい数を計算するときにはありがたいですね。


メモ化フィボナッチの話は、こちらを参考にしています。

Memoized Fibonacci Numbers with Java 8 | Informatech CR Blog

ここ、「そんなMapとか使わなくても変数2つあれば十分だよ」ってコメントついてて「たしかにそうだけど、これは再帰の効率化の例だからね」って返答してて、よくある光景だなーと思いました。

2013-05-22(水) Java8のlambda構文がどのようにクロージャーではないか

[][]Java8のlambda構文がどのようにクロージャーではないか 15:04 Java8のlambda構文がどのようにクロージャーではないか - きしだのはてな を含むブックマーク

Java8にlambda構文が入りましたが、これはクロージャーではない、とされています。

では、どのように「クロージャーではない」のか、ちょっと見てみます。


まず、lambdaを返すメソッドを定義します。

public static Supplier<String> createMessenger(String name, String address){
    return () -> {
        return String.format("私は%s、%sに住んでる", name, address);
    };
}

呼び出すと、こんな感じでSupplierを受け取ります。

Supplier<String> messenger = createMessenger("きしだ", "ふくおか");

このSupplierを実行すると、次のようになります。

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

私はきしだ、ふくおかに住んでる


このSupplierが、name,addressの情報を保持しているように見えます。


lambda中では、次のようにして、lambdaの外側の変数を使っています。

return String.format("私は%s、%sに住んでる", name, address);

lambdaがクロージャーであるかどうかという話では、どのようにしてlambdaの外側の変数を使っているかが大事になります。

クロージャーというのは、言語の実装でいえば、関数と変数環境をあわせたものです。変数環境というのは、変数を保持するテーブルだと考えるといいと思います。変数が使われるときに、変数の内容を取ってくるために使われるテーブルです。

具体的には、ここでのClosureケースクラスの定義がわかりやすいです。

Scalaでパーサーを作ってみる〜10:レキシカルスコープとクロージャ - きしだのはてな


ここで、次のようにEnvironmentとFuncをまとめたものとしてClosureを定義しています。

case class Closure(env: Environment, func: Func)

逆に言えば、関数と変数環境をあわせたものでなければ、クロージャーではないということになります。

つまり、Javaのlambdaは、関数と変数環境をあわせたものではないので、クロージャーではないということです。


ためしに、上記のlambdaを使ったコードのクラスファイルをjavapで逆コンパイルしてみると、次のようなメソッドが追加で定義されています。

private static java.lang.String lambda$0(java.lang.String, java.lang.String);

lambda構文で引数をもたない関数を記述したのに、実際に生成されたメソッドは引数をふたつもつものになっています。そして、この引数に、lambdaの外側で定義した変数が渡されてきます。

Java8のlambdaでは、変数環境が渡されるわけではない、ということですね。


では、クロージャーじゃなければ何が困るかという話になります。

これは、もしlambda構文がクロージャーなら何ができるか考えてみるとわかります。クロージャーであれば変数環境が渡されてくるので、クロージャー内部から外側で定義した変数の内容を書き換えることができるはずです。

試しに先ほどのコードでlambda内部で変数nameの内容を書き換えてみます。

return () -> {
    name = name.trim();
    return String.format("私は%s、%sに住んでる", name, address);
};

そうすると、次のように「ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります」というエラーになります。

f:id:nowokay:20130522144524p:image


lambdaで外側の変数を使うときには、final定義されているか、「事実上のfinal」つまり初期化以外では値が代入されていない変数である必要があります。

ということは、lambda内でlambda外の変数を使おうとすると、lambda内部だけではなくlambdaの外でも変数の値を書き換えてはいけないということです。


ただ、lambda内で使う外部の変数を書き換えてしまうと、感覚的ではない動きになることがあり、プログラムの不具合につながりがちです。そのため、実際問題としてはむしろlambda内で使う外部の変数は書きかえれないほうがよいとも言えます。


JavaScriptでよく見かける例に、次のようなものがあります。

for(i = 1; i <= 10; ++i){
  button[i].onclick = function(){ alert(i + "番目のボタンがおされた"); };
}

n番目のボタンが押されたとき「n番目のボタンがおされた」というメッセージを出したいのに、実際にはどのボタンを押しても「11番目のボタンがおされた」となってしまうものです。


Java8で書き換え可能な変数が使えたとすると次のような感じです。

List<Supplier<String>> list = new ArrayList<>();
for(int i = 1; i <= 10; ++i){
    list.add( () -> String.format("%d番目", i));
}
list.forEach(s -> System.out.println(s.get()));

ぱっと見、「1番目」から「10番目」までが表示されそうです。


finalの制限は配列を使うことで回避可能なので、実際に実行可能なコードだと次のような感じになります。

List<Supplier<String>> list = new ArrayList<>();
for(int[] i = {1}; i[0] <= 10; ++i[0]){
    list.add( () -> String.format("%d番目", i[0]));
}
list.forEach(s -> System.out.println(s.get()));

実行してみると、「11番目」と10回表示されます。こういうコードが自然に書けてしまわないのは、いいことではないかと思います。


結論としては、「Java8のlambdaはクロージャーではないけど、クロージャーでやりたいことはできるし、やってはいけないことができないようになっているので、特に問題はない」と言えると思います。