トップ «前の日記(2005-03-23) 最新 次の日記(2005-03-25)» 編集

日々の破片

著作一覧

2005-03-24

_ コールバックとしてのインナークラス

Javaで、
find . -name \*.zip -exec echo "{}" \;

相当の処理を実装することを想定する。もちろん一発勝負なら簡単なわけだが、find相当の処理を実装したら、それは使い回しを想定したほうが良い。となると、次のようなクラスにするのが良いかな、となる。

public class Finder {
    public static void find(String root, String pattern, Exec exec);
    public interface Exec {
        /**
         * @return findと同じく0なら継続ということにしてみたり。
         */
        int exec(String currentFileName);
    }
}
あるいは、
public class Finder {
    public Finder(String root);
    public void find(String pattern, Exec exec);
    public interface Exec {
        int exec(String currentFileName);
    }
}

このクラスに対する最初の例に相当する呼び出しは次のようになるだろう。

 Finder.find(".", "*.zip", new Finder.Exec() {
         public int exec(String file) {
             System.out.println(file);
             return 0;
         }
     });

実際にJ2SEには、Arrays.binarySearchやArrays.sortのように、Comaparatorのようなインターフェイスを取るメソッドがある。したがって、こういう形で無名インナークラスを利用するのは比較的頻度が高いと想定されてるんじゃないのかな、と思う反面、それほど一般には見かけないような気もする(GUIアプリケーションだとイベントハンドラで結構出てくるが)。というか、内部クラス使うなルールのようなへなちょこ規約を見かけてなんじゃこりゃと思ったというのがあるのだが。

ちなみにArrays.sortは次のように利用する。

public class Sort {
    static class Foo {
        Foo(String initX, int initY) { x = initX; y = initY; }
        public String toString() { return "x=" + x + ", y=" + y; }
        String x;
        int y;
    }
    public static void main(String[] args) {
        Foo[] ary = {
            new Foo("B", 8), new Foo("A", 31), new Foo("A", 8), new Foo("C", 5),
        };
        java.util.Arrays.sort(ary, new java.util.Comparator() {
                public int compare(Object o1, Object o2) {
                    Foo f1 = (Foo)o1; // APIでClassCastExceptionを認めているのでこれで良い
                    Foo f2 = (Foo)o2;
                    int n = f1.x.compareTo(f2.x);
                    if (n == 0) {
                        n = f1.y - f2.y;
                    }
                    return n;
                }
// 追記(るいもさんのコメント参照):こんなことをしてはいけない
//  Note that it is always safe not to override Object.equals(Object). 
// と書いてあるくらいだ。
// Java2の実装によっては、Comparatorのキャッシュか何かを利用する可能性があるってことなんじゃなかろうか。
// たとえば、既にソートした配列をキャッシュしておいてComparatorを見て
// 同じモノを返すとか。本当かなぁ?
// 更に追記:戯さんの指摘通り、Comparatorの
// JavadocにコレクションそのものがSerializableを実装している場合について
// の記述があるけど、TreeMap#comparatorをコンストラクタで設定した場合に
// 生きてくるようだ。というか、TreeMapのJavadocを参照。
// ちなみにTreeMap#putAllで実際に呼び出している。
//
// public boolean equals(Object o) {
//                    return this == o;
//                }
            });
        for (int i = 0; i < ary.length; i++) {
            System.out.println(ary[i]);
        }
    }
}

結果は次のようになる。

$ javac Sort.java
$ java Sort
x=A, y=8
x=A, y=31
x=B, y=8
x=C, y=5
$ 

もちろん、この例であれば、あらかじめclass FooにComparableを実装しておけば良いのだが、ソート方法などが1種類では不足する場合があるため、結局、Comparatorを与えることも必要となる。しかも、その場合は、その比較方法自体が呼び出し時点に特化していたりする。したがって、その特化した処理でのソート方法を一番読みやすい位置に記述するとしたら(最悪なのはComparatorの実装を別ファイルにすることだったりするのだが)結局、無名内部クラスとして記述するのが良いだろう。つまりsortの呼び出し時点に比較方法が示されるからだ。ただし別解としてこの例であればclass Fooのstaticな内部クラスとして複数のComparatorを用意するという方法もあるかも知れない。その場合はsort時点のコードから比較処理の実装自体は距離的に遠くなるが、あるクラスの比較方法の記述位置としては意味のある場所に収まるのだからそれほど悪くはない。ただし、いずれにしろ独立したトップレベルのクラスにしてはならないだろう。

