Javaアプリケーション – おみくじ③コード解説(switch文、format、リファクタリング、try-with-resources)

プログラミング

みなさんこんにちは、一星零哉です。

今回もコード解説の続きをやっていきますよー!

前回の記事

このアプリケーションのGitHubリポジトリ

switch文

switch文は条件分岐判定の記法の一つです。

if文で書くこともできますが、特定の変数の値に応じて処理を分岐させたい場合や、判定する条件が3つ以上ある場合はswitch文のほうが見やすい場合があります。

int ftn = (int) Math.round(Math.random() * 10);
String rst;

switch(ftn) {
case 0:
	rst = "大吉";
	break;
case 1:
	rst = "吉";
	break;
case 2:
	rst = "中吉";
	break;
case 7:
	rst = "末吉";
	break;
case 8:
	rst = "凶";
	break;
case 9:
	rst = "大凶";
	break;
default:
	rst = "小吉";
}

おみくじアプリだとこの部分ですね。

ftnに代入された値が”case”に一致するかどうかを判定します。

たとえば乱数生成と四捨五入によってftnに1が代入された場合は以下のように処理されていきます。

  1. ftnが0かどうか判定する→一致しないので次の判定に移る
  2. ftnが1かどうか判定する→一致するのでこのケースの場合の処理が実行されていく
  3. String型の変数rstに「吉」が代入される。
  4. “break;”句によりswitch文を抜ける

もしftnに代入された値が3~6の場合は、default句の処理対象となります。

switch文を書く上で重要なのは、各処理の終わりに必ず”break;”を書くことです。

breakは実行中の処理を強制的に終わらせる句です。

これを書き忘れると、後続の処理をしなくていいのに処理が続いてしまいます。

今回出した例でいえば、「1」以降はもう処理しなくていいのにcase 2以降も判定がされていってしまうということです。

このプログラムだと、すべてのcaseでbreak文を忘れると「小吉」しか出ないおみくじになってしまいます。あまりご利益を感じないおみくじですね。

ちなみに、上記switch文をif文に書き換えると以下のようになります。

int ftn = (int) Math.round(Math.random() * 10);
String rst;

if(ftn == 0) {
	rst = "大吉";
} else if(ftn == 1) {
	rst = "吉";
} else if(ftn == 2) {
	rst = "中吉";
} else if(ftn == 7) {
	rst = "末吉";
} else if(ftn == 8) {
	rst = "凶";
} else if(ftn == 9) {
	rst = "大凶";
} else {
	rst = "小吉";
}

swtich文を見た後だとごちゃごちゃした感が否めないですね。

format

formatはStringクラスのメソッドで、指定された書式の文字列と引数を使って、書式付き文字列を返してくれます。

String msgTemple = "今日の運勢は %sです。";
String msg = String.format(msgTemple, rst);

第一引数に書式を、第二引数に文字列を渡します。

今回だと、第一引数が「今日の運勢は %sです。」で、第二引数が吉凶ですね。

こうしてあげることで、「今日の運勢は大吉です。」という書式にフォーマットすることができます。

書式の中の「%s」は、第二引数で渡される文字列を設定する位置を表しています。

このパラメータは設定する値のデータ型に応じて、以下のように使い分ける必要があります。

%b真偽値
%s文字列(String)
%c文字(Char)
%f浮動小数点

日時もフォーマットすることができます。こちらはパラメータが多いので、基本的なものを別の表に分けてまとめます。

%tH時間(00~23)
%tM分(00~59)
%tS秒(00~60)
%tB英語での月名(Januaryなど)
%tb英語の月名の省略形(Janなど)
%tY
%tm月(01~12)
%td日(01~31)

日付のフォーマットは以下のように記述します。

String fmt = "%tY/%tm/%td %tH:%tM:%tS";
Date date = new Date();
String date_fmt;

date_fmt = String.format(fmt, date.getTime(), date.getTime(), date.getTime(), date.getTime(), date.getTime(), date.getTime());

System.out.println(date); // Tue Feb 18 06:29:46 JST 2025 のように表示される
System.out.println(date_fmt); // 2025/02/18 06:29:46のように表示される

実行結果は以下の通りです。

上記の例では少々冗長になってしまいましたが、Dateのインスタンス化で取得できる日付とフォーマット後で形式が変化していることがお分かりかと思います。

リファクタリング

プログラムの動作を変えることなく内部のコードを改善することを「リファクタリング」といいます。

リファクタリングのメリットとしては以下のようなものがあげられます。

  • コードが読みやすくなる(可読性が向上する、なんていいます)
  • バグを発見やすくなる
  • メンテナンスをしやすくなる
  • 機能追加や改修がしやすくなる

今回のプログラムを改めて見てみましょう。

package SampleOmikuji;

import java.util.Scanner;

public class SampleOmikuji {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		System.out.println("おみくじを引きますか");
		System.out.println("引く場合は\"y\"を入力してください。");

		Scanner scanner = new Scanner(System.in);
		String inputChar = scanner.nextLine();

		if(!inputChar.equals("y")) {
			return;
		}

		int ftn = (int) Math.round(Math.random() * 10);
		String rst;

