4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

6.2 条件分岐~新Java Basic編 / AI対応版

4
Last updated at Posted at 2023-02-04

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『新 Java Basic編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Basic編 / AI対応版』はこちらのリンクから購入できます。

この講座は、以下のような皆様にお薦めします。

  • AIが書いたJavaのコードを読んで、正しさと設計の良し悪しを判断できるようになりたい方
  • 新人のシステムエンジニアとして、Javaの基礎を仕組みから身に付けたい方
  • AIを前提とした開発へ学び直し(リスキリング)をしたい方
  • 「Oracle認定Javaプログラマ」の取得を目指し、Javaの基礎を固めたい方

AIとチャットできるオールインワンの学習環境をリリースしました

Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。

Codinableの画面構成

学習に必要なものは、すべてCodinableの中で完結します。

  • 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
  • インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
  • 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
  • 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます

AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。

AIチャットに書き換えを指示し、差分を確かめて実行する

AIチャットは、受講者ご自身が生成AIサービス(Anthropic、OpenAI、Googleのいずれか)のAPIキーを登録して使うBYOK(Bring Your Own Key)方式です。利用料金は各社から直接請求されます。APIキーを登録しなくても、AIチャット以外の機能はすべて使えます。

Codinableのインストーラは、以下のGitHubのReleasesページからダウンロードできます(64ビット版のWindows 10/11、管理者権限は不要)。

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

6.2 条件分岐

チャプターの概要

チャプター6.1では、順次・分岐・反復という3つの制御構造と、ブロックによるスコープを取り上げました。本チャプターでは、分岐を記述するif文、switch文とswitch式、三項演算子を、AIが書いたコードを読んで正しさを判断する観点から整理します。条件の順序と境界値、複数の条件の組み合わせ、すべての値を網羅しているかという3点が、分岐のレビューの中心になります。題材には、ECサイトにおける配送料の決定を使います。

6.2.1 if文の構造と条件の順序

本レッスンでは、if文の流れをフローチャートとして読み、条件の順序や境界値の誤りに気付けるようになることを目指します。

単純分岐と多岐分岐

条件分岐には、処理を2系統に分ける単純分岐と、3系統以上に分ける多岐分岐があります。条件式にはboolean型の変数か、評価結果がboolean型になる式を指定します。

単純分岐の2つの形を、フローチャートで比べます。

if文とif-else文のフローチャート

  • if文は、条件式がtrueなら処理を実行してから終了する
  • if文の条件式がfalseなら、処理を実行せずに終了する
  • if-else文は、条件式がtrueなら処理(1)、falseなら処理(2)を実行する
  • if-else文の2つの流れは、どちらも終了へ合流する

購入金額が10000円以上なら配送料300円、未満なら900円とする例を以下に示します。

  • 06_statement: Main_If_1
int deliveryFee; //【1】配送料
if (10000 <= totalPrice) { //【2】
    deliveryFee = 300;
} else {
    deliveryFee = 900;
}
System.out.println("deliveryFee => " + deliveryFee);
  • 【1】deliveryFeeはif-else文の後でも使うため、if-else文の外側で宣言しています
  • 【2】ifブロックとelseブロックのどちらを通っても値が代入されるため、初期値が無くてもコンパイルエラーになりません
  • 仮にelseブロックが無ければ、値が決まらない流れが残るため、最後の行がコンパイルエラーになります

ifブロックの命令文が1行だけなら{ }を省略できますが、後から追加した行が条件の外に出る誤りを招くため、本コースでは省略しません。

上から順に評価される条件

if-else-if文では、上から順に条件式が評価され、最初にtrueになった分岐だけが実行されます。

3系統の分岐の流れを、フローチャートで示します。

if-else-if文のフローチャート

  • 最初に条件式Aを判定し、trueなら処理(1)を実行する
  • 条件式Aがfalseなら、条件式Bを判定し、trueなら処理(2)を実行する
  • 条件式Bもfalseなら、処理(3)を実行する
  • 処理(1)、処理(2)、処理(3)のいずれを実行しても、終了へ合流する

配送料を10000円以上で300円、5000円以上で600円、それ未満で900円とする仕様を、正しい順序と誤った順序で比べます。

  • 06_statement: Main_If_2
