WisdomSoft - for your serial experiences.

6.1 パッケージ

Javaの仮想マシンは実行時に動的に必要なクラスを読み込むダイナミック・リンキングの仕組みになっていますが、クラスが配置されている場所を指す情報をプラットフォームやファイルシステムに依存できません。そこで、クラスの位置と名前空間を記述する方法としてパッケージが提供されています。

6.1.1 パッケージ宣言

オブジェクト指向では、多くの開発の場で再利用できる汎用的なクラス群を作成することができます。そうすることで飛躍的に生産性を向上させることができますが、Color クラスや Point クラス、Size クラスや Image クラスなど、誰もが思いつきそうな名前のクラスはすぐに他者、または他社が開発したクラスライブラリと名前が衝突することが予想できます。便利なクラスライブラリはできる限り再利用するべきですが、多くのライブラリを使うと、どうしても名前が衝突する可能性が高くなってしまいます。

そこで、クラス名が衝突する可能性を極めて低くする手段としてパッケージという機能をがあります。これは、オブジェクト指向一般で名前空間と呼ばれている機能に近いのですが、Java の場合はディレクトリ構造と同期している点でやや異なります。名前空間とは、クラスやインタフェースなどが宣言されているトップレベルの階層に、さらに開発名などを与える機能です。名前空間を宣言すれば、以下の階層でどのような識別子を用いても、他の開発チームと識別子が衝突することはなくなります。

しかし、Java のパッケージは名前空間とは異なり、ただクラスに名前を与えるのではなく、クラスの位置を表す明示的な情報として利用されます。すなわち、パッケージとはクラスにアクセスするためのパスのようなものなのです。オペレーティング・システムのディレクトリ構造のように、 クラスやインタフェースを木構造的に配置する手段として利用することができます。 パッケージを宣言するには、ソースファイルの先頭に package キーワードを用いて次のように記述します。

package 宣言
package パッケージ名;

パッケージを宣言することによって、そのソースコード内の型は指定したパッケージ名に属します。パッケージがどのように利用されるかは、開発者や利用システムに依存します。通常、パッケージはローカル・コンピュータ上のファイル・システムに格納されますが、複雑な実装では、分散ファイル・システムやデータベースに格納することも可能であるとされています。しかし、一般的な実装ではファイル・システムに格納されるため、パッケージの構造はシステムのディレクトリ構造を表すと思って間違いはないでしょう。

これまでのサンプルコードはパッケージを宣言していません。パッケージを宣言しない場合は無名パッケージとして扱われています。これは、クラスがカレント・ディレクトリに関連付けられていることを表しています。無名パッケージのクラスを起動するには、カレントディレクトリからアクセスしなければなりません。カレント・ディレクトリの概念はオペレーティング・システムに依存します。通常は、クラスファイルが配置されているディレクトリにクラスが関連付けられています。実践的な開発では、main() メソッドを宣言するアプリケーションの起動用のクラスを無名パッケージに記述し、再利用可能なクラスライブラリをパッケージに配置します。無名パッケージに対して、明示的に package 宣言によって名前が与えられているパッケージを名前付きパッケージと呼びます。

パッケージは一般的なファイル・システムにおけるディレクトリ構造に酷似しているため、パッケージの中にサブパッケージを含めることも可能です。これは、ディレクトリの中に、さらにサブディレクトリを配置することと同じです。パッケージ内に宣言されているクラスやインタフェース、そしてサブパッケージをパッケージ・メンバと呼びます。フィールドやメソッドがクラスのメンバだったように、実はクラスやインタフェースも、パッケージのメンバなのです。

パッケージ宣言は、必ず型宣言の前に記述しなければなりません。つまり、インタフェースやクラス宣言より前に書かなければならないため、必然的にソースファイルの先頭に記述することになります。パッケージがどのような働きを持つかは、理論より実践して見せた方が早いでしょう。

コード1
package test;

class Test {
	public static void main(String args[]) {
		System.out.println("Kitty on your lap");
	}
}
実行結果
>java test.Test
Kitty on your lap

コード1の Test クラスは test パッケージのメンバです。ファイルの先頭で package を用いたパッケージ宣言を記述していることに注目してください。このパッケージをファイル・システムに格納する場合、Test クラスは test サブディレクトリに格納されなければなりません。

これまでのような無名パッケージに宣言されたクラスはカレントディレクトリから単純名だけでアクセスすることができました。しかし、名前付きパッケージのメンバにアクセスするには、パッケージ名を指定した限定名を指定する必要があります。コード1の実行結果を見ると、アプリケーションを起動するために test.Test というように、クラスを限定名で指定しています。ただし、test.Test クラスを格納しているクラスファイルは、パッケージにしたがって test ディレクトリに配置されていなければなりません。

パッケージをファイル・システムに格納する実装の場合、パッケージへのアクセスはファイルパスに変換してクラスファイルを検索します。限定名におけるドット . 記号は、ファイル・システム固有のディレクトリ記述子に変換されます。

例えば、Windows であればクラス test.Test へのアクセスはファイルパスに変換され test\Test と解釈することができます。仮想マシンは、test サブディレクトリの中に Test クラスが存在すると認識するでしょう。このとき、Windows のディレクトリ記述子は円記号 \ です。UNIX システムであれば、ディレクトリ記述子は \ ではなくスラッシュ / なので test/Test となります。このような理由から、.class ファイルはパッケージに従って適切なサブディレクトリに配置されなければ見つからないのです。パッケージ・メンバにはサブパッケージを指定することもできるため、パッケージ宣言で kitty.pack.util という名前を指定すれば、このパッケージは kitty/pack/util ディレクトリに配置されることを表します。

