2016/06/05

好きな作品のモザイクTシャツを作るための画像をRMagickで作る

ネットのTシャツショップですごく粗いけど、分かる人には分かってしまうモザイク調のTシャツが売っていた。

自分でも、好きな何かの画像から、こんなモザイクTシャツを作ってみたくなった(販売目的ではなく自分専用)。そこでTシャツ作成サービスを調べると、違う柄を1枚ずつでも10枚、20枚と頼めば割引になるサービスが最近はあるようだった。1枚だけではなくたくさん頼んでみたい。

が、たくさんの画像をモザイクにしていくのは面倒くさいので、最近使い始めたプログラミング言語 Ruby で簡単なモザイク化プログラムを作ってみた。画像処理ソフトウェア ImageMagick を Ruby で扱えるようにした RMagick を使った。



このプログラムを使うと、以下に示すように画像がモザイク化される。



まずは元のTシャツと似たようなモザイク。特徴的なカラーで、よく分かる。



こちらも特徴的なカラー。映画まだなんだろうか。



ベタ塗りの多いアニメ絵でなく、映画ポスターから。写真などもいけそう。



わんこ飼いたい。



荒木先生は同じキャラでも色んな色で描かれるから、定番のカラーリングの絵を探すのが大変だった。



あらためて見ると、兄弟でズボンの色が違うのだなぁ。



この粗さでも、なんとなく複数キャラいることが分かる。「アオいいよね」「アオいい…」



こちらは国民的な青。



こちらも青。ここまで粗くしても色やポーズで分かるキャラデザ。さすがカプコン。



最近Netflixで見ておもしろかった。元の絵がタイル調なのもよいかも。



9人が同じコスチュームだと、さすがに判別は難しい。逆に、このほうが分かる人は分かる感じになっていいかも。



これもなんだか分からないが、いいぞ…という気がしてくる。



ノーコメント。



なかなかよい感じになった。この調子で好きな作品のモザイクTシャツを作ってみようと思う。

2015/12/04

重複コード撲滅に役立つIntellij IDEAの機能

この記事はJetBrains IDE Advent Calendar 2015の4日目です。

2年ほど前、命名規約やプログラミングの慣習に違反したお行儀のよくないコードをIntellij IDEAを使って見つけて、改善していくことを書きました。「publicなメソッドの名前がget〜なのに戻り値がvoidで、どうやらgetしてきた何かをフィールドにsetしてて、別なアクセサで取得するらしい…」みたいなアレです。

そういった行儀の悪さも困りものなのですが、既存のコードベースの上に機能追加したり不具合修正したりする上でもう1つ厄介なのは、コードの重複です。

  • 大人数が横串を通す時間もなく作ったので、各人が同じものをあちこちで実装してしまう
  • コードベースが数十万行などと巨大なので、既に誰かが実装済みなことを知らずに実装してしまう
  • 改修するとテストが大変なので、コピーしてちょっとだけ挙動を変えた実装を作ってしまう

私の職場では、そんな感じでコードの重複が積み重なってきたようです。すると、ある不具合を修正したつもりでも、他のところにコピーされていて直しきれなかったりするので、なかなか骨が折れます(つらい)。

最近IDEAを職場で使い始め、そういう重複コードを少しつづ潰せて便利でしたので、カンタンにご紹介します。

Inspection

まずはIDEAに備わった強力なコード静的解析(Inspection)を使います。IDEAの「Preference(設定パネル)>Editor>Inspections」内をduplicate(重複)などと検索して、設定を見つけてONにしてみてください。

重複に関するInspectionは、開いているファイルに対してはその場で実行してくれます。またAnalyze>Inspectionから、プロジェクト全体など指定したスコープで一括で実行することもできます。

Javaでの重複に関するInspectionをいくつか紹介してみます(他にJS、Maven、Java EEなどなど様々なコードの重複を見つけるInspectionも備わっています)。

Control flow issues > Duplicate condition in ‘if’ statement

if/else文で異なる分岐に同じ条件が繰り返ししているのを見つけてくれます。例えば以下の例でいうと、isA(booleanの変数だったり、booleanに評価される式)が重複としてハイライトされます。

if (isA || isB) {
  ...
} else if (isA || isC) {
  ...
}

Control flow issues > Duplicate condition on ‘&&’ or ‘||’

&&や||で同じ式が繰り返されているとハイライトされます。以下の例でいうとisAです。

if (isA || isB || isC || isD||isA) {
  ...
}

if (isA && isB || isC || isA && isD) {
  ...
}

isAが3文字のboolean変数だと目視でも重複に気づけるかもしれません。しかし、これがもし複雑な式だと、見落としてしまいそうです(実際にお仕事で見つかったときは驚きました)。

Internationalization issues > Duplicate String Literal

Stringリテラルの重複を見つけてくれます。「なんとか区分」「なんとかコード」みたいな定数が重複しまくっていると大変ですよね。そういうときに役立ちます。

上の例で言うとCONSTANT_A、OTHER_CONST、keyが重複したリテラルです。

class Foo {
    private static final String CONSTANT_A = "ABC1234";
    void bar() {...}
}

class Hoge {
    private static final String OTHER_CONSTANT = "ABC1234";
    void fuga() {
        String key = "ABC1234";    
        ...
    }
}

IDEAでは、これら見つかった重複に対し、

  • どれか1つを残して、他から参照するようにする
  • 指定したクラスに定数を新設して、それを参照にする

といったQuick Fixをかけることができて、あっという間に重複を除去できて、大変便利です(何百も重複しているリテラルを見つけたときは気が滅入りかけましたが、IDEAに救われました)。

Locate Duplicate

Locate Duplicateはさらに強力で、その名の通り、複数ステップに及ぶ似たようなコードを見つけてくれます。

ステップに登場してくるローカル変数、フィールド、メソッド、クラス名、リテラルなどを匿名化(型があっていれば、別な名前でも重複と推測する)して、「ちょっと違うけど、この辺が重複してそうですよ」ということまで調べてくれます。

例えば次の例では、2つのメソッドmatrixinizeの5行とtablenizeの5行それぞれは、変数名やリテラルなどが微妙に異なっていますが、重複としてハイライトされます。こうして見つけた重複から共通部分を抽出し、それを使うように修正できるでしょう。

public static List<List<String>> matrixinize(String e, int count) {
    System.out.println("matrixnize begin");
    List<String> row = Collections.nCopies(count, e);
    List<List<String>> matrix = Collections.nCopies(count, row);
    System.out.println("matrixnize end");
    return matrix;
}

public static List<List<String>> tablenize(String e, int count) {
    System.out.println("tablenize begin");
    List<String> row = Collections.nCopies(count, e);
    List<List<String>> table = Collections.nCopies(count, row);
    System.out.println("tablenize end");
    return table;
}

つい先日リリースされたばかりのIDEA 15からは、Locate Duplicatesも開いているファイルに対しては、その場で実行してくれるようになりました。14まではいちいち、メニューのAnalyze>Locate Duplicateからたどって設定パネルを操作してようやく動作するので少し手間でしたが、裏でよしなにやってくれるようになって、非常に便利です!! ただ、あまりに重複だらけのコードを相手にしていると、若干うざいです(つらい)。

見つけた重複は、深刻さにもよりますが、カンタンなものはIDEAの様々なリファクタリング(メソッドの抽出とか、メソッドオブジェクトの抽出とか)でサクサク潰しています。

おしまい

こうしたIDEAの機能を活用し、がんばってダメなコードや重複を減らして、後任者に恨まれないようにしていきたいです。

2015/04/06

Kibana4活用事例を話しました

4月4日(土)に名古屋で行われた Elasticsearch勉強会 で、パフォーマンス分析やアクセス分析にKibana4を活用してる、という事例を発表してきました。

発端

スライドにしたように、去年からログ分析ツールKibanaをお仕事で使ってて、最近では検索エンジンElasticsearchもシステムに使えないかなぁと画策しています。

ただ、ほとんど初期値のまま運用していたので、そろそろパフォーマンスとかセキュリティとか考えなきゃなあ、なんて思って

