はじめに

Javaでアプリケーションを開発する際、コードが増えるにつれて「どこに何があるかわからない」「名前が衝突する」「内部の実装を勝手に使われる」といった問題が発生しがちです。これらの問題を解決するのが、パッケージアクセス修飾子です。

この記事では、パッケージの基本概念から命名規則、import文の使い方、4つのアクセス修飾子の使い分け、そしてJava 9以降で導入されたモジュールシステムの概要まで、段階的に解説します。大規模プロジェクトでもコードを適切に整理・保護できるようになることを目標とします。

パッケージとは何か

パッケージ(Package)とは、関連するクラスやインターフェースをグループ化するための仕組みです。ファイルシステムにおけるフォルダのような役割を果たし、コードの整理と名前空間の管理を実現します。

パッケージの3つの役割

パッケージには、主に以下の3つの重要な役割があります。

役割 説明
コードの整理 関連するクラスを論理的にグループ化し、可読性を向上させる
名前空間の管理 異なるパッケージでは同じクラス名を使用できる
アクセス制御 パッケージ単位でのアクセス制限を実現する

パッケージの構造

パッケージ名はディレクトリ構造と対応しています。

flowchart TB
    subgraph "ファイルシステム"
        direction TB
        src["src/"]
        com["com/"]
        example["example/"]
        app["app/"]
        model["model/"]
        service["service/"]
        User["User.java"]
        Order["Order.java"]
        UserService["UserService.java"]
        
        src --> com
        com --> example
        example --> app
        app --> model
        app --> service
        model --> User
        model --> Order
        service --> UserService
    end
    
    subgraph "パッケージ"
        direction TB
        P1["com.example.app.model"]
        P2["com.example.app.service"]
    end

この例では、User.javaOrder.javacom.example.app.modelパッケージに、UserService.javacom.example.app.serviceパッケージに属します。

パッケージの宣言

クラスをパッケージに所属させるには、ソースファイルの先頭にpackage文を記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ファイル: src/com/example/app/model/User.java
package com.example.app.model;

public class User {
    private String name;
    private String email;
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    // getter/setter省略
}

package文は必ずソースファイルの最初(コメントを除く)に記述する必要があります。

デフォルトパッケージ

package文を省略したクラスはデフォルトパッケージ(無名パッケージ)に属します。

1
2
3
4
5
6
// package文がない場合、デフォルトパッケージに属する
public class SimpleApp {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

デフォルトパッケージは以下の理由から推奨されません。

問題点 説明
他パッケージからアクセス不可 名前付きパッケージからデフォルトパッケージのクラスをimportできない
名前衝突のリスク 名前空間がないため、クラス名の衝突が起きやすい
整理が困難 プロジェクトが大きくなると管理が難しくなる

小さなテストプログラム以外では、必ずパッケージを使用するようにしましょう。

パッケージの命名規則

パッケージ名には業界で広く採用されている命名規則があります。これに従うことで、他のライブラリやプロジェクトとの名前衝突を防げます。

逆ドメイン名規則

パッケージ名は、所属する組織のインターネットドメイン名を逆順にしたものを先頭に付けるのが標準的な規則です。

ドメイン パッケージ名の先頭
example.com com.example
oracle.com com.oracle
google.com com.google
github.io io.github

命名規則の詳細

パッケージ名を決める際は、以下のルールに従います。

ルール 説明
小文字のみ すべて小文字で記述する com.example.myapp
英数字とアンダースコア 使用できるのは英数字と_のみ com.example.my_app
数字で始めない 各セグメントは数字で始めてはいけない com.example.app2 は可、com.example.2app は不可
予約語を避ける Javaの予約語は使用しない intclasspublicなどは避ける
ドット区切り 階層を.で区切る com.example.app.service

実際のパッケージ構成例

一般的なJavaプロジェクトでは、以下のようなパッケージ構成が採用されます。

com.example.myapp
├── controller       # Webコントローラー
├── service          # ビジネスロジック
├── repository       # データアクセス層
├── model            # ドメインモデル
│   ├── entity       # エンティティクラス
│   └── dto          # データ転送オブジェクト
├── config           # 設定クラス
├── exception        # カスタム例外
└── util             # ユーティリティクラス

この構成は、レイヤードアーキテクチャクリーンアーキテクチャの考え方に基づいています。機能ごとにパッケージを分けることで、責任の分離と保守性の向上を実現できます。

import文の使い方

異なるパッケージのクラスを使用するには、import文を使ってクラスを参照します。

基本的なimport

クラスを個別にimportする方法が最も基本的です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.example.app.service;

import com.example.app.model.User;
import com.example.app.model.Order;

public class UserService {
    public User createUser(String name, String email) {
        return new User(name, email);
    }
    
