※本記事は、“Bruce Eckel on switch expressions, arrow syntax, and case null” の翻訳記事です。

2022年5月7日 | 8分読む

Bruce Eckel


switch 文は絶えず進化しています。最近の3つの改善のうち、矢印構文、case null オプションおよびswitch 式の3つがあります。

 

[このシリーズでは、Java 8以降にJava言語に追加された新機能について説明します。これは、2021年12月に公開された「On Java 8」の第2版に追加された資料から引用しています。これらの記事のソースコードおよびブック全体については、GitHubから、詳細なインストール手順とともに入手できます。—編集部]

switch 文は絶えず進化しています。最近の3つの改善のうち、矢印構文、 case null オプションおよび switch 式の3つがあります。

 

switch文の矢印構文

JDK 14では、 switch 内のcase 句に新しい矢印構文(->)を使用する機能が追加されました。以下の例ではcolons() は従来の方法を示し、 arrows() は新しい方法を示しています。

// enumerations/ArrowInSwitch.java
// {NewFeature} Since JDK 14
import static java.util.stream.IntStream.range;

public class ArrowInSwitch {
  static void colons(int i) {
    switch(i) {
      case 1: System.out.println("one");
              break;
      case 2: System.out.println("two");
              break;
      case 3: System.out.println("three");
              break;
      default: System.out.println("default");
    }
  }
  static void arrows(int i) {
    switch(i) {
      case 1 -> System.out.println("one");
      case 2 -> System.out.println("two");
      case 3 -> System.out.println("three");
      default -> System.out.println("default");
    }
  }
  public static void main(String[] args) {
    range(0, 4).forEach(i -> colons(i));
    range(0, 4).forEach(i -> arrows(i));
  }
}
/* Output:
default
one
two
three
default
one
two
three
*/

(注:  {NewFeature} コメント・タグによって、JDK 8を使用するGradleビルドから、この例は、除外されます。)

colons()は、フォールスルーを防ぐために、各ケース(最後のケースを除く)の後にブレークを追加する必要があります。 arrows() でコロンを矢印に置き換えると、break 文は不要になります。しかし、これは矢印構文が成し遂げることの一部にすぎません。

同じswitch内でコロンと矢印を混在させることはできません。

 

Switch文のcase null句

JDK 17では、以前に不正とされていた case null 句を switchに含める(プレビュー)機能が追加されています。以前は、次のコードで old()で示すように、 switchの外でnullのケースを確認する必要がありました。

checkNull() は、nullが switch の矢印構文とコロン構文の両方で使用可能であることがわかります。

defaultにnullのcaseが含まれているかどうか疑問に思われるかもしれません。 defaultOnly() は、 default がnullを取得しないことを示し、従って case null がなければ、 NullPointerExceptionがなければ、 switch 文がnullをカバーしていない場合でも、使用可能なすべての値をカバーする必要があると規定しているからです。これは後方互換性の問題です。Javaで突然NULLチェックが強制された場合、既存のコードの多くはコンパイルされません。

一般に、カンマを使用して複数のパターンを1つのcaseに結合できます。 combineNullAndCase().に示すように、nullを別のパターンと組み合せることもできます。combineNullAndDefault()に示すように、case nullとdefault の組み合わせのような便利な使え方も可能です。

// enumerations/CaseNull.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
//   --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
import java.util.function.*;

public class CaseNull {
  static void old(String s) {
    if(s == null) {
      System.out.println("null");
      return;
    }
    switch(s) {
      case "XX" -> System.out.println("XX");
      default   -> System.out.println("default");
    }
  }
  static void checkNull(String s) {
    switch(s) {
      case "XX" -> System.out.println("XX");
      case null -> System.out.println("null");
      default   -> System.out.println("default");
    }
    // コロン構文でも動作します:
    switch(s) {
      case "XX": System.out.println("XX");
                 break;
      case null: System.out.println("null");
                 break;
      default  : System.out.println("default");
    }
  }
  static void defaultOnly(String s) {
    switch(s) {
      case "XX" -> System.out.println("XX");
      default   -> System.out.println("default");
    }
  }
  static void combineNullAndCase(String s) {
    switch(s) {
      case "XX", null -> System.out.println("XX|null");
      default -> System.out.println("default");
    }
  }
  static void combineNullAndDefault(String s) {
    switch(s) {
      case "XX" -> System.out.println("XX");
      case null, default -> System.out.println("both");
    }
  }
  static void test(Consumer<String> cs) {
    cs.accept("XX");
    cs.accept("YY");
    try {
      cs.accept(null);
    } catch(NullPointerException e) {
      System.out.println(e.getMessage());
    }
  }
  public static void main(String[] args) {
    test(CaseNull::old);
    test(CaseNull::checkNull);
    test(CaseNull::defaultOnly);
    test(CaseNull::combineNullAndCase);
    test(CaseNull::combineNullAndDefault);
  }
}