などとツイートしてたら、elastic社のエヴァンジェリスト大谷さんに補足いただき、名古屋でElasticsearch勉強会を開催していただきました。

2年前から東京で勉強会を続けられている大谷さんですが、今回の名古屋を皮切りに、他の地方にも広めていかれたいそうです。ElasticsearchやKibanaを使っている/使ってみたいという方は、Twitterなどで「(地方名)でも勉強会したい」とつぶやいてみるといいかもしれませんね。

所感

  • 運用していく上で注意すべき設定やオススメの管理プラグイン Kopf大量にインデックスする時に性能を高める方法など耳寄り情報いっぱい教えてもらった。
  • Kibana使ってるソシャゲ会社さんとか、巨大Elasticsearchクラスタ組んだりしてる企業さんが参加されて、名古屋界隈でもけっこう広まってるんだなぁと思った。
  • mzpの発表いつもおもしろいし、うまい。
  • 自分の発表はグダグダだったけど、大谷さんから「システム内部のパフォーマンス分析に使っている事例は貴重なので、おもしろかったです」と言われたり、参加者の方から「Kibana4使い倒してるね」「今度教えに来てください」なんて言われたので、結果オーライ。
  • 懇親会では色んなIT業界の闇トークで盛り上がって楽しかった。

2015/03/31

Java SE 8とJMHを使ってインスタンス生成の性能を計測した

megascusさんの「Java SE 8時点ではリフレクションを使ったインスタンス生成はほぼ問題にならない程度に早いらしい」を見かけたので、もがみもJava SE 8(8u40)でマイクロベンチマークのフレームワークJMHを使って測定してみました。

コード

元の記事ではStringだけ測定してましたが、Stringは内部的にchar[]やハッシュコードを事前に計算したintなどのフィールドが初期化されます。これを除外するため、フィールド無しのTooSimpleClassも定義して比較してみました。

またStringの場合、普通はコンストラクタを使わずにリテラルを使うと思うので、そちらも計測。リテラルだと、プールされたインスタンスがよしなに使われるので、高速になるはず。Stringプールに対して、TooSimpleClassではあらかじめ生成したインスタンス(要はシングルトンみたいなものですね)取得を比較します。

ウォームアップは50回、実際の測定は50回行って平均スループットを算出しています。

package net.exoego;

import java.io.IOException;

import org.openjdk.jmh.Main;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.RunnerException;

public class NewInstanceBench {
    public static void main(final String... args) throws ReflectiveOperationException, IOException, RunnerException {
        Main.main(new String[]{"-i", "50", "-wi", "50", "-f", "5"});
    }

    public static class TooSimpleClass {
        public static final TooSimpleClass INSTANCE = new TooSimpleClass();
    }

    private static final Class<String> STRING_CLASS = String.class;
    private static final Class<TooSimpleClass> SIMPLE_CLASS = TooSimpleClass.class;

    @Benchmark
    public static void stringPool() {
        String s = "";
    }

    @Benchmark
    public static void stringConstructor() {
        String s = new String();
    }

    @Benchmark
    public static void stringReflectionCached() throws ReflectiveOperationException {
        String s = STRING_CLASS.newInstance();
    }

    @Benchmark
    public static void stringReflectionNotCached() throws ReflectiveOperationException {
        String s = (String) Class.forName("java.lang.String").newInstance();
    }

    @Benchmark
    public static void simpleSingleton() {
        final TooSimpleClass instance = TooSimpleClass.INSTANCE;
    }

    @Benchmark
    public static void simpleConstructor() {
        TooSimpleClass s = new TooSimpleClass();
    }

    @Benchmark
    public static void simpleReflectionCached() throws ReflectiveOperationException {
        TooSimpleClass s = SIMPLE_CLASS.newInstance();
    }

    @Benchmark
    public static void simpleReflectionNotCached() throws ReflectiveOperationException {
        TooSimpleClass s = (TooSimpleClass) Class.forName("net.exoego.NewInstanceBench$TooSimpleClass").newInstance();
    }
}

実際はmainメソッドは使わず、作ったjarファイルをコマンドラインで実行しました。

