ページ

2014年4月1日

拡張forループでループの回数も同時に欲しいよね、でこんなふうに書いた

昨日のアリエルであったJava8の勉強会で、iteratorをそのままstreamに流せないと初めて知って、いささかめんどくさい感があったりもしますが、世の中、そんなものです。あったかくなってきたとか、朝が夜ではなく朝になってきたとか、桜が咲いたとか、浮かれている場合ではありません。

前回、iterator自分で書いてみない?ってことで、だれも書いてくれなくってとても残念です。なので、自分で書いてみました。

import java.util.*;

public class CollectionUtil {
    public static class Entry<T> {
         int index;
         T value;
         Entry(int index, T value) {
             this.index = index;
             this.value = value;
         }

         public int getIndex() { return index; }
         public T getValue() { return value; }

    }

    public static <T> Iterable<Entry<T>> enumurate(Iterable<T> iterable) {
        final Iterator<T> iter = iterable.iterator();
        return new Iterable<Entry<T>>() {
            public Iterator<Entry<T>> iterator() {
                return new Iterator<Entry<T>>() {
                    int index = 0;
                    public boolean hasNext() {
                        return iter.hasNext();
                    }
           
                    public void remove() {
                        iter.remove();
                    }

                    public Entry<T> next() {
                        return new  Entry<T>(index++, iter.next());
                    }
                };
            }
        };
    }


    public static void main(String[] argv) {
        List<String> l = new ArrayList<String>();
        l.add("hoge");
        l.add("fuga");
        for (Entry<String> entry: CollectionUtil.<String>enumurate(l)) {
             int index = entry.getIndex();
             String v = entry.getValue();
             System.out.println(index + " " + v);
        }
    }
}

色がついていないと、ちょっと読みにくいですね。IDEにでも貼り付けて、綺麗にしてください。

で、僕はPythonista(Pythonを使う人)だったらしいので、enumurateってメソッドを作ってあげて、その人がiterableのオブジェクトを返すことにしてあげました。やっていることは基本的には同じです。なんとかクラスを実装してください、って課題なのに、設問をいきなり無視しています。年寄りはそんなものです。我慢してください。

この手のメソッドを作るときによくやるのが、引数とかで本当の具象クラスを指定することはさすがにないと思いますが、ListやCollectionを指定しちゃうことが多いです。enumurateの中身は引数からiteratorが取れればいいだけなので、Itarableインターフェースを指定します。曖昧にできるところはなるべく曖昧にしておいたほうが、メソッドがいろいろ使いまわせてよいのです。



無名クラス、いっぱい。なぜ、無名クラスにしたのかって言うと、クラス名を何にするか決められなったからです。

Iteratorは、Javaに限らず一般的なプログラミングの概念です。気になる人は、パーフェクトJavaの181ページを読んでください。あれ?無名クラスについては書いていない気がする。井上さん、追加してください。それから、マルチスレッド的なことも、Concurrentを追加してください。その代わりにGUIの部分はなくてもいいです。
それから、enumはやれば出来る子なので、もっと詳しくして欲しいです。

DaoでのResultSetへのアクセスはenumでいいよね

Java8でラムダが使えれば…。
で、DaoでResultSetへのアクセスでインデックスでアクセスする場合、

int index=0;

