Java/基礎/条件分岐

プログラムの流れをコントロールする、フロー制御構文には、反復処理と条件分岐の構文があります。(※ 条件分岐そのものの意味については『中学校技術/プログラムによる計測・制御』などを参照されたし。)

このページでは条件分岐について解説します。

条件分岐の例編集

if文編集

 
Wikipedia
ウィキペディアif文の記事があります。

Javaではif文を用いると、条件分岐をすることができます(他の多くのプログラム言語でも同様、if文は条件分岐の構文です)。

次のサンプルは、Hello worldの“バイリンガル版”です。もちろん、Hello worldをバイリンガルにしても実用的な意義は全くありません。

さっそくサンプルを見てみましょう。

class trans {

  public static void main(String[] args) {

    String gengo;
    gengo = "ja";

    if (gengo == "en") {
      System.out.println("Hello, world.");
    } else if (gengo == "ja") {
      System.out.println("世界よこんにちは");
    }
  }
}
実行結果
世界よこんにちは

もし、 gengo = "ja"; の引用符の中を「en」に変えて gengo = "en"; にすれば、

上記コードを実行すると英語で

Hello, world.

と表示する結果になります。

if文とはこのように、つづく条件の式を評価して、その条件が満たされている場合に、{ } ブロック内の処理を実行する構文でです。

条件判定で用いる等号はイコール記号が2個である事に注意してください。もし条件式でイコール1個だけだと別の意味になり、バグやエラーになります。


仮引数編集

仮引数(かりひきすう)と言われる技法と、条件分岐を組み合わせると、コマンドラインからの入力に応じて、いろいろな処理を行うことが出来るようになります。

「仮引数」とは、コンパイルなどのコマンド入力時に、ファイル名の後ろに、

$ java trans -ja

さらに追加された文字列(上記の例なら「-ja」)は、「仮引数」として認識され、プログラム実行時にその文字列を利用することができます(この仕組み、あるいはこれによって渡された文字列などを、「仮引数」と言います)。

class trans {
  public static void main(String[] args) {
    if (args.length == 0) {
      System.out.println("Hello, world.");
    } else if (args[0].equals("-ja")) {
      System.out.println("世界よこんにちは");
    } else {
      System.err.println("Usage: java trans [-ja]");
    }
  }
}

上記は、if文if-else文と呼ばれる構文を使った例です。


コンパイル方法
$ javac trans.java
$ java trans

上記コマンドを実行すると、

Hello, world

と表示されます。

では日本語の「世界よこんにちは」は、どう表示するのでしょうか? コンパイル後に

$ java trans -ja

と引数に「-ja」をつけてコマンド実行すれば、

世界よこんにちは

のように表示されます。

switch文編集

基本編集

switch文というのは、場合わけに応じて、「case」で示したラベルにジャンプする構文です。

ジャンプしたあと、コードの実行を上から下に向かって実行していきます。

「case 1:」の最後の記号はコロン(:)です。セミコロン「;」とは間違えないようにしてください。

class sample {
  public static void main(String[] args) {

    int a = 2;

    switch (a) {
    
    case 1:
      System.out.println("第1パターンにいる.");
        
    case 2:
      System.out.println("第2パターンにいる.");

    case 3:
      System.out.println("第3パターンにいる.");

    };
  }
}
実行結果
第2パターンにいる.
第3パターンにいる.
解説

switch case文はその名に反して、単にラベルにジャンプするだけの構文なので、ジャンプ先にあるラベル(case 2)の次のラベル(case 3)の実行文もそのまま実行しようとしてしまう。

switch文のこのような仕組みを、フォール・スルー(fall-through)という。


実行文を場合ワケしたい場合はbreak;を使うと、switch文から抜け出すので、結果的に場合ワケした事と同じ結果になる。