JMHのセットアップとかをやってくれるpom.xmlはこちら。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.exoego</groupId>
    <version>0.1-SNAPSHOT</version>
    <artifactId>constructor-benchmark</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jmh.version>1.6.3</jmh.version>
        <javac.target>1.8</javac.target>
        <uberjar.name>benchmarks</uberjar.name>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <compilerVersion>${javac.target}</compilerVersion>
                    <source>${javac.target}</source>
                    <target>${javac.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>${uberjar.name}</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

測定結果

スループット(1秒間に実行できる回数)はこちら。値が大きいほど高速です。

TooSimpleClass String
instance pool 3,124,042,755 times/sec 3,126,290,518 times/sec
constructor 3,198,891,172 times/sec 322,034,032 times/sec
klass.newInstance(cached) 148,542,913 times/sec 94,572,902 times/sec
klass.newInstance 1,071,358 times/sec 1,859,832 times/sec

チャートにするとこちら。

スループットを逆にして、1実行あたりの処理時間(ナノ秒)はこちら。値が小さいほど高速です。

TooSimpleClass String
instance pool 0.32 ns 0.32 ns
constructor 0.31 ns 3.11 ns
klass.newInstance(cached) 6.73 ns 10.57 ns
klass.newInstance 933.39 ns 537.68

StringリテラルでのStringプールからの取得は、1秒で31億回実行できています。これに対し、Stringコンストラクタは約10分の1の3億2千万回です。StringのコンストラクタでなくStringリテラルを使うべき理由は、こういう性能向上なんですね。

通常のコンストラクタを見ると、new String()はnew TooSimpleClass()の約10倍遅かったです。フィールドがちょっと増えただけで、こんなにも違うとは(といっても0.31ナノ秒が3.11ナノ秒に増えた程度ですが)。

今回の測定ではキャッシュされたClassリテラルでのClass#newInstance()呼出は、new String()より3.4倍遅くなりました(元記事では2倍)。実際のクラスではコンストラクタ自体の実行にかかる時間(引数のバリデーションやフィールドの初期化など)がもっと増えてくると思いますので、そこまで目くじらを立てる程の開きではありませんね。

キャッシュしてない場合は2985倍遅くなりましたが、元記事では200倍なので、こちらはかなり開きがあります。JMHでウォームアップを増やし、測定回数も多くしているので正確になってると思いますが、こんなに開くものなのかな。

2014/12/24

LombokとLombok-pg: Javaコードを減量する魔法のスパイス

この記事はJavaアドベントカレンダー2014の12月24日分です。昨日は、 nagaseyasuhito さんによる Mavenでマスター/スレーブ構成のMySQLを起動して結合テストをするぞ という記事でした。Mavenでこんなことまでできるんだなぁということが分かる実践的なコードで、参考にしたいです! 明日は、いよいよアドベントカレンダー最終日、担当は kokuzawa さんになります。

コードをシンプルにできるラムダ式への注目

さて今年の日本のJavaアドベントカレンダーは、4月にJava 8がリリースされたこともあって、Java 8に関連した記事が多かったようです。その中でも、特にラムダ式への注目が際立ちました。

ラムダ式の最たる活用例 Stream APIについては、12月17日のcom4dcさんがデータ処理がどう簡潔に書けるかをコード例で示されています。また12月20日のRyota Murohoshiさんは、JavaのStream APIを学ばれる過程で分かったStream API、引いてはJavaの標準ライブラリの設計思想を紹介されています。ぼくが昨年のアドベントカレンダーで書いたStream APIの始め方 も合わせてどうぞ。

ライブラリのラムダ式対応が進んでいることが分かる例として、12月10日のsuke_masaさんがO/RマッパDomaのラムダ式対応、12月16日のzephiransasさんがふるまいの仕様をラムダ式を使った英語風のDSLで既述できるlambda-behaveを紹介されています。

ラムダ式を活かせるライブラリを自作されてる方もチラホラいらっしゃいました。12月3日のtaichiさんが 軽量WebアプリフレームワークSiden、12月18日のequus52さんがパターンマッチのマッチ式風のライブラリpattern-matching4j を公開しています。

少し違った観点から、12月2日のmike_neckさんが オーバーロードしたメソッドでのラムダ式のワナについて書かれています。ぼくも、ライブラリ自作中にこのワナを踏んでしまったので、将来のJavaでの改善を期待したいところです。

こんな風に多くのJavaプログラマにラムダ式が歓迎されている最大の理由は、なんといっても「わずらわしかった匿名クラスがいらなくなり、コードをシンプルにできる」ところだよな、とぼくは思います(匿名クラス自体は良いアイデアだと思いますが、使うのは面倒でした)。

ラムダ式以外のコードシンプル化方法

本記事は、そんなラムダ式とはまた違ったJavaのコードシンプル化をご紹介します。

次の2つを取り上げます。

  • Javaのボイラープレートを自動生成してくれるライブラリLombok(ロンボク)
  • Lombokに機能を追加したLombok-pg

Javaの冗長なボイラープレート

ボイラープレートとは、「プログラミング言語やフレームワークなどの仕様上、どうしても省略できないお決まりのコード」のことです。

Java 8のラムダ式も、メソッドに渡したい関数(コードのかたまり)を表す匿名クラス、というボイラープレートを削減してくれるものですね。またJava 7で導入されたtry-with-resource文も、ファイル入出力などをするさいのリソース管理のボイラープレート(やリソース管理のうっかりミス)をかなり削減してくれました。Java 5のジェネリクスオートボクシングも、型キャストのボイラープレート(やキャスト失敗の危険)を削減してくれました。

このようにJavaは、バージョンを経るごとに、コードを安全にそしてシンプルに書けるように発展してきました。

ですがJavaには、まだまだ多くのボイラープレートがあります。代表的なものだと、JavaBean規約などで定められているsetter/getter/デフォルトコンストラクタや、equals/hashcodeの実装などが思い浮かびます。

IDEによるボイラープレート自動生成

そんなJavaのボイラープレートのいくつかは、EclipseやIntellij IDEAなどのIDEで、少ない手間で生成できます。IDEを使いこなしている人であれば、さほど不便を感じてないかもしれません。

ただ、IDEのコード生成でも、地味にわずらわしいことがあります。

例えば、フィールドが増えたりしたときに、setter/getter/equals/hashcodeを生成しなおす手作業は、ぼくは面倒に感じます。うっかりequals/hashcodeを修正し忘れてしまうと、HashMapやHashSetなどが意図に反した動きをしてしまいかねません。

また、重要でないgetter/setterなどでコード量がムダに増えてしまうと、本質的でないコードでテストカバレッジが下がってしまいますし、コードを読み書きする上であまり気持ちがよいものではありません。

Lombok:Javaのコード量をダイエットするスパイス

そこでご紹介するのが Lombok です。

Lombokは、Javaのアノテーションプロセッサを利用し、コンパイル時にコードを自動生成しすることで、ボイラープレートを削減してくれるものです。これにより「自動生成しなおす煩わしさ」や「コード量のうっとうしさ」から、だいぶ解放されます。

ちなみに、Lombokという名前は、インドネシアのロンボク島から取ったようです。ロンボク島は、独特のスパイス料理があるらしく、Java(ジャワ島もインドネシアの島です)にピリッとひと味加える、というような意図なんでしょうね。

そんなLombokについては、既に色々な記事で書かれており、使っている方も多いかもしれません。まだ知らなかった方へは、この辺の記事をオススメします。

コード例や使い方の紹介は上記に譲りますが、次節で、できることをざっとご紹介します。

Lombokでできる自動生成

紹介するのはLombok 1.14.8の標準機能です。まだ不安定な実験的機能は除いてます。

アノテーションをつけるだけで、さまざまなボイラープレートが自動生成されます。それぞれのアノテーションのパラメータで細かい制御もできます。

簡単な評点

数が多いので、流し読みしやすいように、独断と偏見で4段階評価をつけていきます(ぼくのJavaの使い方によるものなので、あくまで目安とお考えください)。それぞれ、こんな意味です。

★★★
よく使う。絶対覚えたい!!!
★★☆
まあまあ使いそうなので覚えておこう!
★☆☆
ほとんど使わなそう。必要なときに調べて使えればいいかな。
☆☆☆
お役御免。Java自体にその機能が入ったとか、お仕事的に…などの理由で。
Lombokの標準機能

こんなことができます。

@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
★★★
コンストラクタの自動生成。
@Getter
@Setter
★★☆
アクセサ・メソッドの自動生成。
@EqualsAndHashcode
@ToString
★☆☆
すべてのオブジェクトに共通するメソッド、equals/hashcode/toStringの自動生成。
@Data
★★★
シンプルなPOJOやJavaBeanの自動生成。 一緒に使うことが多い@RequiredArgsConstructor, @Getter, @Setter(finalでないフィールドのみ), @ToString, @EqualsAndHashCodeをセットでやってくれます。
@Value
★★★
イミュータブルなクラスの自動生成。@Dataと同じく、@RequiredArgsConstructor, @Getter, @ToString, @EqualsAndHashCodeをセットでやってくれます。すべてのフィールドがfinalなので、セッタ・メソッドはありません。クラスもfinalになるので、サブクラスでミュータブルなフィールドが定義されることもなくなります。
@Cleanup
☆☆☆(Java 6までだったら★★★
try文での自動リソース管理。Java 7にtry-with-resource文が追加されことで役目を終えましたが、Java 6までの時代では助かりました。
@SneakyThrows
★★☆
メソッド内で発生した、処理しようがない&伝播もさせたくない検査例外を、非検査例外に自動変換してくれます。例外をどう扱うべきかに注意が必要ですが、非検査例外にしたくなるときはしばしばあります。
@Synchronized
★☆☆
メソッドをsynchronizedメソッドへ自動変換。ぼくは普段の開発ではほとんど並行処理を書かないので、使うことは少なそうです。
@Log
★☆☆
ログ出力に使うLoggerフィールドの自動生成。これもSpringなどを使ってログ処理を織り込んでいるので、ぼくにはあまり使いみちはなさそうです。

またアノテーションではなく、新たなキーワード val も追加されます。valを使うと、変更不能なローカル変数を簡単に書くことができます。具体的には、ローカル変数の型名とfinal修飾子を省略することができます。Scalaのvalや、C#のvarなど、他のプログラミング言語にある機能をJavaで使えるようになります。見た目は、

// final String name = "hoge"; と同じ
val name = "hoge";

キーワードまで追加できてしまえるなんて! 将来のJava言語でも、こういう便利なキーワードが追加されるのを期待したいです。ただ、IDEによって使えたり使えなかったりするようです(IDEAでは使えませんでした……)。

Lombokが提供してくれる自動生成、なかなか便利なものが揃っているのではないでしょうか。

次節からは、このLombokにさらに機能を加えたLombok-pgを紹介します。

Lombokにさらにひと味加えたLombok-pg

peichhorn/lombok-pgは、Philipp Eichhornさんによって開発された、Lombokの機能拡張版です。

名前のpgには、Lombokに追加したい実験的機能の遊び場(PlayGround)という意味があるそうです。Lombokのイメージが赤唐辛子なのに対し、実験版であるLombok-pgは青唐辛子(熟してない)ところも象徴的です。

残念ながら、Lombok-pgの開発は2012年を最後に途絶えており、ドキュメントも中断しています(Philippさん自身からは開発終了の音沙汰もないようですが……)。

しかし幸いにも、Lombok-pgはLombok本体の開発コミュニティでも注目されているようで、Lombok-pgで追加された機能を本体にマージする動きが始まっています。実際、いくつかの機能が、まだ実験的機能という位置づけではありますが、既に取り込まれているようです。

次節からは、Lombok-pgでどんな機能が追加されたかを見ていきます。

Lombok-pgが追加した機能

Lombok-pg 0.11.3 の機能の数々を簡単に分類し、ざっと紹介していきます。

この記事を書くために一通り触ってみたのですが、残念ながら2012年に開発が中断しているため、JavaのバージョンやIDEによっては使えない機能もチラホラありました。Java 8とIDEAで動くようにforkしたいなぁ……。

メソッドに機能を織り込むアノテーション

これらは、自分が書いたメソッドにコードを追加して、何らかの機能を付与してくれるものです。

@Rethrow
@Rethrows
★★☆
無視したい検査例外を非検査例外に変換してくれるLombok本体の@SneakyThrowsの亜種で、変換する際に例外にセットするメッセージを設定したり、どの非検査例外に変換するかを指定したりできます。
@Rethrowsは、複数の@Rethrowを束ねることで、より複雑な例外の変換規則を定義するものです。
@Validate.With
@Validate.NotEmpty
@Validate.NotNull
★★☆
メソッドやコンストラクタの引数に対するバリデーション処理の呼び出しを自動生成してくれます。バリデーション処理とは、引数が前提条件(nullではない、など)を満たしているかを検査し、満たしていなければ例外を投げるような処理です。
@Sanitize.With
@Sanitize.Normalize
★☆☆
メソッドやコンストラクタの引数に対するサニタイズ処理の呼び出しを自動生成してくれます。サニタイズ処理とは、値から望ましくない部分(例えば特定のTMLタグなど)を除去するような処理です。
@WriteLock
@ReadLock
★☆☆
並行処理のためのものです。メソッドを、@Synchronizedよりも粒度の細かい(syncrhonizedより性能が高まりやすい)ロックを行うように自動変換してくれます。Java 5で導入されたConcurrency FrameworkのReadWriteLockが使われます。ReadWriteLockの使用は、従来のsynchronized修飾子やsynchronized文よりもかなり手間でしたが、これが簡単に書けるようになります。
@Await
@Signal
@AwaitBeforeAndSignalAfter
★☆☆
これも並行処理のためのものです。メソッドを、何らかの条件が満たされるまで待機(Await)したり、その条件を満たした信号(Signal)を送るように自動変換してくれます。
@SwingInvokeLater
@SwingInvokeAndWait
☆☆☆
メソッドを、AWTのEventQueueをチェックして実行タイミングを変えるようにしてくれます。
メソッドを自動生成するアノテーション

これらは、お決まりのメソッドを自動生成してくれるものです。

@AutoGenMethodStub
★☆☆
メソッドのスタブ(仮実装)を自動生成してくれます。あるインタフェースのメソッドのうち、実装したいものだけを実装すればよくなります。
Java 8では、スタブや基本実装をインタフェースに定義できる言語機能デフォルト・メソッドが入りました。しかし、デフォルト・メソッドではないメソッドがたくさんあるインタフェースを実装するときには、このアノテーションは役立ちそうです。
@BoundSetter
★☆☆
@Setterの亜種で、値をセットしたときに何らかの処理を挟み込む特別なセッタ・メソッドを生成してくれます。セットの監視には、java.beans.PropertyChangeListenerで行います。これは関数型インタフェース
@Builder
@Builder.Extension
★★★
「生成時にたくさんのパラメータが必要なオブジェクト」などの生成を助けるBuilderパターンが使えるクラスを自動生成してくれます。Builderオブジェクトのメソッド名に”with”などの接頭詞をつけたり、指定してもしなくてもよいパラメータのデフォルト値を決めたりなど、さまざまな制御ができます。
これはLombok本家でも実験的機能として取り込まれています。
@Singleton
★★☆
指定したクラスのシングルトンインスタンスとそれを取り出すstaticメソッドを自動生成してくれます。ただし、コンストラクタをprivateにするなどまではしてくれません。
@EnumId
★★★
「列挙型のインスタンスそれぞれを識別できる値を、対応するインスタンスに変換するファクトリメソッド」、簡単にいうと「1→HEART, 2→SPADE、3→DIAMOND、4→CLUB、5→JOKERみたいな変換をやってくれるメソッド」を自動生成してくれます。Stringやintで扱っていた「なんとか区分」を列挙型に変換するということは、なかなかの頻度であるので、便利です。
Lombok本家にはまだ実験的機能にもなっていませんが、取り込もうという議論があるようです。
@FluentSetter
★☆☆
@Setterの亜種で、戻り値がvoidでなくインスタンス自身なメソッドを自動生成してくれます。これにより、メソッドチェーンができるようになります。
@Accessorsという、より汎用的で機能を今後追加しやすい名前で、Lombok本家でも実験的機能として実現されています。
@LazyGetter
★★☆
@Getterの亜種で、「最初にゲッタが呼び出されたときに初めてフィールドの値を生成し、以降はその値を返す」ゲッタ・メソッドを自動生成してくれます。値の生成が重たい処理である場合に、値が必要になるまで生成を後回しにすることで、性能向上を期待できます。
@ListenerSupport
★☆☆
AWTなどのリスナを登録したり、発火したりするコードを自動生成してくれます。
@VisibleForTesting
★☆☆
クラスやメソッドを、テストコード(呼び出し元のクラス名が”Test”で終わる)からはpublicで見えるように変換してくれます。クラスやメソッドの可視範囲をテスト目的でゆるめざるをえない時に使えそうです。
@Warning
★☆☆
ちょっと特殊で、コードを生成するのではなく、コンパイル時に任意の警告メッセージを表示させることができます。このアノテーションをつけたクラスやメソッドに関して、開発者に何らかの注意(まだこういうバグがあるのでこのURLを見てね、とか)を喚起したい場合に便利そうです。
@Action
@Function
@Predicate
☆☆☆(Java 7とそれ以前なら★☆☆
メソッドを任意の関数型インタフェースのオブジェクトにカプセル化します。関数型インタフェースとして、LombokはAction0(引数なし)~Action8(8引数)、Function0~Function8、Predicate0~Predicate8を提供しています。他の関数型インタフェースを指定することもできます。
Java 7までで関数型インタフェースを活用したいときには少し便利だったかもしれません。しかしJava 8では、メソッドを関数型インタフェースのインスタンスに変換する言語機能メソッド参照が導入されたので、出番はなくなりそうです。
言語機能の追加

lombok本家の val のように、Java言語には今のところない言語機能を追加してしまうものです。Javaの常識からかけ離れてしまうので、気をつけて使う必要がありそうです。

残念ながらどれもIDEとの相性が悪く、使用性に難点があります。動くようにforkしたいところです。

@ExtensionMethod
★★★
C#にあるような言語機能 拡張メソッドが使えるようになります。ここでいう拡張メソッドとは 「あたかも第1引数のインスタンスメソッドかのように呼びだせる、staticメソッド」です。これによって、自分でメソッドを追加することのできないクラスやインタフェース……(例えばStringや配列、Iterableなど)……にインスタンスメソッドを追加したかのようなコードが書けます。
例えば、
public class ExtensionMethods {
    public static <T> T orElse(T thisValue, T defaultValue) {
        return thisValue != null ? thisValue : defaultValue);
    }
}

@ExtensionMethod(ExtensionMethods.class)
public class ExtensionMethodsClient {
    void fooMethod(String maybeNull) {
        // staticメソッドorElseが、あたかもインスタンスメソッドかのように呼び出せる
        final String foo = maybeNull.orElse("NULL!!");

        // fooで何かいいことをする
    }
}
みたいに、nullもそこそこ安全に扱えるようになります。他には、Java 8だと配列をStreamに変換するのはStreamクラスやIntStreamクラスなどのstaticメソッドを呼ぶ必要がありますが、それも
array.stream().map(e -> e.foo());
などと書けちゃうわけです。ジェネリクスにもそこそこ対応してます。これはヤバイです。マジヤバです。色々おもしろそうなことができそうです。
Lombok本家でも実験的機能として取り込まれています。
早く主要なIDEで使えるようになって欲しいです……。
yield
★☆☆
いわゆるジェネレータという言語機能をエミュレートしてくれます。Lombok-pgによって生成されるジェネレータは、Iterableのインスタンスとなります(yiledメソッドを使用しているメソッドが、適切に状態遷移して値を生成(ジェネレート)するIterableへと、変換されます)。Lombok-pgが生成するジェネレータのコードは、かなり上手く状態管理されるようです。try-finally文にも対応し、適切にfinally節が実行されます。すごいです。
独自のIterableとIteratorを定義したくなることは稀にあるので、yieldが使えると結構うれしいのですが……。
tuple
☆☆☆
名前はタプル(値の組)ではありますが、今のところ、いわゆるdestructuring(分配束縛)という言語機能をエミュレートするだけのようです。これだけでは、あまり使いみちがないような気がしますが、どうなんでしょう。

最後に

LombokとLombok-pgを使うことで、Javaのボイラープレートの数々を減らせることがお分かりいただけたかと思います。

LombokはJava 8のプロジェクトでも大活躍してくれてるのですが、Lombok-pgはJava 8やIDEで動かない機能がけっこうあるのがもったいないなぁ……。せっかくなので、動くように修正してみようかと思います。どんな風にコードを生成したり、変形したりしているかを理解して、Javaのイライラするボイラープレートを減らせるような新機能を作れるようになりたいですね。

2014/01/14

そのクソコード、Intellij IDEAでチェックできるよ

愛知県でシステムエンジニアとして働く友人のMは、プロジェクトメンバの書くJavaのクソコードに苦しめられているそうです。Mはリードプログラマとして、プロジェクトメンバがあげてくる成果物(ドキュメントとコード)のレビューをする立場にあるらしく、提出されてくる数々のクソコードをTwitterでつぶやいていました。

Mを救うことはできるのでしょうか? もし、クソコードをすばやく見つけることができたら救えるのであれば、救える見込みはあるかもしれません。

コードの問題を見つける静的解析ツール

クソコードとは、おおむね次のような問題のあるコードをさすようです。

潜在的バグ
バグの可能性があるコード。
重複
機能追加やバグ修正を困難にしがちなコードの重複。
設計上の問題
クラスやパッケージ間の依存関係、多すぎるメソッド引数など。
慣習違反
プログラミング言語やライブラリの慣習、コーディング規約などに違反したコード。

多かれ少なかれ良識と技能のあるプログラマが、こういった問題のあるコードを渡されたとき、怒りを込めてクソコードと言ってしまうのでしょう。

そのようなソースコードに含まれる問題をすばやく見つけてくれるツールが、いくつかあります。ソースコードなどから作られたプログラムは動かさずに、ソースコード(またはコンパイルして得られたバイトコード)だけを検査することから、静的検査ツールとか静的コード分析ツールなどと呼ばれます。Javaプログラミングで代表的なものをあげると、

Checkstyle
コーディングスタイルからの逸脱を中心に検査します。多様なIDEやエディタで使えます。
FindBugs
既知のバグのパターンにもとづいて潜在的バグを検査します。多様なIDEAやエディタ、JenkinsなどのCIツールで使えます。
Intellij IDEAのCode Quality Features
Intellij IDEA専用ですが、非常に強力なので取り上げます。

静的解析ツール vs 実世界のクソコード

静的解析ツールは、果たして、どれくらいMを救うことができるのでしょうか?

広すぎる変数のスコープ

そのとおりですね。Javaプログラミングでは、変数は、使う直前で&必要なスコープで宣言するのが良いです。主な理由は、

  • 変数の宣言位置と使われる位置が近づくことで、コードが読みやすくなる。
  • 変数が使われるブロックを抜けたら、その変数を意識しなくてよくなる。
  • もし変数が宣言されるブロックに到達しなければ、その変数を宣言するコードは実行されくなるので、実行速度が向上する。

メソッドの冒頭で変数を宣言してしまう(スコープを広くしすぎる)のは、変数のブロックスコープがない言語をやっていた人がJava言語をやると、よくやってしまうようです。せいぜい数行~十数行であれば気にならないかもしれませんが、数十行~数百行もあるメソッドで、十数個もの変数にこれをやられると、どの変数がどこで使われるのか、わけが分からなくなります。

さて、この問題は、静的検査ツールでは検知してくれるのでしょうか?

IntelliJ IDEA 13.0.1 Checkstyle 5.7 FindBugs 2.0.3 with fb-contrib plugin 5.0

Data flow issues > Scope of variable is too broad
データフローの問題 > 変数のスコープが広すぎる。

なし なし

IDEAでのみ、検知する設定を見つけることができました(見つけられていないだけで、他の2つにもあるかもしれません。以下同様)。

またIDEAでは、Quick Fixという修正までやってくれる機能もあります。これを使えば、修正も非常に容易です。

無意味な初期化

これもありがちですね。最初に作られたFooインスタンスは、初期化のための計算リソースとメモリを消費だけして消えていきます。まったくムダです。

また次のようなバリエーションもあります。

String foo = null;
foo = "foo";

int bar;
bar = 1;

このようなコードは、どうやら、変数の宣言と代入を分けないといけない言語をやっていた人が、よく書いてしまうようです。宣言と代入を同時にできるJavaでは、基本的には、同時にやるべきです。

これも、IDEAなら検知することができます。

IntelliJ IDEA 13.0.1 Checkstyle 5.7 FindBugs 2.0.3 with fb-contrib plugin 5.0

Probable bugs > Unused assignment
潜在バグ > 使われていない代入

なし なし

メソッドの命名が慣習に違反している

これには、いくつもの問題が混在しています。こういう、どこをとっても問題だらけというのも、クソコードの特徴のひとつかもしれません。

まず1つ目の問題は、エラーかどうかの判定に結果コードを返していることです。基本的にJavaでは、結果コードでエラーかどうかを判定するのはあまり望ましくありません。代わりに、Javaが持つ例外機構を使います。

Javaの例外の仕組みをよく知らない人が設計すると、結果コードを使ってしまうのかもしれません。

次のようなケースであれば、結果コード的なものはOKかもしれません。

  • HTTPレスポンスのステータスコードのように、外部に既にあるコードを使う場合。
  • Set#add(Object)が返すboolean(追加した要素が含まれていなければtrue、既に含まれていたらfalse)のように、どちらが成功でエラーかという区別をしない場合。

このクソコードには、もうひとつ「命名が慣習に違反している」という問題があります。

Javaでは普通、booleanを返すメソッドはisXXX、canXXX、hasXXX、containsXXXなどというように、疑問詞(Yes/Noで答えられる動詞や助動詞)のように命名します。逆に、booleanメソッドでないメソッドでそのような名前をつけるのは、混乱を招くため、避けるべきです。

ご覧のとおり、isErrというメソッド名でありながら、文字列で結果コード(おそらく"0"が成功で、"1"や"2"などがエラー)を返しているようです。紛らわしいので、すべきではありません。さらに細かいことを言うと、Javaではクラスやメソッドやパッケージの名前などでは、Errなどと略さずにErrorなどど命名すること慣習です。また、結果コードが1などと数値でありながら、文字列を使っているのも問題です。

なお万が一の可能性ですが、isErrメソッドがbooleanのラッパーオブジェクトであるBooleanインスタンスを返しており(それゆえequalsメソッドを持ち)、equalsでStringの"1"と比較してしまっている可能性もゼロではありません。もしそうだとしたら、さらにクソです。

まず、あるメソッドが結果コードを返すように作られているかどうかは、残念ながら、静的検査ツールで見つけだすことは難しいようです。

もうひとつの問題、メソッドの名前がおかしいかどうかであれば、検査することはできます。

IntelliJ IDEA 13.0.1 Checkstyle 5.7 FindBugs 2.0.3 with fb-contrib plugin 5.0

Naming convention > Boolean method name must start with question word
命名慣習 > booleanメソッドの名前は、疑問詞で始まらなければならない。

Naming convention > Non-Boolean method name must not start with question word
命名慣習 > booleanメソッドでないメソッドの名前は、疑問詞で始まってはならない。

なし なし

IDEAの標準では、次の動詞または助動詞が疑問詞としてデフォルトで登録されています。

  • is
  • can
  • has
  • should
  • could
  • will
  • shall
  • check
  • contains
  • equals
  • add
  • put
  • remove
  • startsWith
  • endsWith

広すぎる例外

おそらく、次のようなコードをいっているのでしょう。

public void foo() throws Exception {
    // いろんな検査例外が投げられる
}

このクソコードの問題は、メソッドが一体どのような例外を投げる可能性があるのか、具体的なことが分からなくなってしまうことです。各例外ごとにcatch節を分け、それぞれに適した例外処理をすることが難しくなります。

次のように、投げられる具体的な検査例外をひとつひとつ明示するよう書きなおすべきです。

public void foo() throws IOException, SQLException {
    // 省略
}

実際に投げられる例外とthrows句を比較するので、このような問題は静的検査ツールの得意とするところです。

IntelliJ IDEA 13.0.1 Checkstyle 5.7 FindBugs 2.0.3 with fb-contrib plugin 5.0

Error Handling > Overly broad 'throws' clause
例外処理 > あまりに広いthrows句

Coding Problems > Illegal Throws
コーディングの問題 > 不正な例外スロー

なし

広い例外が許されるときもあるでしょう。例えば、インターフェースや抽象クラスのメソッドを定義する際、何らかの例外を投げる可能性がある(が実装するまで分からない)ときには、throws Exceptionとしても許されることはあるでしょう。java.util.concurrent.Callableクラスのcallメソッドが、その例です。

ただ、ライブラリやフレームワークではそういうこともあるでしょうが、普通のアプリケーションでは、よくないと思います。

例外の無視

殴るのはともかく、非常に困ったコードですね。発生した例外を捨て、新しい例外を投げています。これだと、キャッチされた例外が持っていたスタックトレースが失われてしまい、例外の分析が困難になります。絶対にやってはいけません。

「キャッチした例外を無視してはいけない」とはよく言われますが、これを「catch節が空ではいけない」と解釈し、「MyExceptionを投げているからOKだろう」と思っているのかもしれません。

} catch (Exception e) {
    // do nothing
}
IntelliJ IDEA 13.0.1 Checkstyle 5.7 FindBugs 2.0.3 with fb-contrib plugin 5.0

Error Handling > 'throw' inside 'catch' block which ignores the caught exception
例外処理 > catch内のthrowが、捕まえた例外を無視している

なし

fb-contrib > LostExceptionStackTrace

例外クラスを定義するときは、次のように、発生元となった例外を引数にするようなコンストラクタを定義するとよいでしょう。

public class MyException extends RuntimeException {
    public MyException(String message, Exception cause) {
        super(message, e);
    }

    public MyException(Exception cause) {
        super(cause);
    }

    public MyException(String message) {
        super(message);
    }

    public MyException() {
        super("default message");
    }
}

課題

残念ながら、静的解析ツールといえど、万能ではありません。

実際、Mが出くわしたいくつかのクソコードは、残念ながら紹介したツールでは、いまはまだ検知できないようです。ツールのプラグインを書くことでできるようになるかもしれませんので、将来の課題としておきましょう。

一応、どういったものがあるか見ておきましょう。

setterメソッドがセット以上のことをしている

おそらく次のように、setterメソッドが値を返しているのでしょう。

public String setFoo(String arg) {
    this.someField = arg;
    return "foo" + arg;
}

JavaBeansの仕様1.01では、setterメソッドの戻り値はvoidとすることが標準とされています。慣習に違反しているので、値を返すことをやめるか、メソッド名をもっと適切な名前に変えるべきでしょう。

さすがに、次のような、setterメソッドですらない、なんてことは……ないですよね?

public String setFoo() {
    return "foo" + someField;
}

変数名が変数の型にふさわしくない

止めて(辞めて?)欲しいですね。

isEditableという変数名からは、普通、その変数の型がbooleanであると期待します。ところが実際はStringです。そうかと思うと、文字列として"true"を渡しています。一体、何がしたいのでしょうか…? ひょっとすると、booleanを知らないでプログラミングしているのかもしれません。Codeacademyなどで勉強するといいのではないでしょうか。

残念ながら、変数名から予想される変数の型が、実際の変数の型と合わないことを検知するルールは、見つかりませんでした。

その代わりに、変数に入れる値を次のようにメソッドにしている場合には、最初の方で紹介した「メソッドの命名が慣習に違反している」ことをチェックするルールを使うことができそうです。

private void caller() {
    String editable = isEditable();
    System.out.println(editable);
}

// メソッド名からbooleanを返すべきだが返していないことをチェックできる
private String isEditable() {
    if (someCondition()) {
        return "true";
    } else {
        return "false";
    }
}

まとめ

Intellij IDEAを使えば、Mが出くわしたような、実に様々なタイプのクソコードを検知できそうです。さらに、ここで紹介したチェックルールは、IDEAが提供するもののごく一部に過ぎません。実に多様なクソコードを検知するルールが用意されれています。また、各ルールは、様々なオプションで細かく制御することができます。

CheckstylesやFindBugsも、IDEAほどには強力ではないですが、なかなか捨てたものはありません。ここで紹介していないルールが役立つこともあることでしょう。それに、FindBugsは自分でルールを追加できるようですから、自分が出くわしたバグパターンやクソコードをプラグイン化として作ってみることで、より一層使いやすいものになることでしょう。

ということで、Mくん、職場のコードレビューにはIntellij IDEAを使ってみたら、捗るんじゃないでしょうかね! え、転職するって?

2013/12/24

Collectorを征す者はStream APIを征す(部分的に)

cero_tさんが書かれたラムダ禁止について本気出して考えてみた - 9つのパターンで見るStream API は、とても良い記事です。Stream APIで実際に書いてしまいそうな、従来のfor/if文より読みにくいコード、予期せぬ不具合を起こしうるコードを例示し、いくつかの教訓を導き出しています。

何より、社内で「ラムダ式禁止、Stream API禁止」という悲しいことにならないように手を打とう! ラムダ式やStream APIの使い方やうれしさ、そして危ういところを社内の勉強会などで広げていこう! という、前向きな主張がすばらしいです。私も見習っていこうと思います。

さて本記事では、cero_tさんが挙げられている8の教訓のうち、次の2点(カッコ内は私の補足です)について、少し異論を述べたいと思います。

  • (4) collectをしたら一度ローカル変数に入れよう!(collectで作られたオブジェクトを、説明的変数に代入する)
  • (6) streamからのstreamとか、ネストしたstreamとかは避けよう!(Streamで使うCollectorなどで新たなStreamを作ることは避けよ。代わりにfor文などを用いよ)

これら教訓には一理あると思いますが、ちょっと引っかかるところがありました。

cero_tさんは、この2つの教訓を導いたサンプルコードを「禁止度A 業務では使わない方が良いレベル」としています。しかし、私の目には、サンプルコードにはまだまだ改善の余地があるように思えました。

そこで「こうすれば、より良くStream APIを使えるのでは?」という改善できる点を示した上で、「使うべきか、使わないべきか」を論じてみようと思います。

前置き

この記事を書くにあたっては、次の環境でコードのコンパイル&実行をしました。

  • Intellij IDEA 13.0.1 Ultimate
  • JDK8b120 for Windows 64 bit

使ったJDK8はベータ版です。というのも、Stream APIが導入されるJava SE 8(JDK8)は、まだ正式リリースされていないのです(正式リリースは2014年3月予定)。ひょっとすると、ベータ版の何らかのバグでたまたま動いている(あるいは動いていない)ことがあるかもしれません。もし見つかれば、教えていただけると幸いです。

なおcero_tさんのサンプルコードを引用するにあたって、意味が変わらない範囲で一部改変してます。主な改変内容は、

  1. 型推論を助けるために型引数を補ったり、
  2. 何らかのオブジェクトを説明用変数に抽出したり(これはcero_tさんの推奨する教訓にもありますね)
  3. メソッド参照を活用するため、Empクラスのフィールドをgetterメソッドでカプセル化したり
  4. Collectorsクラスが持つstaticメソッドを、staticインポートを使って短く書き直し

です。改変した理由は、最初の2つが当てはまるのですが、元のコードが当方の環境ではコンパイルできなかったためです。深くは追及していないのですが、JDK8で強化されるターゲット型推論のバグなのか限界なのか、あるいは筆者のコードのジェネリクス周りがダメダメなのか……。ラムダ式の型が、思ったように型推論されないことがありました。ターゲット型推論やジェネリクスに詳しい人に見ていただけると、うれしいのですが |д゚)チラッ

