Javaの理論と実践: ファイナル・アンサー?

finalキーワードを有効に使用するためのガイドライン

final は、クラスやメソッドの宣言では過度に使用される一方、インスタンス・フィールドの宣言では十分に活用されないなど、誤った使い方をされることの多いキーワードです。今月は、Javaの実践者であるBrian Goetz氏が、final の有効な使用法に関するいくつかのガイドラインを説明します。

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix

Brian Goetz は18 年間以上に渡って、専門的ソフトウェア開発者として働いています。彼はカリフォルニア州ロスアルトスにあるソフトウェア開発コンサルティング会社、Quiotixの主席コンサルタントであり、またいくつかのJCP Expert Groupの一員でもあります。2005年の末にはAddison-Wesleyから、Brianによる著、Java Concurrency In Practiceが出版される予定です。Brian著による有力業界紙に掲載済みおよび掲載予定の記事のリストを参照してください。



2002年 10月 01日

よく似たCのconst キーワードと同様に、final は、そのコンテキストによっていくつかの異なる意味を持ちます。final キーワードは、クラス、メソッドまたはフィールドに適用できます。クラスに適用された場合は、そのクラスがサブクラス化できないことを意味し、メソッドに適用された場合は、そのメソッドがサブクラスによってオーバーライドできないことを意味します。フィールドに適用された場合は、コンストラクターごとに一度だけそのフィールドに値を代入する必要があること、そして一度代入された値は変更できないことを意味します。

ほとんどのJavaテキストでは、final キーワードの使用とその結果が適切に記述されていますが、final をいつ、どの程度使用するかということについてはほとんど何も示されていません。私の経験では、final は、クラスやメソッドに対しては非常に多く使用され (一般的に、開発者は、それがパフォーマンスを向上させると誤って信じているので)、それが最も適切であるクラス・インスタンス変数の宣言では十分に使用されていません。

このクラスはなぜfinalなのか

特にオープン・ソースのプロジェクトでは、開発者がクラスをfinal と宣言するのが非常に一般的です。しかし、理由について示すものは何もありません。その後、少したって、特に元の開発者がそのコードの維持にかかわっていない場合、他の開発者は「クラスXはなぜfinal と宣言されたのか」という疑問を必ず持つようになるでしょう。多くの場合、その理由は誰も知りません。その理由を知っている人や推測した人がいるとしても、その答えはほぼ必ず「速くなるから」です。クラスやメソッドをfinal と宣言すれば、コンパイラーが、メソッドの呼び出しをより簡単にインライン化できるというのが一般的な認識ですが、この認識は誤っています (あるいは少なくとも、非常に誇張されています)。

final クラスとメソッドは、既存のコードの再利用や既存のクラスの機能拡張を制限するので、プログラミング時に非常に不便である場合があります。値の不変性を強調するなど、クラスが適切な理由でfinal と宣言されている場合は、final を使用する利点が、この不便さを上回っているべきです。パフォーマンスの向上を重視すれば、ほぼ必ず、優れたオブジェクト指向設計を行うための原則に対して妥協することになります。これは、パフォーマンスの向上が小さかったり起こらなかったりした場合、実に不適切なトレードオフとなります。

早い段階の最適化

パフォーマンス上の理由から、プロジェクトの初期段階でメソッドやクラスをfinal と宣言することは、いくつかの理由で適切な考えではありません。まず、設計の初期段階は、実行サイクル数レベルのパフォーマンスの最適化について考えるのに適切な時期ではありません。final の使用によって、そうした決定が設計を制限する可能性がある場合はなおさらです。第二に、メソッドまたはクラスをfinal と宣言することによって得られるパフォーマンス上の利点は通常ありません。さらに、複雑でステートフルなクラスをfinal と宣言すると、オブジェクト指向の設計を妨害し、より小さく、より整合性のあるクラスに簡単にリファクタリングできないため、あらゆるものが詰め込まれた膨張したクラスとなります。

Javaのパフォーマンスに関する数多くの神話と同様に、クラスやメソッドをfinal と宣言することによってより優れたパフォーマンスが得られるという誤った信念は、非常に広まっていますが、ほとんど検証されていません。メソッドやクラスをfinal と宣言すると、コンパイラーは、実行時に、それが呼び出すメソッドのバージョンであるということを明確に判別できるため、より積極的にメソッドの呼び出しをインライン化できるようになるというのが現在の主張です。しかしこれは断じて真実ではありません。クラスXがfinal クラスYに対してコンパイルされるからといって、クラスYの同じバージョンが実行時にロードされるわけではありません。そのため、コンパイラーは、final であってもそうでなくても、このような複数のクラスにまたがったメソッドの呼び出しを安全にインライン化することはできません。メソッドがprivate である場合のみ、コンパイラーは自由にメソッドをインライン化することができ、そのような場合、final キーワードは冗長となります。