if (10000 <= totalPrice) {        // 正しい順序
    deliveryFee = 300;
} else if (5000 <= totalPrice) {
    deliveryFee = 600;
} else {
    deliveryFee = 900;
}
  • 06_statement: Main_If_3
if (5000 <= totalPrice) {         // 誤った順序
    deliveryFee = 600;
} else if (10000 <= totalPrice) { // この分岐には到達しない
    deliveryFee = 300;
} else {
    deliveryFee = 900;
}
  • 誤った順序では、12000円も最初の「5000円以上」を満たすため、配送料が600円になります
  • 2つ目の分岐はどの購入金額でも実行されず、コンパイラはこの誤りを検出しません
  • 範囲が重なる条件では、先に書く条件ほど範囲を狭くする必要があります

購入金額の数直線の上で、2つの順序がどの範囲をどの分岐に割り当てるかを図で比べます。

購入金額の数直線と条件の順序

  • 上段は正しい順序で、数直線の0〜4999円が「900円」、5000〜9999円が「600円」、10000円以上が「300円」の3つの区間に分かれる
  • 下段は誤った順序で、5000円以上がすべて「600円」の区間になり、「300円」の区間が無くなる
  • 下段の「10000 <= totalPrice」の分岐には「到達しない」というラベルが付く
  • 数直線上の4999と5000、9999と10000が境界値として強調され、境界値の前後で配送料が切り替わる

このように処理が切り替わる値を境界値と呼びます。<と<=の取り違えは境界値でだけ結果が変わるため、レビューでは境界値とその前後の値で結果を確かめます。

レビューの観点

  • 範囲が重なるif-else-if文で、狭い範囲の条件が先に書かれているかを確かめる。到達しない分岐が無いかを確かめる
  • 境界値が仕様どおりか(「以上」は<=、「より大きい」は<)を確かめ、境界値の前後で実行して結果を確かめる
  • 分岐の後で使う変数が、すべての流れで値が決まるようになっているかを確かめる。そのために意味の無い初期値(0など)が入れられていないかを確かめる
  • { }が省略され、ブロックに含めたつもりの行が条件の外に出ていないかを確かめる

6.2.2 複数の条件の組み合わせ

本レッスンでは、2つ以上の変数で分岐する仕様を表で整理し、AIが書いた条件式が仕様のすべての組み合わせを正しく表しているかを判断できるようになることを目指します。

直交表で仕様とコードを突き合わせる

顧客種別と購入金額の組み合わせで配送料が決まる、次の仕様を考えます。条件となる変数の値の組み合わせと結果を一覧にした表を、直交表と呼ぶことがあります。

【表】顧客種別・購入金額・配送料の直交表

顧客種別 購入金額 配送料
ゴールド会員 3000円以上 0円
ゴールド会員 3000円未満 600円
一般会員 5000円以上 300円
一般会員 5000円未満 900円

直交表の各行と、コードの分岐の対応を図で示します。

直交表の行とコードの分岐の対応

  • 左に直交表の4行(ゴールド会員・3000円以上・0円、ゴールド会員・3000円未満・600円、一般会員・5000円以上・300円、一般会員・5000円未満・900円)が並ぶ
  • 右にif文の4つの分岐(0円、600円、300円、900円)が並ぶ
  • 直交表の各行から、対応する分岐へ1本ずつ線でつながり、4行すべてに行き先がある
  • 対応の無い行があれば「仕様の抜け」、対応の無い分岐があれば「仕様に無い処理」になる

論理演算子とネストの比較

この仕様は、論理演算子でもネストでも表現できます。2つのコードを比べます。

  • 06_statement: Main_If_7
if (customerType == GOLD_CUSTOMER && 3000 <= totalPrice) {
    deliveryFee = 0;
} else if (customerType == GOLD_CUSTOMER && totalPrice < 3000) {
    deliveryFee = 600;
} else if (customerType == GENERAL_CUSTOMER && 5000 <= totalPrice) {
    deliveryFee = 300;
} else {
    deliveryFee = 900;
}
  • 06_statement: Main_If_8
