Dev Study
← 解説「パッケージはフォルダではなく名前」に戻る

サンプルコードで身につける: パッケージはフォルダではなく名前

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

1src/service に置いても動く

service; と書いたを src/service に、入口を src/app に置いた構成です。 でコンパイルと実行ができ、同じ配置を でもビルドできました。の接頭辞も も、Java としては必須ではありません。ただしこれは を載せていない素の Java で、Boot では app と service の上に共通の根のが要ります。

src/service/UserService.java と src/app/Main.java をビルドして実行(JDK 25 / Maven 3.9.16。各1回)コマンド出力
$ find src -type f | sort
src/app/Main.java
src/service/UserService.java
[exit 0]
 
$ javac -d out -sourcepath src src/app/Main.java
[exit 0]
 
$ find out -type f | sort
out/app/Main.class
out/service/UserService.class
[exit 0]
 
$ java -cp out app.Main
user-1 (Main is app.Main, service is service.UserService)
[exit 0]
 
# 同じ配置を Maven でビルドする。pom.xml に足したのは次の3行(Maven 3.9.16)
<build>
<sourceDirectory>src</sourceDirectory>
</build>
 
$ mvn -B -o -ntp -q package; echo "mvn exit $?"
mvn exit 0
[exit 0]
 
$ find target/classes -type f | sort; ls target/*.jar
target/classes/app/Main.class
target/classes/service/UserService.class
target/flat-1.0.0.jar
[exit 0]
 
$ jar tf target/flat-1.0.0.jar | grep class
app/Main.class
service/UserService.class
[exit 0]
 
$ java -cp target/flat-1.0.0.jar app.Main
user-1 (Main is app.Main, service is service.UserService)
[exit 0]

2package 宣言と、出力先と、起動の仕方

package com.example.demo; と書いたクラスをコンパイルすると、出力は out/com/example/demo/Hello.class になります。起動できるのは、の起点(out)との両方が合っているときだけです。ファイルのあるディレクトリへ移動して Hello としても、wrong name のエラーになります。末尾は、名前をそのままファイル名にした平らな置き方で、これも見つかりません。

javac と java の実行結果(JDK 25)コマンド出力
$ find src -type f
src/com/example/demo/Hello.java
[exit 0]
 
$ javac -d out src/com/example/demo/Hello.java
[exit 0]
 
$ find out -type f
out/com/example/demo/Hello.class
[exit 0]
 
##### (2) how to run it
$ java -cp out com.example.demo.Hello
Hello from com.example.demo.Hello
[exit 0]
 
$ java -cp out Hello
Error: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: Hello
[exit 1]
 
$ cd out/com/example/demo && ls && java Hello
Hello.class
Error: Could not find or load main class Hello
Caused by: java.lang.NoClassDefFoundError: Hello (wrong name: com/example/demo/Hello)
[exit 1]
 
$ cd out/com/example/demo && java -cp . com.example.demo.Hello
Error: Could not find or load main class com.example.demo.Hello
Caused by: java.lang.ClassNotFoundException: com.example.demo.Hello
[exit 1]
 
$ java -cp out com/example/demo/Hello
Hello from com.example.demo.Hello
[exit 0]
 
$ java -cp out/com/example/demo Hello
Error: Could not find or load main class Hello
Caused by: java.lang.NoClassDefFoundError: Hello (wrong name: com/example/demo/Hello)
[exit 1]
 
##### (2b) flat file name does not work
$ mkdir -p flat && cp out/com/example/demo/Hello.class flat/com.example.demo.Hello.class && ls flat
com.example.demo.Hello.class
[exit 0]
 
$ java -cp flat com.example.demo.Hello
Error: Could not find or load main class com.example.demo.Hello
Caused by: java.lang.ClassNotFoundException: com.example.demo.Hello
[exit 1]

3同じ名前のクラスが2つの JAR にあるとき

com.example.util.Version という同じ名前のクラスを、a.jar と b.jar の両方に入れた実験です。 に並べる順番を入れ替えるだけで、エラーも警告も無いまま、使われるクラスが変わりました。なお、入口の app.Main は逆ドメインの接頭辞を持たないパッケージですが、問題なく動いています。

クラスパスの順番を入れ替える(JDK 25)コマンド出力
$ javac -d dup/m -cp dup/a.jar dupmain/app/Main.java
[exit 0]
 
$ 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

4親子に見えるパッケージに特別な関係は無い

jp.co.example.shop の中だけで使えるクラス(修飾子なし)を、同じパッケージから使うと動き、jp.co.example.shop.order から使うとコンパイルエラーになりました。下の階層は別のパッケージで、親の中身を見る特権はありません。(JLS 7.1)にも there is no special access relationship と書かれています。

パッケージの中だけで使えるクラスを、下の階層から使う(JDK 25)コマンド出力
$ find pp -type f
pp/jp/co/example/shop/order/OrderService.java
pp/jp/co/example/shop/SameshopUser.java
pp/jp/co/example/shop/ShopSecret.java
[exit 0]
 
$ javac -d outpp pp/jp/co/example/shop/ShopSecret.java pp/jp/co/example/shop/SameshopUser.java && java -cp outpp jp.co.example.shop.SameshopUser
secret
[exit 0]
 
$ javac -d outpp pp/jp/co/example/shop/ShopSecret.java pp/jp/co/example/shop/order/OrderService.java
pp/jp/co/example/shop/order/OrderService.java:3: error: ShopSecret is not public in jp.co.example.shop; cannot be accessed from outside package
import jp.co.example.shop.ShopSecret;
^
pp/jp/co/example/shop/order/OrderService.java:6: error: ShopSecret is not public in jp.co.example.shop; cannot be accessed from outside package
String read() { return ShopSecret.value(); }
^
2 errors
[exit 1]

5Node は場所で指し、Java は名前で指す

TypeScript では、自分のコードは相対パスで、ライブラリはパッケージ名で します。ライブラリの実体は node_modules という別の場所にあります。Java は、自分のコードもライブラリも同じ完全修飾名で指し、同じクラスパスから探します。世界で重ならない名前を、npm ではパッケージ名(と)が担い、Java ではクラスの名前そのものが担っています。

// TypeScript / Node: 自分のコードは「場所」で指す(このファイルから見た相対パス)
import { OrderService } from './order.service.js';

// ライブラリは「パッケージ名」で指す。実体は node_modules という別の場所にある
import { Injectable } from '@nestjs/common';

// Java では、自分のコードもライブラリも同じ書き方(完全修飾名)で指す
//   import jp.co.example.shop.order.OrderService;      自分のコード
//   import org.springframework.stereotype.Service;      ライブラリ
// どちらも「クラスパスのどこかにある、この名前のクラス」という意味で、場所は書かない

@Injectable()
export class OrderFacade {
  constructor(private readonly orders: OrderService) {}
}

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