またユーティリティコードやそのテストコードは、GitHubレポジトリ exoego/StreamUtilities で公開しています。まだJDK8のお試し程度のものです。つたないですが、Stream APIのflatMapやcollectを使うことで何ができそうかについて、ちょっとしたサンプルになったかなぁ、と思います。

Collectorsのメソッドを組み合わせ、ニーズにあったCollectorを作ろう part1

cero_tさんが、最初に「禁止度A 業務では使わない方が良いレベル」として上げたコードを見てみましょう。"部署(dept)を、人数の少ない順に並べるという処理"です。

Collectorsクラスのstaticメソッド(groupingBy、counting、toList)が組み合わさった、collectの使い方の好例です。なおこれ以降、staticメソッドのクラス名修飾(Collectors.)は、読み流しやすくするため、staticインポートによって省略していきます。

List<Dept> sortDept1(List<Emp> list) {
    return list.stream()
               .collect(Collectors.groupingBy(Emp::dept, Collectors.counting()))
               .entrySet()
               .stream()
               .sorted(Comparator.comparingLong(Entry::getValue))
               .map(e -> e.getKey())
               .collect(Collectors.toList());
}

groupingByが作るCollectorによって、何らかのグループ別に(今回は部署)、複数の要素を集約(今回は個数)した結果のMapを、さらにstream処理したい……。とてもありそうな例です。