if (customerType == GOLD_CUSTOMER) {
    if (3000 <= totalPrice) {
        deliveryFee = 0;
    } else {
        deliveryFee = 600;
    }
} else { // 一般会員の場合
    if (5000 <= totalPrice) {
        deliveryFee = 300;
    } else {
        deliveryFee = 900;
    }
}
  • Main_If_7は直交表の各行を1つの分岐に対応させた形で、顧客種別の判定と境界値3000が2回ずつ登場します
  • Main_If_8は外側で顧客種別を一度だけ判定し、内側で顧客種別ごとの境界値を判定するため、判定と境界値が1か所ずつになります
  • Main_If_7の最後のelseは「一般会員の5000円未満」だけでなく、想定外の顧客種別もすべて受け止めて900円にしてしまいます

ネストさせたコードが、顧客種別と購入金額の2段階でどう判定しているかを図で示します。

条件分岐のネストによる配送料の決定

  • 外側のif文は、顧客種別として「ゴールド会員か」を判定する
  • ゴールド会員の場合、内側のif文が「3000円以上か」を判定し、trueなら配送料は0円、falseなら600円になる
  • ゴールド会員でない場合、内側のif文が「5000円以上か」を判定し、trueなら配送料は300円、falseなら900円になる

同じ判定や境界値が何度も登場する書き方は、境界値が変わったときに直し漏れが起きやすくなります。一方でネストが深くなりすぎると読みにくくなるため、仕様の構造(どの変数で先に大きく分かれるか)に合わせて形を選びます。

レビューの観点

  • 直交表のすべての行に対応する分岐があり、仕様に無い分岐が無いかを確かめる
  • 最後のelseが、想定外の値まで黙って受け止めていないかを確かめる
  • 同じ判定や境界値が何か所にも重複していないかを確かめる
  • !と||が重なった条件式は、ド・モルガンの法則(レッスン4.1.3参照)で肯定の形に直して意味を確かめる

6.2.3 switch文とフォールスルー

本レッスンでは、従来のswitch文の流れを読み、breakの書き忘れによるフォールスルーを見抜けるようになることを目指します。

値で分岐するswitch文

switch文は、1つの式の値に応じて処理を多系統に分岐させます。指定できる式の型は、従来の書き方ではint・short・byte・charとそのラッパークラス、String、列挙型(チャプター15.1参照)です。Stringのラベルはequals()と同じ値の比較で照合され、式がnullだとNullPointerExceptionが発生します。

式の値によって分岐する流れを、フローチャートで示します。

switch-case文のフローチャート

  • 最初に式を評価する
  • 式の値がラベル1ならば処理(1)、ラベル2ならば処理(2)、ラベルnならば処理(n)を実行する
  • どのラベルにも当てはまらない場合は、defaultの処理(d)を実行する
  • どの処理を実行しても、終了へ合流する

顧客種別0〜3(一般・ゴールド・プラチナ・ダイヤモンド)で配送料を決める例を以下に示します。

  • 06_statement: Main_Switch_1
int deliveryFee = 0; //【1】
switch (customerType) {
case 0, 1: // 一般会員またはゴールド会員
    deliveryFee = 900;
    break;
case 2: // プラチナ会員
    deliveryFee = 600;
    break;
case 3: // ダイヤモンド会員
    deliveryFee = 0;
    break;
}
  • case 0, 1:のように、1つのcase句にラベルを複数並べられます
  • 0〜3を網羅していることを保証しているのは書いた人であり、コンパイラではありません
  • 【1】そのため仮の初期値0が必要になり、想定外の値(例えば5)でも配送料0円のまま処理が進んでしまいます

フォールスルー

case句の最後のbreakは省略してもコンパイルが通ります。breakが無いと、処理はswitch文を抜けずに次のcase句へ流れ込み、次のbreakまで実行が続きます。この挙動をフォールスルーと呼びます。

case 2のbreakが欠けた場合の流れを図で示します。

breakの欠落によるフォールスルー

  • 式の値が2のとき、case 2に入り「deliveryFee = 600」を実行する
  • case 2の最後にbreakが無いため、そのままcase 3へ流れ込み「deliveryFee = 0」を実行する
  • case 3のbreakでswitch文を抜け、配送料は600円ではなく0円になる
  • 正しい流れ(breakがある場合)は、case 2の後で直ちにswitch文の出口へ進む

レビューの観点

  • 従来のcase ...:形式で、各case句の最後にbreakがあるかを確かめる。意図的なフォールスルーには、その旨のコメントがあるかを確かめる
  • 想定外の値に対するdefaultがあるか、仮の初期値のまま処理が進まないかを確かめる
  • Stringの式がnullになりうる場合の扱いを確かめる
  • 新しく書くコードなら、アロー形式やswitch式(レッスン6.2.4参照)で書くよう指示する

