Java Basic 中級編 ~③メインメソッドを修正しないで拡張する~

イントロダクション

前回は、メインメソッドを修正しない形での実装を行いました。

この段階では、まだ役割分担を行っただけでこれがどんな効果があるのか?というところがわからないと思います。

なので、今回は、「〇×あてゲーム」を拡張することを考えていきたいと思います。
作成したプログラムコードはGithubにアップしてあります。

メインメソッドを修正しないで拡張する

今までに「役割分担」を行ったので、この役割分担を活用していきます。
Lv2Mainクラスはメインメソッドを動かすクラスなので、これ以外のクラスを実装していきます。

  • Lv2Main
  • MarubatsuConsole
  • MarubatsuUtils

Lv2Mainを見る

まずは、メインメソッドを見てみます。Lv2Main

/**
 * Javaの基本レベル2:小さなレベルのプログラムを拡張する。
 * 今回は、〇か×か当てるゲーム。
 */
public class Lv2Main {
    /** 終了フラグ */
    private static boolean isFinish;

    public static void main(String[] arg) {
        isFinish = false;
        // 標準入力を受け取るクラスをインスタンス化
        Scanner scan = MarubatsuUtils.getScanner();

        while (true) {
            // ゲーム開始文言
            MarubatsuConsole.printGameStart();
            // 標準入力を受け取る
            int input = scan.nextInt();
            // isFinishがtrueならば処理終了。
            if (isFinish) {
                MarubatsuConsole.printTerminated();
                break;
            }
            // 0か1の値を返却する
            int res = MarubatsuUtils.nextInt(2);

            // 0: 〇 1: ×で当たったかどうかの判定
            boolean isAtari = MarubatsuUtils.judgeAtariOrNot(res, input);
            MarubatsuConsole.printAtatiorNot(isAtari, input);

            // 続けるのかどうか判定する
            if (MarubatsuConsole.printNextPlayOrNot(scan)) {
                break;
            }
        }
    }
}

コメントを並べただけですが、以下の順序で処理を行っています。

  1. 標準入力を受け取るクラスをインスタンス化
  2. ゲーム開始文言
  3. 標準入力を受け取る
  4. isFinishがtrueならば処理終了。
  5. 0か1の値を返却する
  6. 0: 〇 1: ×で当たったかどうかの判定
  7. 続けるのかどうか判定する

上の番号で行くと2~7がループ処理の中にあります。
なので、7の「続けるか判定する」部分でNo(続けない)を選択するまで無限ループします。

拡張ポイントを見つける

そして、役割分担を行ったところ、つまりは、下のクラスを使用している部分が拡張ポイントになります。
Githubにアップしてあります。

具体的には、以下のコメント部分です。

  • ゲーム開始文言
  • isFinishがtrueならば処理終了。の終了表示部分
  • 0か1の値を返却する
  • 0: 〇 1: ×で当たったかどうかの判定
  • 続けるのかどうか判定する

もしも、他の部分を拡張したいと思たのならば、メインメソッドを少し修正する必要があります。
何かの処理をほかのクラスに任せてある状態(MarubatsuConsole, MarubatsuUtilsを使用している状態)ならば
メインメソッドを修正する必要がありません。
しかし、直接メインメソッドを修正する必要がある場合はやはり、修正する必要があります。

このような実装方法を理解すると、下のようなプログラムが作れます。

※ビルドするのに3分くらい時間がかかっています。
このプログラムは、メインの処理部分も、別クラスにしているので、まったく別のプログラムを起動するように修正することもできます。
しかし、現状では、テキストRPGを作成するところに注力しているので役割分担を行いそれぞれの役割を拡張して実装しています。
具体的には、テキストファイルを読み、それをもとにデータ(ステータスやアイテム)を生成してそれをゲームの中で使用する形での実装を行っております。
ちょっと残骸が残っていますが。。。

拡張するとき

先に示したように、別のクラスを呼び出している(使用している)部分を拡張、つまり処理を追加することができるので、次の部分を拡張することができます。

  • ゲーム開始文言
  • isFinishがtrueならば処理終了。の終了表示部分
  • 0か1の値を返却する
  • 0: 〇 1: ×で当たったかどうかの判定
  • 続けるのかどうか判定する

具体的には、ゲーム開始文言を変更することができる。とか、当たったかどうかの履歴を付ける。とか
そこらへんは、実装者のアイディア次第でどこまでも広げることができます。

ゲームの開始文言を変更する

先ほど、拡張する例としてゲームの開始文言を変更するということを上げました。これを実際にやってみます。

やっている手順としては、以下の通りです。

  1. JUnitテストケースの作成
  2. 現状のプログラム実行確認
  3. プログラムの修正、実行確認(目視で確認)

最後の実行確認(目視で確認)に関しては、すべてプログラムで確認することができます。
そのためには、ちょっと面倒なことをする必要があるので、割愛しました。
具体的には標準出力の出力先を変更して(別のPrintStreamを使用)そのストリーム内の出力内容と期待値を比較するという形になります。

現状としては、単純に〇×あてゲームの初期表示文言を変更するだけなので、目視で確認しました。

0: 〇 1: ×で当たったかどうかの判定

この部分は上記の動画では、挙動がおかしくなっていました。なので、これを修正し想定通りの実行結果が得られるようにプログラムを修正します。

実行した結果は下のようになります。

******************************
*「〇×あてゲーム」 0: 〇 1: ×。 *
******************************
※0は「〇」を表し1は「×」を表します。
1
はずれ:×
続けますか? 0: 続ける  1: やめる
0
******************************
*「〇×あてゲーム」 0: 〇 1: ×。 *
******************************
※0は「〇」を表し1は「×」を表します。
1
はずれ:×
続けますか? 0: 続ける  1: やめる
1

その前に、問題点を明確にします。

今回の問題点は「あたりのときの値、つまりは、『〇』が当たりなのか?『×』が当たりなのか?」が明確でないというところです。
なので、これも修正します。

先ほどと同じようにまずは、修正ポイント(先ほどは拡張ポイントと記載しました。)を見つけます。
実行結果を見ると、「はずれ:×」のように当たりはどちらなのか?がわからない状態ですので、これを明確にします。

具体的には、「「〇」があたりです。「×」が当たりですなどの文言を表示するように修正」する形で対応しようと考えております。

当然、他に良いアイディアがあれば、それを実装するとよいと思います。

当たり判定処理の修正

今回作成した「当たり判定処理」は、MarubatsuUtils#judgeAtariOrNotで実装しているのでこれを修正します。

現状のプログラムは下のようになっています。

    /**
     * 生成した乱数と、入力値が等しいか判定する。
     *
     * @param res 生成した乱数
     * @param input 入力値
     * @return true: 等しい false: 違う
     */
    public static boolean judgeAtariOrNot(int res, int input) {
        // 0: 〇 1: ×で当たったかどうかの判定
        return res == input;
    }

このプログラムの問題点は、〇と×のどちらが当たりなのか表示されない点です。
なので、これを表示するようにプログラムを修正する必要があります。

テストケースを作成する

先ほど初期表示の文言を表示するテストケースを作成しました。
これに追加して、「〇と×のどちらが当たりなのか表示する」テストケースを実装します。

仕様から考える

今回の要件(どのように動いたらよいか?)は「〇と×のどちらが当たりなのか表示する」ということです。
これを確かめるプログラムを考えます。

確認項目をリストアップ

  1. あたりは「〇」「×」どちらか表示する
  2. 予想を入力したユーザーの入力は「〇」「×」どちらか表示する

簡単ですが、2項目になります。

これもテストケースを作成する

ちなみに、このクラスのテストケースは以前、他のテストケースを作成していたので、これに今回のテストケースを追加します。

やったことは、同じです。

  1. 現状のプログラム実行確認
  2. プログラムの修正
  3. プログラムの実行確認

ちょっと長めの動画になりましたが、行ったことをそのまま動画にしてあります。
ポイントとしては、テストケースのプログラムの実装、作成方法、考え方(この記事に記載)を実際に行った
動画にしてあります。
別な言い方をすると、自分がこの作業を行ったものを動画にしました。

でわでわ。。。

Java Basic 中級編 ~②メインメソッドを修正しない形を作る~

イントロダクション

前回アプリケーションを作り、それを運用すること、拡張することについて考えてみました。

まとめると次のようなことが必要になるということを記載しました。

  • 各プログラム間(Javaのクラス同士)の依存度を限りなく低くする。
  • プログラムを拡張するのに、元のプログラムをほとんど変更しない。
  • Javaを使えるレベルの知識(技術)がある人なら、誰が見てもわかるようなコードを書く。
  • 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える

今回は、プログラムを拡張していくための基本になる考え方と実践方法について記載していきたいと思っています。
例として「〇×あてゲーム」を拡張していく形でプログラム(クラス)間の依存度を低くした形のプログラミング方法について記載していきたいと思います。

メインメソッドを修正しない形を作る

前回作成したプログラムは下のものになります。

public class Lv2Main {
    /** 終了フラグ */
    private static boolean isFinish;

    public static void main(String[] arg) {
        isFinish = false;
        // 標準入力を受け取るクラスをインスタンス化
        Scanner scan = new Scanner(System.in);

        while (true) {
            System.out.println("「〇×あてゲーム」 0: 〇 1: ×。");
            // 標準入力を受け取る
            int input = scan.nextInt();
            // isFinishがtrueならば処理終了。
            if (isFinish) {
                System.out.println("プログラムを終了します。");
                break;
            }
            // 0か1の値を返却する
            Random rdm = new Random();
            int res = rdm.nextInt(1);

            // 0: 〇 1: ×で当たったかどうかの判定
            boolean isAtari = res == input;
            String value = input == 0 ? "〇" : "×";
            if (isAtari) {
                System.out.println("あたり:" + value);
            } else {
                System.out.println("はずれ:" + value);
            }
            System.out.println("続けますか? 0: 続ける  1: やめる");
            int next = scan.nextInt();
            if (next == 1) {
                break;
            }
        }
    }
}

この状態のプログラムはよく見かける。。。と思われる形のプログラムです。
つまりは、処理を1つのファイル内にすべて書いている形のプログラムということです。

このブログ的に表現すると「Javaの基本:上巻」に当たるプログラムの書き方になります。

「Javaの基本:下巻」に当たるプログラムの書き方はこれから説明していきます。

Step1. 役割分担を行う

上のプログラムで行っていることを、例えばチームで作業をするように、役割分担を行いそれぞれのクラスにそれぞれの役割を与えます。

例えば、次のような役割分担を行います。

  1. 〇×あてゲームを起動する役割
  2. 標準入力を受け取る、などの標準入出力をコントロールする役割
  3. 〇×あてゲームの各種判定を行う役割

もともとのプログラムは、全部の処理が書いてあるので、これを切り貼りして改造します。

1-2. クラスを作成する

上記の通り、2つの役割を担当するクラスを作成します。

  1. 〇×あてゲームを起動する役割:Lv2Main ※作成済み
  2. 標準入力を受け取る、などの標準入出力をコントロールする役割: MarubatsuConsole
  3. 〇×あてゲームの各種判定、ユーティリティの提供を行う役割: MarubatsuUtils