cero_tさんも指摘するように、Map<Dept,Long>からStream<Entry<Dept,Long>>を作り出す ".entrySet().stream()"が2行も必要で、ノイズになってしまっています。読みやすさのために、どの行も.を揃えて改行しているため、「.entrySet().stream()のところだけ改行しなければ1行に収まる」という問題ではありません。

cero_tさんは、そこで "collectした後にローカル変数に代入すれば、まだしも読みやすくなるのでは" と、次の改善を示しています。

List<Dept> sortDept2(List<Emp> list) {
    Map<Dept, Long> map = list.stream().collect(groupingBy(emp -> emp.dept(), counting()));

    return map.entrySet()
              .stream()
              .sorted(Comparator.comparingLong(Entry::getValue))
              .map(e -> e.getKey())
              .collect(toList());
}

変数名をより説明的な名前にすれば、collectで何をしたかが分かりやすくなりそうです。この例だとmapという変数名が説明不足なのが、ちょっとイマイチですね。

しかし、それでも".entrySet().stream()"のノイズは残ります。どうすればいいのでしょうか?

こんなときには、Collectors.collectingAndThenを使うことができそうです。Collectors.collectingAndThenは、collectして得られた集約結果に「仕上げ処理」をほどこすCollectorを作り出すユーティリティです。例えば、toList()ではArrayListが作られるのですが、これを変更不能のListに変更するというような使い道があります(javadocより改変して引用)。