6.2.4 switch式と網羅性

本レッスンでは、値を返すswitch式の構造を理解し、「すべての値を網羅しているか」をコンパイラに確かめさせる書き方を判断できるようになることを目指します。

アロー形式と値を返すswitch式

Java 14で、ラベルの後ろに->(アロー)を書くcase句と、値を返すswitch式が正式に導入されました。アロー形式ではフォールスルーが起きません。switch式は分岐の結果を値として返し、すべての値を網羅していないとコンパイルエラーになります。Main_Switch_1と同じ仕様をswitch式で書いたコードを以下に示します。

  • 06_statement: Main_Switch_4
int deliveryFee = switch (customerType) { //【1】
case 0, 1 -> 900;
case 2 -> 600;
case 3 -> { //【2】
    System.out.println("ダイヤモンド会員の場合");
    yield 0;
}
default -> { //【3】
    throw new IllegalArgumentException("不正な顧客種別");
}
}; //【4】
  • 【1】switch式の結果を、変数deliveryFeeに直接代入します。仮の初期値は要りません
  • 【2】ブロックで複数の命令文を書く場合は、返す値をyieldで指定します
  • 【3】intの値は網羅しきれないためdefaultが必須です。ここでは想定外の値で例外を送出しています
  • 【4】全体が1つの代入文なので、最後にセミコロンが必要です

従来のswitch文と値を返すswitch式で、値が決まる仕組みを図で比べます。

switch文とswitch式で値が決まる仕組みの比較

  • 左は従来のswitch文で、仮の初期値0を入れた変数deliveryFeeに、各case句の中で代入する
  • 左では、どのcase句にも当てはまらない値のとき、仮の初期値0のまま出口へ進む
  • 右はswitch式で、各case句が900、600、0の値を返し、その値がdeliveryFeeに代入される
  • 右では、どのcase句にも当てはまらない値はdefaultで例外になり、値が決まらないまま進む流れが無い
  • 右の上部には「網羅していなければコンパイルエラー」というラベルが付く

列挙型による網羅性のチェック

式の型が列挙型(チャプター15.1参照)なら、すべての定数をcase句に書けばdefaultは不要です。その状態で列挙型に定数が追加されると、switch式はコンパイルエラーになり、対応の漏れを教えてくれます。次のコードは説明のための例です。

enum CustomerType { GENERAL, GOLD, PLATINUM, DIAMOND }

int fee = switch (type) { // typeはCustomerType型
    case GENERAL, GOLD -> 900;
    case PLATINUM -> 600;
    case DIAMOND -> 0;
}; // すべての定数を網羅しているのでdefaultは不要
  • 4つの定数をすべて書いているため、defaultが無くてもコンパイルが通ります
  • 安易にdefaultを書くと、定数が追加されたときもコンパイルが通ってしまい、漏れに気付けなくなります

列挙型に定数が追加されたときに、コンパイラが漏れを検出する流れを図で示します。

列挙型の定数追加とswitch式の網羅性チェック

  • 左端はCustomerType列挙型で、GENERAL、GOLD、PLATINUM、DIAMONDの4つの定数に、新しくBRONZEが追加される
  • 中央はdefaultの無いswitch式で、BRONZEのcase句が無いため「コンパイルエラー: 網羅されていない」となる
  • 右はdefaultのあるswitch式で、BRONZEはdefaultに流れ込み、コンパイルは通るが仕様の漏れに気付けない
  • 中央の結果に「良い」、右の結果に「漏れが隠れる」というラベルが付く

型によるswitch(パターンマッチング)

Java 21では、caseのラベルに「型のパターン」を書けるようになり、任意の参照型でswitchできるようになりました。次のコードを以下に示します。

static String describe(Object value) {
    return switch (value) {
        case null -> "nullです";                              // 【1】
        case Integer i when 0 <= i -> "0以上の整数: " + i;     // 【2】
        case Integer i -> "負の整数: " + i;
        case String s -> "文字列: " + s;
        default -> "その他の型: " + value;
    };
}
  • 【1】case nullがあれば、nullでもNullPointerExceptionは発生しません
  • 【2】Integer型なら変数iとして取り出し、whenの条件も満たす場合にだけこの分岐に入ります
  • describe(42)は「0以上の整数: 42」、describe(-5)は「負の整数: -5」、describe(3.14)は「その他の型: 3.14」を返します

