ページ

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で質問があったので、追加です。
前回の記事の中で次のように書いています。

https://ckip.worksap.co.jp/aqua/a46fa1c4-701d-4279-af87-3ba10eedf94b/view?exa=blog

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

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

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

Javaのforループでは2つの値をとることができませんが、自前でイテレータを書くことができます。イテレータを自分で書いた時のお話はこれ。
https://ckip.worksap.co.jp/aqua/8c23f0a7-585d-46ce-ac9e-01313b73d644/view?exa=blog

拡張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は池添さんが書いてくれるはず。