List<String> people = hoge.stream()
                          .collect(collectingAndThen(toList(), list -> Collections.umodifiableList(list)));

そんなcollectingAndThenを使うことで、次のように書き直せるでしょう。

List<Dept> sortDept3(List<Emp> list) {
    Collector<Emp, ?, Stream<Entry<Dept, Long>>> groupingThenStream =  
            collectingAndThen(groupingBy(Emp::dept, counting()), m -> m.entrySet().stream());
    return list.stream()
               .collect(groupingThenStream)
               .sorted(comparingLong(e -> e.getValue()))
               .map(e -> e.getKey())
               .collect(toList());
}

あれ? entrySet().stream()の位置が変わっただけで、やってることはまったく変わりませんね。むしろ、collectingAndThenを使うことで、さらにコードが増え、しかもgroupingThenStreamという変数まで使ってしまいました(変数に入れないと、型推論が思ったように動作してくれなかったので、やむを得ず)。collectingAndThenは必要なかったのでしょうか?

やっと、ここからが本題です。

"groupingByしたあと、さらにstream処理"は、とてもよくありそうな例です。そんなよくありそうな処理を、今回のように毎回書いていては面倒です。

そのためのCollectorを作るユーティリティメソッドを作ってしまいましょう。名前は、例えば、toGroupedEntriesなんてのは,どうでしょうか。

