Dev Study
← 解説「この設計の代償と、後の言語がしたこと」に戻る

サンプルコードで身につける: この設計の代償と、後の言語がしたこと

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

11996年の仕様は何と書いているか

最初の Java (第1版、1996年8月の印刷本を HTML にしたもの)から、の名前と置き場所に関わる文を抜き出しました。は「コンパイラが強制しない慣習」、ディレクトリへの対応は「ごく単純な例」として書かれています。ドメイン名を使う理由は最後の文にあり、同じ趣旨の文は現行の 25 の仕様(6.1 節)にも残っています。

The Java Language Specification 第1版(1996年)7章から逐語。# は補足表
7章の冒頭
Packages that are stored in a file system have certain constraints on the organization of their compilation units to allow a simple implementation to find classes easily.
# ファイルシステムに置く場合の制約は「単純な実装がクラスを簡単に見つけられるように」
 
7.2.1(パッケージをファイルシステムに置く例)
As an extremely simple example, all the Java packages and source and binary code on a system might be stored in a single directory and its subdirectories.
In fact, the standard JavaSoft Java Developer's Kit on UNIX differs from the very simple discipline described here only in that it provides a CLASSPATH environment variable that specifies a set of directories, each of which is treated like the single directory described here.
# 当時の JDK は、この単純な方式に CLASSPATH を足しただけだった
 
7.7 Unique Package Names
This section specifies a standard convention, not enforced by a Java compiler, for generating such unique package names.
# 例に挙がる名前は COM.Sun.java.jag.scrabble のように先頭が大文字だった
The name of a package is not meant to imply anything about where the package is stored within the Internet ...
# 名前を借りているだけで、そのドメインから取得できるという意味は無い
The Java convention for generating unique package names is merely a way to piggyback a package naming convention on top of an existing, widely known unique name registry instead of having to create a separate registry for Java package names.
# 既にある一意な名前の登録簿に相乗りし、Java 専用の登録簿を別に作らずに済ませる

2クラスパスの2つの性質を手元で確かめる

の仕様の申請文( 376)は、について、足りない部品は使おうとするまで分からないことと、同じパッケージのが別々の部品から読まれうることの2点を挙げています。コードは1つ目の再現で、コンパイル後に Helper.class を消しても step 2 までは動き、Helper を初めて使う行でになります。下の実物の (2) は同じ名前のクラスを持つ を2本並べた場合で、エラーも警告も出ず、先に書いた方だけが使われます。

JSR 376 の申請文(逐語)と、クラスパスの実測(JDK 25.0.4.1)コマンド出力
# JSR 376(https://jcp.org/en/jsr/detail?id=376)から
# The class path cannot express relationships between components, so if a necessary component is missing then that will not be discovered until an attempt is made to use it.
# The class path also allows classes in the same package to be loaded from different components, leading to unpredictable behavior and difficult-to-diagnose errors.
 
# (1) 依存の JAR(commons-lang3)をクラスパスに並べ忘れた
$ java -cp out com.example.app.Main
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
at com.example.greet.Greeter.greet(Greeter.java:7)
at com.example.app.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils
...
[exit 1]
 
$ java -cp out:lib/commons-lang3-3.21.0.jar com.example.app.Main
Hello, World!
[exit 0]
 
# (2) 同じ完全修飾名のクラスが a.jar と b.jar の両方にある
$ java -cp dup/m:dup/a.jar:dup/b.jar app.Main
from a.jar
file:/w/dup/a.jar
[exit 0]
 
$ java -cp dup/m:dup/b.jar:dup/a.jar app.Main
from b.jar
file:/w/dup/b.jar
[exit 0]
Java

3モジュールは何本の JAR が宣言しているか

Java 9(一般提供 2017-09-21)で言語に足されたモジュールは のモジュールとは別物で、宣言しなくても従来どおりクラスパスで動きます。 4.1.1 の Web アプリ(雛形1つ)が同梱する JAR 34本のうち、モジュール宣言()を持つのは16本で、 自身の16本と の2本はにモジュール名を1行書いているだけでした。モジュール宣言を持つ も、クラスパスに置けば「名前の無いモジュール」に入ります。

実行可能 JAR の BOOT-INF/lib/ 34本の走査と、置き方による違い(Spring Boot 4.1.1 / JDK 25.0.4.1)コマンド出力
total 34 {'module-info': 16, 'Automatic-Module-Name': 18}
module-info commons-logging-1.3.6.jar META-INF/versions/9/module-info.class
module-info jackson-annotations-2.21.jar module-info.class
module-info jackson-core-3.1.5.jar module-info.class
module-info jackson-databind-3.1.5.jar module-info.class
module-info jakarta.annotation-api-3.0.0.jar module-info.class
module-info jspecify-1.0.1.jar module-info.class
module-info jul-to-slf4j-2.0.18.jar META-INF/versions/9/module-info.class
module-info log4j-api-2.25.5.jar module-info.class
module-info log4j-to-slf4j-2.25.5.jar module-info.class
module-info logback-classic-1.5.38.jar module-info.class
module-info logback-core-1.5.38.jar module-info.class
Automatic-Module-Name micrometer-commons-1.17.1.jar micrometer.commons
Automatic-Module-Name micrometer-observation-1.17.1.jar micrometer.observation
module-info slf4j-api-2.0.18.jar META-INF/versions/9/module-info.class
module-info snakeyaml-2.6.jar META-INF/versions/9/module-info.class
Automatic-Module-Name spring-aop-7.0.9.jar spring.aop
Automatic-Module-Name spring-beans-7.0.9.jar spring.beans
Automatic-Module-Name spring-boot-4.1.1.jar spring.boot
Automatic-Module-Name spring-boot-autoconfigure-4.1.1.jar spring.boot.autoconfigure
...
# spring-boot-* は計9本、spring-* 7.0.9 は計7本。16本すべて Automatic-Module-Name
module-info tomcat-embed-core-11.0.24.jar module-info.class
module-info tomcat-embed-el-11.0.24.jar module-info.class
module-info tomcat-embed-websocket-11.0.24.jar module-info.class
 