    public Order createOrder(User user) {
        return new Order(user);
    }
}

ワイルドカードimport

パッケージ内のすべてのクラスをまとめてimportするには、*(ワイルドカード)を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.app.service;

import com.example.app.model.*;  // modelパッケージの全クラスをimport

public class UserService {
    public User createUser(String name, String email) {
        return new User(name, email);
    }
    
    public Order createOrder(User user) {
        return new Order(user);
    }
}

ただし、ワイルドカードimportには以下のデメリットがあるため、個別importが推奨されます。

デメリット 説明
可読性の低下 どのクラスを使用しているか一目でわからない
名前衝突のリスク 複数パッケージで同名クラスがあると曖昧になる
IDEの支援 ほとんどのIDEは個別importを自動挿入してくれる

完全修飾名による参照

import文を使わずに、完全修飾名(パッケージ名を含む完全なクラス名)で参照することもできます。

1
2
3
4
5
6
7
8
package com.example.app.service;

public class UserService {
    // 完全修飾名で参照
    public com.example.app.model.User createUser(String name, String email) {
        return new com.example.app.model.User(name, email);
    }
}

完全修飾名は、同名のクラスを区別する必要がある場合に有効です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import java.util.Date;

public class DateExample {
    public void compareDates() {
        // java.util.Dateはimport済み
        Date utilDate = new Date();
        
        // java.sql.Dateは完全修飾名で参照
        java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
    }
}

static import

クラスの静的メンバー(staticメソッドやstatic定数)を、クラス名を省略して使用できるのがstatic importです。

1
2
3
4
5
6
7
8
9
// 通常のimport
import java.lang.Math;

public class MathExample {
    public void calculate() {
        double result = Math.sqrt(Math.pow(3, 2) + Math.pow(4, 2));
        System.out.println("結果: " + result);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// static importを使用
import static java.lang.Math.sqrt;
import static java.lang.Math.pow;

public class MathExample {
    public void calculate() {
        // Mathクラス名を省略できる
        double result = sqrt(pow(3, 2) + pow(4, 2));
        System.out.println("結果: " + result);
    }
}

static importは、テストコードで特に有用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;

class UserServiceTest {
    @Test
    void testCreateUser() {
        UserService service = new UserService();
        User user = service.createUser("Taro", "taro@example.com");
        
        // Assertionsクラス名を省略してすっきり書ける
        assertEquals("Taro", user.getName());
        assertTrue(user.getEmail().contains("@"));
    }
}

static importのワイルドカード

静的メンバーもワイルドカードでまとめてimportできます。

1
2
3
4
5
6
7
8
import static java.lang.Math.*;

public class MathExample {
    public void calculate() {
        double result = sqrt(pow(3, 2) + pow(4, 2));
        double area = PI * pow(5, 2);  // PIもMathの静的フィールド
    }
}

ただし、static importの使いすぎは可読性を損なう場合があります。どのクラスのメソッドかわかりにくくなるため、頻繁に使用するメソッドや定数に限定して使用しましょう。

暗黙的なimport

java.langパッケージは自動的にimportされるため、明示的なimport文は不要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Example {
    public void demo() {
        // java.lang.Stringはimport不要
        String message = "Hello";
        
        // java.lang.Systemもimport不要
        System.out.println(message);
        
        // java.lang.Mathもimport不要
        double result = Math.sqrt(16);
    }
}

java.langパッケージには、StringSystemMathIntegerObjectなど、Javaプログラミングで最も基本的なクラスが含まれています。

4つのアクセス修飾子を理解する

Javaには4つのアクセス修飾子があり、クラス、フィールド、メソッド、コンストラクタへのアクセス範囲を制御します。

アクセス修飾子の一覧

修飾子 同一クラス 同一パッケージ サブクラス すべて
public
protected 不可
(なし)デフォルト 不可 不可
private 不可 不可 不可

アクセス範囲を図で表すと以下のようになります。

flowchart TB
    subgraph "アクセス範囲"
        direction TB
        Public["public - どこからでもアクセス可"]
        Protected["protected - 同一パッケージ + サブクラス"]
        Default["デフォルト - 同一パッケージのみ"]
        Private["private - 同一クラスのみ"]
    end
    
    Public --> Protected --> Default --> Private

public(公開)

public修飾子は最も広いアクセス範囲を持ち、どこからでもアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.app.model;

// publicクラス:どこからでもアクセス可能
public class User {
    // publicフィールド:どこからでもアクセス可能(非推奨)
    public String id;
    
    // publicメソッド:どこからでもアクセス可能
    public String getId() {
        return id;
    }
}

publicは以下のケースで使用します。

用途 説明
APIとして公開するクラス 外部から利用されることを想定したクラス
getter/setter カプセル化されたフィールドへのアクセス手段
ユーティリティメソッド 汎用的に使われるメソッド
定数 public static finalで定義する定数

private(非公開)

private修飾子は最も狭いアクセス範囲を持ち、同一クラス内からのみアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.app.model;

public class User {
    // privateフィールド:同一クラス内のみアクセス可能
    private String name;
    private String email;
    private String password;
    
    public User(String name, String email, String password) {
        this.name = name;
        this.email = email;
        // パスワードをハッシュ化(内部実装)
        this.password = hashPassword(password);
    }
    
    // privateメソッド:内部実装として隠蔽
    private String hashPassword(String rawPassword) {
        // ハッシュ化ロジック
        return "hashed_" + rawPassword;
    }
    
    public String getName() {
        return name;
    }
    
    // パスワードのgetterは意図的に公開しない
}

privateカプセル化の基本であり、以下のケースで使用します。

用途 説明
フィールド クラスの内部状態を保護する
ヘルパーメソッド クラス内部でのみ使用する補助的なメソッド
実装の詳細 外部に公開する必要のない内部ロジック

protected(継承用)

protected修飾子は、同一パッケージ内と、パッケージ外のサブクラスからアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.app.model;

public class Animal {
    // protectedフィールド:サブクラスからアクセス可能
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // protectedメソッド:サブクラスでオーバーライド可能
    protected void makeSound() {
        System.out.println("...");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.example.app.animal;  // 別パッケージ

import com.example.app.model.Animal;

public class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    
    @Override
    protected void makeSound() {
        // 親クラスのprotectedフィールドにアクセス可能
        System.out.println(name + "がワンワン吠えています");
    }
    
    public void bark() {
        // protectedメソッドを内部で呼び出し
        makeSound();
    }
}

protected継承を意識した設計で使用します。

用途 説明
サブクラスで使用するフィールド 継承階層内で共有するデータ
オーバーライド可能なメソッド サブクラスで動作をカスタマイズさせるメソッド
テンプレートメソッドパターン フレームワークの拡張ポイント

デフォルト(パッケージプライベート)

修飾子を何も付けない場合はデフォルトアクセス(パッケージプライベート)となり、同一パッケージ内からのみアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.app.internal;

// デフォルトクラス:同一パッケージ内のみアクセス可能
class InternalHelper {
    // デフォルトフィールド
    String data;
    
    // デフォルトメソッド
    void processData() {
        System.out.println("内部処理: " + data);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.example.app.internal;

public class PublicService {
    // 同一パッケージなのでInternalHelperにアクセス可能
    private InternalHelper helper = new InternalHelper();
    
    public void execute() {
        helper.data = "テストデータ";
        helper.processData();
    }
}

デフォルトアクセスはパッケージ内部の実装詳細を隠すのに有効です。

用途 説明
内部クラス パッケージ外に公開しない補助的なクラス
内部メソッド パッケージ内でのみ使用する処理
テストしやすさ テストクラスを同一パッケージに置くことでアクセス可能

アクセス修飾子の比較例

4つのアクセス修飾子の違いを、具体的なコードで確認しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.app.demo;

public class AccessModifierDemo {
    public String publicField = "public";
    protected String protectedField = "protected";
    String defaultField = "default";  // パッケージプライベート
    private String privateField = "private";
    
    public void publicMethod() {
        System.out.println("publicメソッド");
    }
    
    protected void protectedMethod() {
        System.out.println("protectedメソッド");
    }
    
    void defaultMethod() {
        System.out.println("デフォルトメソッド");
    }
    
    private void privateMethod() {
        System.out.println("privateメソッド");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.app.demo;  // 同一パッケージ

public class SamePackageClass {
    public void test() {
        AccessModifierDemo demo = new AccessModifierDemo();
        
        // 同一パッケージからのアクセス
        System.out.println(demo.publicField);     // 可
        System.out.println(demo.protectedField);  // 可
        System.out.println(demo.defaultField);    // 可
        // System.out.println(demo.privateField); // 不可(コンパイルエラー)
        
        demo.publicMethod();     // 可
        demo.protectedMethod();  // 可
        demo.defaultMethod();    // 可
        // demo.privateMethod(); // 不可(コンパイルエラー)
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.example.other;  // 別パッケージ

import com.example.app.demo.AccessModifierDemo;

public class DifferentPackageClass {
    public void test() {
        AccessModifierDemo demo = new AccessModifierDemo();
        
        // 別パッケージからのアクセス
        System.out.println(demo.publicField);     // 可
        // System.out.println(demo.protectedField); // 不可(サブクラスではない)
        // System.out.println(demo.defaultField);   // 不可
        // System.out.println(demo.privateField);   // 不可
        
        demo.publicMethod();     // 可
        // demo.protectedMethod(); // 不可
        // demo.defaultMethod();   // 不可
        // demo.privateMethod();   // 不可
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.other;  // 別パッケージ

import com.example.app.demo.AccessModifierDemo;

public class SubClass extends AccessModifierDemo {
    public void test() {
        // 別パッケージのサブクラスからのアクセス
        System.out.println(publicField);     // 可
        System.out.println(protectedField);  // 可(継承関係があるため)
        // System.out.println(defaultField); // 不可
        // System.out.println(privateField); // 不可
        
        publicMethod();     // 可
        protectedMethod();  // 可(継承関係があるため)
        // defaultMethod(); // 不可
        // privateMethod(); // 不可
    }
}

アクセス制御のベストプラクティス

適切なアクセス修飾子を選択することで、保守性が高くバグの少ないコードを書くことができます。

最小権限の原則

最も重要な原則は「必要最小限のアクセス権限を与える」ことです。

flowchart LR
    A["まずprivateを検討"] --> B["パッケージ内で必要?"]
    B -->|はい| C["デフォルトを検討"]
    B -->|いいえ| D["private"]
    C --> E["継承で必要?"]
    E -->|はい| F["protected"]
    E -->|いいえ| G["デフォルト"]
    F --> H["外部に公開?"]
    G --> H
    H -->|はい| I["public"]
    H -->|いいえ| J["現在の修飾子を維持"]

フィールドはprivateが基本

フィールドは原則としてprivateにし、必要に応じてgetter/setterを提供します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 悪い例:publicフィールド
public class BadUser {
    public String name;  // 外部から自由に変更可能
    public int age;      // 不正な値も設定できてしまう
}

// 良い例:privateフィールド + getter/setter
public class GoodUser {
    private String name;
    private int age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("名前は必須です");
        }
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年齢は0〜150の範囲で指定してください");
        }
        this.age = age;
    }
}

カプセル化のメリットは以下の通りです。

メリット 説明
バリデーション 不正な値の設定を防止できる
変更の影響を局所化 内部実装を変更してもAPIは維持できる
副作用の管理 値の変更時に追加の処理を実行できる
読み取り専用の実現 setterを提供しなければ外部から変更不可

継承を意識したprotectedの使用

protectedは継承を意識して使用します。サブクラスでオーバーライドされることを意図したメソッドに適しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public abstract class AbstractRepository<T> {
    // サブクラスでカスタマイズ可能な接続処理
    protected Connection getConnection() {
        return DataSource.getConnection();
    }
    
    // テンプレートメソッド
    public T findById(Long id) {
        try (Connection conn = getConnection()) {
            return doFindById(conn, id);
        } catch (SQLException e) {
            throw new RepositoryException(e);
        }
    }
    
    // サブクラスで実装する抽象メソッド
    protected abstract T doFindById(Connection conn, Long id) throws SQLException;
}

パッケージプライベートの活用

パッケージプライベート(デフォルト)は、テストしやすさ内部実装の隠蔽のバランスを取るのに有効です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.app.service;

public class OrderService {
    // パッケージプライベートなメソッド(テストで直接呼び出せる)
    BigDecimal calculateDiscount(Order order) {
        // 割引計算ロジック
        if (order.getTotalAmount().compareTo(new BigDecimal("10000")) >= 0) {
            return order.getTotalAmount().multiply(new BigDecimal("0.1"));
        }
        return BigDecimal.ZERO;
    }
    
    public Order processOrder(Order order) {
        BigDecimal discount = calculateDiscount(order);
        order.applyDiscount(discount);
        return order;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.app.service;  // 同一パッケージ

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {
    @Test
    void testCalculateDiscount() {
        OrderService service = new OrderService();
        Order order = new Order(new BigDecimal("15000"));
        
        // パッケージプライベートメソッドに直接アクセス可能
        BigDecimal discount = service.calculateDiscount(order);
        
        assertEquals(new BigDecimal("1500.0"), discount);
    }
}

不変クラスの設計

フィールドをprivate finalにすることで、不変(イミュータブル)クラスを作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final class ImmutableUser {
    private final String name;
    private final String email;
    private final LocalDate birthDate;
    
    public ImmutableUser(String name, String email, LocalDate birthDate) {
        this.name = name;
        this.email = email;
        this.birthDate = birthDate;
    }
    
    public String getName() {
        return name;
    }
    
    public String getEmail() {
        return email;
    }
    
    public LocalDate getBirthDate() {
        return birthDate;
    }
    
    // setterは提供しない
    // 変更が必要な場合は新しいインスタンスを返す
    public ImmutableUser withEmail(String newEmail) {
        return new ImmutableUser(this.name, newEmail, this.birthDate);
    }
}

不変クラスのメリットは以下の通りです。

メリット 説明
スレッドセーフ 複数スレッドから安全にアクセス可能
予測可能 状態が変わらないため動作が予測しやすい
ハッシュキーに使用可能 HashMapのキーとして安全に使用できる

モジュールシステムの概要

Java 9で導入されたモジュールシステム(Java Platform Module System, JPMS) は、パッケージよりも上位の抽象化レベルで、依存関係の管理とカプセル化を実現します。

モジュールとは何か

モジュールとは、関連するパッケージ群をまとめた単位です。モジュールは以下の情報を持ちます。

要素 説明
名前 モジュールの一意な識別子
依存関係 他のモジュールへの依存
公開API 外部に公開するパッケージ
サービス 使用・提供するサービス

モジュール化のメリット

メリット 説明
強力なカプセル化 publicクラスでも、exportしなければ外部からアクセス不可
信頼性の高い構成 起動時に依存関係の欠落を検出
スケーラブルなプラットフォーム 必要なモジュールだけを含むランタイムイメージを作成可能

module-info.javaの基本構文

モジュールを定義するには、プロジェクトのソースルート(src/main/java)にmodule-info.javaを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ファイル: src/main/java/module-info.java
module com.example.myapp {
    // 依存するモジュール
    requires java.sql;
    requires java.logging;
    
    // 外部に公開するパッケージ
    exports com.example.myapp.api;
    exports com.example.myapp.model;
    
    // リフレクション用に開放するパッケージ
    opens com.example.myapp.entity to org.hibernate.orm;
}

主要なディレクティブ

module-info.javaで使用できる主要なディレクティブは以下の通りです。

ディレクティブ 説明
requires 依存モジュールを宣言 requires java.sql;
requires transitive 依存を推移的に公開 requires transitive java.logging;
exports パッケージを公開 exports com.example.api;
exports to 特定モジュールにのみ公開 exports com.example.internal to com.example.test;
opens リフレクション用に開放 opens com.example.entity;
uses サービスの使用を宣言 uses com.example.Service;
provides with サービスの提供を宣言 provides com.example.Service with com.example.ServiceImpl;

実践的なモジュール構成例

典型的なWebアプリケーションのモジュール構成例を見てみましょう。

1
2
3
4
5
6
7
// com.example.app.apiモジュール
module com.example.app.api {
    requires java.logging;
    
    exports com.example.app.api;
    exports com.example.app.api.dto;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// com.example.app.coreモジュール
module com.example.app.core {
    requires com.example.app.api;
    requires java.sql;
    
    exports com.example.app.core.service;
    
    // Hibernateにエンティティパッケージを開放
    opens com.example.app.core.entity to org.hibernate.orm;
}
1
2
3
4
5
6
7
8
// com.example.app.webモジュール
module com.example.app.web {
    requires com.example.app.api;
    requires com.example.app.core;
    requires jakarta.servlet;
    
    // Webパッケージは公開しない(エントリーポイントなので不要)
}

モジュールシステムとアクセス制御

モジュールシステムは、既存のアクセス修飾子の上に追加のレイヤーを提供します。

flowchart TB
    subgraph "アクセス制御の階層"
        direction TB
        Module["モジュール(exports)"]
        Package["パッケージ(アクセス修飾子)"]
        Class["クラス(public/デフォルト)"]
        Member["メンバー(public/protected/デフォルト/private)"]
    end
    
    Module --> Package --> Class --> Member

モジュール化された環境では、以下の条件をすべて満たす必要があります。

  1. クラスがpublicである
  2. パッケージがexportsされている
  3. 使用側モジュールがrequiresで依存を宣言している
1
2
3
4
5
// モジュールAで定義
module moduleA {
    exports com.example.moduleA.api;  // apiパッケージのみ公開
    // com.example.moduleA.internalは公開しない
}
1
2
3
4
5
6
7
8
// com.example.moduleA.apiパッケージ
package com.example.moduleA.api;

public class PublicAPI {
    public void doSomething() {
        // 公開API
    }
}
1
2
3
4
5
6
7
// com.example.moduleA.internalパッケージ
package com.example.moduleA.internal;

public class InternalClass {
    // このクラスはpublicだが、パッケージがexportsされていないため
    // 他のモジュールからはアクセスできない
}

モジュールを使わない場合

既存のプロジェクトや小規模なアプリケーションでは、モジュールシステムを使用しなくても問題ありません。module-info.javaがない場合、コードは無名モジュール(Unnamed Module) として扱われ、従来通りの動作をします。

モジュールシステムの導入は以下のケースで検討しましょう。

ケース 説明
ライブラリ開発 内部APIを確実に隠蔽したい場合
大規模アプリケーション チーム間の境界を明確にしたい場合
カスタムランタイム jlinkで最小限のJREを作成したい場合

まとめ

この記事では、Javaのパッケージとアクセス修飾子について解説しました。

学んだこと

トピック ポイント
パッケージ 関連クラスのグループ化、名前空間の管理、アクセス制御の基盤
命名規則 逆ドメイン名規則(com.example.app)に従う
import文 個別import推奨、static importでテストコードを簡潔に
public どこからでもアクセス可能、APIとして公開するものに使用
private 同一クラスのみ、フィールドの基本はprivate
protected 同一パッケージ + サブクラス、継承設計で使用
デフォルト 同一パッケージのみ、内部実装の隠蔽とテストに有効
モジュールシステム パッケージより上位のカプセル化、Java 9以降で利用可能

アクセス修飾子選択のフローチャート

  1. まずprivateを検討する(最小権限の原則)
  2. 同一パッケージ内で必要ならデフォルト
  3. サブクラスで使用するならprotected
  4. 外部に公開するならpublic

適切なパッケージ構成とアクセス修飾子の選択により、大規模プロジェクトでも保守しやすく、安全なコードを実現できます。

参考リンク