Collectorsが提供するcollectingAndThenを使うことで、次のようにサクッと書けてしまいました。

private static <T, K, A, V, M extends Map<K, V>> Collector<T, ?, Stream<Entry<K, V>>> toGroupedEntries(
        final Function<? super T, ? extends K> keyMapper,
        final Collector<? super T, A, V> downstream,
        final Supplier<M> mapFactory) {
    return Collectors.collectingAndThen(Collectors.groupingBy(keyMapper, mapFactory, downstream),
                                        map -> map.entrySet().stream());
}

public static <T, K, A, V> Collector<T, ?, Stream<Entry<K, V>>> toGroupedEntries(
        final Function<T, ? extends K> keyMapper, final Collector<T, A, V> downstream) {
    return toGroupedEntries(keyMapper, downstream, HashMap::new);
}

これを使うことで、次のように書き直すことができます。

List<Dept> sortDept4(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept, counting()))
               .sorted(comparingLong(Entry::getValue))
               .map(e -> e.getKey())
               .collect(toList());
}

いかがでしょうか? toGroupedEntriesという名前の良し悪しはともかく、「グループ化した結果から、EntryのStreamを作る」という意図がより明瞭になったと思います。Mapを入れる説明用変数も不要になり、コードもだいぶ短くできそうです。また今度は型推論も思うように動いてくれたので、Collectorを変数に入れる必要はなくなりました。

これくらい分かりやすくなれば、「禁止度B やや疑問を呈されるけど、積極的に使いたいレベル」まで下げられるのではないでしょうか?

「これはよくありそう」だけど「ありもの(標準ライブラリ)のクラスやメソッドじゃ足りない…」そんなときは、抽象化し、便利な道具を作るチャンスです。

せっかくJavaにもラムダ式が導入され、関数オブジェクト的なものの利用が用意になったのですから、Functionなどを注入して挙動を柔軟に制御するような道具をじゃんじゃん作っていきたいですね。

そして、標準ライブラリのクラスCollectorsなどには、そういった道具を作る材料となる、基本的なメソッドが揃っていると思います。ぜひ、JDK8をダウンロードし、Collectorsクラスのソースコードやjavadocをのぞいてみてください。

Collectorsのメソッドを組み合わせ、ニーズにあったCollectorを作ろう part2

先ほどの例には、まだ改善できるところがあります。

List<Dept> sortDept4(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept, counting()))
               .sorted(comparingLong(Entry::getValue))
               .map(e -> e.getKey())
               .collect(toList());
}

この ".map(e -> e.getKey()).collect(toList())" という「要素に何らかの変換をかましてListに集約」というのは、よくありそうです。もちろん、このままでもまったく問題はないのですが、ちょっと工夫をしておきましょう。

「変換してから集約」というニーズのために、Collectors.mappingというメソッドが用意されています。次のように使えます。

List<Dept> sortDept5(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept, counting()))
               .sorted(comparingLong(Entry::getValue))
               .collect(mapping(Entry::getKey, toList()));
}

このmapping(mapper, toList())も、よく使いそうなパターンです。より使いやすく、メソッド化してしまいましょう。名前は安直ですが、toListMappedとしておきましょう。実装は、GitHubレポジトリをご覧ください。

List<Dept> sortDept5(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept, counting()))
               .sorted(comparingLong(Entry::getValue))
               .collect(toListMapped(Entry::getKey));
}

これくらいだと、あえてメソッド化するほどではないかもしれません。

が、このtoListMapped、groupingByなどと組み合わせることで、本領を発揮するのです!

例えば、次のような例を考えてみましょう。空白文字をトリムしてから文字数でグループ化するか、文字数でグループ化したあとにトリムするか……。すなわち、mapしてからcollectするか、collect中にmapするかです。

