オブジェクト指向言語に付随する機能

オブジェクト指向パラダイムの本質は であるということは間違いないのですが、Java を代表とする実在のプログラミ ング言語には、その他にもプログラマをサポートする重要な機構がいくつか備 わっています。ここでは、それらのうち代表的なものとして、 「パッケージ」「例外」「ガベージコレクション」 を取り上げます。

パッケージ

クラスの特徴として「まとめる」仕組みという説明がありましたが、パッケー ジは、そのクラスをさらにまとめる仕組みです。実際の開発現場でプログラム の数が増えてくると、管理も大変になってきます。一般にファイルが増えてき た場合、フォルダを適宜作って分類整理することで見通しがよくなります。 Javaでも同じようにプログラムをフォルダに分けて管理することが出来ます。 Javaの場合このフォルダを「パッケージ」と呼びます。パッケージ(フォルダ) に分かりやすい名前をつけておけば、どこにプログラムがあるかが分かりやす くなるというわけです。 ここでは、例えばhogehogeパッケージを作るとすると、まず「hogehoge」という名 前のフォルダを作り、このhogehogeというフォルダの下でプログラムを作成して みます。 package hogehoge; class PackageTest{ public void showMessage(){ System.out.println("パッケージのテスト"); } } 注意する点は1点だけで、 このプログラムがhogehogeパッケージの中にあることが分かるように一番上に package hogehoge; と記述する必要があります。つまり package パッケージ名; とし、必ず一番先頭に記述します。このPackageTestクラスをコンパイルするに は、SDK 環境でコマンドプロンプトからコンパイルする場合には、 I:\foobar>javac hogehoge\PackageTest のようにすると、hogehogeフォルダに「PackageTest.class」というファイルが 出来上がります。同じくhogehogeフォルダの下にPackageTestクラスを呼び出す クラスは、以下のようになります。 package hogehoge; class PackageTestCall{ public static void main(String[] args){ PackageTest packageTest = new PackageTest(); packageTest.showMessage(); } } このクラスをコンパイルして実行する場合は注意が必要です。この PackageTestCallクラスはhogehogeパッケージの中にあります。ですので、実行 するときもhogehogeパッケージの中にあるPackageTestCallクラスと指定しなけ ればいけません。普通フォルダの区切り文字には「\」や「/」を使いますが、 パッケージの区切り文字は「.」(ピリオド)を使いますので、hogehogeパッ ケージのPackageTestCallクラスを実行したい場合は I:\foobar>java hogehoge.PackageTestCall と指定します。パッケージの中にパッケージを作ることも出来ます。hogehoge パッケージの中にsubpackageパッケージを作成し、その中にクラスを作成する 場合は package hogehoge.subpackage; と記述します。 このクラスを実行したい場合は同じように java hogehoge.subpackage.クラス名 と指定すればいいわけです。 実際に活用するときには、重なりを避けるべく、ドメインの逆順をパッケージ名 にすることがなされています。例えば阪南大学独自のパッケージを作るとすると、 package jp.ac.hannan-u.foobar; のようにすることで、世界で一意のパッケージ名をつくれる、というわけです。 また、階層が深いものを再利用するときには、パッケージをまとめるための jar ファイルというものを作るのですが、ここでは詳細は略します。

例外

例外をひとことで表現すると、「戻り値とは違う形式でメソッドから特殊 なエラーを返す仕組み」 といえるかもしれません。特殊なエラーとは、 たとえば通信障害、ファイルアクセス障害などです。また障害じゃなくてもファ イルが空などのように戻り値として適切な値が対応しない場合も対応します。 従来はエラーコードとして値をきめて、それをサブルーチンの戻り値として返 して呼び出し側に通知していたのですが、それだと、メインルーチンの判定処 理のバグをコンパイラでは見つけられないですし、さらに、似て非なるエラー 判定・対応のロジックがいろいろなところで関連しながらつながってしまい、 非常に冗長になってしまいます。「例外」とは上の問題を解決するための機構 です。

例外の基本