一方、実行時環境とJITコンパイラーにはどのクラスが実際にロードされるかについてより多くの情報があり、前述のコンパイラーよりはるかに優れた最適化の決定を行うことができます。実行時環境では、Yを拡張するクラスがロードされないことが認識されると、Yがfinal であるかどうかに関係なく、Yのメソッドの呼び出しを安全にインライン化できます (Yのサブクラスが後でロードされる場合に、そのようなJITでコンパイルされたコードを無効にできる場合のみ)。そのため、final は、グローバルな依存関係の分析を実行しない素朴な実行時オプティマイザーの有用なヒントとなる一方で、実際にはコンパイル時に多くの最適化を有効にするわけではなく、実行時の最適化を実行する賢いJITには必要とされないのが現実です。


デジャビュー -- 再びregisterキーワード

最適化の決定を行うためのfinal の使用は、使用すべきでないとされるCのregister キーワードに非常によく似ています。register キーワードは、オプティマイザーを助けたいというプログラマーの願望が動機となっていますが、現実にはあまり助けにならないことがわかりました。そうでないことを信じたいものですが、コンパイラーは通常、特に現代のRISCプロセッサーでは、コードの最適化の決定において人間より優れています。事実、ほとんどのCコンパイラーは、register キーワードを完全に無視します。初期のCコンパイラーは最適化をまったく行わなかったのでそれを無視しました。また、現代のCコンパイラーは、それがなくてもより優れた最適化の決定を行えるので無視します。どちらの場合も、register キーワードは、Javaクラスやメソッドに適用されるfinal キーワードとよく似て、パフォーマンスを向上させることはほとんどありません。コードを最適化するには、効果的なアルゴリズムを使用したり冗長な計算を行わないなどの大きな利点をもたらす最適化のみ行い、実行サイクル数レベルの最適化はコンパイラーとJVMに任せましょう。


finalを使用して、不変性を保存する

パフォーマンスは、クラスやメソッドをfinal と宣言する適切な理由ではありませんが、final クラスを作成する適切な理由もあります。最も一般的なのは、変更不可であると意図されたクラスを変更不可のままfinal が維持する場合です。変更不可のクラスは、オブジェクト指向の設計の単純化に非常に有効です。変更不可のオブジェクトでは、防衛的なコーディングがそれほど必要なく、同期の要件が緩和されます。変更不可として作成したクラスが、後日変更可能になるような方法で拡張されるという前提を自分のコードに組み込みたくはないでしょう。変更不可のクラスをfinal と宣言することにより、このタイプのエラーがプログラムに入り込まないことが保証されます。

クラスやメソッドにfinal を使用するもう一つの理由は、メソッド間のリンケージが破壊されるのを防止することです。たとえば、クラスXのあるメソッドの実装が、メソッドMの特定の振る舞いを想定しているとします。XまたはMをfinal と宣言することにより、Xが誤って振る舞うような方法で派生クラスがMを再定義するのを防止します。こうした内部の依存関係を使用せずにXを実装するほうがより優れているかもしれませんが、必ずしも実践的ではありません。final の使用により、将来の互換性のない修正を防止することができます。

finalクラスやメソッドを使用しなければならない場合は、その理由を文書化する

どのようなイベントでも、メソッドまたはクラスをfinal と宣言する場合は、その理由を文書化してください。そうでなければ、将来の保守管理者が、適切な理由があったかどうかについて混乱することになり (適切な理由がないことが多いので)、そうしなければならない利点もないのに、決定に従わざるをえないでしょう。多くの場合、クラスやメソッドをfinal と宣言するかどうかの決定は、クラスがどのように相互に作用し拡張されるかについてより適切な情報が得られる開発プロセスの後半まで遅らせるのが上策と思われます。そうすれば、クラスをfinal にする必要が全然ないことがわかったり、あるいは、final がより小さくシンプルなクラスに適用されるようクラスをリファクタリングできるかもしれません。


finalフィールド

final フィールドは、final クラスやメソッドとは非常に異なるので、同じキーワードを共有するのは適正ではないと言っていいでしょう。final フィールドは読み取り専用のフィールドで、その値は、構築時 (あるいは、static final フィールドの場合クラスの初期化時) に一度だけ設定されることになっています。前述のように、final クラスやメソッドの場合は、final を本当に使用する必要があるかどうか、いつも自問すべきです。一方、final フィールドではそれとは反対の自問を行ってください。このフィールドは本当に変更可能である必要があるでしょうか。その答えがノーとなる場合の多さに驚かれるかもしれません。

文書化の価値