@Test
public void use_case_difference_between_ToListMapped_and_ToList() {
    List<String> src = asList("a   @  on@ cat @tri @by".split("@"));

    Map<Integer, List<String>> mapThenCollect = src.stream()
                                                   .map(s -> s.trim())
                                                   .collect(groupingBy(s -> s.length(), toList()));
    assertThat(mapThenCollect.get(1), is(asList("a")));
    assertThat(mapThenCollect.get(2), is(asList("on", "by")));
    assertThat(mapThenCollect.get(3), is(asList("cat", "tri")));

    Map<Integer, List<String>> mapWhileCollecting = src.stream()
                                                       .collect(groupingBy(s -> s.length(),
                                                                           toListMapped(s -> s.trim())));
    assertThat(mapWhileCollecting.get(2), is(asList("by")));
    assertThat(mapWhileCollecting.get(3), is(nullValue()));
    assertThat(mapWhileCollecting.get(4), is(asList("a", "on", "tri")));
    assertThat(mapWhileCollecting.get(5), is(asList("cat")));
}

あまり実用的な例ではなかったかもしれませんが、こういうcollect中にmapしたときって、たまに出くわすことでしょう。

ところがCollectors.mappingは、標準ライブラリということで汎用的に作られているので、ちょっとクドくなります。こんなときにtoListMappedのようなメソッドを作っておくと、地味に便利です。気の利いたIDEなら、toListまで入力すれば、toListMappedを候補に出してくれて、staticインポートまで追加してくれちゃいますからね!

Streamの入れ子(ネスト)を書きやすく part1

次にcero_tさんが、「禁止度A 業務では使わない方が良いレベル」に上げたのは、Streamの入れ子です。入れ子の例として「Collectorで行う集計ロジックの中で、データをフィルタするために、集計結果をstreamを使う」というケースを挙げられています。

まず、元のコードを見てみましょう(型推論が思うように動かずコンパイルできなかったため、Collectorを変数に入れることで解決しています)。

Map<Dept, Long> groupByDeptAndFilter1(List<Emp> list) {
    Collector<Emp, ?, Long> countHighSalary = collectingAndThen(toList(),
                                                                emps -> emps.stream()
                                                                            .filter(e -> e.salary() > 1000)
                                                                            .count());
    return list.stream().collect(groupingBy(Emp::dept, countHighSalary));
}

筆者は、これはけっこう読みやすい方だと思います。Streamを入れ子にすること自体は、一概に「可読性が低い」と言えないと思います。

むしろ問題なのは、「1度Listに集約したのに、Streamにする必要が出てきた」でも「.stream()をかますのが冗長」です。これって、先ほどのmap.entrySet().stream()と同じですね。

そこで、Streamに集約するtoStream()を用意してしまいましょう。なぜ標準ライブラリにtoStreamがないかは、不思議です。ニーズがありそうなものですが……。

Map<Dept, Long> groupByDeptAndFilter1_2(List<Emp> list) {
    Collector<Emp, ?, Long> countHighSalary = collectingAndThen(toStream(),
                                                                emps -> emps.filter(e -> e.salary() > 1000)
                                                                            .count());
    return list.stream().collect(groupingBy(Emp::dept, countHighSalary));}

これで、empsがListではなくStreamになったので、すぐさまフィルタなどのStreamのメソッドを活用できるようになりました。

このように「収集されたStreamを立て続けに操作」というのは、これまた非常によくありそうです。ですので、collectAndThen(toStream(), finisher)を、メソッドとして抽出してみましょう。名前は、またまた安直ですが、toStreamThenとしておきましょう。

Map<Dept, Long> groupByDeptAndFilter1_3(List<Emp> list) {
    return list.stream()
               .collect(groupingBy(Emp::dept,
                                   toStreamThen((Stream<Emp> emps) -> emps.filter(e -> e.salary() > 1000)
                                                                          .count())));
}

Emp::deptでグループ化し、グループ別にStreamに集約してフィルタしたた件数、というのがわかりやすくなったと思います。型推論も思ったとおりに動くようになり、一時変数も不要になりました。

これくらい意図が明確になって読みやすくなれば、CollectorなどでのStreamの入れ子は許容してもよいと思います。

Streamの入れ子(ネスト)を書きやすく part2 別のアプローチ

それからcero_tさんは、Collectors.collectingAndThenを使わないアプローチで、次のような例も示しています。結果は同じになるのですが、だいぶ複雑になっています。

Map<Dept, Long> groupByDeptAndFilter2(List<Emp> list) {
    return list.stream()
               .collect(groupingBy(Emp::dept))
               .entrySet()
               .stream()
               .collect(toMap(entry -> entry.getKey(),
                              entry -> entry.getValue()
                                            .stream()
                                            .filter(emp -> emp.salary() > 1000)
                                            .count()));
}

なぜcero_tさんがこんな例を示されたのかは分かりません。たしかに、collectingAndThenを知らないとなかなか最初の例のようには書けませんが、コードレビューで「collectingAndThenを使って」と指摘し、最初のように書きなおさせればいいことのように思います。

ともかく、あえてこの読みにくい、長ったらしいコードを改善してみましょう。

まず「groupingByして、結果のMapからEntryをStream」です。これには、先程作ったユーティリティtoGroupedEntriesを使えますね。

Map<Dept, Long> groupByDeptAndFilter2_2(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept, toList()))
               .collect(toMap(entry -> entry.getKey(),
                              entry -> entry.getValue()
                                            .stream()
                                            .filter(emp -> emp.salary() > 1000)
                                            .count()));
}

toMapの中で、toListで集約されたリストをStreamにしてフィルタ処理&計数しています。ここには、先ほど作ったユーティリティtoStreamThenが使ってみましょう。

Map<Dept, Long> groupByDeptAndFilter2_3(List<Emp> list) {
    return list.stream()
               .collect(toGroupedEntries(Emp::dept,
                                         toStreamThen((Stream<Emp> emps) -> emps.filter(emp -> emp.salary() > 1000)
                                                                                .count())))
               .collect(toMap(entry -> entry.getKey(), entry -> entry.getValue()));
}

Mapにグループ化する方法(グループ分けの条件を決めるEmp::dept、どのようにグループ化するかを決めるtoStreamThen)が1箇所に集まり、分かりやすくなりました。また、toMapの中がだいぶシンプルになりました。

さて、先に示した実装のとおり、toGroupedEntriesは、groupingByによって得られたMap<Dept, Long>をStream<Entry<Dept,Long>>に変換するだけのシンプルなCollectorを作っているだけです。ということは、Map→Stream→Mapと、箱を移し替えて、また戻しているようなムダをしてしまっています。

ということで、toGroupedEntriesの代わりに、groupingByを使って書き直しましょう。

Map<Dept, Long> groupByDeptAndFilter2_4(List<Emp> list) {
    return list.stream()
               .collect(groupingBy(Emp::dept,
                                   toStreamThen((Stream<Emp> emps) -> emps.filter(emp -> emp.salary() > 1000)
                                                                          .count())));
}

最初のアプローチで示した改善結果と同じものになりました。

まとめ

cero_tさんが提言された問題と教訓について、Collectorを積極的に作っていくという別なアプローチで改善案を示してまいりました。

  • 適切なCollectorを作ることで、collectをより読みやすく、便利にしよう!
  • Collectorをかんたんに組み上げるための様々なユーティリティが、Collectorsには揃っている!
  • 今回作ってみたtoGroupedEntries、toStreamThenは、わりと便利!
  • Collectorsの使用例やtoGroupedEntriesは、exoego/StreamUtilities を見てね!

蛇足

よくStream APIとの比較に出されるC#のLINQでは、ずばり、GroupByメソッドというのがあります。今回示したtoListMappedやtoStreamThenのように、変換しながら集約したり、集約結果をいじるためのオーバーロードが、GroupByには用意されています。

ところが、JavaのCollectors.groupingByには、そのようなオーバーロードはありません。その代わりに、mappingやcollectingAndThenなどを組わせることで、応用ができます。

Javaの標準ライブラリって、「原始的・汎用的・基礎的なものだけ提供する、あとは組み合わせて使えよ」という精神なのかなぁ。