dto.setX(resultSet.getInt(++index);
dto.setY(resultSet.getInt(++index);
dto.setZ(resultSet.getInt(++index);

ってアクセスさせるとメンテナンス性が落ちるよね、って話しでした。個人的にはカラムでのアクセスで問題ないと思っていますが、インデックスでのアクセスもenumでアクセスさせれば、もう少し、メンテナンス性が上がります。

こんなかんじですね。

enum ColumnName  {
    column1,
    column2,
    column3;

    public int getIndex() {
        return ordinal() + 1;
    }

    public static String getSelectStatement() {
        StringBuilder sb = new StringBuilder();
        for (String s: ColumnName.values()) {
            if (sb.length()>0) {
                 sb.append(",")
             }
             sb.append(s):
          }
     }
}


SQL文を組み立てるときは


    String sql = "SELECT " + ColumnName.getSelectStatetement() + " FROM SOME_TABLE"

ResultSetは、

    dto.setX(rs.getInt(ColumnName.column1));
    dto.setY(rs.getInt(ColumnName.column2));
    dto.setZ(rs.getInt(ColumnName.column3));

ほら。フィールドが追加されてもenumに値をセットするだけで、順番も意識しなくてよくなりました。

まあ、enumに値を追加することに神経を集中してdtoに詰めるのを忘れちゃいそうですね。

で、これがもう少しメンテナンス性は挙げられるようになる、と書いていた理由です。
ということで、技術は楽をするための道具です。言語に新しく追加されたものは、世界中の頭のいい人が、もっと楽をしたいと思って追加されるものです。なので、マゾな人以外は積極的に新しいものを使って、楽をできるところは楽をして、本質的に難しいところに開発者は専念すべきなのです。そのために、新しい技術を追い求めるのです。いやいや、それは嘘です。僕は新しいものが好きだから、追い求めるだけです。


それから、そのうち、リフレクションしたくなってくるかもしれません。でも、それすると、メンテナンス性が更に高まりますが、パフォーマンスは少し落ちます。我慢してください。その次がバイトコード生成ですが、そこまで行くと、もう、あちらの世界の住人になって、もう、戻ってこれません。気をつけてください。

追記
======

最近は朝、家を出る時が夜ではなく朝になってきて、ちょっと嬉しいです。

ということで、昨日の続き。昨日のように慌ただしいタイミングじゃないのでゆっくりと書けます。

まず、前提としてenumはやれば出来る子です。どこかでenumは所詮はクラスだよ、と書いたつもりですが、所詮はクラスなので、クラスでできることは大体出来ます。Effective Javaにはシングルトンもenumでできるよ、って書いていたように思いますが、僕は実際にやったことはないです。

さて、所詮はクラス、されどクラスなので、enumもinterfaceを身にまとってかっこ良く振る舞ってくれます。
例えばこんな感じ。

interface Ge {
    public void urya();
}

enum Ho implements Ho {
    H {
        public void urya() { System.out.println("Hoo"); }
    },
    G {
        public void urya() { System.out.println("Gee"); }
    }
}

で、 Ho.H.urya()って感じで使えます。なので、

for (Ho h: Ho.values) {
    h.urya();
}

で全部回りますね。


じゃあ、本題です。Daoでdtoにブツを詰め込むときに、場所が離れていると詰めるのを忘れがちになります。頑張ればHibernateがやっているようにもっとスマートにできなくもないですが、それならHibernate使えよ、ということになってしまいます。なので、現実的な妥協点は次のような感じ。
変数名とかメソッド名は割と適当なので、適当に置き換えてください。。


interface Binder {
    public void bind(int position, ResultSet rs, Dto dto);
}

enum Column implements Binder {
    FIELD_A {
        public void bind(int position, ResultSet rs, Dto dto) {
            dto.setField_A(rs.getInt(position + 1);
        }
     },
     FIELD_B {
        public void bind(int position, ResultSet rs, Dto dto) {
            dto.setField_B(rs.getString(position + 1);
        }
     };


     public String listFields() {
         StringBuilder sb = new StringBuilder();
         for (Column c: Column.values()) {
             if (sb.length() > 0) {
                 sb.append(",");
              }
              sb.append(c.name());
          }

           return sb.toString();
    }
}


で、もう一度、SQL文を組み立てるときは、

    String sql = "Select " + Column.listFields() + " From Some_table";

Dtoに値を放り込むときは、前回の記事で書いたIndexableIteratorがある前提で

    for (IndxableIterator.Pair<Column> pair: new IndexableIterator<Column>(Column.values())) {
        c.getValue().bind(c.getIndex(), resultSet, dto);
     }

ってやれば関連が強いものが近くに置かれて忘れ難くなった。フィールドが追加されたら、enumのところとdtoに追加すればいい。で、interfaceとenumはDTOの中にInner Classとして宣言すれば、関連性がより強くなって忘れにくい、変更箇所が局所化できますね。
それに、DAOの方はコードの変更が必要なくなるおまけ付き。

Java8のラムダとか、FunctionalInterafaceが入ってくると、各bindメソッドがラムダ式でもっと簡単に書けて、初期化の引数として渡せるので、かなりスッキリするはずです。


ということで、これくらいがJava7までの現実的な妥協点じゃないでしょうか?

最期におさらいをすると、enumはあなたが思っている以上にできる子なのです。

拡張forループでループの回数も同時に欲しいよね。

ちょっとだけ、拡張forで質問があったので、追加です。
前回の記事の中で次のように書いています。



> 拡張forループはいいのですが、今がコレクションの何番目か、ってのが取れたらもっと良かったのに…。

これは言語仕様としてはインデックスの位置が取れないと言うだけです。ちょっと別の言語で、goの場合は次のように書けてインデックスも同時に取得できます。便利ですね。

array := []string {"hoge", "fuga"}
for index, value := range array {
   // do something...
}

Javaのforループでは2つの値をとることができませんが、自前でイテレータを書くことができます


拡張forループで使えるようにするには、Iterableをimplementすればいいだけです。
で、こんな感じで使えるようになれば、まあ目的は達成できますね。

public class IndexableIterator<T> implements Iterable<T> {
   public static class Entry<T> {
        final int index;
        final T value;
        private Entry(int index, T value) {
            this.index = index;
            this.value = value;
        }
        public int getIndex() { return index; }
        public T getValue() { return value; }
    }


   // TODO: implement
}

というような感じで、IndexableIteratorを作って、実際に使う場面では

List<String> l = new ArrayList<String();
l.add("hoge");
l.add("fuga");
for (IndexableIterator.Entry<String> entry: new IndexableIterator<String>(l)) {
    int index = entry.getIndex();
    String v = entry.getValue();
    // do something...
}

みたいな感じですね。あとは、IndexableIteratorのTODOってところを実装すればいいだけです。



で、Java8のStream使って書こうかと思ったら、標準ライブラリからいつの間にかzipメソッドがなくなっているのね・・・。なので、Java8のstreamは池添さんが書いてくれるはず。

Java8のString::join

実際のコードを書きたかったのですが、すぐに思い出せないので、わずかながらの記憶で書きます。java8を使わない人も楽しめるはず。

SQL文を組み立てるときに、時々こんな感じのコードを見かけます。

コード1 ::
StringBuilder sb = new StringBuilder()
boolean isFirst = true;
for (int i=0; i<10; i++) {
    if (!isFirst) {
        sb.append(",");
    }
    sb.append("?");
}

もしくは、

コード2::
sb.append("column1");
sb.append(",");
sb.append("column2");
sb.append(",");
sb.append("column3");

ちょっと例が悪いですが、多分、こんな感じのコードにであったことはあると思います。アリエルのコードの場合、StringUtilsと言うクラスがいて、joinメソッドがあります。public static String StringUitls.join( Collection<String> elems, String delimiter)みたない感じです。

コード1のようなコードは、

String s = StringUtils.join(StringUtils.multiply("?", 10), ",")

って書けます。Javaは演算子のオーバーロードができないので、multipyってメソッドで*相当のことをしています。このへんはPythonの影響を受けているので、そっちを見るのがいいのかも。

コード2は

String s = StringUtils.join(new String[]{"column1", "column2", "column3"}, ",");

みたいな感じです。
アリエルの人たちは、「Stringにjoinがなくてめんどくさいな〜。でも、おれらはStringUtilsがあるからかんけーないね。」って思っていました。

さてさて、みんなが待ちに待ったJava8は華やかなlambdaやstream api、ついでにCalendar API(古いやつがスレッドセーフじゃないってどういうことじゃ!)も仲間に入れてあげるとして、そんな今どきの人に隠れてStringクラスの地味な拡張がjoinメソッドの追加です。アリエルはすぐにjava8に対応するので、これで十数行、全体のコード行が短くなるはず。えー、誤差の範囲です…。

で、Stringクラスにjoinメソッドが追加されて、コード1,2は次のようになります。

String s = String.join("," StringUtils.multiply("?", 10))
String s = String.join(", ", new String[]{"column1", "column2", "column3"});

スッキリ。
さて、Sting::joinはどうやって作っているのでしょうか?ちょっとだけコードを覗きます。3月11日のmercurialのリポジトリのコードです。

    public static String join(CharSequence delimiter,
            Iterable<? extends CharSequence> elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        StringJoiner joiner = new StringJoiner(delimiter);  // ①
        for (CharSequence cs: elements) {
            joiner.add(cs);  // ②
        }
        return joiner.toString(); // ③
    }

StringJoinerという新しいクラスが登場しています(①)。このクラスは、addした文字列(②)をdelemiterで連結してくれるクラスですね。最期に、文字列の出力です(③)。アリエルのStringUtils.joinはStringBuilderで文字列をその場で組み立てていました。ちょうど、コード1を汎用化した感じですね。


StringJoinerはコンストラクタが2つあって、
  StringJoiner(delimiter)
  StringJoiner(delemiter, prefix, suffix)

こんな感じで使えますね

StringJoiner sj = new StringJoiner(", ");
sj.add("column1");
sj.add("column2");
sj.add("column3");
sj.toString();   // column1, column2, column3になる

SQL文の組み立てだと最初と最期に( )が追加したいので、そういう時は、

StringJoiner sj = new StringJoiner(", ", "(", ")");
sj.add("column1");
sj.add("column2");
sj.add("column3");
sj.toString();   // (column1, column2, column3)になる

ほら、べんり。StingJoinerの実装でも、内部的なデータの保持はStringBuilderのオブジェクトを作っていて、その人にappendをしているだけですが、APIとしてはスッキリしていて、クラスの目的も明確ですね。





次のコードはStringJoinerのStringBuilderを取得しているコードです。valueはStringBuilderのクラスフィールドです。コード1だとisFirstというbooleanの値を見て、delemiterを追加するかどうか決めていましたが、value自体をフラグの代わりに使って、こんな風に書くとそういうフラグはいらないんですね。

  private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }

それから、状況によっては、value.length()>0でdelemiterを追加するかどうかを決めるというのもできますね。


「私は、Stream APIとlambdaがあれば十分よ。String::joinなんていらない子よ」って。ごめんなさい。そういう人はそっちを使ってあげてください。

JITはすごいんだよ、とか

前回のリフレクションのベンチマークはJITの影響を受けています。Javaはコンパイル時だけじゃなくって実行時にももっと早くコードを実行できないか考えてくれるすごい子なのです。またはJITがないと使い物にならない速度でしか動かないかもしれません。普通に使用する分には可愛い子なのですが、ベンチマークするにはJITは厄介な子です。実際のソフトウエアとベンチマークでは最適化のされ方が異なるので、速度がかなり違ってきます。

で、前回の結果をJITのあるなしで計測すると次のようになります。

JITあり
=======
$ java timing.TimeCalls
Java version 1.7.0_51
Java HotSpot(TM) 64-Bit Server VM
24.51-b03
Oracle Corporation

Direct call using member field:
 231 7 6 6 6
 average time = 6 ms.
Direct call using passed value:
 8 6 6 6 6
 average time = 6 ms.
Call to object using member field:
 30 6 7 6 7
 average time = 7 ms.
Call to object using passed value:
 12 7 6 6 7
 average time = 7 ms.
Reflection call using member field:
 74 32 30 37 31
 average time = 33 ms.
Reflection call using passed value:
 179 151 145 127 120
 average time = 136 ms.

Direct call using member field:
 7 7 6 3 6
 average time = 6 ms.
Direct call using passed value:
 6 6 6 7 5
 average time = 6 ms.
Call to object using member field:
 6 6 5 7 6
 average time = 6 ms.
Call to object using passed value:
 6 6 6 6 6
 average time = 6 ms.
Reflection call using member field:
 39 31 34 38 28
 average time = 33 ms.
Reflection call using passed value:
 114 117 114 117 112
 average time = 115 ms.

JITなし
=======
$ java -Djava.compiler=none timing.TimeCalls
Java version 1.7.0_51
Java HotSpot(TM) 64-Bit Server VM
24.51-b03
Oracle Corporation

Direct call using member field:
 711 669 665 664 670
 average time = 667 ms.
Direct call using passed value:
 732 732 742 751 740
 average time = 741 ms.
Call to object using member field:
 730 696 675 676 676
 average time = 681 ms.
Call to object using passed value:
 673 686 673 681 677
 average time = 679 ms.
Reflection call using member field:
 4505 4487 4444 4448 4441
 average time = 4455 ms.
Reflection call using passed value:
 7293 7342 7386 7318 7359
 average time = 7351 ms.

Direct call using member field:
 680 666 672 679 667
 average time = 671 ms.
Direct call using passed value:
 742 736 736 744 742
 average time = 740 ms.
Call to object using member field:
 671 673 671 672 670
 average time = 672 ms.
Call to object using passed value:
 681 679 674 670 670
 average time = 673 ms.
Reflection call using member field:
 4453 4563 4521 4593 4565
 average time = 4561 ms.
Reflection call using passed value:
 7453 7307 7323 7348 7415
 average time = 7348 ms.


まあ、JITありに比べてない方は100倍遅いですね。ただし、今回は傾向は同じです。別の種類のベンチマークだと傾向が同じになるとは限りません。今回はループで何回も回していますが、最適化のされ方によってはループが回らなかったりします。

最終的にはDaoでDTOに値を詰めるって話にするつもりで、その場合メソッドの直接の呼び出しだとコストはほとんどかからない、リフレクションだとこれくらい、バイトコードを作るのはリフレクションよりは速そうだ、ってことぐらいがわかればイイかな、と。

本当はhibernateを使うのがいいのですが、これからのhibernateの導入は敷居が高いので、それじゃ似たようなコードのコピペをせずに、daoのコードをどれくらい少なくできるのか?ってことです。


それから、昔10倍遅かったと記憶していましたが、JITなしではちょうど10倍ぐらい遅いですね。記憶はあっていたのかもしれません。



http://www.ibm.com/developerworks/jp/java/library/j-jtp12214/

あれからJavaのリフレクションのスピードはどうなったのか?

僕が一番最近Javaのリフレクションのパフォーマンスを計測したのは、今のアリエルの開発を始めたぐらいで、ライブラリの選定をしている時でした。jdkのバージョンも覚えていません。それより前はいつやったのか覚えていません。
その時の結果の詳細は残っていませんが、リフレクションは通常のアクセスより10倍以上遅い、って印象だけが残っています。印象でものを語るのは良くないし、バージョンが上がるに連れて改善されているのかもしれないので、もう一度計測してみました。

以前は自分でコードを書いていたのですが、今回はサボって http://www.ibm.com/developerworks/jp/java/library/j-dyn0603/ にあるコードで試します。このページには1.4での計測結果ものっていて、いいかも。

ちなみに、今回はLinux上のSunのJDKでコンパイル・ビルド・実行しています。それから、結果に乗っている最初の一回目の数値は無視してください。

まず、リフレクションでオブジェクトを作るときの結果です。

Java version 1.7.0_51
Java HotSpot(TM) 64-Bit Server VM
24.51-b03
Oracle Corporation

Direct Object creation:
 49 1 2 1 2
 average time = 2 ms.
Reflection Object creation:
 44 3 3 2 3
 average time = 3 ms.
Direct byte[8] creation:
 10 7 8 11 10
 average time = 9 ms.
Reflection byte[8] creation:
 12 8 8 8 8
 average time = 8 ms.
Direct byte[64] creation:
 101 79 37 8 7
 average time = 33 ms.
Reflection byte[64] creation:
 7 7 7 8 8
 average time = 8 ms.

Direct Object creation:
 2 2 1 3 1
 average time = 2 ms.
Reflection Object creation:
 2 4 5 4 4
 average time = 4 ms.
Direct byte[8] creation:
 9 6 7 8 8
 average time = 7 ms.
Reflection byte[8] creation:
 8 8 10 9 8
 average time = 9 ms.
Direct byte[64] creation:
 9 8 11 9 7
 average time = 9 ms.
Reflection byte[64] creation:
 9 11 11 10 8
 average time = 10 ms.

オブジェクトの作成は若干リフレクションの方が遅いですが、使用用途にもよりますが、この程度であればリフレクションを使ってもそれほどオーバーヘッドもなく、問題ないでしょう。


次にオブジェクトの中のフィールドへのアクセスです。昔、偉い人が「privateなんて飾りです。リフレクションをシラン人はわからんのです。」って言っていましたが、それがこういうことです。
で、結果ですが、


Java version 1.7.0_51
Java HotSpot(TM) 64-Bit Server VM
24.51-b03
Oracle Corporation

Direct access using member field:
 8 6 6 7 6
 average time = 6 ms.
Reference access to member field:
 28 7 7 5 7
 average time = 7 ms.
Reflection access to member field:
 3544 3486 3437 3433 3482
 average time = 3460 ms.

Direct access using member field:
 7 5 7 6 6
 average time = 6 ms.
Reference access to member field:
 6 6 6 6 6
 average time = 6 ms.
Reflection access to member field:
 3538 3555 3515 3545 3482
 average time = 3524 ms.

で、reference accessはまあ、どうでもいいです。reflection accessだと、10倍どころじゃないですね。600倍ぐらい遅いです。数回だけならまだしも、アクセス数が増えると使い物にならないですね。JRubyとかJPythonは、基本的にメンバー変数に直接アクセスはしないんだろうか?

最後にメソッドの呼び出しです。

Java version 1.7.0_51
Java HotSpot(TM) 64-Bit Server VM
24.51-b03
Oracle Corporation

Direct call using member field:
 8 7 6 6 3
 average time = 6 ms.
Direct call using passed value:
 11 6 3 6 7
 average time = 6 ms.
Call to object using member field:
 10 6 7 7 6
 average time = 7 ms.
Call to object using passed value:
 8 6 5 7 6
 average time = 6 ms.
Reflection call using member field:
 71 32 30 29 33
 average time = 31 ms.
Reflection call using passed value:
 158 139 138 122 117
 average time = 129 ms.

引数なしでの呼び出しは、通常の5倍遅いです。引数ありでは20倍の遅さです。メンバー変数へ直接アクセスすするよりは、メソッドを通してアクセスするほうが効率は良さそうですね。

IBMのサイトの結果と比べると…、IBMのサイトのグラフが対数になっていて、よくわかりません。多分、あんまり変わっていないです。

ということで、

- リフレクションでのオブジェクトの作成はオーバーヘッドがほとんどない
- メンバーフィールドにアクセスするのは遅すぎるのでやらないほうがいい。
- メソッド呼び出しは5から20倍遅いので、使う場合は注意する