ここで重要なことは次の点だ。

無名内部クラスの記述方法は確かに一見、ごちゃごちゃしているし、それに概念的にはほんのちょっぴり高度かも知れない。

でも、そんなことは全然関係ないってことだ。プログラムの可読性っていうのは、開発時には問題にならない。だからクソみたいなコードが生産されるってことは知ってる人は知っている。開発している人間にとっては第3者がどう思おうが、とりあえずそのコードを書いてしまえるし、かつテストまでしてしまうものだ。

可読性が重要となるのは、メンテナンスする時だ。それは大抵、最初に書いた人間ではない誰か別の人間がコードを読むことから始まる。その時に始めて可読性っていうのはものを言うのだ。

わかるかな? ばりばり書いている時は、多少の読みにくさとかはそんなに重要ではないってことだ。だから、内部クラスが難しいとか、ごちゃごちゃしているかなんてことは、どうでも良い。重要なのは、ある処理を読み下しているときに、はるか離れたところをいちいち参照しなければならないようなコードを書くなってことだ。おお、この配列をソートするのですか。で、Comparatorを指定しているってことですな。で、どういう比較をしてるんだ? それはどこだ? となるのは最低だということ。ここで例を示したように、おお、この配列をソートするのですか。で、Comparatorではこういう比較をしているのですね、なるほど。――やっぱりこうあるべきだ。

さらに付け加えると、世に出ているコーディング規約は、1に間違いを入れないように、2に単純に、3にどうでも良い機能だけを利用するように制限(これは1や2とは直交してると思うんだけど。たとえば上であげた無名内部クラスが良い例だ。比較がその場にあるんだから間違いにくいし、単純だ。しかし、どうでも良くない優れた機能だから使うなと3がのさばるわけだね)、となっているように思える。でも、重要なのは、1でも無い。大抵、初回リリース時はうんざりするほど受け入れテストとかすると思うんだけど。そうでなければそれは御愁傷様としか言いようがないけど。だからよほどのへまをしない限り、コーディング規約で抑制可能な程度のバグなんて入らないものだ。もっととんでもない複数の条件が重なった場合に発動するようなバグが入るのだ。そして、もちろん、3であるはずがない。2だけだ。

でも、メンテなんてのは、ちょっと違う。上で書いた、とんでもない複数の条件が重なって発動した問題を解決するために行う時を想定しなきゃならないからだ。この時、最悪の状態ってのは、細い細い回線で仮想端末越しにあれこれするようなことだ。そのような場合でもちゃんと読めるようなソースを書くこと。つまり、必要な情報が80×24に収まること、昔からのUnixルールに従っていること、本当はこいつが1番重要なのだ。そうでなければ、気楽なものだな。

#すると重複コードはどうなるよ? という疑問が当然出てくるのであるが、それはそれ、また幾つかの切り分けの考え方があるのだ。だから、コードを書くのはおもしろいし、間違いなく機械的には(読み取って修正するのも機械に任せられるようにならない限りは)処理できないってことでもある。

本日のツッコミ(全4件) [ツッコミを入れる]
_ るいも (2005-03-24 21:25)

どうでもいい事ですが、この場合はequalsメソッドは実装(というかオーバライド)不要ですね。そもそもComparatorインターフェースにequalsメソッドがあるというのが謎ではありますが。

_ arton (2005-03-24 21:37)

あ、そうか。Object#equalsがありましたね、そう言えば。っていうか、今まで、この無駄なequalsを実装してたな……

_ (2005-09-25 07:09)

例えば、SerializeしてからDeSerializeしたときに<br>以前と"同じ"Comparatorなんだよと自己主張させれるため、<br>だったりしませんか?<br>「Serializable を実装してください。その理由は、コンパレータを直列化可能データ構造 (TreeSet、TreeMap など) の中で順序付けメソッドとして使用できるからです。」だそうです。<br>つまり(?)、ソート対象データの道連れでソート屋さんもSerializeしちゃうと。

_ arton (2005-09-25 09:28)

ああ、なるほど。<br>確かにTreeMapなんかに継承してComparatorを後付けできるような仕組みがありますね。配列のソートにとらわれすぎていたようです。


2003|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|

ジェズイットを見習え