まず、以下のコードを例とします。 public void execute(String fileName) { // コンパイル・エラーとなる File file = new File(fileName); file.createNewFile(); file.deleteOnExit(); } このメソッドexecuteでは、ファイルを新たに作成するメソッドcreateNewFile を呼び出しています。createNewFileは、クラスjava.io.Fileのメソッドで、 IOExceptionというチェック例外(クラスjava.io.IOExceptionのオブジェクト) をthrowするように定義されています。 チェック例外とは、必ず例外処理を記述しなければならない例外のことです。 これに対し、例外処理の記述が任意である例外のことを「非チェック例外」と 呼びます。 しかし、メソッドexecuteではIOExceptionを処理するコードを記述していない ので、上のメソッドを含むプログラムをコンパイルすると、「例外 java.io.IOExceptionは報告されません。スローするにはキャッチまたは、ス ロー宣言をしなければなりません。」というエラー・メッセージが表示されま す。 この例は、例外の構文を説明するためではなく、ここでのポイントは、「コン パイル・エラーになる」ということで、これは、Java の例外機構の優れた特徴 です。以下、この点について解説します。

チェックの強制

ここでは、Javaに例外機構が存在しない状態を考えます。その条件で、さらに メソッド createNewFileを自作しなければならないとします。 メソッドcreateNewFileを利用するときのことを考えると、ファイルの作成に失 敗した場合には、そのことを通知する仕組みが必要です。もし、ファイルの作 成に失敗しても、それを知るすべがないと、「ファイルの作成は成功した」と いう、ある意味誤解したままその後の処理が進められます。これが原因で重大 な問題が発生するかもしれません。その場合、問題の原因究明は非常に大変で す。なので、メソッドcreateNewFileには、ファイル作成の成否をboolean 値で 表し、それを戻り値として返すというロジックを組み込むことにします。
クラスFileのメソッドcreateNewFileもboolean型の戻り値を返しますが、この戻り 値はファイルを「新たに作成したかどうか」を示すものであり、「ファイル の作成の成否を示すのでありません。作成しようしているのと同名のファイル がすでに存在する場合新たなファイルは作成されませんが、その後の処理を続 けることは可能なので、例外とするのではなく、戻り値で判断でき るようにしておき、その扱いを呼び出し側に委ねていると考えられます。
そのようなメソッドcreateNewFileを利用する場合、メソッドexecuteは以下の ように記述することになります。 public void execute(String fileName) { File file = new File(fileName); if (file.createNewFile()) { file.deleteOnExit(); } } ここで、Javaに例外機構がなく、さらに上で説明した自作のメソッドを利用 するという前提で、もう一度リスト1を見てください。このコードでは、メソッ ドcreateNewFileを呼び出す際に、ファイル作成の成否についての考慮がなさ れていません。ところが、もし例外機構がないとすると、リスト1のように記 述してもコンパイルできてしまいます。つまり、メソッド createNewFileでは、 せっかくファイル作成の成否を戻り値で判断できるようにしていても、利用す る際にそれを調べなかったら、その工夫は“無駄な努力”になってしまうので す。 上の例により、例外機構がなかったらどのようなことが起こりうるかが分かっ たと思います。実際には、Javaには例外機構がありますから、それを有効に活 用すれば、このような問題は起きません。 Javaの例外機構では、ファイル作成の成否を、メソッドを呼び出した側に強制 的にチェックさせることができます。例えば、クラスFileのメソッド createNewFileの場合、先述したようにthrows節にIOExceptionが指定されてい ます。そのため、コンパイラは、メソッドcreateNewFileを呼び出しているコー ドを見つけたら、そのコードに IOExceptionを処理する記述があるかどうかを 調べます。そして、記述がない場合には、先ほどのようなエラー・メッセージ を表示するのです。つまり、「createNewFileというメソッドは、ファイルの 作成に失敗した場合、IOExceptionを投げるので、そのときのための処理を記 述してください」とコンパイラが教えてくれるわけです。 この仕組みのおかげで、ファイル作成に失敗しているのに、後続の処理が続行 されるという事態を避けることができます。上のプログラムでは、ファイル作 成の成否をif文で判定し、ファイルが作成された場合にのみ後続の処理を行う ようにしています。この条件の下では、上のプログラムのように記述するのが 正しいコーディングです。

例外をどこで処理するか

ここまでの説明で、Javaの例外機構によって、メソッドの呼び出し元にチェッ クを強制できることのメリットをご理解いただけたでしょう。続いては、その 例外をどのように扱うのかということについて考えてみます。 先ほども説明しましたが、リスト1のコードをコンパイルできるようにするた めには、以下の2とおりの方法のうち、いずれかによってプログラムを修正す る必要があります。
  1. throws節を記述する
  2. try/catch構文を使って例外処理を記述する
これらのうち、(1)の方法で修正した例が以下に示すメソッドexecuteです。 public void execute(String fileName) throws IOException { File file = new File(fileName); file.createNewFile(); file.deleteOnExit(); } この方法で修正する場合には、いくつか注意点があります。 まず、throws節を追加すると、このメソッドの呼び出し元に例外処理の記述を 強制することになるという点です。つまり、それまで何の問題もなかった個所 でコンパイル・エラーが発生する可能性が生じてしまいます。

ここで、throws節の意味を再度考えてみると、上で、「チェックを強制する」 とありますが、これは別の言い方をすると「このメソッドは、こういう例外を 投げることがあるから、その場合の処理をよろしく!」と呼び出し元に「知ら せる」ということです。そして、このメソッドの呼び出し元は、「よし、頼ま れた」と認識して処理を行うわけです。しかし、throws節を使う方法の場合、 「例外処理を頼まれたけれど、この処理は呼び出し元のほうでよろしく」とい うように、例外を呼び出し元に投げるだけです。つまり、その例外処理を放棄 しているととらえることもできます。もちろん、throws節を用いる方法を適用 したメソッドも、「発生する可能性のある例外を知らせる」という役割は果た しています。しかし、上述したように、その処理は呼び出し元に任せているた め、場合によっては、メソッドの呼び出し元に迷惑をかけることもあるのです。

また、以下に示したプログラムが、try/catch構文を追加した例です。

public void execute(String fileName) { try { File file = new File(fileName); file.createNewFile(); file.deleteOnExit(); } catch (IOException ioe) { // 例外を処理する ioe.printStackTrace(); } } この例のcatchブロックでは、エラー情報のスタック・トレースを表示するだけ ですが、実際には、同ブロックには、さまざまな処理を記述することができま す。例外が発生した場合、それに対処するために、そのメソッド内で何らかの 処理を行う必要がある場合には、(2)の方法を使わなければなりません。通常、 アプリケーションで例外が発生した場合、エラー・ログを出力するといった処 理が必要になります。つまり、実際のアプリケーションでは、(2)の方法を使う ほうが多いということです(ただし、後ほど「そのアプリケーション専用の例 外クラスを作る」の節で述べますが、(1)の方法を適用したほうがよい場合もあ ります)。

ガベージコレクション(GC)

クラスを利用する時には (static なメソッドを除いて)実行時にインスタンス を作成しないといけません。また、インスタンスを複数つくることもでき、そ れぞれに機能を持たすことができます。インスタンスを作成するとメモリ領域 を確保するわけで、Java プラットフォームでは、オブジェクトはメモリ容量の 許す限り幾つでも作成することが可能であり、明示的にメモリ上から削除する 必要もありません。Java 実行環境 (JRE) は、それ以上使わないオブジェクト を特定し、削除してくれます。このプロセスは Garbage Collection (ゴ ミ集め)と呼ばれています。
ちなみに C や C++ では、不要になったメモリ領域はプログラマが明示的に指定 して解放する必要がありました。これは、過去のメモリが貴重な時代には、メ モリの有効利用という意味で非常に重要でした。

しかし、不必要な領域を解放しわすれてプログラムがどんどん膨れ上がって動 作停止にいたったり、逆にまだ利用してる領域を解放してしまうとプログラム は不正な動作をしてしまう、ということがありました。

なので、 C や C++ でも別途(標準では用意されていない) GC 用ライブラリ を用意することで、メモリ管理を楽にする、ということも出来るようになって います。しかし、これはあくまでもオプション扱いです。Java では標準で GC が完備されています(時間制御が厳しい組み込み用ではどうなってるかは謎で す)

以下、かなり話が細かくなりますが、できるだけの説明をしてみます。

データ・エリア管理

GC (Garbage Collector) は、ヒープに対する自動記憶域管理システムです。メ モリ上のヒープ内で不要なった領域 (garbage) を再利用可能にし (recycle) 、断片化した領域を再配置する (defragmentation) 、バック・グラウンドのオー バーヘッド・プロセスとして起動します。 Java にはポインタが無く、その帰結として、データエリア管理という概念があ りません。Java では、データエリアの管理は GC が自動的に行ってくれるので、 プログラマが意識して管理するものではありません。メモリ・リークや領域違 反が発生すれば GC のバグです。プログラマは、インスタンス管理として、GC の動作を補助することになります。

ポインタとランダム・アクセス

一般に、メモリ上のアドレス/オフセット値を明示的に指定する仕組みをポイ ンタと呼びます。ポインタを持つ言語の場合は、メモリ上の任意のリソースに、 いつでもランダム・アクセス可能なことが特徴です。アプリケーション・ロジッ クに依存しないで、オーバーヘッド処理によってメモリ領域の要/不要を判断 することはできません。したがって、自分で割り当てた領域は、自分が責任を 持って解放してやる必要があります。メモリ解放の遺漏(解放し忘れること) を、メモリ・リーク (memory leak) と呼び、不正なポインタによって、他の領 域のデータを壊してしまうことを、ストレージ・オーバーレイ(記憶域保護違 反)と呼びます。 メモリ・リークが発生すると、実行環境が配下として割り当てたメモリ領域を 全てクリーンナップするまで、永遠にメモリの当該領域を占有し続けることに なります。ランダム・アクセス可能な仕組みの下では、メモリ・リークを解消 することは極めて困難です。というのも、ポインタでランダム・アクセス可能 な仕組みの下では、占有されているメモリ領域が、解放し忘れているのか、先々 利用する可能性があるので居座っているのかを判断することが、原理上できな いからです。特定の処理で、メモリの使用率が上昇する場合に限り、メモリ・ リークの可能性が疑われることになります。

参照とインスタンス管理

Java には、ユーザが利用するポインタの仕組みがありません。ポインタの代わ りになるのは、オブジェクトに対する参照です。参照は、演算子 new によって JVM から返される、テーブルで管理されたシンボリックなものです。デバイス の論理アドレスに依存せず、コードが明示的に指定できるものではありません。 new で返される参照は変数に代入して初めてアプリケーションの処理対象にな ります。JVM から参照が返されるのは一度きりなので、あるオブジェクトの参 照のコピーを保持する全ての変数がメモリ上からドロップされたら、再び同じ インスタンスを参照することは不可能です。つまり、一時点でコンピュータ制 御が到達可能なコードの範囲内に、当該オブジェクトへの参照が無くなったら、 当該オブジェクトのインスタンスが占有しているメモリ領域には、永遠にコン ピュータ制御が到達不可能であると判断できるわけです。 オブジェクトを参照していた変数がそのスコープから外れると、通常はその変 数の参照はドロップされます。或いは、その変数に特別な値である null 値を 代入することで、明示的に GC の回収対象にすることも出来ます。一般に、プ ログラムは、同じオブジェクトに対して複数の参照を持つので、全ての参照が ドロップされていなければ、 GC の対象とならないようになっています。 このような、死んだオブジェクトの識別ロジックをマーキングと呼び、その処 理の間はアプリケーションが停止します。死んだオブジェクトのマーク (mark)、回収 (sweep)、断片化の解消とヒープサイズの圧縮 (compact) が GC の主な仕事になります。

GC の進化

現代的な GC は、単純な mark-sweep-compact のみではなく、複数のアルゴリ ズムを組み合わせて、実行時のリソース使用率に応じて、動的に動作を変えて います。ここで、簡単に GC アルゴリズムを歴史にそって紹介します。

インクリメンタル GC

そもそも、GC プロセスは、メモリ(セントラル・ストレージ)上の物理アドレ スに依存する処理を実行するスレッドで、実行時にはアプリケーションの処理 を停止させるものです。一旦 GC が起動するや、GC が終了するまでアプリケー ション処理が停止してしまう場合は、サーバサイドなどで、スタック階層が深 い場合は、秒単位で JVM 上の業務アプリケーションを停止させます。これを嫌っ て、GC 処理を中断可能にして、タイムスライス(CPU 時分割)でアプリケーショ ンと平行して処理できるようにしたのが、インクリメンタル GC です。

コンカレント GC

インクリメンタル GC は、タイムスライスによって、長期間のアプリケーショ ンの停止を避ける方法ですが、何れにせよ、アプリケーションが停止すること に変わりはありません。並行処理可能な部分を平行に実行する GC が、コンカ レント GC です。コンカレント GC の場合、マーキング以外の時間はアプリケー ションが停止しません。最も長時間掛かるのがコンパクトの間だったので、コ ンカレントでない GC と比べると、殆ど停止しないといえます。

世代別 GC

JVM 1.3 以上で採用されている世代別 GC は、ヒープを、若い世代 (young generation) 、古い世代 (Old Generation) 、永続的な世代 (permanent generation) の三つに分け、オブジェクトを生存時間に応じてコピーしていき ます。オブジェクトの生存時間が短い若い世代には、一時オブジェクトを初め として、ゴミになるオブジェクトが占める割合が圧倒的に高いために、若い世 代に対して集中して GC を行うことで、少ないコストで高い効果が挙げられま す。 若い世代の領域も、エデン・スペース (eden space) と二つの生存スペース (survival space) に分けられます。エデンス・ペースには、スタック状のデー タ構造でオブジェクトを割り当て、これがオーバーフローすると、生きている オブジェクトだけが生存スペースにコピーされ、エデンス・ペースは空の状態 に戻り、再びオブジェクトの割り当てが可能になります。若い世代の領域のコ ピーによる GC を scavenge (廃品回収、腐肉喰らい)と呼びます。 これらの領域の使用率と CPU 使用率に応じて、世代間コピーや mark-sweep-compact などの、複数のアルゴリズムの GC 処理を組み合わせて実 行することで、リソースの消費を抑え、アプリケーションの停止時間が短くな るように調整されています。 GC の挙動は、複数の処理を組み合わせて効率を上げるように設計されているた め、明示的に挙動を指定することはできません。JVM のパラメタで与えた構成 情報に従ってバックグラウンド処理として動作します。
講義用スタイル
印刷用スタイル