		switch(ftn) {
		case 0:
			rst = "大吉";
			break;
		case 1:
			rst = "吉";
			break;
		case 2:
			rst = "中吉";
			break;
		case 7:
			rst = "末吉";
			break;
		case 8:
			rst = "凶";
			break;
		case 9:
			rst = "大凶";
			break;
		default:
			rst = "小吉";
		}

		String msgTemple = "今日の運勢は %sです。";
		String msg = String.format(msgTemple, rst);

		System.out.println(msg);

	}

}

上記の実装例は、まずは機能要件を満たすプログラムを書き上げる目的で書き上げたものです。

可読性のことはあまり考慮していません。

このプログラムを見て、私は下記修正したいなと思いました。

  • 変数の宣言は1か所にまとめる
  • Scannerクラスをクローズしていないため、クローズの処理を追加する
  • 吉凶の設定のせいでmainメソッドが長くなっているため、別のメソッドに切り分ける

上記に基づいてリファクタリングしたコードがこちらです。

package SampleOmikuji;

import java.util.Scanner;

public class SampleOmikuji {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		String inputChar;
		int ftn;
		String rst;
		String msgTemple = "今日の運勢は %sです。";
		String msg;
		
		System.out.println("おみくじを引きますか");
		System.out.println("引く場合は\"y\"を入力してください。");
		
		try(Scanner scanner = new Scanner(System.in)){
			inputChar = scanner.nextLine();

			if(!inputChar.equals("y")) {
				return;
			}

			ftn = (int) Math.round(Math.random() * 10);
			rst = SetFortune(ftn);
			
			msg = String.format(msgTemple, rst);

			System.out.println(msg);
		}

	}
	
	private static String SetFortune(int ftn) {
		String rst;
		
		switch(ftn) {
		case 0:
			rst = "大吉";
			break;
		case 1:
			rst = "吉";
			break;
		case 2:
			rst = "中吉";
			break;
		case 7:
			rst = "末吉";
			break;
		case 8:
			rst = "凶";
			break;
		case 9:
			rst = "大凶";
			break;
		default:
			rst = "小吉";
		}
		
		return rst;
	}

}

いかがでしょうか?

switch文を利用して吉凶を設定する処理を別のメソッドに切り分けたことでmainメソッドがかなり見やすくなったのではないかなと思います。

また、プログラム内で使用している変数を各メソッドの上部で宣言することで、どのような変数があり、初期値が何なのかを一目で把握することができます。

try-with-resources

try-with-resourcesとは、プログラムのリソース管理のために使用される構文です。

try(Scanner scanner = new Scanner(System.in)){
	inputChar = scanner.nextLine();

	if(!inputChar.equals("y")) {
		return;
	}

	ftn = (int) Math.round(Math.random() * 10);
	rst = SetFortune(ftn);
	
	msg = String.format(msgTemple, rst);

	System.out.println(msg);
}

tryの後ろの()内でインスタンス化されたオブジェクトは、tryブロックを抜ける際に必ずクローズされます。

ScannerやDB接続時に使用するConnectionオブジェクトをはじめ、そのオブジェクトを使用するブロックを抜ける際は、必ずクローズされるようにしましょう。

これを徹底することで、メモリリークを避けることができます。

メモリリークとは、もう使わないメモリ領域を確保したまま放置することで利用可能な領域が失われる現象です。

変数を宣言すること、オブジェクトをインスタンス化することはどれもメモリ領域の一部を占有し、使用することです。

もう使わないのにクローズすることなく放置しておくと、利用可能なメモリ領域はどんどん減ってしまいます。

そこで、使わなくなったオブジェクトはクローズしてやらなければならないのです。

ここで、「なぜscannerクラスだけクローズするの?」との疑問が浮かぶのではないかと思います。

Scannerクラス以外にも、このプログラムでは複数の変数を宣言してメモリ領域を使用しています。

実は、scannerクラス以外の変数もちゃんとクローズされているんです。

Javaには「ガベージ・コレクション」という仕組みがあり、不要になったリソースを自動的に開放してくれます。勝手にやってくれるので、Javaでは明示的に記述する必要はありません。

しかし、ガベージ・コレクションが開放する対象は内部リソースにとどまります。Stringや自分で開発たクラスですね。

一方でScannerクラスやConnectionクラスなど、外部リソースを直接扱う(可能性のあるものを含め)クラスはガベージ・コレクションの対象にはなりません。

明示的にクローズしない限り、メモリ領域を確保し続けます。

なので、「外部リソースを扱うことができるオブジェクトは必ずクローズする」ことを意識しましょう。

正直、この程度のプログラムでメモリリークを起こすことはありませんが、早いうちからその意識を持っておくことは重要です。

ちなみに一星は初期実装の時クローズを忘れていました(笑)

try-with-resoucesはtry-finallyで書き換え可能です。

try{
    Scanner scanner = new Scanner(System.in);
    // 後続処理は上記のtryブロック内と同じなので省略
} finally {
    scanner.close();
}

まとめ

今回のまとめです!

  • switch文
    • 単純な条件分岐をすっきりみやすく!
  • format
    • 書式を操作する
  • リファクタリング
    • 動作を変えることなくより高品質なコードに!
  • try-with-resources
    • 外部リソースを扱えるオブジェクトは確実にクローズ!

参考

format try-with-resources リソース・リークの回避

コメント