# 同じ JAR をクラスパスに置いた場合(@ の後ろの値は実行ごとに変わる)
$ java -cp spring-core-7.0.9.jar:slf4j-api-2.0.18.jar Who.java
org.springframework.core.SpringVersion -> unnamed module @4450d156 named=false
org.slf4j.Logger -> unnamed module @4450d156 named=false
java.lang.String -> module java.base named=true
[exit 0]
 
# モジュールパスに置いた場合
$ java -p (module path) --add-modules spring.core,org.slf4j Who.java
org.springframework.core.SpringVersion -> module spring.core named=true
org.slf4j.Logger -> module org.slf4j named=true
java.lang.String -> module java.base named=true
[exit 0]

4Kotlin はソースの置き場所だけを外した

は Java と同じ で動く言語で、公式の規約は、Kotlin だけのプロジェクトならソースの置き場所から共通の接頭辞を省くよう勧めています。 jp.co.example.shop.service と宣言したファイルを src/service/ に置いてもコンパイルは通りましたが、出力の は jp/co/example/shop/service/ の下に出ます。省けるのはソースの側だけで、Java と混在するプロジェクトでは Java と同じ構成にするよう同じ規約に書かれています。

Kotlin 2.4.20 / JDK 25.0.4.1 での実測と、Coding conventions の逐語コマンド出力
# src/service/OrderService.kt の1行目
package jp.co.example.shop.service
 
--- sources
src/model/Order.kt
src/service/OrderService.kt
--- classes
target/classes/jp/co/example/shop/model/Order.class
target/classes/jp/co/example/shop/service/OrderService.class
target/classes/jp/co/example/shop/service/OrderServiceKt.class
target/classes/META-INF/shop.kotlin_module
--- run
$ java -cp target/classes:kotlin-stdlib-2.4.20.jar jp.co.example.shop.service.OrderServiceKt
order 42 from jp.co.example.shop.service.OrderService
jp.co.example.shop.model.Order
[exit 0]
 
# Kotlin Coding conventions「Directory structure」から
# In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted.
# On JVM: In projects where Kotlin is used together with Java, Kotlin source files should reside in the same source root as the Java source files, and follow the same directory structure: each file should be stored in the directory corresponding to each package statement.

5言語ごとに、一意な名前をどこから取り、どこに書くか

名前が重ならないようにする方法と、その長い名前がどこに現れるかを、各言語の公式文書の範囲で並べた表です。Go は既存の一意な名前(URL)を借りる点で Java と同じ発想で、違いは名前がディレクトリに現れないことと、名前が取得先を兼ねることです。これらの言語が Java の規則を見て方式を決めたと述べた資料は見つかっていないので、表が示すのは「方式が違う」ことまでです。

名前の出所と、長い名前が現れる場所(Java の行の実測は JDK 25、Kotlin は 2.4.20)表
Java
一意な名前の出所 : ドメイン名を逆さにして借りる(慣習。登録簿は作らない)
長い名前が現れる所 : 各ファイルの package 宣言と、ソースと .class の両方のディレクトリ
同じ名前が2つある時: 1本のクラスパスで、先に見つかった方だけが使われる
 
Kotlin(JVM)
一意な名前の出所 : Java と同じパッケージ名
長い名前が現れる所 : package 宣言と .class のディレクトリ。ソースのディレクトリは、Kotlin だけのプロジェクトなら共通の接頭辞を省ける
 
Node
探し方 : 呼び出したファイルの場所から上へ node_modules を探す
長い名前が現れる所 : アプリ内の import は相対パスで書くので、ソースのディレクトリには現れない
同じ名前が2つある時: パッケージごとに自分用の依存を持てるので、別の版が共存できる
 
Rust
一意な名前の出所 : crates.io に公開するクレートの名前は、先着順で割り当てられる(クレートはコンパイルと配布の単位)
長い名前が現れる所 : クレートの中のパスはクレートの根から始まるので、ソースのディレクトリには現れない
同じ名前が2つある時: 同じクレートの別の版が1つのビルドに入りうる(型は別物として扱われる)
 
Go
一意な名前の出所 : URL を借りる(名前は取得先も兼ねる)
長い名前が現れる所 : go.mod に1回と、import の文字列。ディレクトリは go.mod の場所からの相対
Java との違い : Java の仕様は、名前はインターネット上の所在(取得先)を意味しない、と書いている

図やコードに出てくる用語(タップで説明)