これらのクラスに必要な処理を切り貼りして、各クラスに移植します。
その結果を以下に記載致します。

※リンクはGithubにアップしたコードです。
Lv2Main

package jp.zenryoku.tutorial.level2;

import java.util.Scanner;

/**
 * Javaの基本レベル2:小さなレベルのプログラムを拡張する。
 * 今回は、〇か×か当てるゲーム。
 */
public class Lv2Main {
    /** 終了フラグ */
    private static boolean isFinish;

    public static void main(String[] arg) {
        isFinish = false;
        // 標準入力を受け取るクラスをインスタンス化
        Scanner scan = MarubatsuUtils.getScanner();

        while (true) {
            // ゲーム開始文言
            MarubatsuConsole.printGameStart();
            // 標準入力を受け取る
            int input = scan.nextInt();
            // isFinishがtrueならば処理終了。
            if (isFinish) {
                MarubatsuConsole.printTerminated();
                break;
            }
            // 0か1の値を返却する
            int res = MarubatsuUtils.nextInt(2);

            // 0: 〇 1: ×で当たったかどうかの判定
            boolean isAtari = MarubatsuUtils.judgeAtariOrNot(res, input);
            MarubatsuConsole.printAtatiorNot(isAtari, input);

            // 続けるのかどうか判定する
            if (MarubatsuConsole.printNextPlayOrNot(scan)) {
                break;
            }
        }
    }
}

MarubatsuConsole>※修正した後のコードがアップしてあります。

package jp.zenryoku.tutorial.level2;

import java.util.Scanner;

public class MarubatsuConsole {
    /**
     * 〇×あてゲームの開始文言を表示
     */
    public static void printGameStart() {
        System.out.println("「〇×あてゲーム」 0: 〇 1: ×。");
    }

    /**
     * 〇×あてゲームの終了文言を表示
     */
    public static void printTerminated() {
        System.out.println("プログラムを終了します。");
    }

    /**
     * 当たったかどうかを表示する。
     * @param isAtari 当たり判定の結果
     */
    public static void printAtatiorNot(boolean isAtari, int input) {
        String value = input == 0 ? "〇" : "×";
        if (isAtari) {
            System.out.println("あたり:" + value);
        } else {
            System.out.println("はずれ:" + value);
        }
    }

    /**
     * 〇×あてゲームを続けるかを表示、Yes or Noを取得する
     * @param scan
     * @return true: 続ける  false: やめる
     */
    public static boolean printNextPlayOrNot(Scanner scan) {
        boolean playNext = false;
        System.out.println("続けますか? 0: 続ける  1: やめる");
        int next = scan.nextInt();
        if (next == 1) {
            playNext = true;
        }
        return playNext;
    }
}

MarubatsuUtils>※修正した後のコードがアップしてあります。

package jp.zenryoku.tutorial.level2;

import java.util.Random;
import java.util.Scanner;

public class MarubatsuUtils {
    /** 標準入力を受け取るクラス */
    private static Scanner scan;
    /** 乱数の生成クラス */
    private static Random rdm;

    /**
     * 標準入力を受け取るクラスを生成、取得する。
     * ※シングルトン実装
     * @return Scanner
     */
    public static Scanner getScanner() {
        if (scan == null) {
            scan = new Scanner(System.in);
        }
        return scan;
    }

    /**
     * 乱数生成クラスがインスタンス化されていなければ、インスタンス化します。
     * すでにインスタンス化しているときは、既存のインスタンスを使用します。
     * ※シングルトン実装
     *
     * @param bound
     * @return 生成した乱数
     * @see <a href="https://docs.oracle.com/javase/jp/8/docs/api/java/util/Random.html">Random</a>
     */
    public static int nextInt(int bound) {
        if (rdm == null) {
             rdm = new Random();
        }
        return rdm.nextInt(bound);
    }

    /**
     * 生成した乱数と、入力値が等しいか判定する。
     *
     * @param res 生成した乱数
     * @param input 入力値
     * @return true: 等しい false: 違う
     */
    public static boolean judgeAtariOrNot(int res, int input) {
        // 0: 〇 1: ×で当たったかどうかの判定
        return res == input;
    }

}

メインメソッドは、すっきりしたコードになったと思います。

メインメソッドでは、どんな処理をしているかが一目瞭然になり。各クラスはその処理だけが書いてある形になります。

もちろん、引数と返り血があるので、呼び出し元に多少なりとも影響が出ます。

しかし、これでメインメソッドを変更しなくても〇×あてゲームを拡張する準備ができました。

ポイント

メインメソッドには、大まかな処理の順序を書く。具体的には下のような形です。

// <無限ループ開始>
// ■ゲーム開始文言を表示
// ■標準入力を受け取る
// ■isFinishがtrueならば処理終了。
// ■0か1の値を返却する
// ■0: 〇 1: ×で当たったかどうかの判定
// ■続けるのかどうか判定する

<やったこと>

  1. これらの処理をはじめの状態では、すべてLv2Mainクラスに記述していましたが、これをクラス別に分けました。
  2. 作成した各クラスをメインメソッドで呼び出して実行する。この時に初めの動きと変わらないことを確認しました。

これにより、プログラムのコードがすっきりして(したと自分は思います。。。)、メインメソッドを修正しなくても次の部分の処理が、修正が可能になりました。

1. ■ゲーム開始文言を表示: MarubatsuConsoleの修正
2. ■標準入力を受け取る: MarubatsuUtilsの修正
3. ■0か1の値を返却する: MarubatsuUtilsの修正
4. ■0: 〇 1: ×で当たったかどうかの判定: MarubatsuUtilsの修正
5. ■続けるのかどうか判定する: MarubatsuConsoleの修正

「そのように作ったんだから当たり前だろ?」と思った方、正常でございます。
このような作り方が、クラスを拡張する、アプリケーションを拡張するときに、役立つのです。

今回の「〇×あてゲーム」のような小さなレベルのプログラムはファイル一枚で何も問題はありませんが、会社の業務で使用するような大きなアプリケーションでは、いろんな機能が必要なので、役割分担を行い、機能拡張がしやすい形で、プログラムを組んでいきます。

拡張する準備をする

初めに「必要になること」について記載したのですが、触れていない部分があります。次の部分です。

  • 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える

具体的にどのようなことか?これについて、記載したいと思います。

JavaならばJUnit

JavaのテスティングフレームワークといえばJUnitです。
具体的に、どのように使うか記載したいと思います。

役割分担をしたクラスのテストクラス作成

作成した各クラスのテストケースを作成するためのクラスを作成します。
クラス名は、「対象のクラス名 + Test」の形で作成します。

  • MarubatsuConsole: MarubatsuConsoleTest
  • MarubatsuUtils : MarubatsuUtilsTest

作成したコードは次の通りです。ただし、MarubatsuConsoleTestは、標準出力の内容を取得して。。。とちょっと面倒なので、今回は実装しません。

<JUnit実行結果>

package jp.zenryoku.tutorial.level2;

import org.junit.jupiter.api.Test;

import java.util.Scanner;

import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.matchers.JUnitMatchers.either;

public class MarubatsuUtilsTest {
    /** テスト対象クラス(全てstaticメソッドなのでインスタンス不要) */
    private static MarubatsuUtils target;

    @Test
    public void testGetScanner() {
        Scanner scan = MarubatsuUtils.getScanner();
        // インスタンスが取得できていることを確認
        assertNotNull(scan);
    }
    @Test
    public void testNextInt() {
        int res = MarubatsuUtils.nextInt(2);
        // インスタンスが取得できていることを確認
        boolean isRondom = res == 0 || res == 1;
        assertTrue(isRondom);
    }

    @Test
    public void testIsAtari() {
        assertTrue(MarubatsuUtils.judgeAtariOrNot(0, 0));
        assertTrue(MarubatsuUtils.judgeAtariOrNot(1, 1));

        assertFalse(MarubatsuUtils.judgeAtariOrNot(0, 1));
        assertFalse(MarubatsuUtils.judgeAtariOrNot(1, 0));
    }
}

でわでわ。。。

Java Basic 中級編 ~①アプリケーション設計を考える~

イントロダクション

Javaの基本文法が理解できたところで、実際に動くものを作ってみたいと思うのが人情だと思います。
基本文法だけでも、簡単なプログラムを作ることができます。
例えば、下のようなものです。

単純なゲームループ

1. アプリケーションを作るために

今までに、Javaの基本を学習してきました。大まかに次のようなことを学習しました。

  1. メインメソッドを動かすこと
  2. Stringやintなどのデータ型があり、それぞれに意味があること
  3. 条件分岐、繰り返しなどの文法(if文、switch, while, forなど)
  4. 自分で作ったクラスもデータ型として宣言することが出来て、呼び出す(動かす)事ができること
  5. JavaAPIで提供しているクラス(java.lang.Math, java.util.Scannerなど)を使用できること

これらのことを、このブログでは「Java Basic」と呼んでいました。

この基本に対して1段上の領域があります。それは「オブジェクト指向」と呼ばれている考え方なのですが、これの解釈が人によって千差万別なので、この言葉は使わないようにしようと考えております。

2. アプリケーションを作り運用することも考える

どちらかといえば、「エクストリーム・プログラミング」のほうが近いように思います。

エクトリーム・プログラミングに関しては上記のリンク先を参照してください。
早い話が、次のようなものです。

ソフトウェア品質 を向上させ、変化する顧客の要求への対応力を高めることを目的としたソフトウェア開発プロセスである。アジャイルソフトウェア開発の一つとして、短い開発サイクルで頻繁に「リリース」することを推奨することで、生産性を向上させ、新しい顧客の要求を採用するためのチェックポイントを導入することを意図している。

これを実現するために、次のようなことが求められます。

  • 各プログラム間(Javaのクラス同士)の依存度を限りなく低くする。
  • プログラムを拡張するのに、元のプログラムをほとんど変更しない。
  • Javaを使えるレベルの知識(技術)がある人なら、誰が見てもわかるようなコードを書く。
  • 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える

個人で実現するのは、結構な苦労ですが、一人でアプリのリリースまでやろうと考えるならやっておきたいところです。

3. 基本が大前提

Javaでなくてもそうですが、基本ができないと応用はできません。
基本というのは、上記でいうところの「Java Basic」は、たとえて言うならば、「Javaの基本」という本の上巻に当たります

「じゃ、実際どの部分が基本なの?」という疑問が出ると思います。
これは、自分の見解ですが、下のように考えています。

つまりは、Javaだけでなくプログラミングの基本には次のような段階があると思うということです。

  • 「Javaの基本:上巻」は小さなレベルのプログラム(アプリケーション)が作れるレベルの基本。
  • 「Javaの基本:下巻」は小さなレベルのプログラムを拡張して、どんどん新しい機能を追加していけるレベルの基本

今後は、「Javaの基本」という本の下巻に当たる部分を学習します。

