Java で1つのソースコードファイルに複数のクラスを記述する

実験で書いてみましたが業務で利用するといろんな人がびっくりするのでやめましょう

テストコードならありなのかなぁ

前書き的なやつ

よくライブコーディングで見かけるやつが気になったので整理しました。 Java でも 1 つのソースコードファイルに複数のクラスを定義することができます。

  • example/app/Application.java
    • 定義してるクラス
    • Application クラス
    • public static void main メソッドを定義してる
  • example/test/Utility.java
    • 定義してるクラス
    • Utility クラス
    • Item クラス ?
    • Option クラス ??
package example.app;

import example.test.Utility;

public class Application {

    public static void main(String[] args) {

        var utility = new Utility();
        System.out.println("app: " + utility.doSomething(args[0], args[1]));
    }
}
package example.test;

public class Utility {

    public static void main(String[] args) {

        var utility = new Utility();
        System.out.println("test: " + utility.doSomething(args[0], args[1]));
    }

    public Option doSomething(String name, String value) {

        return new Option(new Item(name, value));
    }
}

class Item {
    private final String name;
    private final String value;

    Item(String name, String value) {
        this.name = name;
        this.value = value;
    }

    @Override
    public String toString() {
        return "Item[name=" + this.name + ",value=" + this.value + "]";
    }
}

class Option {
    private final Item item;

    Option(Item item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return "Option[item=" + this.item.toString() + "]";
    }
}

後者の Utility.java に3つもクラスが定義されてるのは、普段見かけないからすごい不思議な感じがすると思います。

個人的には Application クラスから example.test.Option を返り値とする Utility#doSomething を利用できてるのが不思議です。

理屈

パッケージに関する Java 言語仕様が関係してます。

少なくとも1つはファイル名と同じクラスやインターフェイスが存在すること、それらのクラスやインターフェイスの可視性が public であることがコンパイル可能であることの制約になっています。

結局、 Utility.java で定義している ItemOption はただのパッケージスコープになっているということです。

パッケージがファイルシステム(7.2.1)に格納されるとき,ホストシステムは,次のいずれかが真の場合は,型名及び(.java 又は .jav のような)拡張子から構成される名前でファイル内にその型を見つけられなければ,コンパイル時エラーとする制約を課すことができる。

  • 型を宣言しているパッケージの他のコンパイル単位のコードによって,その型が参照されている。
  • 型を public と宣言している(それ故,潜在的には他のパッケージ内のコードからアクセスできる)。

実践

コンパイルしてみましょう。成功するはずです。

