はじめに
この記事はJava Advent Calendar 2024の第一日目の記事です。
言い出しっぺなので何か書きます、とは言ったものの特にネタもないので、Hello Worldしてみたいと思います。まあ普通のHello Worldではつまらないのでちょっと捻ってみようかなとは思いますが
こんにちは、世界。
まずは、本当に普通のHello Worldを書いてみましょう。JavaでHello Worldを書きましょう、となったらだいたいこうなると思います。
package jp.hatano.advent2024.main;
public class AdventApp {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
実行してみましょう。
% javac -sourcepath src src/jp/hatano/advent2024/main/AdventApp.java -d out/production/advent2024
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
Hello, World!
こんにちは、世界、オブジェクトから
とはいえ、これだけだとstatic
なメソッドを実行しただけです。クラスを宣言した甲斐がない。…ちなみに最近のJavaではクラス宣言もpublic static void main
もいらなくなるらしいですが、まあここでは、とりあえずクラス宣言したんだからクラス使いましょうよ、という方向に振りましょう。次のバージョンはこうしてみます。
package jp.hatano.advent2024.main;
import java.io.PrintStream;
public class AdventApp {
public static void main(String[] args) {
AdventApp application = new AdventApp();
application.sayHello(System.out);
}
public void sayHello(PrintStream out) {
out.println("Hello, World!");
}
}
実行結果は特に変わらないので省略。
こんにちは、世界、ニーハオ、ホアンイン。
Javaアプリケーションを国際化するには、ResourceBundle
を使うというのがセオリーだった…と思うんですが…いまもそうなのかな?ここではちょっと違う方法をとってみることにします。まずAdventMessageProvider
というインターフェースを用意します。
package jp.hatano.advent2024.main;
public interface AdventMessageProvider {
String getHello();
}
このインターフェースを実装するクラスを一つ作りましょう。パッケージをjp.hatano.advent2024.providers
とします。
package jp.hatano.advent2024.providers;
import jp.hatano.advent2024.main.AdventMessageProvider;
public class JapaneseMessageProvider implements AdventMessageProvider {
@Override
public String getHello() {
return "こんにちは、世界";
}
}
AdventApp
の実装をこれらに合わせるように変えましょう。
package jp.hatano.advent2024.main;
import java.io.PrintStream;
import jp.hatano.advent2024.providers.JapaneseMessageProvider;
public class AdventApp {
public static void main(String[] args) {
AdventApp application = new AdventApp(new JapaneseMessageProvider());
application.sayHello(System.out);
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
コンストラクタにAdventMessageProvider
を渡すようにして、sayHello()
ではAdventMessageProvider#getHello()
の戻り値を使うのがミソです。実行結果:
% javac -sourcepath src src/jp/hatano/advent2024/main/AdventApp.java -d out/production/advent2024
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
こんにちは、世界
EnglishMessageProvider
クラスを作成してJapaneseMessageProvider
の代わりに呼ぶようにすれば、Hello Worldに戻すこともできます。こんな風にすることも可能。
package jp.hatano.advent2024.main;
import java.io.PrintStream;
import jp.hatano.advent2024.providers.JapaneseMessageProvider;
import jp.hatano.advent2024.providers.EnglishMessageProvider;
public class AdventApp {
public static void main(String[] args) {
AdventApp application = new AdventApp(new EnglishMessageProvider());
application.sayHello(System.out);
application = new AdventApp(new JapaneseMessageProvider());
application.sayHello(System.out);
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
さて、ここからが本番です。(え?
ここまでに作ったクラスはこのままにしてAdventApp
だけ以下のように作り替えてみましょう。
package jp.hatano.advent2024.main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
public class AdventApp {
private static final String[][] listProviders = {
{ "Japanese", "jp.hatano.advent2024.providers.JapaneseMessageProvider" },
{ "English", "jp.hatano.advent2024.providers.EnglishMessageProvider" },
};
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
for ( int i = 0 ; i < listProviders.length ; i++ ) {
System.out.printf("%d: %s\n",i,listProviders[i][0]);
}
System.out.print("Your choice: ");
String input = reader.readLine();
int inputNumber = Integer.parseInt(input);
if ( inputNumber >= 0 && inputNumber < listProviders.length ) {
Class<?> aClass = Class.forName(listProviders[inputNumber][1]);
AdventApp application = new AdventApp((AdventMessageProvider)aClass.getConstructor(new Class[]{}).newInstance(new Object[] {}));
application.sayHello(System.out);
}
} catch ( IOException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
実行してみます。
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
0: Japanese
1: English
Your choice: 0
こんにちは、世界
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
0: Japanese
1: English
Your choice: 1
Hello, World!
これで、クラスを作成したら配列に文字列を追加していくだけで動作を拡張していくことができる、というわけです。
え?配列を書き換えて毎回コンパイルするのは嫌だ?
設定値を外出しする。
こういうテキストファイルを作成します。
jp.hatano.advent2024.providers.JapaneseMessageProvider
jp.hatano.advent2024.providers.EnglishMessageProvider
そして、AdventApp
を以下のように変更します。
package jp.hatano.advent2024.main;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class AdventApp {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new FileReader("list.txt"));
while ( reader.ready() ) {
String providerName = reader.readLine();
Class<?> aClass = Class.forName(providerName);
AdventApp application = new AdventApp((AdventMessageProvider)aClass.getConstructor(new Class[]{}).newInstance(new Object[] {}));
application.sayHello(System.out);
}
} catch ( IOException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
実行してみましょう。
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
こんにちは、世界
Hello, World!
リストの記載順を入れ替えればちゃんと出力順が入れ替わります。
リスト読むとこ複雑なんで分離しちゃいましょう
AdventMessageProviderFactory
というクラスを導入します。
package jp.hatano.advent2024.main;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class AdventMessageProviderFactory implements Iterable<AdventMessageProvider>, Iterator<AdventMessageProvider> {
private final List<AdventMessageProvider> listProviders = new ArrayList<>();
public AdventMessageProviderFactory(String filename) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
while ( reader.ready() ) {
String providerName = reader.readLine();
Class<?> aClass = Class.forName(providerName);
listProviders.add((AdventMessageProvider)aClass.getConstructor(new Class[]{}).newInstance(new Object[] {}));
}
} catch (IOException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public int size() {
return listProviders.size();
}
public AdventMessageProvider get(int i) {
return listProviders.get(i);
}
public AdventMessageProvider get(String name) {
for ( AdventMessageProvider provider : listProviders ) {
if ( provider.getClass().getName().equals(name) ) {
return provider;
}
}
return null;
}
@Override
public Iterator<AdventMessageProvider> iterator() {
return listProviders.iterator();
}
@Override
public boolean hasNext() {
return iterator().hasNext();
}
@Override
public AdventMessageProvider next() {
return iterator().next();
}
}
これを使ってこうじゃ。
package jp.hatano.advent2024.main;
import java.io.*;
public class AdventApp {
public static void main(String[] args) {
AdventMessageProviderFactory factory = new AdventMessageProviderFactory("list.txt");
for ( AdventMessageProvider provider : factory ) {
AdventApp application = new AdventApp(provider);
application.sayHello(System.out);
}
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
複雑な部分はfactoryへ追い出して、AdventApp
本体はすっきりしましたね。
size()
とかget()
を実装しておいたのでこういう風にもできます。
package jp.hatano.advent2024.main;
import java.io.*;
public class AdventApp {
public static void main(String[] args) {
AdventMessageProviderFactory factory = new AdventMessageProviderFactory("list.txt");
AdventApp application = new AdventApp(factory.get(0));
application.sayHello(System.out);
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
でもやっぱりテキスト用意するの面倒
クラスをjp.hatano.advent2024.providers
パッケージに追加するだけで勝手に使えるようになるといいですよね。ということでFactoryを書き換えていきましょう。
package jp.hatano.advent2024.main;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.nio.file.Paths;
public class AdventMessageProviderFactory implements Iterable<AdventMessageProvider>, Iterator<AdventMessageProvider> {
private final List<AdventMessageProvider> listProviders = new ArrayList<>();
public AdventMessageProviderFactory(String packageName) {
String currentClassPath;
String thisPackageName = getClass().getPackageName().replace(".",File.separator);
String targetPackageName = packageName.replace(".",File.separator);
try {
URL classPath = getClass().getResource(".");
if ( classPath != null ) {
currentClassPath = Paths.get(classPath.toURI()).toAbsolutePath().toString().replace(thisPackageName,targetPackageName);
File[] providerFiles = new File(currentClassPath).listFiles();
if ( providerFiles != null ) {
for (File providerFile : providerFiles) {
String providerAbsolutePath = providerFile.getAbsolutePath();
String filename = providerAbsolutePath.substring(providerAbsolutePath.lastIndexOf(File.separatorChar)+1);
String className = packageName + "." + filename.replace(".class","");
Class<?> aClass = Class.forName(className);
listProviders.add((AdventMessageProvider) aClass.getConstructor(new Class[]{}).newInstance(new Object[]{}));
}
}
}
} catch (URISyntaxException | NoSuchMethodException | IllegalAccessException | InstantiationException |
InvocationTargetException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public int size() {
return listProviders.size();
}
public AdventMessageProvider get(int i) {
return listProviders.get(i);
}
public AdventMessageProvider get(String name) {
for ( AdventMessageProvider provider : listProviders ) {
if ( provider.getClass().getName().equals(name) ) {
return provider;
}
}
return null;
}
@Override
public Iterator<AdventMessageProvider> iterator() {
return listProviders.iterator();
}
@Override
public boolean hasNext() {
return iterator().hasNext();
}
@Override
public AdventMessageProvider next() {
return iterator().next();
}
}
ここまでやると何やってるかさっぱり分からないとかになりそうですがまあ読んでみてください。(ちなみにAdventApp
のfactory呼び出しのところの引数はパッケージ名に書き換えてやる必要があります。)ここまでやるとProviderクラスを追加すると自動的に読み込まれます。FrenchMessageProvider
クラスを作成してコンパイルしたものを配置しておくだけで(なお、ファイルを直で指定してコンパイルするようにしないと自動的にはclassファイルが生成されないので注意)以下のようになるわけです。
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
Hello, World!
Bonjour le monde.
こんにちは、世界
コンパイルしたclassファイルを配置するだけで自動的にアプリが読み込んでくれる世界、こんにちは。
少しだけ付け足しておくと、AdventMessageProviderFactory
クラスを用意して中身をそちらに委譲してやることで、AdventApp
側は細かい動作を気にしなくてもほとんど変わらない、シンプルで直観的なコードで対応できることが分かります。あと、メインのコードをいじらなくてもセットアップで動作を変化させることができるというのも。(まあこのコードがお手本になれるレベルなのかははなはだ疑問ではありますが。)
おしまい
ということで特にオチはありませんでした。
本当にどうでもいいことですが、コードはgithubに公開してあります。
さいごに
Java Advent Calendar 2024、明日のエントリーはさくらばさんによるProject Valhalla解説シリーズ「Valueクラスによる最適化」です。