class sample {
  public static void main(String[] args) {

    int a = 2;

    switch (a) {
    
    case 1:
      System.out.println("第1パターンにいる.");
      break;
        
    case 2:
      System.out.println("第2パターンにいる.");
      break;

    case 3:
      System.out.println("第3パターンにいる.");
      break;
    };
  }
}
実行結果
第2パターンにいる.


default

どのcaseにもヒットしない場合の処理をdefaultで下記のように書ける。「case default」ではなく、単に「default」と書くので、間違えないように。


class sample {
  public static void main(String[] args) {

    int a = 5;

    switch (a) {
    
    case 1:
      System.out.println("第1パターンにいる.");
      break;
        
    case 2:
      System.out.println("第2パターンにいる.");
      break;

    default:
      System.out.println("ヒットしませんでした.");
      break;
      
    case 3:
      System.out.println("第3パターンにいる.");
      break;
    };
  }
}
実行結果
ヒットしませんでした.


矢印記号->編集

switch文において、case の次をコロンではなくアロー(矢印)記号->にすると、breakがある場合と同じ結果になるので、コードを短縮できます。

つまり、アロー記号を使うことで、フォール・スルーを禁じたことになります。

Java SE 12以降で、アロー記号をこのような機能で使えます。

コード例
class sample {
  public static void main(String[] args) {

    int a = 2;

    switch (a) {
    
    case 1 ->  System.out.println("第1パターンにいる.");
        
    case 2 ->  System.out.println("第2パターンにいる.");

    case 3 ->  System.out.println("第3パターンにいる.");

    };
  }
}
実行結果
第2パターンにいる.


2行以上書きたい場合は、単に波カッコ{ }で、くくれば可能です。

class sample {
  public static void main(String[] args) {

    int a = 2;

    switch (a) {
    
    case 1 ->  System.out.println("第1パターンにいる.");
        
    case 2 -> { System.out.println("第2パターンにいる.");
                System.out.println("次.");}

    case 3 ->  System.out.println("第3パターンにいる.");

    };
  }
}
実行結果
第2パターンにいる.
次.


アロー条件式でもdefault文が使えます。

class sample {
  public static void main(String[] args) {

    int a = 14;

    switch (a) {
    
    case 1 ->  System.out.println("第1パターンにいる.");
        
    case 2 -> { System.out.println("第2パターンにいる.");
                System.out.println("次.");}

    default -> System.out.println("ヒットせず.");
    
    case 3 ->  System.out.println("第3パターンにいる.");

    };
  }
}
実行結果
ヒットせず.


歴史編集

switch文のコロンと矢印の新旧については、歴史的には、コロン「:」を使うジャンプ方式のほうが古く、標準C言語が古くからサポートしている方式もコロンによる方式です。

そもそも、アセンブラのジャンプ命令が、ほぼ同じ方式です。(というか、おそらくアセンブラのジャンプ命令を手本に、C言語の黎明期(れいめいき)にswitch文の仕様が制定されたのだろうと思われる。)

ですが、コロンによる方式は、上述のフォールスルーのように、場合分けの用途で用いるには紛らわしく(まぎらわしく)、欠点があります。

また、コロンの方式は、break文を毎回書く必要があったりと、コードに定型文を書く手間が増えます。もしコロン方式で、breakを書き忘れるミスをすると、バグにもなりかねます。このため、コロンの方式は、多くの用途では非効率です。

そこで今後はJavaプログラミングでは、(ジャンプ目的ではなく)場合分けの目的なら、なるべくアロー記号を使うほうが、バグなどのミスが減るし意図も明確になるので、なるべくアローを使うほうが望ましいでしょう。

switch式とenum編集

Java SE 12以降の 比較的に新しい機能で、関数の戻り値のように、各caseから値を返す機能がある。 「switch」とは区別して、Javaでは下記のようにswitch, yield を組み合わせて用いる手法のことを「switch」という。

caseから値を返すにはyield を使う。