今までの学習方法(古い時代の学習方法だと思います。。。)は、とりあえず何かしらのアプリケーションを組み続けてそこから、自分で上にも書きましたが、次のことを理解していきました。

  • 各プログラム間(Javaのクラス同士)の依存度を限りなく低くする。
  • プログラムを拡張するのに、元のプログラムをほとんど変更しない。
  • Javaを使えるレベルの知識(技術)がある人なら、誰が見てもわかるようなコードを書く。
  • 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える

これらのようなことを理解するのには、自分の場合、大体3年くらいかかりました。
あくまでも、基本が理解できたというレベルです。

そこから、このような基本を応用し、実践していくことが必要になりますが、それこそは実際にやってみるしかありません。

基本ができたら、そこから先は自分の作りたいものをどんどん作っていくということが最大の課題になると思います。自分も時間を見つけてやっています。いまだに完成までいかないものがあります。。。

やはり、「モチベーションの維持と、健康の維持」が大きな課題となります。

4. 小さなレベルのプログラムを拡張する

先ほどから、「小さなレベルのプログラムを拡張する」と記載していますが、これは一体どういうことなのか?これについて、記載したいと思います。

基本はプログラムを修正しない

小さなレベルのプログラムを拡張するときに、なるべくプログラムを修正しないようにプログラムを組んでいくというところが、このレベルでの基本になります。
それには、クラスとクラスの関係をうまく作るということがカギになるのですが、具体的なコードを見ていくほうが早いので、サンプルコードを見ていきましょう。

〇×あてゲーム

単純なアプリケーション「〇×あてゲーム」を作成しました。下のようなコードで動きました。
※コードはGithubにあります

/**
 * Javaの基本レベル2:小さなレベルのプログラムを拡張する。
 * 今回は、〇か×か当てるゲーム。
 */
public class Lv2Main {
    /** 週r等フラグ */
    private static boolean isFinish;

    public static void main(String[] arg) {
        isFinish = false;
        // 標準入力を受け取るクラスをインスタンス化
        Scanner scan = new Scanner(System.in);

        while (true) {
            System.out.println("「〇×あてゲーム」 0: 〇 1: ×。");
            // 標準入力を受け取る
            int input = scan.nextInt();
            // isFinishがtrueならば処理終了。
            if (isFinish) {
                System.out.println("プログラムを終了します。");
                break;
            }
            // 0か1の値を返却する
            Random rdm = new Random();
            int res = rdm.nextInt(2);

            // 0: 〇 1: ×で当たったかどうかの判定
            boolean isAtari = res == input;
            String value = input == 0 ? "〇" : "×";
            if (isAtari) {
                System.out.println("あたり:" + value);
            } else {
                System.out.println("はずれ:" + value);
            }
            System.out.println("続けますか? 0: 続ける  1: やめる");
            int next = scan.nextInt();
            if (next == 1) {
                break;
            }
        }
    }
}

次回

このプログラムをなるべく修正しなくても機能拡張できるように修正していきます。
現状のプログラムは、修正しようとしたら必ずメインメソッドを修正する必要があります。これを解消するために試行錯誤します。

でわでわ。。。

JS Google Client library ~GoogleのJSクライアントライブラリを使う~

イントロダクション

ここではYoutube Data APIを使用するためのサンプルコードが動かなかったので、それを動かすための調査を行いました。

まずは、下のコードでエラーが出ていなので根本的なところを見直します。

var request = gapi.client.youtube.search.list({
    q: q,
    part: 'snippet'
  });

このコードでは、「gapi.client」以降のプロパティ(youtube以降)が参照できないのでそれを解決するっための調査を行いました。まずは使用している「クライアントライブラリ」に関して調べてみました。

すると、「youtube」というプロパティがなくなったのか。。。まぁ、ありませんでした。詳細は以下に記載します。

クライアントライブラリ

JavaScriptでのGoogle Client libraryを使用して各種Google APIを使用したいと考えています。
githubを参考にしています。

概要

JavaScript用のGoogleAPIクライアントライブラリは、JavaScriptクライアントアプリケーション開発者向けに設計されています。多くのGoogleAPIへのシンプルで柔軟なアクセスを提供します。
JavaScriptクライアントライブラリを使用して、WebアプリケーションからPeople、Calendar、DriveなどのGoogleAPIを操作できます。開始するには、このページの指示に従ってください。

入門

JavaScriptクライアントライブラリを使用してAPIリクエストを作成する方法はいくつかありますが、それらはすべて同じ基本パターンに従います。

  1. アプリケーションはJavaScriptクライアントライブラリをロードします。
  2. アプリケーションは、APIキー、OAuthクライアントID、およびAPIディスカバリドキュメントを使用してライブラリを初期化します。
  3. アプリケーションは要求を送信し、応答を処理します。

APIリクエストを行う方法

Step1: ライブラリをロードする

サンプルコードです。

<script src="https://apis.google.com/js/api.js"></script>
<script>
function start() {
  // 2. Initialize the JavaScript client library.
  gapi.client.init({
    'apiKey': 'YOUR_API_KEY',
    // Your API key will be automatically added to the Discovery Document URLs.
    'discoveryDocs': ['https://people.googleapis.com/$discovery/rest'],
    // clientId and scope are optional if auth is not required.
    'clientId': 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
    'scope': 'profile',
  }).then(function() {
    // 3. Initialize and make the API request.
    return gapi.client.people.people.get({
      'resourceName': 'people/me',
      'requestMask.includeField': 'person.names'
    });
  }).then(function(response) {
    console.log(response.result);
  }, function(reason) {
    console.log('Error: ' + reason.result.error.message);
  });
};
// 1. Load the JavaScript client library.
gapi.load('client', start);
</script>

サンプルコードの「'YOUR_API_KEY'」と「'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com'」の部分にそれぞれの値を入力する。
各API(今回はYoutube Data API)の認証情報ページからAPIキーとクライアントIDを設定、取得ができます。

しかし、エラーが。。。

参考サイトのコードでは下のようになっていましたが。。。

}).then(function(response) {
    console.log(response.result);
  }, function(reason) {
    console.log('Error: ' + reason.result.error.message);
  });

それを修正して、次のようにしたら、原因がわかりました。

  }).then(function(response) {
    console.log(response.result);
  }, function(reason) {
    console.log('Error: ' + reason);
  });
};

【出力結果】

{error: 'idpiframe_initialization_failed', details: "Not a valid origin for the client: https://zenryok…egister this origin for your project's client ID."}
details: "Not a valid origin for the client: https://zenryokuservice.com has not been registered for client ID XXXXXXXX.apps.googleusercontent.com. Please go to https://console.developers.google.com/ and register this origin for your project's client ID."
error: "idpiframe_initialization_failed"

つまりは実行するURLが登録されていないということでした。

調べてみるとOAUTH2.0の設定ができていないのが原因であろうということでした。

OAUTH2.0の設定

googleのページを参考にします。
認証情報を設定してやると結局は、クライアントIDが作成されるので、初めに作成したクライアントIDで問題はなさそうです。
改めてエラーメッセージを見てみるとJSONで、返却されていました。その内容は下のようになっていました。

{
  "error": {
    "code": 403,
    "message": "People API has not been used in project XXXX before or it is disabled. 
    Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=XXX then retry. 
    If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
    "status": "PERMISSION_DENIED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.Help",
        "links": [
          {
            "description": "Google developers console API activation",
            "url": "https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=XXX"
          }
        ]
      },
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "SERVICE_DISABLED",
        "domain": "googleapis.com",
        "metadata": {
          "consumer": "projects/XXXX",
          "service": "people.googleapis.com"
        }
      }
    ]
  }
}

調べてみると「Google People API」を使用するようです。そしたら、そのままリクエストを送信したほうが良いので、Googleクライアントライブラリを使用しない形で実装することにします。

普通にGETリクエスト

こちらの動画を参考に検索リクエストを送信してみました。
すると、クォータが超過したようで、テストできなくなりました。

error:
code: 403
errors: [{…}]
message: "The request cannot be completed because you have exceeded ...

これは日を改めて実行すれば、大丈夫なので、今日はここまでにしようと思います。

XAMMP 動かない ~Error: Apache shutdown unexpectedly.~

エラーが起きた状況

久しぶりに、XAMMPを起動して自分のホームページを回収・増築しようと考えました。
しかし、テスト用のXAMMPが動かないので、本番環境(レンタルサーバー)にアップする以前の状態になってしまいました。。。

Error: Apache shutdown unexpectedly.

下のようなエラーメッセージが出ました。

11:33:45 [Apache] Error: Apache shutdown unexpectedly.
11:33:45 [Apache] This may be due to a blocked port, missing dependencies,
11:33:45 [Apache] improper privileges, a crash, or a shutdown by another method.
11:33:45 [Apache] Press the Logs button to view error logs and check
11:33:45 [Apache] the Windows Event Viewer for more clues
11:33:45 [Apache] If you need more help, copy and post this
11:33:45 [Apache] entire log window on the forums

そして、下のようなエラーダイアログが表示されました。

VCRUNTIME140.dllが見つからないため、コードの実行を続行できません。」というメッセージが書いていました。つまりは、このファイルがあればよいと判断します。

VCRUNTIME140.dllを調べる

こちらのページを参考にするとVC++がインストールされている必要があるようです。

  1. こちらにアクセス
    下のほうにこんな一覧があるので、赤悪の部分をクリックします。

    対象になるパッケージをダウンロードします。
  2. VC_redist.x64.exeをダウンロード
  3. ダブルクリックでインストール

そしたら、Moodleというものを、インストールしていたので、そのほかに下のようなエラーが出ました。

$CFG->dataroot is not configured properly, directory does not exist or is not accessible! Exiting.

Moodleは使用しないので、XAMMPを再インストールすることにしました。

最終結果

下のように起動確認が取れました。

Android App 〜画面作成を行う〜

画面作成

作成したのは、以下の機能です。

  1. 多言語化
  2. 画面に文字列を表示する

stringc.xmlを使う

下のように作成した*strings.xmlの中にある「android\:id="@+id\/top_title"」をアプリに反映するというところです。
次のファイルに以下の1行を追加しました。

android:id="@+id/top_title"

全体を記述すると下のようになります。

<activity_main.xml>

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/top_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.534"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.05" />

</androidx.constraintlayout.widget.ConstraintLayout>

<strings.xml>

<resources>
    <string name="app_name">目標達成アプリ</string>
</resources>

これを実行した結果、下のように表示されました。

テキストのサイズ変更

したのようなプロパティ(属性)を設定することで、テキストのサイズを変更できました。

        android:layout_width="match_parent"
        android:layout_height="100dp"

そして、自動サイズ調整はしたのようにやるみたいです。

        android:autoSizeTextType="uniform"
        android:autoSizeMinTextSize="12sp"
        android:autoSizeMaxTextSize="100sp"

これを記述するとAndroid APIのレベル別のactivity_main.xmlが作成されるようです。
※Android Studioで「Override XXX」と表示されるのでそれをクリックしました。

最終的に、下のようなファイルが作成されました。

イメージの追加