final フィールドには、いくつかの利点があります。フィールドをfinal と宣言することには、クラスを使用したり拡張する開発者にとって、文書を作成する上での貴重な利点があります。それにより、クラスがどのように機能するかを説明できるだけでなく、設計の決定時にコンパイラーの力を借りることができます。final メソッドの場合と異なり、final フィールドを宣言することにより、オプティマイザーはより優れた最適化の決定を行うことができます。これは、コンパイラーが、そのフィールドの値が変わらないことを認識していると、その値をレジスターに安全にキャッシュできるからです。さらに、final フィールドは、フィールドが読み取り専用となるようコンパイラーに強制し、さらなるレベルの安全を提供します。

フィールドがすべてfinal プリミティブであったり変更不可のオブジェクトへのfinal 参照であるクラスのような極端な場合は、クラス自身が変更不可になります。これは非常に便利な状況です。クラスが完全に変更不可でない場合でも、その状態の一部を変更不可にすることにより、開発を大幅に単純化できます。final フィールドの現行の値を参照していることを保証したり、他の人がオブジェクトの状態のその部分を変更していないことを確実にするために同期を取る必要はありません。

それではなぜfinal フィールドはそれほど多く使用されていないのでしょうか。1つの理由は、特にコンストラクターが例外をスローするオブジェクト参照の場合に、正確に使用するのが若干難しいからです。final フィールドは、各コンストラクターで一度だけ初期化されなければならないので、final オブジェクト参照の作成が例外をスローした場合、フィールドが初期化されていなければ、コンパイル時にエラーが発生することがあります。コンパイラーは通常十分に賢いので、if...else ブロックなどの排他的なコード・ブランチのそれぞれで初期化が一度だけ行われるよう機能しますが、try...catch ブロックでは多くの場合それほど寛大ではありません。たとえば、ほとんどのJavaコンパイラーは、リスト1のコードを受け入れないでしょう。

リスト1. final参照フィールドの無効な初期化
public class Foo { private final Thingie thingie;
  public Foo() {
    try { thingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      thingie = Thingie.getDefaultThingie();
    }
  }
}

しかし、リスト2のコード (同等なもの) は受け入れるでしょう。

リスト2. final参照フィールドの有効な初期化
public class Foo { private final Thingie thingie;
  public Foo() {
    Thingie tempThingie;
    try { tempThingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      tempThingie = Thingie.getDefaultThingie();
    }
    thingie = tempThingie;
  }
}

finalフィールドの制限

final フィールドにもいくつかの重要な制限があります。配列参照はfinal として宣言できますが、その配列の要素は宣言できません。つまり、public final 配列フィールドを公開したり、リスト3に示されたDangerousStates クラスなどのメソッドによってそうしたフィールドの参照を戻すクラスは、変更不可ではありません。同様に、オブジェクト参照がfinal フィールドとして宣言されても、それが参照するオブジェクトは変更可能である可能性があります。final フィールドを使用して変更不可のオブジェクトを作成する場合は、配列や変更可能のオブジェクトの参照がクラスからエスケープするのを防止しなければなりません。配列を繰り返し複製せずにそれを行う簡単な方法は、リスト3のSafeStates クラスなどで配列をList に変えることです。

リスト3. 配列参照の公開によりクラスを変更可能にする
// Not immutable -- the states array could be modified by a malicious 
caller
public class DangerousStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };
  public String[] getStates() { return states;
  }
}

// Immutable -- returns an unmodifiable List instead
public class SafeStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };
  private final List statesAsList = new AbstractList() {
        public Object get(int n) { return states[n];
        }
        public int size() {
          return states.length;
        }
      };
        public String[] getStates() { return statesAsList;
  }
}

CやC++ のconst の使用と同様に、なぜfinal は、配列や参照オブジェクトに適用するよう拡張されなかったのでしょうか。C++ のconst のセマンティクスや使用は非常にわかりにくく、それが式のどこに現れるかによって異なる意味を持ちます。Java設計者は、こうした混乱から私たちを救おうとしたのですが、残念ながら、その過程で新たな混乱を招いてしまいました。


まとめの言葉

クラスやメソッド、フィールドでfinal を有効に使用するためのいくつかの基本的なガイドラインがあります。特に、final はパフォーマンス管理ツールとして使用しないようにしましょう。プログラムのパフォーマンスを向上させるには、制約の少ないはるかに優れた方法が他にあります。final は、それがプログラムの基本的なセマンティクスを反映する場所で使用し、クラスが変更不可であると意図されていること、あるいは、フィールドが読み取り専用であると意図されていることを示してください。final クラスやメソッドを作成する場合は、その理由を明確に文書化してください。みなさんの仲間にきっと感謝されることでしょう。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=224165
ArticleTitle=Javaの理論と実践: ファイナル・アンサー?
publish-date=10012002