/* Output:
XX
default
null
XX
XX
default
default
null
null
XX
default
Cannot invoke "String.hashCode()" because "<local1>" is null
XX|null
default
XX|null
XX
both
both
*/

式としてのswitchの使用

これまで、 switch は常に文であり、switch 文は値を生成しません。JDK 14では、switch も式にすることができ、この方法で使用すると、switchは値を生成することができます。

// enumerations/SwitchExpression.java
// {NewFeature} Since JDK 14
import java.util.*;

public class SwitchExpression {
  static int colon(String s) {
    var result = switch(s) {
      case "i": yield 1;
      case "j": yield 2;
      case "k": yield 3;
      default:  yield 0;
    };
    return result;
  }
  static int arrow(String s) {
    var result = switch(s) {
      case "i" -> 1;
      case "j" -> 2;
      case "k" -> 3;
      default  -> 0;
    };
    return result;
  }
  public static void main(String[] args) {
    for(var s: new String[]{"i", "j", "k", "z"})
      System.out.format(
        "%s %d %d%n", s, colon(s), arrow(s));
  }
}

/* Output:
i 1 1
j 2 2
k 3 3
z 0 0
*/

上の例にある colon()のように従来のスタイルのコロン構文では、新しい yield キーワードを使用してswitchから結果を返します。 yieldを使用する場合、ブレークは必要ないことに注意してください。実際、ブレークを追加すると、コンパイル時にエラーが発生し、switch 式から抜け出そうとするフラグが立てられます。

switch文の中で yieldを使おうとすると、コンパイラは switch式の外で yield を使ったというエラーメッセージを出します。

switch 式では、 arrow()colon(), と同じ効果を持ちますが、構文がより一層すっきりし、よりコンパクトに読みやすくなります。たとえば、信号機をシミュレートする次のようなクラスを考えてみましょう。

// enumerations/EnumSwitch.java
// {NewFeature} Since JDK 14

public class EnumSwitch {
  enum Signal { GREEN, YELLOW, RED, }
  Signal color = Signal.RED;
  public void change() {
    color = switch(color) {
      case RED -> Signal.GREEN;
      case GREEN -> Signal.YELLOW;
      case YELLOW -> Signal.RED;
    };
  }
}

caseにBLUEを追加せずに列挙型のSignal にBLUEを追加した場合、この switch 式がすべての入力可能な値を網羅していないためJavaコンパイル・エラーが発生します。このようにして、コードの変更時に case が漏れないようにコンパイラが保証します。

case に複数の文または式が必要な場合は、次のように波括弧で囲まれたブロック内に配置します。

// enumerations/Planets.java
// {NewFeature} Since JDK 14

enum CelestialBody {
  MERCURY, VENUS, EARTH, MARS, JUPITER,
  SATURN, URANUS, NEPTUNE, PLUTO
}

public class Planets {
  public static String classify(CelestialBody b) {
    var result = switch(b) {
      case  MERCURY, VENUS, EARTH,
            MARS, JUPITER,
            SATURN, URANUS, NEPTUNE -> {
              System.out.print("A planet: ");
              yield b.toString();
            }
      case  PLUTO -> {
              System.out.print("Not a planet: ");
              yield b.toString();
            }
    };
    return result;
  }
  public static void main(String[] args) {
    System.out.println(classify(CelestialBody.MARS));
    System.out.println(classify(CelestialBody.PLUTO));
  }
}
/* Output:
A planet: MARS
Not a planet: PLUTO
*/

注:複数行の case式から値を生成する場合、例え矢印構文が既に含まれている場合でもyieldキーワードを使用する必要があります。

 

より詳しい情報