読者です 読者をやめる 読者になる 読者になる

【Java】lambdajを使ってLINQっぽいことをやる

流石に平日は(時間的な問題と環境的な問題で)じっくり腰を据えて試すことが出来ないので、「とりあえずコンパイルは通るけど実行したらどうなるかわからない」ってレベルでしか実験できてません。以下のサンプルコードは実行しても意図しない結果になるかもしれません。言い訳終わり。

今回の要件は、POSTメソッドでやってきた「IDs」と言うパラメータを「;」で分割し、String[]を取得。その配列をList<Integer>に変換してみましょうと言うもの。

普通ならこんな感じかなーと。

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String[] ids = req.getParameter("IDs").split(";");
    
    List<Integer> idList = new ArrayList<Integer>();
    
    for(String id : ids) {
        idList.add(Integer.parseInt(id));
    }
}

もしC#だったら変換部分はこれだけでいいわけですね。

var idList = ids.Select(x => int.Parse(x)).ToList();

これぐらい簡便に書けたらいいなーと思うわけです。lambdajだとどうなるかやってみましょう。

lambdajでLINQのSelectっぽいことをやる

このLambdajFeaturesと言う記事のConverting, indexing and sorting itemsと言うセクションが参考になりそうです。

渡したCollectionの型からメソッドを呼ぶconvertメソッド、特定の型のプロパティを抽出するextractメソッド、プロパティからMapに変換するindexメソッド、プロパティでソートするsortメソッドなどが紹介されています。これを見るとわかる通り、基本的に「あるPOJOのプロパティを元に変換する」と言う考え方がベースになっています。

今回はInteger.parseIntを呼びたいんですが、引数となるCollectionはString[]なので、このままだとどれも使えません。ただ、convertメソッドに関してはConverterインターフェースを実装したクラスを作れば自分で変換メソッドを定義できるみたいなので、convertメソッドでやってみましょう。

ConverterインターフェースのFは引数となるクラス、Tは変換後のクラスを指定してあげましょう。

public class IntParseConverter implements Converter<String, Integer> {
    public int convert(String s) {
        return Integer.parseInt(s);
    }
}
List<Integer> idList = convert(ids, new IntParseConverter());

どうやらConverterインターフェースのconvertメソッドをリフレクションで呼んでいるみたいなので、インスタンスを渡してあげるだけでOKです。ワーオ。簡単だね!ってアホか!こんなもんのためにわざわざ1つクラスを作るなんてアホらしすぎてやっていけません。かと言ってラムダ式が書けるわけではないので、無名クラスを使いましょう。

List<Integer> idList = convert(ids, new Converter<String, Integer>(){
    public int convert(String s) {
        return Integer.parseInt(s);
    }
});

foreach使うのと記述量変わらない…と言うか…増えてる気がする…。

lambdajでLINQのWhereっぽいことをやる

ま、まぁ、LINQだってSelectの中身次第ではforeachより長くなったりします。寧ろ強みは色んなメソッドと組み合わせて使えることでしょう。と言うわけで、先ほどのconvertメソッドと何かを組み合わせてWhereっぽいことをやってみましょう。

とりあえず要件として「intに変換できないidは無視する」としてみましょうか。

まずは従来の方法で考えてみます。

List<Integer> idList = new ArrayList<Integer>();
int i = 0;

for(String id : ids) {
    
    try {
        i = Integer.parseInt(id);
    } catch(NumberFormatException e) {
        continue;
    }
    
    idList.add(i);
}

次にLINQバージョン。

var i = 0;
var idList = ids.Where(x => int.TryParse(x, out i)).Select(_ => i).ToList();

そもそもJavaにはTryParseがないので、ここまでとはいかなくとも、こんな感じにスマートに書けたらいいですね。

LINQのWhereに対応しそうなものを探していくとFiltering on a conditionに書いてあるfilterメソッドが参考になりそうです。

filterメソッドで指定するMacher<T>はLambdaJMatcherクラスを継承すればいいみたいなんですが、Predicateインターフェースを実装した方が早そうです。filterではapplyと言うメソッドをリフレクションしているみたいなので、そのメソッドを無名クラスで記述しましょう。

List<Integer> idList = convert(filter(new Predicate<String>(){
                    public boolean apply(String s) {
                        try {
                            Integer.parseInt(s);
                            return true;
                        } catch(NumberFormatException e) {
                            return false;
                        }
                    }
                }, ids), new Converter<String, Integer>(){
                    public int convert(String s) {
                        return Integer.parseInt(s);
                    }
                });

何コレ。

ちょっと関数型っぽくなりましたが、String[] idsが凄いところにいるせいで、コードを見ても何をどうしたいのか直感的にわかりません。filterの引数の順序が逆ならまだ救いようがあったのかもしれませんが。

こんなコードが各箇所に埋め込まれていたら地獄でしかありませんし、今まで通りforeachを使ったほうがまだマシでしょう。

まとめ

そもそも何でこんなことになったのか考えてみるに、例えばfilterメソッドであればPredicate<T>.applyでやる処理だけを、convertメソッドであればConverter<F, T>.convertでやる処理だけを渡してあげたいわけです。でもそれが出来ないからわざわざ無名クラスを作り、その中で使うメソッドをオーバーライドじみた形で書いているわけです。そんなの、ただの今までのJavaじゃないですか。

高階関数が使えればそもそもこんなことしなくて済むんですよ。filterメソッドのMacher<T>に、C#でならFunc<T, bool>を渡せば済む話だし、convertメソッドであればConverter<F, T>の代わりにFunc<F, T>が渡せれば本当はそれでいいはずなんです。

じゃあそのFuncに代わるものはないのか?と考えてみたところ、lambdajにはClosureもあるしそれを渡せばいいんじゃね?と気づいたので、次回はこいつを使ってLINQライクなものが作れないか考えてみようと思います。