しかし、case 変数のようにcaseの場合わけに変数を用いる場合、下記のように列挙型(enum)で場合分け変数のとりうる値をすべて網羅的に定義しないといけないように仕様が決まっている。

下記コードの場合、enumのブロックがmain関数のあるclass sample の外に出ているので、コンパイル無しのjava コマンドでは機能せずエラーになる。なので、コンパイルおよび実行のコマンド

javac sample.java
java sample

を実行のこと。

ソース例
class sample {
  public static void main(String[] args) {

    ctype a = ctype.c2;
    int res = switch (a){

        case c1 -> {System.out.println("第1パターンにいる.");
                     yield(20);
                    }

        case c2 -> { System.out.println("第2パターンにいる.");
                    yield(33);
                   }

        case c3 -> { System.out.println("第3パターンにいる.");
                    yield(50);
                   }
    };

    System.out.println(res);
  }
}

enum ctype {
  c1,
  c2,
  c3;
}
実行結果
第2パターンにいる.
33


enum というのは、列挙型を宣言するためのキーワードです。

enumをmain関数の外で書く場合、Java18以降のバージョンでは、上記のように main 関数よりも下部のブロックに書くほうが良い。main関数よりも上だと、java18以降の新しいバージョンでは認識せずエラーになる場合がある。(昔のバージョンではmain関数よりも上でも認識できた。)

なおjava 18以降を使う場合は、文字コードのUTF-8標準化なども注意のこと。windowsの場合、SHIFT-JISだとjava18以降ではエラーになる。


enumは次のようにmain関数の中で書いても構わない。その場合、コンパイルしないでもインタプリタ的に実行できる。

ソース例
class sample {
  public static void main(String[] args) {

    enum ctype {
      c1,
      c2,
      c3;
    }

    ctype a = ctype.c2;
    int res = switch (a){

        case c1 -> {System.out.println("第1パターンにいる.");
                     yield(20);
                    }

        case c2 -> { System.out.println("第2パターンにいる.");
                    yield(33);
                   }

        case c3 -> { System.out.println("第3パターンにいる.");
                    yield(50);
                   }
    };

    System.out.println(res);
  }
}


無理やりenumを使わない方法を試みた場合

もし、無理やりにenumを用いずに final int などを使って場合わけに使うつもりの変数を宣言してみたところで(Javaの定数値の修飾子は final)、 コンパイル試行時にエラー文で「エラー: switch式がすべての可能な入力値をカバーしていません」とされてしまい、 コンパイル失敗するだけである(Java 17 で2021年11月25日に確認)。

では「すべて」のパターンをカバーしようと case default を試してみても、 エラー文「エラー: patterns in switch statementsはプレビュー機能であり、デフォルトで無効になっています。」 とされるだけである。

enum要素は重複できない。

一般の変数の宣言で、同じ名前の変数を2度以上は宣言できないように、enumの要素名もそのenumグループ内では2度以上は宣言できない。この仕様により、要素名が異なれば、必ず別々のenum要素が宣言されている事が保証される。

なお、とりあえず「enum要素」と言ったが、「列挙子」とか「列挙定数」とかとも言う。

enum要素は書き換え不可

まず一般に、Javaでは変数を書き換え不可として宣言するときは final キーワードを使う。enum要素は宣言時にそれぞれ自動的に final として宣言されるので、原理的に書き換えを防げる。

実際、たとえば上記コードでctype.c2 = ctype.c3;とか挿入して実行してみても、

エラー: final変数c2に値を代入することはできません

とエラーメッセージが出てコンパイル不能な結果に終わるだけである。

この仕様と、さらに別仕様としてenum変数名が重複できない仕様と合わせて、enum要素のプログラム中における一意性がより確実に保証される。


入れ子は不可のようだ

java のenumにおいて、enumの中に別のenumを入れるのは、仕様が知られていない。C言語でも、enumの中にenumを入れるのは認められてないので、javaもそれを踏襲していると思われる。なお、「入れ子」構造のことを一般にIt用語でネストと言う。つまりenumのネストはJavaにも無いと思われる。