説明を文言でするよりも、動画の方がよいと思いました。

サポートライブラリ追加

ここで、エラーが発生しています。これは、AppCompatというサポートライブラリが無いために起きているエラーです。
これをインストールするためには、Java11が必要になります。

JDK11インストール

WidnowsでのAndroid Studioが起動できなかったので、現在はMacで作業をしていますので、home brewを使用してインストールしました。参考サイトはこちらです。

下のコマンドでインストールできました。

brew install java11

しかし、次のようなエラーが出ました。

Invalid Gradle JDK configuration found

これは、プロジェクト構成(Project Structure) -> SDKLocation -> JDKの設定でJava11tを設定し、改めてビルドしたらなおりました。

Android Studioアップグレード

そして、初めのイメージを設定する部分ですが、次のようなエラーが出ました

Sets a drawable as the content of this ImageView. Allows the use of vector drawable when running on older versions of the platform.

これは、Android Studioをアップデートしてください。というものでした。。。

こちらのサイトを参考にAndroid Studioは下のようにConfigure -> Check for Updateを選択します。

しかし、選択肢が「Download」しかなかったので、結局新しいものをダウンロード、インストールすることになりました。
最終的に置き換えるという形で、アップグレードしました。

まだ、エラーが解消されません。。。

This view is not constrained. It only has designtime positions, so it will jump to (0,0) at runtime unless you add the constraints

ここのサイトを参考にすると設定を追加すると治るということなので、次のようにプロパティを追加しました。

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/ic_add_mokuhyo"
        tools:layout_editor_absoluteX="50dp"
        tools:layout_editor_absoluteY="222dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="VectorDrawableCompat" />

追加したのは、次の部分です。

        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"

そして、英語、日本語と多言語化した場合は、下の3つのファイルに文字列を追加する必要がありました。

  • strings.xml(default)
  • strings.xml(ja)
  • strings.xml(en)

イメージのレイアウト

結構手こずりました。下のサイトを参考に学習しました。

  1. Image Asset Studioを使用する

色々と試したけど結局実行するデバイスを>色々と試したけど結局実行するデバイスを 新しくして試すことにしました。。。

どうやらしているする属性が違うようでした。こちらのサイトでありました。

 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <ImageView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:src="@drawable/my_image"
         android:contentDescription="@string/my_image_description"
         />
 </LinearLayout>
 android:src="@drawable/my_image"

この部分が自動生成したものと値が違う。。。

そして、レイアウトの設定も問題がありそうなので、本家のサイトを参考にレイアウトも学習します。

何かしら触っていると、わかってくるような感じで説明がうまくできないのですが、 各値を設定してやると、見た目も変更されるのでそれで、自分の思った通りに修正するのが、早いと思います。

画像が表示されいない問題

これの原因がわかりました。SDKのバージョン別にactivity_main.xmlが存在していました。

これが原因で、一向に画像が表示されなかった。。。というわけでした。。。

最終的に作成したactivity_main.xmlは下のようなものです。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TableRow
            android:layout_width="178dp"
            android:layout_height="192dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <ImageView
                    android:id="@+id/imageView3"
                    android:layout_width="80dp"
                    android:layout_height="80dp"
                    android:layout_marginStart="40dp"
                    android:layout_marginTop="10dp"
                    android:contentDescription="目標追加・一覧"
                    android:src="@drawable/ic_add_mokuhyo"
                    android:visibility="visible"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    tools:srcCompat="@drawable/ic_add_mokuhyo" />

                <TextView
                    android:id="@+id/textAddMokuhyo"
                    android:layout_width="180dp"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="10dp"
                    android:autoSizeMaxTextSize="40dp"
                    android:autoSizeMinTextSize="18dp"
                    android:text="目標追加・一覧"
                    android:textSize="20dp"
                    android:visibility="visible"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/imageView3" />
            </LinearLayout>

        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"></LinearLayout>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"></LinearLayout>
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"></LinearLayout>
        </TableRow>

    </TableLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

LinerLayoutを学ぶ

よく使用されるであろうレイアウトの1つとして「LinerLayout」があります。ドキュメントにも載っているので基本的なレイアウトなのであろうと思いこれを学ぶことにしました。

LinearLayout は、すべての子を垂直または水平の一方向に揃えるビューグループです。android\:orientation 属性でレイアウトの方向を指定できます。

とりあえずは、コンポーネント(ボタンなどの部品)を並べてみます。そして、プロパティ(android\:XXXX)の値を変えてどのような配置になるかみてみたいと思います。

ここで、着目するプロパティを以下に示します。

プロパティ名 内容 
android\:orientation レイアウトの方向を指定
android\:layout_weight 画面上で占めるスペースという観点で「重要度」を表す値をビューに指定します。この weight 値が大きいほど、親ビューの残りのスペースを埋めるように拡大されます。子ビューに weight 値を指定することで、ビューグループの残りのスペースを、宣言された weight の割合に応じて割り当てることができます。デフォルトの weight 値は 0 です。
android\:layout_height
android
:layout_width
android\:layout_weight
それぞれの値を0dp、0dp、1に設定することで均等配分することができます。

ちょっと試してみましたが、他のプリパティと組み合わせる必要があるので、色々やってみないと理解できません。

次は、合わせて出てきた、TextViewに関してもプロパティを見てみます。

感想

  1. レイアウトの使い方、センタリングなど、やり方を調べると色々出てくるので手が止まることは少なくなりそうだ。
  2. レイアウトマネージャー(動画にある画面)での操作はわかりやすいが、プロパティの場所を探すのが大変だった。

でわでわ。。。

UMLの書き方読み方~シーケンス図~

シーケンス図の書き方

IT専科というサイトを参考にするとシーケンス図とは。。。

シーケンス図とは、クラスやオブジェクト間のやりとりを時間軸に沿って表現する図です。

アジャイルモデリング(AM)というサイトを参考にして学習します。そして、下のような図があります。

シーケンス図1

これは、手書きなのでちょっとわかりずらいですが、ポイントとしては、次のような部分です。

  1. 登場人物が3人(アプリケーションを含む)

  2. それぞれの時間軸が縦に伸びている

  3. 縦軸に対して、横軸は「何かしらの動きを示す」

大まかに、このようなところがポイントになります。
アジャイルモデリング(AM)というサイトには、下のような説明がありました。

これがシーケンス図と呼ばれている理由は明らかでしょう。ロジックの実行順序(シーケンス)がメッセージ(横向きの矢印)の順序で示されています。最初のメッセージは左上から始まり、次のメッセージはそのすぐ下に書く、というように表していきます。

シーケンス図の内訳

サービスレベルのシーケンス図

参考サイトには、下のような説明があります。

図の上部に横に並んでいる箱は、分類子またはそのインスタンスを表します。この分類子は通常、ユースケース、オブジェクト、クラス、またはアクターです。オブジェクトとクラスにはメッセージを送ることができるため(オブジェクトは操作の呼び出しを通じて、クラスは静的操作の呼び出しを通じてメッセージに応答します)、これらをシーケンス図に含めるのは筋が通っています。アクターも、利用シナリオを開始したり、利用シナリオで能動的な役割を果たすので、シーケンス図に含めることができます。オブジェクトにはUML標準の「名前: クラス名」という書式でラベルをつけます。この名前は必須ではありません(図で名前の付いていないオブジェクトのことを無名オブジェクトと呼びます)。クラスには「クラス名」という書式でラベルを付け、アクターには「アクター名」の書式で名前を付けます。オブジェクトのラベルには下線が引かれていますが、クラスとアクターには引かれていないことに注意してください。たとえば、図3の学生オブジェクトにはある学生という名前が付けられています。これが名前付きオブジェクトです。それに対してゼミのインスタンスは無名オブジェクトです。Studentのインスタンスに名前が付けられているのは、複数の場所でメッセージのパラメータとして使われているためです。ゼミのインスタンスの方は、図の他の場所で参照する必要がないので、無名にしておくことができます。図2では、学生クラスが永続性フレームワーククラスにメッセージを送っています(永続性フレームワーククラスには\<\>というステレオタイプを付けてもよかったのですが、図を簡潔にしておくために付けませんでした)。クラスに対して送られたメッセージは、すべて静的メソッドとして実装します。これについては後で説明します。

まとめると次のようになります。
上の図で示した「登場人物」は、「ユースケース、オブジェクト、クラス、またはアクター」

  • ユースケース:「申込用紙を書く」とか、「送信ボタンを押下」などのように人の動き、作業を示す。
  • オブジェクト:クラスとかインスタンスのこと(厳密にはインスタンスの事)
  • アクター  ;人のモデル(人を示す絵)

そして、オブジェクトはメッセージを送信することができるので、ほかのオブジェクトを呼び出し、何かしらの処理を行わせることができます。その処理が終わったら、また元のオブジェクトの線に戻ってきます。下のような矢印のことです。

※引用した文言の中に「すべて静的メソッドとして実装します」とありますが、これはこちらのサイトでそのように実装しているということです。別に静的メソッドである必要はありません

そして、上記の手書きの画像をきれいに書くと下のようになるようです。

ここで、シーケンス図に使用される図をまとめると、IT専科というサイトの表を借りると下のようになります。

構成要素一覧
要素 表示形式 意味
ライフライン(Lifeline) ライフライン 記号 使用するオブジェクトやクラスを表現します。どちらか一方なら省略可能です。
実行仕様(ExecutionSpecification) 実行仕様 記号 生成されているライフラインが実行状態であることを意味します。
停止(Stop) 停止 記号 生成されたライフライン自体の消滅を意味します。
メッセージ(Message) 同期(Synchronous)メッセージ 同期メッセージ 記号 送り先のライフラインの実行に同期されるメッセージを意味します。メッセージ名には具体的な関数やINCLUDEディレクティブ等を記入します。
非同期(Asynchronous)メッセージ 非同期メッセージ 記号 送り先のライフラインの実行に同期されないメッセージを意味します。メッセージ名には具体的な関数やINCLUDEディレクティブ等を記入します。
応答(Reply)メッセージ 応答メッセージ 記号 送り先のライフラインから送り手への戻り値を意味します。メッセージ名には戻り値を格納する具体的な変数名等を記入します。
ファウンド(Found)メッセージ ファウンドメッセージ 記号 図解上にない送り手から送られた、もしくは送り手がダイアグラム上にないことを意味します。
ロスト(Lost)メッセージ ロストメッセージ 記号 意図された受け手に送られていない、もしくは受け手がダイアグラム上にないことを意味します。

▲PageTop

制御構造の記述

シーケンス図では、制御構造を表現するために「複合フラグメント」を使用します。種類および、記述例は次の通りです。

複合フラグメントの種類

複合フラグメントには、次の種類があります。

複合フラグメント一覧
InteractionOperator 読み 意味
ref 相互作用使用(InteractionUse) 別のシーケンス図を参照することを表します。
alt オルタナティブ(Alternative) 分岐処理を表します。
opt オプション(Option)

条件を満たした場合のみ実行される処理を表します。