case句が上から順に照合される様子を図で示します。

型によるswitchのcase句の照合順

  • 値が上から「case null」「case Integer i when 0 <= i」「case Integer i」「case String s」「default」の順に照合される
  • 値42は2番目で一致し、値-5は2番目のwhenの条件を満たさないため3番目で一致する
  • 値3.14はどの型にも一致せず、defaultに進む
  • 「case Integer i」を「case Integer i when 0 <= i」より上に書くと、下の分岐に到達できないためコンパイルエラーになる

型による分岐は、継承やインタフェース(チャプター11.1、チャプター12.1参照)と組み合わせると効果を発揮します。

レビューの観点

  • 値を決めるための多岐分岐が、仮の初期値を使うswitch文ではなく、switch式で書かれているかを確かめる
  • 列挙型のswitch式に不要なdefaultがあり、定数の追加に気付けない形になっていないかを確かめる
  • intやStringのswitch式のdefaultが、想定外の値を黙って既定の値にせず、例外などで知らせているかを確かめる
  • 型によるswitchで、条件の広いcase句が狭いcase句より先に書かれていないか、nullの扱いが決まっているかを確かめる

6.2.5 三項演算子と分岐の書き方の選択

本レッスンでは、三項演算子の構造を理解し、仕様に合った分岐の書き方をAIに指示できるようになることを目指します。

三項演算子

三項演算子(条件演算子)は、条件式 ? 式1 : 式2の形で、条件式がtrueなら式1、falseなら式2の値を返します。レッスン6.2.1の配送料の例を三項演算子で書いたコードです。

  • 06_statement: Main_TO
int deliveryFee = 10000 <= totalPrice ? 300 : 900;
  • 変数の宣言と値の決定が1行で済むため、値が決まらない状態の変数が生まれません
  • if-else文のMain_If_1と同じ結果になります

三項演算子が値を返す流れを図で示します。

三項演算子の評価の流れ

  • 最初に条件式10000 <= totalPriceを評価する
  • trueなら式1の300、falseなら式2の900が評価される
  • 評価された一方の値だけが、変数deliveryFeeに代入される
  • 式1と式2のうち、選ばれなかった方は評価されない

分岐の書き方の選び方

分岐の書き方は、分岐の数と「値を決めるだけか、処理を切り替えるか」で選びます。三項演算子を入れ子にすると読みにくくなるため、3系統以上ならswitch式かif-else-if文を使います。

書き方を選ぶ判断の流れを図で示します。

分岐の書き方を選ぶ判断の流れ

  • 最初の判断は「分岐の目的は値を1つ決めることか」である
  • 値を決める場合、「2系統か」を判断し、2系統なら三項演算子、3系統以上なら次の判断へ進む
  • 3系統以上で「1つの値(数値・文字列・列挙型・型)で分かれるか」を判断し、そうならswitch式、範囲や複数の条件ならif-else-if文になる
  • 値を決めるのではなく処理を切り替える場合は、if文、またはアロー形式のswitch文になる

レビューの観点

  • 三項演算子が入れ子になり、読みにくくなっていないかを確かめる
  • 1つの値で分かれる多岐分岐が、長いif-else-if文で書かれていないかを確かめる
  • 値を決めるだけの分岐で、変数の宣言と代入が離れ、値が決まらない流れが生まれていないかを確かめる

このチャプターで学んだこと

本チャプターでは、以下を取り上げました。

  1. if-else-if文は上から順に評価され、範囲が重なる条件では狭い範囲を先に書く必要があること
  2. 境界値の<と<=の誤りは境界値でだけ結果が変わるため、境界値とその前後で確かめること
  3. 複数の変数による分岐は、直交表の各行とコードの分岐を突き合わせてレビューできること
  4. 従来のswitch文ではbreakの欠落がフォールスルーを招き、網羅性は書いた人が保証するしかないこと
  5. switch式は値を返し、網羅していなければコンパイルエラーになるため、列挙型と組み合わせると漏れをコンパイラに検出させられること
  6. 分岐の書き方は、分岐の数と「値を決めるか、処理を切り替えるか」で選ぶこと
4
3
0

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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?