$ ${JAVA_HOME}/bin/javac -version
javac 11.0.8
$ ${JAVA_HOME}/bin/javac -verbose -d out example/app/Application.java example/test/Utility.java
[/modules/java.transaction.xa/module-info.classを読込み中]
[/modules/jdk.internal.jvmstat/module-info.classを読込み中]
[/modules/jdk.jartool/module-info.classを読込み中]
[/modules/jdk.crypto.ec/module-info.classを読込み中]
[/modules/jdk.jshell/module-info.classを読込み中]
[/modules/java.datatransfer/module-info.classを読込み中]
[/modules/java.desktop/module-info.classを読込み中]
[/modules/jdk.naming.rmi/module-info.classを読込み中]
[/modules/jdk.jdeps/module-info.classを読込み中]
[/modules/jdk.jsobject/module-info.classを読込み中]
[/modules/jdk.jfr/module-info.classを読込み中]
[/modules/jdk.security.jgss/module-info.classを読込み中]
[/modules/java.logging/module-info.classを読込み中]
[/modules/java.smartcardio/module-info.classを読込み中]
[/modules/jdk.rmic/module-info.classを読込み中]
[/modules/java.instrument/module-info.classを読込み中]
[/modules/jdk.dynalink/module-info.classを読込み中]
[/modules/jdk.pack/module-info.classを読込み中]
[/modules/jdk.naming.dns/module-info.classを読込み中]
[/modules/java.se/module-info.classを読込み中]
[/modules/java.security.sasl/module-info.classを読込み中]
[/modules/jdk.charsets/module-info.classを読込み中]
[/modules/jdk.internal.vm.compiler.management/module-info.classを読込み中]
[/modules/jdk.internal.ed/module-info.classを読込み中]
[/modules/java.rmi/module-info.classを読込み中]
[/modules/jdk.sctp/module-info.classを読込み中]
[/modules/jdk.security.auth/module-info.classを読込み中]
[/modules/jdk.internal.vm.ci/module-info.classを読込み中]
[/modules/jdk.jlink/module-info.classを読込み中]
[/modules/java.base/module-info.classを読込み中]
[/modules/java.sql/module-info.classを読込み中]
[/modules/jdk.unsupported.desktop/module-info.classを読込み中]
[/modules/jdk.compiler/module-info.classを読込み中]
[/modules/java.net.http/module-info.classを読込み中]
[/modules/jdk.xml.dom/module-info.classを読込み中]
[/modules/jdk.scripting.nashorn.shell/module-info.classを読込み中]
[/modules/jdk.management.agent/module-info.classを読込み中]
[/modules/java.management/module-info.classを読込み中]
[/modules/jdk.management/module-info.classを読込み中]
[/modules/jdk.internal.le/module-info.classを読込み中]
[/modules/jdk.zipfs/module-info.classを読込み中]
[/modules/jdk.jstatd/module-info.classを読込み中]
[/modules/jdk.internal.vm.compiler/module-info.classを読込み中]
[/modules/java.compiler/module-info.classを読込み中]
[/modules/java.xml.crypto/module-info.classを読込み中]
[/modules/jdk.httpserver/module-info.classを読込み中]
[/modules/jdk.aot/module-info.classを読込み中]
[/modules/jdk.jcmd/module-info.classを読込み中]
[/modules/java.scripting/module-info.classを読込み中]
[/modules/jdk.localedata/module-info.classを読込み中]
[/modules/jdk.editpad/module-info.classを読込み中]
[/modules/java.prefs/module-info.classを読込み中]
[/modules/java.xml/module-info.classを読込み中]
[/modules/jdk.unsupported/module-info.classを読込み中]
[/modules/java.sql.rowset/module-info.classを読込み中]
[/modules/jdk.net/module-info.classを読込み中]
[/modules/jdk.jdi/module-info.classを読込み中]
[/modules/jdk.internal.opt/module-info.classを読込み中]
[/modules/java.naming/module-info.classを読込み中]
[/modules/jdk.crypto.cryptoki/module-info.classを読込み中]
[/modules/jdk.management.jfr/module-info.classを読込み中]
[/modules/jdk.hotspot.agent/module-info.classを読込み中]
[/modules/jdk.jdwp.agent/module-info.classを読込み中]
[/modules/java.security.jgss/module-info.classを読込み中]
[/modules/jdk.attach/module-info.classを読込み中]
[/modules/jdk.accessibility/module-info.classを読込み中]
[/modules/jdk.javadoc/module-info.classを読込み中]
[/modules/jdk.scripting.nashorn/module-info.classを読込み中]
[/modules/jdk.crypto.mscapi/module-info.classを読込み中]
[/modules/jdk.jconsole/module-info.classを読込み中]
[/modules/java.management.rmi/module-info.classを読込み中]
[ソース・ファイルの検索パス: .]
[/modules/java.base/java/lang/Object.classを読込み中]
[/modules/java.base/java/lang/String.classを読込み中]
[/modules/java.base/java/lang/Deprecated.classを読込み中]
[/modules/java.base/java/lang/Override.classを読込み中]
[/modules/java.base/java/lang/annotation/Annotation.classを読込み中]
[/modules/java.base/java/lang/annotation/Retention.classを読込み中]
[/modules/java.base/java/lang/annotation/RetentionPolicy.classを読込み中]
[/modules/java.base/java/lang/annotation/Target.classを読込み中]
[/modules/java.base/java/lang/annotation/ElementType.classを読込み中]
[example.app.Applicationを確認中]
[/modules/java.base/java/io/Serializable.classを読込み中]
[/modules/java.base/java/lang/AutoCloseable.classを読込み中]
[/modules/java.base/java/lang/System.classを読込み中]
[/modules/java.base/java/io/PrintStream.classを読込み中]
[/modules/java.base/java/lang/Appendable.classを読込み中]
[/modules/java.base/java/io/Closeable.classを読込み中]
[/modules/java.base/java/io/FilterOutputStream.classを読込み中]
[/modules/java.base/java/io/OutputStream.classを読込み中]
[/modules/java.base/java/io/Flushable.classを読込み中]
[out\example\app\Application.classを書込み完了]
[example.test.Utilityを確認中]
[out\example\test\Utility.classを書込み完了]
[example.test.Optionを確認中]
[/modules/java.base/java/lang/Byte.classを読込み中]
[/modules/java.base/java/lang/Character.classを読込み中]
[/modules/java.base/java/lang/Short.classを読込み中]
[/modules/java.base/java/lang/Long.classを読込み中]
[/modules/java.base/java/lang/Float.classを読込み中]
[/modules/java.base/java/lang/Integer.classを読込み中]
[/modules/java.base/java/lang/Double.classを読込み中]
[/modules/java.base/java/lang/Boolean.classを読込み中]
[/modules/java.base/java/lang/Void.classを読込み中]
[/modules/java.base/java/lang/invoke/StringConcatFactory.classを読込み中]
[/modules/java.base/java/lang/invoke/MethodHandles.classを読込み中]
[/modules/java.base/java/lang/invoke/MethodHandles$Lookup.classを読込み中]
[/modules/java.base/java/lang/invoke/MethodType.classを読込み中]
[/modules/java.base/java/lang/invoke/CallSite.classを読込み中]
[out\example\test\Option.classを書込み完了]
[example.test.Itemを確認中]
[out\example\test\Item.classを書込み完了]
[合計317ミリ秒]

ちゃんと実行できます。

$ ${JAVA_HOME}/bin/java -cp out example.app.Application a 1
app: Option[item=Item[name=a,value=1]]

$ ${JAVA_HOME}/bin/java -cp out example.test.Utility b 2
test: Option[item=Item[name=b,value=2]]