はじめに
自己紹介
皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。
いずれもJava EE(Jakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。
Udemy講座のご紹介
この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをQiita内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。
この講座は、以下のような皆様にお薦めします。
- Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
- 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
- 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
- 今後、フリーランスエンジニアとしてのキャリアを検討している方
- 「Chat GPT」のエンジニアリングへの活用に興味のある方
- 「Oracle認定Javaプログラマ」の資格取得を目指している方
- IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方
この記事を含むシリーズ全体像
この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。
9.1 パッケージとインポート
チャプターの概要
このチャプターでは、クラス名を分類するための仕組みであるパッケージと、異なるパッケージからクラスをインポートする方法について学びます。
9.1.1 パッケージ
パッケージとは
すでに説明したように、1つのJavaランタイムの中において、同一の名前を持つ複数のクラスを宣言することはできません。従ってJavaアプリケーションの規模が大きくなると、クラス名が衝突する可能性が生じます。そこでパッケージを利用することによって、この問題の発生を回避します。
Javaにおけるパッケージとは、クラス名を階層構造に分類するための仕組みです。パッケージは、インターネット環境におけるドメインのように、各階層の識別名をドットで区切って表記します。ただしパッケージはドメインとは逆に、最上位は一番左であり、右に行くにしたがって下位になります。
ここで説明のために、文字列を表すStringクラスを例に取り上げます。Stringクラスは、Java SEのクラスライブラリによって提供されるものですが、そのパッケージは「java.lang」です。従ってパッケージ名とクラス名を足し合わせた名前は「java.lang.String」になります。「java」から始まるパッケージ名は、Java SEによって提供されるクラスライブラリであることを意味しています。Java SEには「java.lang」以外にも、「java.util」「java.io」といった様々なパッケージがあり、数多くのクラスがその特性に応じて分類されています。
このようにパッケージ名とクラス名を足し合わせた名前を、FQCN(Fully Qualified Class Name;完全修飾クラス名)と呼び、FQCNからパッケージ名を取り除いた部分を単に「クラス名」または「単純名」と呼びます。
なおチャプター7.1で取り上げたPersonクラスのように、クラスに対して明示的にパッケージ名を付与しない場合、名前のないパッケージ(これをデフォルトパッケージと呼ぶ)に所属していることになります。デフォルトパッケージの使用は、実際のJavaアプリケーション開発では回避した方が望ましいでしょう。
パッケージの命名規約
パッケージもクラスなどと同様に一種の識別子のため、レッスン2.1.5で触れたルールに則ってさえいれば、任意の名前を付けることができます。ただしパッケージの個々の階層には、英語小文字と数字のみを用いて命名するのが、一般的なコーディング上の規約です。
パッケージの命名において、1つの単語が長くなりすぎてしまった場合は、アンダースコアで区切ることもあります。ただし例えば"springframework"のように、意味的な区切りを入れたくない場合は、多少長くても英語小文字と数字のみで名前を付けるケースもあります。
パッケージ命名の考え方
先に「1つのJavaランタイム内で同一の名前を持つ複数のクラスは宣言不可」という話をしましたが、ここで言う「名前」はFQCNを指します。つまりFQCNさえ重複しなければ、クラス名は衝突しません。例えば本コースで独自に「pro.kensait.String」というクラスを作成したとしても、このクラスは「java.lang.String」とは別物として扱われます。
Javaによるアプリケーション開発は世界中で行われており、様々なクラスライブラリがコミュニティやベンダーによって提供されています。これらのクラスライブラリ同士の名前衝突を回避するためにも、FQCNは「世界中で一意になるような名前を付ける」というのが基本的な考え方です。そのためパッケージ名には、ドメイン名を逆さにした名前が使われるケースが一般的です。例えばGoogle社からは様々なOSSのクラスライブラリが提供されていますが、そのパッケージ名は「com.google」から始まります。
パッケージの宣言
クラスに対して何らかのパッケージを宣言する場合、パッケージ名をpackage文によって宣言します。
以下の構文を見てください。
package パッケージ名;
package文は、クラスの中で1行目に一度だけ宣言する必要があります(違反するとコンパイルエラー)。
例えばあるクラスに対してpro.kensait.pkg1パッケージを宣言する場合は、1行目に以下のように記述します。
package pro.kensait.pkg1;
パッケージとクラスファイル
パッケージを宣言した場合、そのクラスのクラスファイルの配置場所は、パッケージの階層構造に合わせる必要があります。
例えばpro.kensait.pkg1パッケージに属するFooクラスの場合、クラスファイルであるFoo.classは、proディレクトリ→kensaitディレクトリ→pkg1ディレクトリの下に配置します。
【図9-1-1】パッケージとクラスファイルの関係
+-- 任意のディレクトリ(クラスパスが設定されている)
+-- pro
+-- kensait
+-- pkg1
+-- Foo.class
この理由には、javaコマンドによってクラスを実行するときの仕組みが関係しています。javaコマンドについてはセクション2.1で説明済みですが、このときは実行対象のクラスはデフォルトパッケージのものになっていました。デフォルトパッケージではなく、何らかのパッケージに所属するメインクラスを実行する場合は、以下のように引数にFQCNを指定します。
java pro.kensait.pkg1.Foo
このようにjavaコマンドを投入すると、クラスパス(デフォルトはカレントディレクトリ)を起点に、パッケージ階層に合わせたディレクトリが検索され、そこに置かれたFoo.classファイルが実行されます。
9.1.2 インポート
異なるパッケージへのアクセス
これまでのレッスンはすべてのクラスがデフォルトパッケージという前提でしたが、あるクラスから異なるパッケージのクラスにアクセスするためには、何らかの方法によってクラスの名前を解決する必要があります。
例として、以下のようなFQCNを持つ4つのクラスがあるものとします。
- pro.kensait.pkg1.Foo
- pro.kensait.pkg1.Bar
- pro.kensait.pkg2.Baz
- pro.kensait.pkg1.util.Qux
FooクラスからBarクラスへのアクセスのように、同一のパッケージに所属しているクラスに対しては、クラス名だけでアクセスが可能です。逆にFooクラスからBazクラスへのアクセスのように、異なるパッケージに所属しているクラスに対しては、クラス名だけではアクセスできません。またFooクラスからQuxクラスへのアクセスも、パッケージは階層が異なると上位・下位に関わらず異なるパッケージと見なされるため、同じくクラス名だけではアクセスできません。
異なるパッケージのクラス名を解決するためには、都度FQCNを指定するか、あるいは後述するアクセス対象クラスをインポートするか、いずれかの方法を取る必要があります。これらの方法については、この後説明します。
FQCNによる名前解決
パッケージが異なるクラスの名前を解決する方法の1つに、都度FQCNを指定する方法があります。例えばFooクラスの中で、パッケージの異なるBazクラスのインスタンスを生成するコードは、以下のようになります。
pro.kensait.pkg2.Baz baz = new pro.kensait.pkg2.Baz();
ただし上記のコードは、一見しても必ずしも読みやすいコードには見えないでしょう。またBazクラスが登場するたびに毎回FQCNを指定しなければならない、というのも冗長です。そういった点から、異なるパッケージへのアクセスには、特別な理由がない限りはもう1つの方法であるインポートを使用するようにしてください。
インポートによる名前解決
次に、インポートによって名前解決する方法について説明します。この方法では、以下の構文のようにimport文を記述します。
import アクセス対象クラスのFQCN;
このようにimport文にアクセス対象のクラスのFQCNを指定します。import文は、クラス宣言の外側に記述する必要がありますが、1行目はそのクラス本体のpackage文を記述するため、package文とclass宣言の中間部分に記述します。
例えば、先に挙げたpro.kensait.pkg1.Fooクラスからpro.kensait.pkg2.Bazクラスにアクセスするためには、Fooクラスのコードは以下のようになります。
package pro.kensait.pkg1;
import pro.kensait.pkg2.Baz; //【1】
class Foo {
void doSomething() {
Baz baz = new Baz(); //【2】
}
}
このコードのように、ひとたびあるクラスをインポートする【1】と、そのクラスの中ではクラス名を指定する【2】だけで名前解決が可能です。このようにインポートを使用すると、都度FQCNを指定するよりも簡潔にコードを記述することができます。
オンデマンドインポート
インポートには、単一インポートとオンデマンドインポートの2種類があります。
先に説明したimport文は個々のクラスのFQCNを指定していましたが、これは単一インポートと呼ばれているものです。もう1つのオンデマンドインポートは、同一パッケージに所属するすべてのクラスを一括でインポートする、というものです。
以下の構文を見てください。
import アクセス対象クラスのパッケージ.*;
このようにオンデマンドインポートではパッケージ名のみを指定し、最後に"*"を記述します。
例えばpro.kensait.pkg2パッケージのクラスをオンデマンドインポートする場合は、以下のようにimport文を記述します。
import pro.kensait.pkg2.*;
このようにオンデマンドインポートをしても、そのパッケージに所属するすべてのクラスが「読み込まれて」しまい、リソースに影響が出る、というわけではありません。あくまでもオンデマンドインポートは、一括して名前解決するための仕組みです。
またオンデマンドインポートは、構造上の下位のパッケージまでインポートされるわけではありません。例えば上記のようにimport文を宣言した場合、「pro.kensait.pkg2.utilパッケージ」までインポートされるわけではないため、注意が必要です。
単一インポートとオンデマンドインポートのどちらを選択するべきかについては、一概には決められません。ただし単一インポートの方が「どういったクラスをインポートしているのか」を一目瞭然で確認できるため、基本的には単一インポートを使用した方が良いでしょう。
名前衝突の優先順位
パッケージが異なる同一名のクラスが複数あり、そのクラス名をインポートによって解決する場合、どのような優先順位になるのかを理解しておく必要があります。
例えばBazというクラスが、3つのパッケージ、pro.kensait.pkg1、pkg2、pkg3に、それぞれ存在しているものとします。このとき、pro.kensait.pkg1パッケージに所属するFooクラスにおいて、「Baz」というクラス名をコードに記述した場合に、どのように名前解決が行われるのかを説明します。
まず以下にFooクラスのコードを示します。
package pro.kensait.pkg1;
import pro.kensait.pkg2.Baz; //【1】
import pro.kensait.pkg3.*; //【2】
class Foo {
void doSomething() {
Baz baz = new Baz(); //【3】
}
}
この例では、3つあるBazクラスのうち1つは、Fooクラスと同一パッケージ(pkg1)に存在しています。またpkg2のBazクラスが単一インポート【1】で指定され、pkg3のBazクラスもオンデマンドインポート【2】で指定されています。
このコードにおいて、【3】で「Baz」とクラス名を記述していますが、このBazは以下のような優先順位で名前解決されます。
(1)単一インポートの指定にBazクラスがあったら、それが採用される。
(2)同一パッケージ内にBazクラスがあったら、それが採用される。
(3)オンデマンドインポートに指定されたパッケージの中にBazクラスがあったら、それが採用される。
この例では単一インポートが指定されており、(1)の条件に合致することになるため、採用されるのはpro.kensait.pkg2パッケージのBazクラスになります。
Java SEクラスライブラリのインポート
Java SEクラスライブラリを使用するときも、先に説明したいずれかの方法で名前を解決する必要がありますが、通常はインポートを使用します。
例えばjava.util.ArrayListクラスにアクセスするのであれば、以下のようにimport文を指定します。
import java.util.ArrayList;
ただしjava.langパッケージに限っては、すべてのクラスが自動的にインポートされるため、import文を記述する必要はありません。これまでStringクラスを使用するときに、特にインポートを意識する必要がなかったのは、このためです。
スタティックインポート
スタティックインポートとは、クラスが保持するスタティックなメンバーへのアクセスを、簡潔に記述するための機能です。スタティックなメンバーは必ず何らかのクラスに属しており、名前が衝突することはないため、この機能は名前解決というよりは、単にコードを簡潔に記述するためのものと考えるべきでしょう。
スタティックインポートにも、単一とオンデマンドがあります。単一のスタティックインポートは、以下のような構文になります。
import static FQCN.スタティックメンバー名;
このように、importキーワードの後ろにstatic修飾子を記述する点が特徴です。
次にオンデマンドのスタティックインポートは、以下のような構文になります。
import static FQCN.*;
例えばJava SEの数値計算ライブラリにjava.lang.Mathクラスがありますが、このクラスには数多くのスタティックメソッドがユーティリティとして用意されています。
Mathクラスのround()メソッド(四捨五入)を呼び出すコードは、以下のようになります。
long x = Math.round(3.6); // 4が返る
このときimport static文を以下のように記述し、round()メソッドをスタティックインポートすると、
import static java.lang.Math.round;
round()メソッドを呼び出すためのコードは、以下のようにクラス名を省略して記述することができます。
long x = round(3.6); // 4が返る
このようにスタティックインポートを使用すると、同じスタティックメソッドを何度も呼び出すケースでは、コードを簡潔に記述することができます。特にユーティリティメソッドの呼び出しを多用するケースでは、積極的に使用すると良いでしょう。
このチャプターで学んだこと
このチャプターでは、以下のことを学びました。
- パッケージはクラス名を階層構造に分類するための仕組みであること。
- FQCNの意味やクラス名の衝突を回避する仕組みについて。
- パッケージの命名は、ドメイン名を逆さにした名前が使われるケースが一般的であること。
- パッケージ階層とクラスファイルの配置場所の関連性について。
- インポートによる名前解決の方法について。
- 単一インポートとオンデマンドインポートの違いについて。
- スタティックインポートの機能について。