このように、パッケージはクラスの位置を表すための抽象的な位置情報です。それがローカルなファイル・システムに格納されているか、分散されたネットワーク上に格納されているか、あるいはデータベースに格納されているかは実装に依存する問題です。開発者は、これを制限してはいけません。クラスライブラリの開発では、こうした実装の格納方法を抽象化して、パッケージという形で他のクラスを参照することができるのです。

初心者にとって、こうしたシステムの中枢に触れる作業は複雑で難しいと感じるかもしれません。しかし、Java はプラットフォームに依存してはならない言語なので、クラスの位置を表すための情報もまた、抽象化しなければならないのです。

Java コンパイラは、宣言されたクラスごとにクラスファイルを生成します。Java 仮想マシンがアプリケーションを実行するときは、それらのクラスがお互いに、実行時に呼び出されて機能するという性質を持ちます。これは、Java のプログラムの実行方法が、常にダイナミック・リンクであることを意味します。実行時に必要なプログラムをメモリに読み込むダイナミック・リンクという方式は、プログラムが部品ごとに独立しているため、再利用性や拡張性に富んでいます。ただし、プログラムを正しく実行するには、それぞれの部品、つまり、クラスファイルの位置を仮想マシンに対して指示する必要があります。しかし、クラスファイルの位置を指定する方法が、固有のファイル・システムに依存すれば、Windows ディレクトリに配置されている Java プログラムを UNIX システムに移して実行した場合、ファイルを見つけることはできなくなる可能性があります。そこで、システムに依存することなく、クラスファイルの位置を指定する手段がパッケージなのです。パッケージの重要性がわかっていただけたでしょうか?パッケージのおかげで、私たちは実装に配置されるクラスの位置を想定することなく、抽象的にクラスにアクセスすることができるのです。

6.1.2 クラスの公開

これまでのプログラムは、すべてのクラスが同一の無名パッケージ内に宣言されていました。しかし、異なるパッケージのメンバを呼び出そうとした場合は、やはりセキュリティの問題が浮上します。パッケージの開発者にとって、他のパッケージから内部処理専用のクラスにアクセスされるのは好ましいことではありません。

そこで、通常のクラス及びインタフェース宣言では、他のパッケージからアクセスすることはできないようになっています。しかし、パッケージの目的は再利用可能なクラス群を外部に公開することにあります。クラスやインタフェースを、他のパッケージからも見えるようにするには、public アクセス修飾子を宣言で指定します。

クラスとインタフェース宣言に用いることができるアクセス修飾子は public のみです。宣言で public が指定されているクラス、またはインタフェースであれば、他のパッケージからも利用できるようになるため、再利用可能な汎用クラスを公開するクラスライブラリを作成することができます。他のパッケージから名前付きパッケージの型を利用するには、パッケージ名から指定しなければなりません。

もう 1 つ、public を指定したクラス及びインタフェース宣言で重要な注意点があります。他のパッケージに対しても公開する型を宣言する場合は、ソースファイルの名前をその型の識別子と同じにしなければなりません。最初に、Java のソースファイルの名前はクラス名と同じにする必要があると説明したのはこのためです。ソース内に他のパッケージに公開する型が存在しない場合は、ソースファイルの名前が制限されることはありません。

例えば、public 修飾子を指定した Kitty クラスを宣言する場合、ファイル名は Kitty.java でコンパイルされなければならないのです。

コード2
package dokuhon.java;
public class A {}
class Test {
	public static void main(String args[]) {
		dokuhon.java.A obj = new dokuhon.java.A();
		System.out.println(obj);
	}
}
実行結果
>java Test
dokuhon.java.A@5224ee

コード2は、dokuhon.java パッケージのメンバ A クラスを宣言しています。A クラスは public アクセス修飾子を指定した公開メンバなので、ソースファイルは A.java という名前で独立させなければなりません。public が指定されているクラスやインタフェースが、他のパッケージから利用することができるか実験を行うため、無名パッケージの Test.java ファイル内で A クラスのインスタンスを作成しています。もし、A.java が正しくコンパイルされ、適切なディレクトリに配置されていれば、Test.java をコンパイルすることができるでしょう。A クラスを利用するには、先に A.java をコンパイルしておく必要があります。

他のパッケージから A クラスを利用するには、A クラスが所属するパッケージ名から指定する必要があります。このように、パッケージ名を含めたクラスやインタフェースのフルネームのことを完全限定名と呼びます。コード2の A クラスの完全限定名は dokuhon.java.A となります。無名パッケージのクラスやインタフェースは、その単純名が完全限定名であると考えることができます。また、プリミティブ型の場合は int や char などのキーワードが完全限定名となります。

同一パッケージ内の型を利用する場合は単純名でコンパイルすることができましたが、他のパッケージの型を利用するには完全限定名で指定しなければなりません。そうすることによって、多くのクラス群を提供するパッケージを併用しても、名前が衝突することはありません。パッケージの開発者は、他の開発者のライブラリとの名前の衝突を気にすることなく、直観的な名前をクラスやインタフェースに命名することができます。

とはいっても、インターネットで配布するなどの公開を行う場合、パッケージに適当な名前をつけてはいけません。Java は、広く公開するパッケージの名前には、小文字の ASCII 文字でインターネット・ドメイン名を先頭に指定することを推奨しています。つまり、パッケージ名を com や org で始め、そのサブパッケージに開発チームの名前などを指定します。もしくは、ISO で定められている英字2文字の国別コードでも良いとされています。この規則に従ったパッケージ名を一意なパッケージ名と呼びます。例えば次のようなものが考えられるでしょう。

org.omg.CORBA
jp.wisdomsoft.test

一意なパッケージ名は従わなければならないものではありません。しかし、インターネットで広くパッケージを配布する場合は、こうした命名規則には従うべきでしょう。