さて、enum は switch文がなくても使える。

ソ-ス例
class sample {
  public static void main(String[] args) {

    enum ctype {
      c1,
      c2,
      c3;
    }

    System.out.println(ctype.c2);
  }
}

実行結果

c2


enum は ordinal()メソッドにより、要素の宣言順の値を調べることも可能です。なお、変数値を割り当てているわけではありません(後述)。

ソース例
class sample {
  public static void main(String[] args) {

    enum ctype {
      c1,
      c2,
      c3;
    }

    System.out.println(ctype.c1.ordinal() ); 
    System.out.println(ctype.c3.ordinal() );
  }
}

実行結果

0
2

なお、ordinal()とは別にvalues()メソッドと言うのがあります。

コード例
class sample {
  public static void main(String[] args) {

    enum ctype {
      c1,
      c2,
      c3;
    }

    System.out.println(ctype.c3.values() ); 
    
  }
}
実行例
[Lsample$1ctype;@564718df

このように、ordinal の値とはまったく違います。

また、クラス名「sample」,enum型名「stype」などの文字列があることから、javaのenum変数では、内部的にはクラス名とenum型名を一緒に管理している事がわかります。


enum は値を持ちますが、しかしenum変数は整数型とは異なります。enum変数に整数型を代入しようとしてもエラーになります。

また、基本的にenum変数は定数です。宣言後に、あとからenum変数の所持する整数値を変更することは出来ません。(宣言時に所持する値を指定することは出来ます。コンストラクタなどを使います。)

0,1,2以外の順番の以外の値(コード値)を与えるには、下記のようにします。

ソース例
class sample {
  public static void main(String[] args) {
    enum ctype {
        c1( 108),
        c2( 20),
        c3( 55), 
      ;
      
      // 下記のコンストラクタ用の変数を事前に定義する必要がある
      // private はなくとも良い
      int setval;
      
      // コンストラクタ
      // private はなくとも良い
      ctype(int setval) {
        this.setval = setval;
      }
      
      // public はなくとも良い
      int question() {
        return this.setval;
      }
    } // ここまで enum
    
    // 単にクラス関数を呼び出す処理
    int answer = ctype.c1.question();
    System.out.println(answer);
    
    // ordinal の確認
    System.out.println(ctype.c1.ordinal() );
  } // main の終わり
}
実行例
108
0

Java の enum 関連の機能はクラスを用いて実装されているので、上記のように、クラス内の関数のように、enum内の関数を定義できます。

また、そのenum内の関数を用いて、値をやりとりします。

値を割り当ててても、別に宣言の順番の情報は消えないので、ordinal()メソッドの結果は同じままです。

コンストラクタのthis.setval = setval;は、別に右辺と左辺で同じ変数名を使う必要は無く、もし まぎらわしければ下記ソース(抜粋)のように右辺を変えてもいい(ただし、関数の引数に合わせる必要はある)。


抜粋

      // 下記のコンストラクタ用の変数を事前に定義する必要がある
      int setval;
      
      // コンストラクタ
      //private 
      ctype(int x) {
        this.setval = x;
      }

なお、Javaのenumはクラス的な機能を持ちますが、しかし追加のインスタンスを生成(new)できない仕様です。enumの宣言がインスタンスの生成を兼ねており、そのとき以外はインスタンスを生成できないので、結果として定数的に振舞います。もしインスタンスを生成できてしまうと、新たな要素を追加できてしまいかねないのでenumの定数的な一貫性が無くなってしまい意味を成さなくなってしまうので、enumでは仕様的にインスタンスを生成できない仕組みになっています。

また、Javaのenumは継承できない仕様です。しかしインターフェースinterface の利用は可能です。インターフェースはメソッドの定義ですので、enumの定数要素の部分には関係しないからでしょう。

外部サイト編集