週末プログラマの日記

プログラミングメインで日記とかも書きます

JavaでHello World

どうも皆さんこんにちは!週末プログラマのふじたです。
今回はこのブログの初投稿でございますので、Hello Worldを書いてみました。

動いているところ

f:id:fujita-weekend:20200415225339g:plain
動作確認

ソース

HelloWorldMain.java

public class HelloWorldMain {

    public static void main(String[] args) {
        Logo logo = new Logo("../data/HelloWorld.txt");
        logo.printLogo();
    }

}

Logo.java

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Logo {
    /**ロゴの各行の文字列を保持する配列 */
    private List<LogoLine> logoLines;

    /**
     * 引数としてロゴファイルパスを渡し、ロゴを初期化する
     * @param path
     */
    public Logo(String path){
        try (FileReader fis = new FileReader(path);
            BufferedReader bis = new BufferedReader(fis)){

            this.logoLines = new ArrayList<>();
            String line = bis.readLine();
            int i = 1;
            while (line != null) {
                this.logoLines.add(new LogoLine(i, line));
                line = bis.readLine();
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * ロゴをコンソール画面に出力する
     */
    public void printLogo(){
        // コンソール画面を初期化
        System.out.print("\033[2J");

        int lineSize = this.logoLines.size();
        long sleepTime = 1000;
        Random rnd = new Random();

        // ロゴ出力処理
        while (!isAllLineWrited()) {
            // ランダム行選択
            int lineNum = Math.abs(rnd.nextInt(lineSize));
            LogoLine line = this.logoLines.get(lineNum);

            if(line.lineLength() == 0){
                continue; // 選択した行の長さが0の場合は、次の行選択処理へ移る
            }

            // ランダムカラム選択
            int col = Math.abs(rnd.nextInt(line.lineLength())) + 1;

            // 文字出力
            line.printColChar(col);

            // スレッドスリープ
            try {
                Thread.sleep(sleepTime);
            } catch (Exception e) {
                e.printStackTrace();
            }

            // スリープ時間減少(最低50ms)
            if (sleepTime - 30 >= 0) {
                sleepTime -= 30;
            }
        }
    }

    /**
     * 保持するロゴの各行がすべて出力されたか判定する
     * @return
     */
    public boolean isAllLineWrited(){
        for (LogoLine logoLine : logoLines) {
            if (!logoLine.isAllWrited()) {
                return false;
            }
        }
        return true;
    }
}

LogoLine.java

import java.util.Arrays;

/** 出力ロゴ構成行
 *  ロゴ文字列のある行の文字列とその行数を保持する
 */
public class LogoLine extends Thread {
    /**割り当て行数 */
    private int line;
    /**行文字列 */
    private String lineString;
    /**カラム書き込み済みフラグ配列 */
    private boolean[] writedCol;

    /**
     * 割り当て行数とその行が保持する文字列を渡して初期化する
     * 列の位置とカラム書き込み済みフラグ配列は、それぞれ1とfalseで初期化する
     * @param line
     * @param lineString
     */
    public LogoLine (int line, String lineString){
        this.line = line;
        this.lineString = lineString;
        this.writedCol = new boolean[lineString.length()];
        Arrays.fill(writedCol, false);
    }

    /**
     * 保持する文字列のうち指定された列の文字列を出力する
     * 指定された列の文字列が出力済みだった場合と全ての文字列が出力済みだった場合は、呼び出し元に何もせずに戻る
     * @param col
     */
    public void printColChar(int col){
        // 配列長超過判定
        if(col > lineString.length()) {
            return;
        }

        // 行文字列出力済み判定
        if (writedCol[col - 1] || isAllWrited()){
            return;
        }

        // 保持する行の指定された列にカーソルを移動する
        System.out.printf("\033[%d;%df", this.line, col);

        // 保持する文字列の指定された列に対応する文字を出力する
        System.out.print(this.lineString.charAt(col - 1));

        // 指定された列に対応するカラム書き込み済みフラグをtrue(書き込み済み)に変更する
        this.writedCol[col - 1] = true;
    }

    /**
     * カラム書き込み済みフラグ配列がすべてtrueになっているか判定する
     * @return
     */
    public boolean isAllWrited(){
        for (boolean b : writedCol) {
            if(!b){
                return false;
            }
        }
        return true;
    }

    /**
     * 保持する文字列の長さを返す。
     * @return
     */
    public int lineLength(){
        return this.lineString.length();
    }

    /**
     * 保持する行数を返す
     * @return
     */
    public int getLine() {
        return this.line;
    }
}

HelloWorld.txt

      ///     ///   //////////   ///          ///          //////
     ///     ///   ///          ///          ///        ///     ///
    ///////////   /////////    ///          ///        ///      ///
   ///     ///   ///          ///          ///         ///     ///
  ///     ///   //////////   //////////   //////////     //////

                                        ///         ///    //////     ////////     ///         //////
                                        ///  ///   ///  ///     ///  ///    ///   ///         ///   ///
                                        /// ///// ///  ///      /// ////////     ///         ///    ///
                                        /////  /////   ///     /// ///   ///    ///         ///    ///
                                        ///     ///      //////   ///     ///  //////////  ////////

ソースの説明

掲載したソースの説明をさせていただきたいと思います、、、が、うぉいHelloWorld.txtがプレビューで見たら変なことになってる(笑) 私が使っているモニターのサイズが問題なんでしょうか?

概要

テキストファイル(HelloWorld.txt)に保存した文字列のロゴを、コンソールのセル単位 = 半角一文字ごとにランダムに表示するプログラムです。 テキストファイルに保存した情報を変えることで、別のロゴを出力することができます。

HelloWorld.txt

出力するロゴ = 文字列を保存しています。プログラムは起動すると、このテキストファイルの文字列を各行ごとに読み取り配列として保持します。 ちなみに、Java13からヒアドキュメントがサポートされております。ですので今回ヒアドキュメントでの実装も考えたのですが、ロゴのデータをjavaファイルから切り離したほうが今回作成するプログラムの趣旨に合うのかな、と思ってテキストファイルに取り出しました。

Logo.java, LogoLine.java

それぞれロゴの本体と各行を表しています。 Logo.java#printLogoで文字を出力する行と列をランダムに選択し、該当する行のオブジェクト(LogoLineインスタンス)のLogoLine#printColCharを実行します。その結果は、この記事の冒頭でお見せした通りです。 少しでもHello Worldの見た目が楽しくなるように心がけました(笑)

コンソール画面の初期化処理やカーソルの移動処理では、エスケープシーケンスを使用しております。 私の開発環境はWindows 10なのですが、Windowsエスケープシーケンスを使用してちょっと凝ったコンソール画面の動きを実装しようとすると、期待通りに動かなかったり不安定な挙動になったりして割とうまくゆきません。今回は、Cygwin上で実行することで不安定な挙動になることを避けました。 歴史的にWindowsでは、ANSIエスケープコードをサポートしていなかったという背景があります。私もそこらへんはあまり詳しくはないのですが、いい記事を見つけましたらこの記事で共有させていただきます。

しれっとLogoLineクラスがThreadを継承しているのは、リファクタリングフェーズでマルチスレッドにしてみようかなぁ、と何となく考えているからです。今後気が向いたら実装することもあるかもしれません。

まとめ

f:id:fujita-weekend:20200415222950p:plain そこそこ面白いHello Worldが書けたと思います。 コンソールアプリって作っていてシンプルに楽しいと感じるので好きです。

ここまで読んでくださった方、真にありがとうございます。 気が向いた方は、Twitterのフォローをお願いします。 twitter.com

今回の記事に関する質問や意見などは、この記事のコメント欄やTwitterにてどうぞご気軽に上げていただければと思います。