5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaAdvent Calendar 2024

Day 1

こんにちは、世界

Last updated at Posted at 2024-11-29

はじめに

この記事はJava Advent Calendar 2024の第一日目の記事です。

言い出しっぺなので何か書きます、とは言ったものの特にネタもないので、Hello Worldしてみたいと思います。まあ普通のHello Worldではつまらないのでちょっと捻ってみようかなとは思いますが

こんにちは、世界。

まずは、本当に普通のHello Worldを書いてみましょう。JavaでHello Worldを書きましょう、となったらだいたいこうなると思います。

AdventApp.java (version1)
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もいらなくなるらしいですが、まあここでは、とりあえずクラス宣言したんだからクラス使いましょうよ、という方向に振りましょう。次のバージョンはこうしてみます。

AdventApp.java (version2)
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というインターフェースを用意します。

AdventMessageProvider.java
package jp.hatano.advent2024.main;

public interface AdventMessageProvider {
    String getHello();
}

このインターフェースを実装するクラスを一つ作りましょう。パッケージをjp.hatano.advent2024.providersとします。

JapaneseMessageProvider.java
package jp.hatano.advent2024.providers;

import jp.hatano.advent2024.main.AdventMessageProvider;

public class JapaneseMessageProvider implements AdventMessageProvider {
    @Override
    public String getHello() {
        return "こんにちは、世界";
    }
}

AdventAppの実装をこれらに合わせるように変えましょう。

AdventApp.java (version3)
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に戻すこともできます。こんな風にすることも可能。

AdventApp.java (version3-2)
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だけ以下のように作り替えてみましょう。

AdventApp.java (version4)
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!

これで、クラスを作成したら配列に文字列を追加していくだけで動作を拡張していくことができる、というわけです。
え?配列を書き換えて毎回コンパイルするのは嫌だ?

設定値を外出しする。

こういうテキストファイルを作成します。

list.txt
jp.hatano.advent2024.providers.JapaneseMessageProvider
jp.hatano.advent2024.providers.EnglishMessageProvider

そして、AdventAppを以下のように変更します。

AdventApp.java (version5)
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();
    }
}

これを使ってこうじゃ。

AdventApp.java (version6)
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()を実装しておいたのでこういう風にもできます。

AdventApp.java (version7)
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を書き換えていきましょう。

AdventMessageProviderFactory.java
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クラスによる最適化」です。

5
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?