【Java】package privateなメンバーはその気になれば外部プロジェクトからアクセスできる

# はじめに
Javaにおけるアクセス制御修飾子には以下の4種類があります。

* public
  * どこからでもアクセス可能
* protected
  * 同じパッケージ内か、もしくはサブクラスのインスタンスからアクセス可能
* アクセス修飾子なし（package private）
  * 同じパッケージ内からのみアクセス可能
* private
  * 同じクラス内からのみアクセス可能

これらは見ての通り、下に行けば行くほどアクセス可能範囲が狭いです。publicとprotectedが公開API、package privateとprivateが非公開APIとされます。

今回話題にしたいのは、下から2番目のpackage privateです。package privateというのが正式な呼び方かわかりませんが、その呼ばれ方の通り限りなくprivateに近いものなのだろうと思っていたのですが、案外そうでもなかったぜというのがこの記事の趣旨です。

# 結論
package privateが「同じパッケージ内からのみアクセス可能」という理解は正しいが、外部のプロジェクトにおいても**同じ名前のパッケージを作成する**ことで、そのパッケージ内からはアクセス可能になる。

# サンプルコード
話としては上に書いた通りで、大したコードは出てこないですが一応書きます。

## 呼ばれる側のコード

```java
package foo.bar.internal;

public class PackagePrivateSample {
    public void publicMethod() {
        System.out.println("PackagePrivateSample.publicMethod");
    }

    void packagePrivateMethod() {
        System.out.println("PackagePrivateSample.packagePrivateMethod");
    }

    protected void protectedMethod() {
        System.out.println("PackagePrivateSample.protectedMethod");
    }

    private void privateMethod() {
        System.out.println("PackagePrivateSample.privateMethod");
    }
}
```

上記をビルドしてjarファイルを作成します。

## 呼ぶ側のコード
こちらは上記と同じプロジェクト内ではなく、上記でビルドしたjarを参照して別プロジェクトとして作成します。

仮にGradleプロジェクトとしたら、以下のような感じでローカルのjarを参照します。
test_package_private-1.0-SNAPSHOT.jarが該当のjarファイルで、libフォルダに置いたものとします。
（それ以外の記述は自動生成そのままにしているだけなのでなくてもいいかもです）

```gradle
plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation files('lib/test_package_private-1.0-SNAPSHOT.jar')
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

test {
    useJUnitPlatform()
}
```

まずは、参照できないパターンです。

```java
package foo.bar;　 // <- パッケージが違う

import foo.bar.internal.PackagePrivateSample;

public class UseSample {
    public static void main(String[] args) {
        var sample = new PackagePrivateSample();
        sample.publicMethod();

        // 以下は呼べない（コンパイルエラー）
        // sample.privateMethod();         <- 'privateMethod()' has private access in 'foo.bar.internal.PackagePrivateSample'
        // sample.packagePrivateMethod();  <- 'packagePrivateMethod()' is not public in 'foo.bar.internal.PackagePrivateSample'. Cannot be accessed from outside package
        // sample.protectedMethod();       <- 'protectedMethod()' has protected access in 'foo.bar.internal.PackagePrivateSample'
    }
}
```

実行結果

```
PackagePrivateSample.publicMethod
```

続いて、参照できるパターンです。

```java
package foo.bar.internal;  // <- パッケージが同じ！

public class UseSample {
    public static void main(String[] args) {
        var sample = new PackagePrivateSample();
        sample.publicMethod();

        // 呼べる！
        sample.packagePrivateMethod();

        // ついでにこれも呼べる
        sample.protectedMethod();

        // これはさすがに呼べない
        // sample.privateMethod();  <- 'privateMethod()' has private access in 'foo.bar.internal.PackagePrivateSample'
    }
}
```

実行結果

```
PackagePrivateSample.publicMethod
PackagePrivateSample.packagePrivateMethod
PackagePrivateSample.protectedMethod
```

# 余談
同じパッケージ内に同じ名前のクラスを作って、その中からprivateメンバーを呼べちゃうかどうかも調べましたが、それはさすがに無理みたいです。
（クラスは作れるが、参照がそちらに上書きされて元のほうが参照できなくなる）

# 終わりに
まあこんなことやるか？と言われたら普通やらないとは思うのですが、ちょっとびっくりしました。
Effective Javaには、テストを容易にするためにprivateなメンバをpackage privateにすることは許容されるという趣旨のことが書かれていますし、あんまり気にしなくてもいいと思いますが、知識としては持っておいてもいいかもしれません。