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

Java魂2章

前回に引き続き、Java魂の2章、「ファイナル(final)ストーリー」のおかしな部分の紹介。この章は1章ほどおかしな部分は多くない*1

まず、2.6 finalクラス*2

実は、Introspectorを拡張してより機能豊富なクラスを構築するつもりでした。しかし困ったことに、クラス唯一のコンストラクタがprivateなため、このクラスの拡張はできません。
〜略〜
ほとんどのシングルトンクラスは、finalと宣言するべきではありません。
〜略〜
この問題は、例2-18のように、protectedコンストラクタで解決できます。

2.6 finalクラスより

まず、Introspectorクラス*3はシングルトンではなく、ユーティリティクラスだ。Javaではこの他ユーティリティクラスとして、MathクラスSystemクラスがあるが、これらはfinalクラスになっている。Introspectorに関してもfinalクラスにすべき、というなら分かるのだが、ここでは「ほとんどのシングルトンクラスは、finalと宣言すべきではありません」と、まさにとんちんかんなことを言っている。

そもそも、このようなクラスを継承によって拡張しようとするのは、差分プログラミングの典型例であり、Javaでは(出来たとしても)やるべきではない*4

さらに、「拡張可能なシングルトン」として、以下のコードを紹介している。

package oreilly.hcj.finalstory;
public class Singleton {
  private static final Logger LOGGER = Logger.getLogger(Singleton.class);
  public static Singleton instance = null;
  private final String[] params;

  public static void init(final String[] params) {
    // ...初期化を行う..
    instance = new Singleton(params);
  }

  protected Singleton(final String[] params) {
    this.params = params;
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(Arrays.asList(this.params).toString());
    }
  }
}
例2-18 拡張可能なシングルトンより

まず、このプログラムにはシングルトンとは関係ない部分で駄目な部分がある。

final String[] params = {"hoge", "piyo"};
Singleton.init(params);
params[0] = "foo";
params[1] = "bar";

このようなコードによって、意図せずにSingleton内部のデータに変更が加えられてしまう。これは、この著者も紹介していることだが、自分で見事にはまってますね。なさけない。

また、このクラスはシングルトンとしても不合格だ。そもそも、シングルトンのコンストラクタは不用意にprotectedに出来るようなものではない。以下のコードのように、簡単に親のシングルトン属性を剥奪できてしまう。コンストラクタをprivateにしておくだけで、このような事態を防ぐことが出来る。

public class NotSingleton extends Singleton {
    public NotSingleton() {
        super(null);
    }
}

シングルトンを拡張したければ、Singletonのサブクラス化Singleton の問題回避できるか?のように、考慮すべき事例はたくさんある。この本のように、シングルトンのコンストラクタをprotectedにするだけでは駄目なのだ。それはもはやシングルトンではなくなる。

つづいて2.7 finalメソッド。

このクラスを使用する人が何を望むかをすべて把握するのは無理でしょうから、支障のない範囲で何でも可能にしておく方が賢明です。
メソッドをfinalにするのが適切であるのは、読み取り専用のプロパティを使っている場合です。

2.7 finalメソッドより

これは前回もヘルパメソッドで言及したが、よくない考え方である。「支障のない範囲で」といっているが、読み取り専用でないプロパティでも、finalにしないことで支障はでる。

public class Accessor {
    private int i = 10;
    /**
     * return 0以外の整数
     */
    public int get() { return i; }
    /**
     * @param i 0以外の整数
     */
    public void set(int i) {
        if (i != 0)
            throw new IllegalArgumentException();
        this.i = i;
    }
}

class ExAccessor extends Accessor {
    private int newI = 10;
    public int get() { return newI; }
    public void set(int i) { newI = i; }
}

前回のヘルパメソッドで使用した例から、アクセッサ部分だけ抜き出し、著者のアドバイスどおりfinalをとったものである*5。さて、以下のクライアントコードは、Accessorのiが0にならないことに依存したコードである。

public static void someMethod(Accessor accessor) {
    if (accessor == null)
        throw new NullPointerException();
    int i = 100 / accessor.get();
}

このクライアントコードはAccessorクラスのJavadocを信じている。そのためのJavadocだし、それ自体は何も悪くない。ただし、このクラスに0がセットされたExAccessorクラスが渡された瞬間、someMethodは予期せぬエラーを発生させる。かといって、常に以下のように書かなければならないのであろうか?

public static void someMethod(Accessor accessor) {
    if (accessor == null)
        throw new NullPointerException();
    if (accessor.get() == 0)
        throw new IllegalArgumentException();
    int i = 100 / accessor.get();
}

そんなことは断じてない。というより、あってはならない。これを回避するのは簡単で、Accessorクラスのアクセッサをfinalにするだけである。

このように、読み取り専用でないプロパティであっても、アクセッサには理由がない限りfinalをつけるべきだろう。この本の著者が言う、

finalにしなくてはならないのかどうかがよくわからないときは、メソッドでfinalというキーワードは使わないでおきましょう。

2.7 finalメソッドより

なんていう滅茶苦茶な指針はもってのほかである。よく分からないなら、きちんと調べるべきであり、「わからないからfinalじゃなくていいや」なんて、まともなプログラマがやることとは思えません*6

これで3600円・・・払う価値はありません。他にもJava中上級者向けの本はいくらでもあります。そちらにしましょう。

*1:量より質な感じ。駄目じゃん。

*2:1章では初っ端から駄目だったけど、2章はすごいね!

*3:1.4のJavadocなのは、この本が1.4を対象としているからです

*4:C++ではその辺の事情はJavaとは違う。templateによるMix-inとか、ポリシーによる設計とか

*5:引数のチェックも追加している

*6:初心者には分かりやすい指針が必要だ、という反論が聞こえてきそうですが、この本の対象読者は「中級から上級のJavaプログラマ」であり、「分かりやすい指針」よりも、「正しい情報」の方が必要だとおもいます