par パラレル(Parallel) 並列処理を表します。
loop ループ(Loop) ループ(繰り返し)処理を表します。
break ブレイク(Break) 処理の中断を表します。
critical クリティカル(Critical) マルチスレッド環境での同期処理など、排他制御を表します。
assert アサーション(Assert) 処理が妥当であるための定義を表します。
neg 否定(negation)

本来、実行されるはずがない処理(メッセージ)であることを表します。

ignore 無効(ignore) あまり重要な処理(メッセージ)ではないことを表します。
consider 有効(Consider) 重要な処理(メッセージ)であることを表します。

基本的な処理を表現する

ここでいう「基本的な処理」とは、次のものを指します。

  1. 参照(REF)
  2. 条件分岐(ALT)
  3. 条件判断(OPT)
  4. 並列処理(PAR)
  5. 反復処理(LOOP
  6. 中断(BREAK
  7. クリティカルセッション(CRITICAL
  8. アサート(ASSERT
  9. 不正なシーケンス(NEG
  10. 無効(IGNORE
  11. 有効(CONSIDER

このような形で記述します。あとは、どのような動きを表現したいのか?を考えるだけです。
しかし、これらの「動き」を考えるためには、プログラミングの基礎を理解する必要があります。
※よかったら参考にどうぞ、Java Basic学習フロー

具体的には、「じゃんけんゲーム」を作成しようと考えたときには、どのような画面で、ユーザーの入力はどのように行うのか?などの「人間レベル」の動きから、「入力値からどのような処理をして勝敗の判定を行うか?」という「プログラムレベル」の動きを考える必要があるためです。

こんなところで失礼します。

でわでわ。。。

Android App 作成 ~アプリの基礎を学ぶ~

Androidアプリの基礎

下のページにあるチュートリアルを行っていきます。
参考サイト:Android Developperのサイトです。

そして、Androidアプリを作成するうえで理解しておきたいのが、下のようなライフサイクルと呼ばれるものです。
LifeCycle

具体的には、「アクティビティのライフサイクル」ということです。

「初めてのアプリを作成する」という項目は飛ばします。以前やったので。。。

概要をつかむ

Androidアプリを作成するための材料(コンポーネント)として以下のものがあります。

  • アクティビティ
  • サービス
  • ブロードキャスト レシーバ
  • コンテンツ プロバイダ

つまりは、上のような「グループに分けられたクラスがありますよ」ということです。

これらのクラスがそれぞれ、次のように説明されています。

アクティビティ
アクティビティは、ユーザーとやり取りするためのエントリ ポイントです。これは、1 つのユーザー インターフェースを持つ 1 つの画面で表されます。たとえば、メールアプリには、新着メールの一覧を表示するアクティビティ、メールを作成するアクティビティ、そしてメールを閲覧するアクティビティがあります。メールアプリでは、これらの複数のアクティビティが一体となって 1 つのユーザー エクスペリエンスを形成しますが、それぞれのアクティビティは他のものから独立しています。したがって、これらのアクティビティのいずれかを、別のアプリから開始することができます(メールアプリが許可している場合)。たとえば、カメラアプリからメールアプリの新規メールを作成するアクティビティを開始できます。そのようにして、ユーザーが写真をメールで共有できるようにします。アクティビティは、システムとアプリ間における次の重要なインタラクションを行えるようにします。
アクティビティをホストしているプロセスを継続的に実行するために、ユーザーの現在の操作内容(画面の表示)を追跡。
以前に使用されたプロセス(停止されたアクティビティ)のうち、ユーザーが再度アクセスする可能性があるものを検知し、それらの優先順位を上げてプロセスを維持。
アプリがプロセスを強制終了した場合に、ユーザーが以前の状態を復元したアクティビティに戻れるように支援。
アプリ間でのユーザーフローをアプリが実装する手段と、システムがそれらのフローを連携させるための手段を提供(ここでは最も一般的な例を説明します)。
アクティビティは Activity クラスのサブクラスとして実装します。Activity クラスの詳細については、デベロッパー ガイドのアクティビティをご覧ください。

サービス
サービスは、さまざまな理由によりアプリをバックグラウンドで実行し続けるための汎用エントリ ポイントです。長期間の操作やリモート プロセスを処理するためにバックグラウンドで実行されるコンポーネントです。サービスにはユーザー インターフェースがありません。たとえば、サービスはユーザーが別のアプリを使用している間にバックグラウンドで音楽を再生したり、ユーザーが別のアクティビティを操作している間にそれを妨げることなくネットワークからデータを取得したりします。アクティビティなどの他のコンポーネントが、サービスを開始して実行したり、サービスとやり取りするためにサービスにバインドしたりすることができます。アプリの管理方法についてサービスがシステムに通知するセマンティクスは 2 つあり、それぞれ異なる意味を持っています。開始されたサービスが、作業が終了するまで自身の実行を維持するようシステムに指示します。これにより、ユーザーがアプリから離れても、バックグラウンドでデータを同期したり、音楽を再生したりできます。バックグラウンドでのデータの同期や音楽の再生は、開始されたサービスとしてはそれぞれ異なるタイプと認識され、システムがそれらのサービスに対して行う処理もそれぞれ異なります。
音楽の再生はユーザーが直接意識しているものなので、アプリはフォアグラウンドになりたいことをユーザーに通知で知らせることで、システムにそれを指示します。この場合、システムはそのサービス プロセスの実行を維持するよう最善を尽くします。このプロセスが終了するとユーザーが不満を覚えるからです。
通常のバックグラウンド サービスは、その実行をユーザーが直接意識していません。したがって、システムはより柔軟にそのプロセスを管理できます。ユーザーにとってより緊急な課題を処理するために RAM が必要となった場合には、プロセスを強制終了し、後から再開できます。
他のアプリ(またはシステム)がサービスを利用することを明示しているなら、バインドされたサービスが実行されます。これは基本的に、別のプロセスに API を提供するサービスです。これにより、これらのプロセス間に依存関係があることをシステムは認識します。プロセス A がプロセス B のサービスにバインドされている場合に、システムはプロセス B とそのサービスの実行を A のために維持する必要があることを認識します。さらに、プロセス A がユーザーにとって優先度が高い場合は、プロセス B もユーザーにとって重要であるとみなされます。サービスは良くも悪くもその柔軟性から、さまざまな上位レベルのシステム コンセプトにおいて、非常に有用な構成要素となってきました。ライブ壁紙、通知リスナー、スクリーン セーバー、入力方法、ユーザー補助機能サービス、その他多くの主要なシステム機能はすべて、アプリが実行するサービスとしてビルドされ、その実行時にシステムによりバインドされます。
サービスは Service のサブクラスとして実装されます。Service クラスの詳細については、デベロッパー ガイドのサービスをご覧ください。

ブロードキャスト レシーバ
ブロードキャスト レシーバは、通常のユーザーフローを外れて、システムがアプリにイベントを配信できるようにするコンポーネントです。これにより、アプリはシステム全体のブロードキャスト アナウンスに応答できます。ブロードキャスト レシーバは明確に定義されたアプリへのエントリであるため、システムは実行中でないアプリに対してもブロードキャストを配信できます。したがってアプリが、たとえば、近づいているイベントについてユーザーに知らせる通知を投稿するためのアラームをスケジューリングすることができます。アプリの BroadcastReceiver にアラームを配信することにより、そのアラームが作動するまでアプリの実行を維持する必要がなくなります。多くのブロードキャストの発信源はシステムです — たとえば、画面がオフになったことを通知するブロードキャスト、電池が残り少ないことを通知するブロードキャスト、画像がキャプチャされたことを通知するブロードキャストなどです。アプリから発信されるブロードキャストもあります — たとえば、端末にデータがダウンロードされ使用できることを他のアプリに知らせる場合などです。ブロードキャスト レシーバがユーザー インターフェースを表示することはありませんが、ステータスバー通知を作成して、ブロードキャスト イベントの発生時にユーザーにアラートできます。一般的には、ブロードキャスト レシーバは他のコンポーネントへの単なるゲートウェイであり、最小限の作業を行うことが前提となっています。たとえば、JobScheduler を使用してイベントに基づいた何らかの作業を実行する JobService をスケジュール設定する場合などです。
ブロードキャスト レシーバは BroadcastReceiver のサブクラスとして実装され、各ブロードキャストは Intent オブジェクトとして配信されます。詳細については、BroadcastReceiver クラスをご覧ください。

コンテンツプロバイダ
コンテンツ プロバイダは、ファイル システム、SQLite データベース、ウェブ、またはアプリからアクセス可能な他の永続的なストレージの場所に保存できるアプリデータの共有されている部分を管理します。コンテンツ プロバイダを介して、他のアプリがデータをクエリしたり、修正したりできます(コンテンツ プロバイダが許可している場合)。たとえば、Android システムはユーザーの連絡先情報を管理するコンテンツ プロバイダを提供しています。したがって、適切なパーミッションさえあれば、アプリからコンテンツ プロバイダに ContactsContract.Data などをクエリして、特定の人物に関する情報を読み取ったり書き込んだりできます。このような一般的なケースのために多くの API やサポートが組み込まれているため、コンテンツ プロバイダをデータベースの抽象化として考えたくなるかもしれません。しかし、システム設計の観点では、コンテンツ プロバイダには別の目的があります。システムから見て、コンテンツ プロバイダは、URI スキームにより識別される名前付きデータ項目を公開するための、アプリへのエントリ ポイントです。したがって、アプリは自身が保有するデータを URI 名前空間にどのようにマッピングし、その URI を他のエンティティに渡してデータにアクセスできるようにするかを決めることができます。アプリの管理に関して、システムでは以下が実行できます。
URI の割り当てはアプリが実行中であるかどうかには影響されないため、その URI を所有しているアプリが終了していても URI は維持されます。システムが確認する必要があるのは、該当 URI からアプリのデータを取得する必要があるときに、所有しているアプリが実行中であることだけです。
URI は重要で詳細なセキュリティ モデルも提供します。たとえば、アプリは画像の URI をクリップボードに配置することができますが、他のアプリが自由にアクセスできないようにコンテンツ プロバイダをロックしたままにできます。別のアプリがクリップボードのその URI にアクセスしようとした場合には、システムが一時的な URI パーミッションを付与して、その URI にあるデータのみにアクセスすることを許可します。その他のデータにはアクセスできません。
コンテンツ プロバイダは、アプリだけに公開されている、他で共有されていないデータを閲覧したり書き込んだりする場合にも役立ちます。
コンテンツ プロバイダは ContentProvider のサブクラスとして実装され、他のアプリがトランザクションを実行できるようにする API の標準セットを実装する必要があります。詳細については、デベロッパー ガイドのコンテンツ プロバイダをご覧ください。

これらを使って作る

上記のコンポーネント(部品)をしようしてAndroidアプリを作成します。この基礎になる部分をちゃんと理解すればほぼ極めたといってよいと思います。※勝手な想像です。なぜなら自分もこれから学習するので(笑)

なので、この基礎部分をしっかり学習したいと思います。

「概要をつかむ」のまとめ

Androidアプリを作るための基本的な材料(クラス)として次の4つがある。

  • アクティビティ
  • サービス
  • ブロードキャスト レシーバ
  • コンテンツ プロバイダ

そして、これらの材料を組み合わせてAndroidアプリを作成するということを理解。

次は、順番に「アクティビティ」を理解します。

アクティビティについて

アクティビティについて学習します。まずは、概要にある内容を抜粋を読みます。

main() メソッドで起動するアプリをプログラミングする際の枠組みとは異なり、Android システムでは、ライフサイクルのそれぞれの段階に対応するコールバック メソッドを呼び出すことにより、Activity インスタンス内のコードが開始されます。

つまるところは、以下の文言がキーポイントになります。

一般に、1 つのアクティビティがアプリ内の 1 つの画面を実装します。

例として、次のような説明があります。

アプリのアクティビティの 1 つが「設定」画面を実装し、他のアクティビティが「写真を選択」画面を実装します。

画面=アクティビティであり、このアクティビティの操作により様々な操作(処理)を実装するというわけです。

マニフェストで画面の設定

アプリでアクティビティを使用できるようにするには、マニフェストでアクティビティとその属性を宣言する必要があります。

    <manifest ... >
      <application ... >
          <activity android:name=".ExampleActivity" />
          ...
      </application ... >
      ...
    </manifest >

必須の属性は、アクティビティのクラス名を指定する android\:name だけです。

簡単ですね。つまりわかりやすい=使いやすいということだと思います。
続けて、そのほかの設定に関しては、次の部分にあります。

ラベル、アイコン、UI テーマなどのアクティビティの特性を定義する属性を追加することもできます。

こちらのページにアクティビティの詳細がありました。

構文は以下のようになっています。

<activity android:allowEmbedded=["true" | "false"]
          android:allowTaskReparenting=["true" | "false"]
          android:alwaysRetainTaskState=["true" | "false"]
          android:autoRemoveFromRecents=["true" | "false"]
          android:banner="drawable resource"
          android:clearTaskOnLaunch=["true" | "false"]
          android:colorMode=[ "hdr" | "wideColorGamut"]
          android:configChanges=["mcc", "mnc", "locale",
                                 "touchscreen", "keyboard", "keyboardHidden",
                                 "navigation", "screenLayout", "fontScale",
                                 "uiMode", "orientation", "density",
                                 "screenSize", "smallestScreenSize"]
          android:directBootAware=["true" | "false"]
          android:documentLaunchMode=["intoExisting" | "always" |
                                  "none" | "never"]
          android:enabled=["true" | "false"]
          android:excludeFromRecents=["true" | "false"]
          android:exported=["true" | "false"]
          android:finishOnTaskLaunch=["true" | "false"]
          android:hardwareAccelerated=["true" | "false"]
          android:icon="drawable resource"
          android:immersive=["true" | "false"]
          android:label="string resource"
          android:launchMode=["standard" | "singleTop" |
                              "singleTask" | "singleInstance"]
          android:lockTaskMode=["normal" | "never" |
                              "if_whitelisted" | "always"]
          android:maxRecents="integer"
          android:maxAspectRatio="float"
          android:multiprocess=["true" | "false"]
          android:name="string"
          android:noHistory=["true" | "false"]  
          android:parentActivityName="string" 
          android:persistableMode=["persistRootOnly" | 
                                   "persistAcrossReboots" | "persistNever"]
          android:permission="string"
          android:process="string"
          android:relinquishTaskIdentity=["true" | "false"]
          android:resizeableActivity=["true" | "false"]
          android:screenOrientation=["unspecified" | "behind" |
                                     "landscape" | "portrait" |
                                     "reverseLandscape" | "reversePortrait" |
                                     "sensorLandscape" | "sensorPortrait" |
                                     "userLandscape" | "userPortrait" |
                                     "sensor" | "fullSensor" | "nosensor" |
                                     "user" | "fullUser" | "locked"]
          android:showForAllUsers=["true" | "false"]
          android:stateNotNeeded=["true" | "false"]
          android:supportsPictureInPicture=["true" | "false"]
          android:taskAffinity="string"
          android:theme="resource or theme"
          android:uiOptions=["none" | "splitActionBarWhenNarrow"]
          android:windowSoftInputMode=["stateUnspecified",
                                       "stateUnchanged", "stateHidden",
                                       "stateAlwaysHidden", "stateVisible",
                                       "stateAlwaysVisible", "adjustUnspecified",
                                       "adjustResize", "adjustPan"] >
    . . .
</activity>
属性 説明
android\:allowEmbedded アクティビティを別のアクティビティの子として埋め込み、起動できることを示します。これは特に、別のアクティビティが所有するディスプレイなどのコンテナに子が存在する場合です。たとえば、Wear のカスタム通知に使用されるアクティビティは、Wear が別のプロセスに存在するコンテキスト ストリーム内でこのアクティビティを表示できるようにするため、この宣言が必要です。この属性のデフォルト値は false です。
android\:allowTaskReparenting タスクが次に前面に移動したとき、アクティビティを開始したタスクからアフィニティを持つタスクにアクティビティを移動できるかどうか。移動できる場合は "true"、アクティビティを開始したタスクに留まる必要がある場合は "false" を指定します。この属性が設定されていない場合、対応する 要素の allowTaskReparenting 属性によって設定された値がアクティビティに適用されます。デフォルト値は "false" です。通常、アクティビティを開始すると、アクティビティを開始したタスクにアクティビティが関連付けられ、アクティビティの生存期間中はそこに留まります。この属性を使用すると、現在のタスクが表示されなくなったときに、アフィニティを持つタスクをアクティビティの親として再割り当てすることができます。一般に、アプリに関連付けられたメインタスクにアプリのアクティビティを移動する場合に使用します。たとえば、メール メッセージにウェブページへのリンクが含まれている場合、リンクをクリックすると、そのウェブページを表示できるアクティビティが起動します。そのアクティビティはブラウザアプリで定義されていますが、メールタスクの一部として起動されます。ブラウザタスクをアクティビティの親として再割り当てすると、ブラウザが次に前面に移動したときにアクティビティが表示され、メールタスクが再び前面に移動したときにはそのアクティビティが表示されなくなります。アクティビティのアフィニティは、taskAffinity 属性で定義されます。タスクのアフィニティは、タスクのルート アクティビティのアフィニティを読み取って決定されます。したがって、定義上は、ルート アクティビティは常に、同じアフィニティを持つタスク内に存在します。起動モードに "singleTask" または "singleInstance" が設定されたアクティビティはタスクのルートにのみ存在できるため、親の再割り当ては "standard" モードと "singleTop" モードに限定されます。(launchMode 属性もご覧ください。)
android\:alwaysRetainTaskState アクティビティが割り当てられているタスクの状態を常にシステムで維持するかどうか。維持する場合は "true"、特定の状況でシステムがタスクを初期状態にリセットできるようにするには "false" を指定します。デフォルト値は "false" です。この属性が重要なのは、タスクのルート アクティビティのみです。その他すべてのアクティビティについては無視されます。通常は、ユーザーがホーム画面からタスクを選択し直したような状況で、システムはタスクをクリアします(ルート アクティビティ上のスタックからすべてのアクティビティを削除します)。一般に、ユーザーが特定の時間(たとえば 30 分間)、タスクにアクセスしなかった場合にこの処理が行われます。ただし、この属性が "true" の場合、ユーザーがどのような方法でタスクにアクセスしても、常に前回のタスクの状態に戻ります。これは、たとえば、ウェブブラウザのようなアプリで便利です。これにより、ユーザーは多くの状態(開いている複数のタブなど)をそのまま維持することができます。
android\:autoRemoveFromRecents この属性を指定したアクティビティによって開始されたタスクについて、タスク内の最後のアクティビティが完了するまでオーバービュー画面に表示し続けるかどうか。true の場合、タスクが自動的にオーバービュー画面から削除されます。これは、呼び出し元による FLAG_ACTIVITY_RETAIN_IN_RECENTS の使用よりも優先されます。"true" または "false" のブール値を指定する必要があります。
android\:banner 関連するアイテムに拡張されたグラフィック バナーを提供するドローアブル リソース。 タグで使用すると、特定のアクティビティにデフォルトのバナーを配置します。 タグで使用すると、アプリのすべてのアクティビティにバナーを配置します。Android TV のホーム画面では、システムがこのバナーを使用してアプリを表示します。バナーはホーム画面のみに表示されるため、CATEGORY_LEANBACK_LAUNCHER インテントを処理するアクティビティを持つアプリでのみ指定する必要があります。この属性は、画像を含むドローアブル リソースへの参照(たとえば "@drawable/banner")として設定する必要があります。デフォルトのバナーはありません。詳細については、TV アプリのビルドを開始するページのホーム スクリーンにバナーを配置するをご覧ください
android\:clearTaskOnLaunch タスクがホーム画面から再起動されたときに、ルート アクティビティを除くすべてのアクティビティをタスクから削除するかどうか。常にタスクのルート アクティビティ以外をクリアする場合は "true"、そうでない場合は "false" を指定します。デフォルト値は "false" です。この属性は、新しいタスクを開始するアクティビティ(ルート アクティビティ)にのみ影響します。タスクのその他すべてのアクティビティでは、この属性は無視されます。値が "true" の場合は、タスクの直前の操作や、タスクから離れるときに [戻る] ボタンまたは [ホーム] ボタンを押したかどうかにかかわらず、ユーザーが再びタスクを開始するたびにルート アクティビティから開始します。値が "false" の場合、状況によってはタスクのアクティビティがクリアされることがありますが、常にクリアされるわけではありません(alwaysRetainTaskState 属性をご覧ください)。たとえば、ホーム画面からアクティビティ P を起動し、そこからアクティビティ Q に移ったとします。ユーザーが [ホーム] を押して、再びアクティビティ P に戻りました。通常、ユーザーにはアクティビティ Q が表示されます。前回、P のタスクで Q を実行していたからです。ただし、P でこのフラグが "true" に設定されていると、P をベースにするすべてのアクティビティ(この場合は Q)は、ユーザーが [ホーム] を押したときに削除され、タスクはバックグラウンドに移動します。そのため、ユーザーがタスクに戻ると、P のみが表示されます。この属性と allowTaskReparenting の両方が "true" の場合、親の再割り当てが可能なアクティビティはすべて、アフィニティを共有するタスクに移動され、残りのアクティビティは上記のとおり削除されます。
android\:colorMode 対応端末でアクティビティを広色域モードで表示するようにリクエストします。広色域モードでは、SRGB よりも広い色域でウィンドウをレンダリングして、より鮮やかな色を表示できます。端末で広色域レンダリングがサポートされていない場合、この属性を指定しても効果はありません。ワイドカラー モードでのレンダリングの詳細については、ワイドカラー コンテンツによるグラフィックの拡張をご覧ください。
android\:configChanges アクティビティで処理する設定変更のリストを指定します。実行時に設定の変更が発生すると、デフォルトではアクティビティはシャットダウンおよび再起動されますが、この属性で設定を宣言しておくと、アクティビティの再起動を防止できます。代わりに、アクティビティは実行中のままになり、アクティビティの onConfigurationChanged() メソッドが呼び出されます。

ほかにもありますが、詳細は本家のページを参照ください。

このマニフェストで、各アクティビティの設定を行います。

次回は、画面(アクティビティ)から画面(アクティビティ)を呼び出す方法を学習します。>次回は、画面(アクティビティ)から画面(アクティビティ)を呼び出す方法を学習します。

アクティビティの作成

AndroidStudioでプロジェクトを作成すると、「MainActivity」クラスが出来上がっています。
このクラスが上記のアクティビティになります。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

このアクティビティ(画面)を作成しているのは、
setContentView(R.layout.activity_main);の部分になります。
R.layout.activity_mainの部分は、下の図のように、「activity_main.xml」に定義(描かれて)されています。

「activity_main.xml」を開くと右上のほうにのようなボタンがあるので、「Design」を選択すると画面のイメージが表示されます。

ここで、テキスト「Hello World」が表示されています。この部品が「TextView」という部品です。
この部品を移動してタイトルとして使用しようと思っていますが、その前に。。。

パッケージの修正

デフォルトで作成されたパッケージは「com.example.プロジェクト名」になっていると思います。
これを自作のパッケージ名に修正したいと思いますが、ただ単に修正するとエラーになります。

これは、AndroidManifest.xmlの修正も必要なためです。
使用するパッケージ名を上のXMLに記述します。今回作成するパッケージ名は「jp.zenryoku.mokuhyotasseiap」としてます。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.zenryoku.mokuhyotasseiap">

これで、エラーが解消され、Javaファイルも下のようにエラーが消えました。

多言語対応

作成するアプリは、多言語対応で作成したいと考えております。なので、こちらのページを参考に、多言語化を行います。

まとめると、下の図のように各言語に対応するXMLファイルを作成するというところです。

resを右クリックして、フォルダ名「values」の後ろに対応する言語を付けます。

  • 日本語(values-ja)
  • 英語(values-en)

そして、それぞれのstring.xmlに各言語に対応する文字列を記述します。

XMLの参照方法

多言語の文字列を設定することができたら、次は画面のコンポーネント(部品)にその値を設定します。
必要な処理ととしては、次のものになります。

  1. コンポーネントの取得
  2. 文字列の取得
  3. 値の設定
// コンポーネントの取得
TextView titleView = findViewById(R.id.appTitle);
// 値の設定
titleView.setText(R.string.app_name);

上のような実装で行けそうです。まだ起動確認していません。。。

バーチャルデバイスのインストール

実機がない場合は仮想デバイス(バーチャルデバイス)をインストールする必要があります。
動かしたときのイメージを見るためです。
これは、画面操作で簡単に行けます。

しかしエラーが!

Execution failed for task ':app:compressDebugAssets'.

上記のようなエラーが出ました。
こちらのサイトを参考にすると、Gradleのバージョンを下げてやればいけるというところで。。。

それぞれのバージョンを以下のように設定して再実行しました。
Android Gradle Plugin: 4.2.1
Gradle: 6.7.1

次のエラー

Caused by: org.gradle.internal.metaobject.AbstractDynamicObject$CustomMessageMissingMethodException: Could not find method dependencyResolutionManagement() for arguments

これもGradleのバージョンを次のように変えてやればよい感じでした。
Android Gradle Plugin: 7.0
Gradle: 7.0

しかし、使用しているPCの容量不足のため、AndroidStudioが落ちました。。。
Android開発は、十分にHDの要領があるPCを使用しないと駄目なようです。。。

でわでわ。。。

FreeTts エラー ~mbrola.base

FreeTtsエラー

下のような警告があり、修正してみました。

System property "mbrola.base" is undefined. Will not use MBROLA voices.

インストールしたFreeTtsのディレクトリに「mbrola」というフォルダがあったのでシステムプロパティにそのパスを渡して実行すると下のようにエラーが出ました。

System.setProperty("mbrola.base", "D:\\Apps\\freetts-1.2\\mbrola");

Make sure you FULLY specify the path to
the MBROLA directory using the mbrola.base
system property.

調べてみると、JDKのあるフォルダlibの下にmbrola.jarをコピーしてやればOKということでした。

しかし、MBROLAの音声ファイル(Voice)がダウンロードできなかったので、(502 Bad Gateway)打つ手なしと判断しました。。。

Java 漢字 ひらがな変換 ~ICU4Jを使う~

ICU4Jを使う

Java Speach APIを使用して、入力したテキストを話させるアプリケーションを作成しようとしています。
しかし、日本語に対応したライブラリが見つけられず、また見つけても動かすに至りませんでした。

とりあえず動かせたのが、下のような形で動くFreeTtsというライブラリを使用したものです。

これで、入力した文字を次のように変換するための処理を行うのにICU4Jを使用するつもりです。本家のドキュメントはこちら

ICU4Jの追加

現在使用しているプロジェクト(IntelliJ IDEA)にICU4Jを使用するための設定を行います。
今回(毎回)使用するのはMavenです。Mavenではpom.xmlに依存関係を追加することで、様々なライブラリを使用することができます。
例えばSpringrameworkであれば下のように記載してMavenプロジェクトを更新(再ロード)すればOKです。

SpringBootの依存関係追加

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.5.5</version>
</dependency>

注意点としては、参照するリポジトリへアクセスできるかどうか?という問題がありますが、プロジェクトを更新(再ロード)してできなければリポジトリを追加してやればOKです。※ICU4Jの場合です。

   <repositories>
        <repository>
            <id>icu4j</id>
            <url>https://repo1.maven.org/maven2/</url>
        </repository>
    </repositories>

ICU4Jの追加

ずばり、下のように書きます。チェックすることとしては、最新のバージョンが2.6.1でよいかどうかです。

    <dependencies>
        <dependency>
            <groupId>com.ibm.icu</groupId>
            <artifactId>icu4j</artifactId>
            <version>2.6.1</version>
        </dependency>
    </dependencies>

これで、プロジェクトを更新(再ロード)すれば、ライブラリが追加されています。

これで実装する準備が整いました。

ICU4Jの実装

以前書いた記事でコピーして作成したコードを改造して実行するつもりなので、先に以前作成したコードを改造します。

初めに着手する部分は、BriefVoiceDemoクラスのメインメソッド部分を、部品化することです。
つまりは、メインメソッドはプログラムを起動する部分なので、これを起動される側に変更するというわけです。

具体的には

下のように、プログラムが書いてあります。下のコード中にコメントで次のような部分は自分が追記した部分であり
説明のための文言です。

【改修ポイント】

Synthesizer synthesizer;

public static void main(String[] args) {

    //default synthesizer values
    SynthesizerModeDesc modeDesc = new SynthesizerModeDesc(
            null,       // engine name
            "general",  // mode name use 'general' or 'time'
            Locale.JAPANESE,  // locale, see MBROLA Project for i18n examples
            null,       // prefer a running synthesizer (Boolean)
            null);      // preload these voices (Voice[])

    //default voice values
    Voice voice = new Voice(
            "kevin16",              //name for this voice
            Voice.AGE_DONT_CARE,   //age for this voice
            Voice.GENDER_DONT_CARE,//gender for this voice
            null);                 //prefer a running voice (Boolean)

    boolean error=false;
    for (int r=0;r<args.length;r++) {
        String token= args[r];
        String value= token.substring(2);

        //overide some of the default synthesizer values
        if (token.startsWith("-E")) {
            //modeDesc.setEngineName(value);
        } else if (token.startsWith("-M")) {
            //modeDesc.setModeName(value);
        } else
            //overide some of the default voice values
            if (token.startsWith("-V")) {
                voice.setName(value);
            } else if (token.startsWith("-GF")) {
                voice.setGender(Voice.GENDER_FEMALE);
            } else if (token.startsWith("-GM")) {
                voice.setGender(Voice.GENDER_MALE);
            } else
            //dont recognize this value so flag it and break out
            {
                System.out.println(token+
                        " was not recognized as a supported parameter");
                error = true;
                break;
            }
    }

    /* 1.【改修ポイント】メインメソッドは、自分のクラスをnewして動かすので、部品かするにはこのnew以降の処理がいらない */
    //The example starts here
    BriefVoiceDemo briefExample = new BriefVoiceDemo();
    if (error) {
        System.out.println("BriefVoiceDemo -E<ENGINENAME> " +
                "-M<time|general> -V<VOICENAME> -GF -GM");
        //list all the available voices for the user
        briefExample.listAllVoices();
        System.exit(1);
    }

    //select synthesizer by the required parameters
    briefExample.createSynthesizer(modeDesc);
    //print the details of the selected synthesizer
    briefExample.printSelectedSynthesizerModeDesc();

    //allocate all the resources needed by the synthesizer
    briefExample.allocateSynthesizer();

    //change the synthesisers state from PAUSED to RESUME
    briefExample.resumeSynthesizer();

    //set the voice
    briefExample.selectVoice(voice);
    //print the details of the selected voice
    briefExample.printSelectedVoice();

    /* 2.【改修ポイント】SpeakableListenerは必要な部品なので、削除しない */
    //create a listener to be notified of speech events.
    SpeakableListener optionalListener= new BriefListener();

    /* 3.【改修ポイント】ここから先は、読み上げ処理を実行する部分なので
     *    メインメソッドから呼び出されるように修正する。つまりは、削除する。
     */
    //The Date and Time can be spoken by any of the selected voices
    SimpleDateFormat formatter = new SimpleDateFormat("h mm");
    String dateText = "The time is now " + formatter.format(new Date());
    briefExample.speakTextSynchronously(dateText, optionalListener);

    //General text like this can only be spoken by general voices
    if (briefExample.isModeGeneral()) {
        //speak plain text
        String plainText =
                "Hello World, This is an example of plain text," +
                        " any markup like <jsml></jsml> will be spoken as is";
        briefExample.speakTextSynchronously(plainText, optionalListener);

        //speak marked-up text from Speakable object
        Speakable speakableExample = new BriefSpeakable();
        briefExample.speakSpeakableSynchronously(speakableExample,
                optionalListener);
    }
    //must deallocate the synthesizer before leaving
    briefExample.deallocateSynthesizer();
}

上のコード内にある「【改修ポイント】」読んでもらえば、どの部分を削除するのか、そのまま残すのか?がわかると思います。
最終的に変更したコードは、下のようになります。

private Synthesizer synthesizer;

private SpeakableListener optionalListener;

public BriefVoiceCls() {

    //default synthesizer values
    SynthesizerModeDesc modeDesc = new SynthesizerModeDesc(
            null,       // engine name
            "general",  // mode name use 'general' or 'time'
            Locale.JAPANESE,  // locale, see MBROLA Project for i18n examples
            null,       // prefer a running synthesizer (Boolean)
            null);      // preload these voices (Voice[])

    //default voice values
    Voice voice = new Voice(
            "kevin16",              //name for this voice
            Voice.AGE_DONT_CARE,   //age for this voice
            Voice.GENDER_DONT_CARE,//gender for this voice
            null);                 //prefer a running voice (Boolean)
    // シンセサイザーのセットアップ
    this.createSynthesizer(modeDesc);
    //print the details of the selected synthesizer
    this.printSelectedSynthesizerModeDesc();

    //allocate all the resources needed by the synthesizer
    this.allocateSynthesizer();

    //change the synthesisers state from PAUSED to RESUME
    this.resumeSynthesizer();

    //set the voice
    this.selectVoice(voice);
    //print the details of the selected voice
    this.printSelectedVoice();
    // ここでセットアップ処理はおしまい。

    //create a listener to be notified of speech events.
    optionalListener = new BriefListener();
}

/** このメソッドで話をするようにプログラムを作る。 */
public void execute(String talkMessage) {

}

BriefVoiceCls#execute()を外部(メインメソッド)から呼び出し、プログラムを実行する形で必要な処理を行えるように実装します。

要件を考える

やりたいことは次の通りです。画面(JavaFX)から入力した文字を読み上げる

なので、シンプルに、実行するメソッドの引数には入力した文字列を渡します。
そして、コンストラクタでセットアップ処理、ここでは、シンセサイザーのセットアップや使用する声(Voice)を選択するなどの処理を行っています。
<シンセサイザーのセットアップ>

//default synthesizer values
SynthesizerModeDesc modeDesc = new SynthesizerModeDesc(
       null,       // engine name
       "general",  // mode name use 'general' or 'time'
       Locale.JAPANESE,  // locale, see MBROLA Project for i18n examples
      null,       // prefer a running synthesizer (Boolean)
      null);      // preload these voices (Voice[])

<音声のセットアップ>

//default voice values
Voice voice = new Voice(
        "kevin16",              //name for this voice
        Voice.AGE_DONT_CARE,   //age for this voice
        Voice.GENDER_DONT_CARE,//gender for this voice
        null);                 //prefer a running voice (Boolean)

そして、リスナーも使用しているので残してあります。※デバック用に使用しているようです。

/**
 * Simple SpeakableListener
 *   Prints event type and the source object's toString()
 */
public class BriefListener implements SpeakableListener {

    private String formatEvent(SpeakableEvent event) {
        return event.paramString()+": "+event.getSource();
    }

    public void markerReached(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void speakableCancelled(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void speakableEnded(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void speakablePaused(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void speakableResumed(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void speakableStarted(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void topOfQueue(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }

    public void wordStarted(SpeakableEvent event) {
        System.out.println(formatEvent(event));
    }
}

処理のほとんどが、標準出力に情報を表示するものです。これで、入力した文字を引数に受けて処理を行う準備が整いました。

ICU4Jを使用する

世間でよく使用される言葉として「オブジェクト指向プログラミング」というのがあります。この言葉は、ヒトによっていろいろな解釈があるのでなるべく使わないようにしようと思います。

今回の実装方法としては、上記のようなもののことなのですが「具体的にどうやるか?」を中心に記載したいと思います。

追記すコードは極力少なくする

この記事で、余計な処理=new BriefVoice()のようにインスタンス化してからの処理、を削除しました。
そして、新たに作成したメソッドは「execute()」メソッドです。引数は、Stringで入力した文字が渡される想定です。

つまるところ、BriefVoiceClsのexecute()メソッドを呼び出してやればOKという形にしました。
具体的には、次のようなコードになります。

/** このメソッドで話をするようにプログラムを作る。 */
public void execute(String talkMessage) {
    this.speakTextSynchronously(talkMessage, optionalListener);
}

ここで残る問題は、本記事のタイトルであるICU4Jを使用する、漢字を含む日本語をすべてひらがなに変換する処理を作成することです。

ICU4Jで変換処理を実装

ここで、漢字→ひらがなへの変換処理を担当するクラスを作成します。
クラスの名前は「KanjiConverter」にします。そして、テストファーストの形で実装します。

初めに作成したのは、実際に動かすクラス(KanjiConverter)とこのクラスをテストするためのクラス(KanjiConverterTest)です。
そして、ICU4Jを使用する方法がわからないのでそれを調べます。

参考サイトはこちらです。

ICU4Jは漢字に対応してない?

調べてみると[「kuromoji」]()というライブラリも使用する必要があるということでした。
なので、この依存関係を追加します。※プロジェクトの再ロードを忘れないようにしましょう。

【kuromojiの使い方】
こちらのサイトを参考にしました。
まずは、kuromojiを使用してみるということで、下のようなコードをテストクラスで実行しました。

public class KanjiConverterTest {
    /** static メソッドはstatic修飾子がついてないと参照できない */
    private static KanjiConverter target;

    /** テストクラスをインスタンス化する時に行う処理 */
    @BeforeClass
    public static void init() {
        // 前処理でテスト対象クラスをインスタンス化
        target = new KanjiConverter();
    }

    /** インスタンスが解放されるとき、ガベージコレクションで実行 */
    @AfterClass
    public static void terminated() {
        target = null;
    }

    /** ICU4Jをとりあえず起動してみる */
    @Test
    public void test1() {
        Tokenizer tokeni = new Tokenizer();
        List<Token> list = tokeni.tokenize("本日は晴天なり");
        for (Token t: list) {
            System.out.println("Length: " + t.getAllFeatures().split(",").length);
            System.out.println(t.getSurface() + "\t" + t.getAllFeatures());
        }
    }
}

これで、出y録した結果が以下になります。

Length: 9
本日 名詞,副詞可能,,,,,本日,ホンジツ,ホンジツ
Length: 9
は 助詞,係助詞,,,,,は,ハ,ワ
Length: 9
晴天 名詞,一般,,,,,晴天,セイテン,セイテン
Length: 9
なり 助動詞,,,*,文語・ナリ,基本形,なり,ナリ,ナリ

取得した配列(カンマで区切られた数)は9個で固定のようです。

ならば、入力した文字列を各単語に分けてやればよさそうです。つまり、次の手順を踏みます。

  1. 各カタカナに対応する、発音文字列を前もってマップしておく
  2. 各単語をカタカナに変換
  3. 対応する発音をFreeTtsでスピーチさせる

実装する(テストクラス)

まずは、音声に変換するための文字列をマッピングします。なので、初めのBriefVoiceDemoクラスで書く発音の文字列を決めます。
次のような感じで発音できました。

これらの文字列と各カタカナを関連付けていきます。要領としては以下の通りです。

private static final String[] KANA_LIST = {"ア", "イ", "ウ", "エ", "オ", // 1
                                            "カ", "キ", "ク", "ケ", "コ", // 2
                                            "ガ", "ギ", "グ", "ゲ", "ゴ", // 3
                                            "サ", "シ", "ス", "セ", "ソ", // 4
                                            "ザ", "ジ", "ズ", "ゼ", "ゾ", // 5
                                            "タ", "チ", "ツ", "テ", "ト", // 6
                                            "ダ", "ヂ", "ヅ", "デ", "ド", // 7
                                            "ナ", "ニ", "ヌ", "ネ", "ノ", // 8
                                            "ハ", "ヒ", "フ", "ヘ", "ホ", // 9
                                            "バ", "ビ", "ブ", "ベ", "ボ", // 10
                                            "マ", "ミ", "ム", "メ", "モ", // 11
                                            "ヤ", "ユ", "ヨ", // 12
                                            "ラ", "リ", "ル", "レ", "ロ", // 13
                                            "ワ", "ヲ", "ン", // 14
                                        };

String[] moji = {"ah", "yee" , "hu", "a", "oh" // 1
    , "kah", "kee", "ku", "ckea", "koh" // 2
    , "gaah", "gy", "goo", "gue", "goh" // 3
    , "saeh", "see", "su", "thea", "soh" // 4
    , "zaeh", "zee", "zoo", "zea", "zoh" // 5
    , "taeh", "tiee", "tsu", "te", "toh" // 6
    , "daeh", "dgee", "do", "de", "doh" // 7
    , "naeh", "niee", "nuh", "nea", "noh" // 8
    , "haeh", "hiee", "hu", "hea", "hoh" // 9
    , "baeh", "bee", "boo", "be", "boh" // 10
    , "maeh", "miee", "muh", "me", "moh" // 11
    , "yaeh", "yu", "yoh" // 12
    , "ra", "ri", "ru", "re", "roh" // 13
    , "wa", "oh", "um"}; // 14

// サイズ(長さ)はおなじなので
for (int i = 0; i < KANA_LIST.length; i++) {
    String key = KANA_LIST[i];
    String value = moji[i];
    talkMap.put(key, value);
}

これで入力文字を発音用の文字列に変換し再生します。作成したクラスは次の通りです。

public class BriefVoiceClsTest {
    private static BriefVoiceCls target;
    @BeforeClass
    public static void init() {
        target = new BriefVoiceCls();
    }

    @Test
    public void testTalkVoice() {
        target.execute("本日は晴天なり");
    }
}

<実行結果>

文字を発音用の文字に変換する

今までに、カタカナは変換できましたが、英語や数字に関しては、触れていませんでした。
なので、入力した部分(文字)はNULLになり、スペースで何も発音されません。
ここで、プログラムに修正を加えます。

カタカナを変換するクラス「KanjiConverter」を修正してやります。
マップrに登録した値を変換→カタカナ1文字を発音用の文字列に変換します。

for (char ch : chars) {
    String note = this.talkMap.get(String.valueOf(ch));
    String append = note == null ? " " : note;
    build.append(append + " ");
}

これで変数chに「ア」が入っているときは「ah」という文字列に変換されます。
具体的には「String note = this.talkMap.get(String.valueOf(ch));」の部分で変数「note」に値が入ります。

この状態で「本日は晴天なり」と入力したときには「hoh um zee tsu wa thea yee te um naeh ri 」という文字列に変換されます。
この文字列を発音(Speach)させるとそれっぽく聞こえます。

これで、発音用の文字列に変換できるのですが、以下の部分で想定通りに動きません。

  1. 普通に英語を話したいとき
  2. 数字を読むとき

これらを解決するには、文章を単語に分解した後に、最後の部分、以下のコードを参照ください。

public String convert(String inputText) {
    List<Token> list = tokenizer.tokenize(inputText);
    StringBuilder build = new StringBuilder();

    for (Token token : list) {
        String[] splits = token.getAllFeatures().split(",");
        if (splits[8].equals("*")) {
            build.append(token.getSurface() + " ");
        } else {
            build.append(splits[8] + " ");
        }
    }
    return build.toString();
}

入力した文字列を変数(引数)「inputText」に渡した状態で処理を行います。例えば「本日は晴天なり」がinputTextに入ります。
これを単語に分解しているのが、token.getAllFeatures().split(",");の部分であり、返り値(String[])の8番目には
カタカナが入るのですが、英語や数字の場合は「*」が入っていますのでその場合は単語をそのまま取得して返却する文字列に追加します。

具体的には、下のコードです。

if (splits[8].equals("*")) {
    build.append(token.getSurface() + " ");
} else {
    build.append(splits[8] + " ");
}

でわでわ。。。