BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Java SE 12の拡張switch文/式の完全ガイド

Java SE 12の拡張switch文/式の完全ガイド

ブックマーク

原文(投稿日:2019/02/01)へのリンク

switch文を使用しないコードベースは存在しません。パフォーマンス面からも,if/else文より多く使用する傾向があります。昔ながらのJavaのswitch文はJavaが生まれた時から存在しているので,私たちはそれに -- 特にその特異さに慣れています。

Javaのswitch文の現在のデザインは,C++などの言語に厳密に従っており,既定ではフォールスルーセマンティクスをサポートします。この制御フローは,低レベルのコードを書く場合には便利ですが,レベルの高いコンテキストで使用されるようになると,柔軟性よりもエラーの起こしやすさが上回り始めます。

Java言語でパターンマッチングがサポートされるようになるにつれて,既存のswitch文の不規則さが障害になってきています。switchブロックの制御フロー,switchブロックが既定では全体でひとつのスコープとして扱われること,switchが文としてのみ機能すること,などが問題視されているのです。

Java 12ではswitchが拡張されて,新たな機能が加えられました。これまでswitch文であったものを,通常の拡張switch文または"switch式"として使用することが可能になり,コーディングが簡単になります。

これによって"traditional"または"simplified"という,2つのスコープと制御フロー動作が可能になります。これらの変更は,日々のコーディングを簡略化すると同時に,将来的にswitch文/式でパターンマッチングを使用するための準備段階にもなっています。

新たな機能

JDK 9からは新たに,従来よりも短期間のリリース計画が導入されました。新しい6ヶ月のリリースカデンツは,Javaをより生産的で革新的なものにするために,機能をより早く,高い頻度で提供することを目標としたものです。

最近のJDK 9, 10, 11,そして今後登場するJDK 12(2019年3月19日予定)や次期リリースではすでにこれが採用されており,多くのAPI拡張とともにJava言語の新機能が毎回提供されています。

新しいプロジェクト

こうした拡張や新機能を可能にするためには,言語の特定の側面に集中するプロジェクトが必要になります。そうすることによって,Java言語エンジニアのグループが,巨大なJDKのすべての機能や開発ではなく,新たな機能強化に集中することが可能になるのです。

そのために、多くのプロジェクトが立ち上げられ、それぞれが言語の異なる面に注目しています。これらのプロジェクトには、OpenJDKのネットのWebサイトにあるプロジェクトメニューのセクションからアクセスできます。少し挙げただけでも、PanamaやValhalla、Amber、Jigsaw、Loomなど、数多くのプロジェクトがあります。今回私たちが注目するのはProject Amberです。

Project Amber

Project AmberはCompiler Groupがスポンサです。この名前を見ただけでも、プロジェクトの目標が分かるでしょう。そのとおり -- このプロジェクトの目標は、Oracle JEPプロセス下でJEP候補として受け入れられるような、小さく、生産性を重視したJava言語機能を探求することにあります。

以下に示したすべてのJEPと関連JDK(もしあれば)の一覧で分かるように、Project Amberは現在、インキュベーションないし提供済の状態にあります。

現在進行中のJEO

すでに提供済のJEP

  • JEP 286 ローカル変数の型推論(var) (JDK 10)
  • JEP 323 Lambdaパラメータのローカル変数構文 (JDK 11)

パターンマッチングによるJavaの拡張

パターンマッチングは1960年代から、SNOBOL4AWKのようなテキスト指向言語,HaskellMLなどの関数型言語、さらに最近ではScalaなどのオブジェクト指向言語にまで(最も近くではMicrosoftのC#言語があります)、多種多様なスタイルでプログラミング言語に取り入れられてきました。

Project Amberは、Java言語にパターンマッチングを追加します。パターンマッチングは、オブジェクトから特定条件のコンポーネントを取り出すための共通ロジックを実現にすると同時に、それをより簡潔かつ安全に実行可能にします。例えばJEP 305ではinstanceof演算子(対象とするJDKはまだ決まっていません)が、JEP 325ではswitch式(JDK 12で提供予定)が、これによって拡張されます。

新しいswitch文を使用する

Java SE 9のリリース以来、対話型環境ツール(REPL)のJShellで新しいJava言語やAPI機能を試すことが慣例になりました。IDEなど他のソフトウェアをインストールする必要はありません – JDKだけで十分です。

この機能はすべてのJDKに添付されているので、新たに拡張されたswitch文や、あるいはswitch式など、Java SE 12の新しい言語機能を手軽に試すことができます。必要なのは,JDK 12早期アクセスビルドをダウンロードしてインストールすることだけです。インストールはこちらのリンクから可能です。

ダウンロードが成功したならば,好きな場所にバイナリを解凍して,JDKのbinフォルダを作り,システムのどこからでもアクセスできるようにします。

なぜJShellなのか

Java言語の構文を手早く勉強したり,新しいJava APIやその機能を試したり,複雑なコードのプロトタイプしたりする場合,私はいつも,現代的な言語ならばほとんど用意されている,インタラクティブなプログラミング環境ツールを使うようにしています。

一般的に次のようなプロセスを含む,コードの編集,コンパイル,実行という面倒なサイクルに入らなくていいからです。

  1. プログラム全体を記述する。
  2. コンパイルしてエラーを取り除く。
  3. プログラムを実行する。
  4. どこが悪いのかを見つける。
  5. 編集する。
  6. このプロセスを繰り返す。

JShellとは何か?

インタラクティブなプログラミング環境として,Java Shellの意味を持つJShellツールが用意されたことで,JavaはリッチなREPL(Read-Evaluate-Print-Loop)の実装を持つようになりました。なぜこれが重要なのでしょう?簡単です。JShellが,Java言語や拡張ライブラリの機能を手軽に試し,見つけ,経験するための,高性能で親しみやすい環境を提供してくれるからです。

JShellを使えば,プログラム要素をひとつずつ入力して,その結果をすぐに確認し,必要ならば修正することができます。プログラム全体をコンパイルする代わりに,このREPLでJShellコマンドやJavaコードのスニペットを記述するのです。

JShellの紹介としてはこれで十分ですが,InfoQでは先日,このツールの詳細な解説記事を公開しています。JShellの機能をより詳しく学ぶために,"Hands-on Java 10 Programming with JShell [Video]"と題したビデオトレーニングを用意しました。JShellをマスタするための一助になると思います。このビデオはPackt Webサイトからも入手可能です。

JShellセッションを開始する

ツールとの対話を始めるために最初にすることは,次の手順で新しいJShellセッションを開始することです。

  1. Microsoft Windowsでは,コマンドプロンプトを開いてjshellと入力し,Enterを押すだけです。
  2. Linuxならば,shellウィンドウを開いてjshellと入力し,Enterを押します。
  3. macOS(以前のOS X)を使用しているならば,Terminalウィンドウを開いた上でコマンド"jshell"を入力し,最後にEnterを押します。

さあ! 新しいJShellセッションが起動しました。次のようなメッセージに続いて,jshell>プロンプトが表示されるはずです。

mohamed_taman:?$ jshell --enable-preview
|  Welcome to JShell -- Version 12-ea
|  For an introduction type: /help intro
 
jshell>

最初の行の"Version 12-ea"は,Java SE JDK 12早期アクセス版(early access)を使用していることを示しています。JShellでは情報メッセージの前に縦棒(|)が付けられています。これでJavaコード,あるいはJShellコマンドとスニペットを入力する準備が整いました。

ここで--enable-previewというオプションが指定されていることに気付いたと思います。これは何でしょう?このオプションを使用すると,公式にはJDKの一部になっていない,現時点では"プレビュー"状態の新しい言語機能のロックを解除することができるのです。これらの機能はまだ実験とフィードバックの段階であるため,既定では無効になっています。

このフラグを使うことで,開発者としてプレビュー機能を試して,改善のためのフィードバックを提供したり,あるいは気に入らない部分をフィードバックすることが可能になります。今回の記事では,新しいswitch式の機能を試しますので,JShellセッションの起動時にこのオプションを含めておく必要があります。

注記: これはプレビュー機能ですので,まだAmberメーリングリストにフィードバックを送ることが可能です。

理屈はこのくらいにして,早速新しいswitch文と式のスニペットを使って概念を理解し,この素晴らしい機能の使い方を学ぶことにしましょう。

switch文と式をハッキングする

まず最初に,現在の状況について知っておく必要があります。また,switch文の現行エディションの状況や,さらには日常のコーディングで私たちが行っている方法についても確認しておきましょう。

Dayを変数として持つ単純なメソッドを考えてみましょう。このメソッドは通常の日(Sat, San, Mon...など)を含むDay列挙をスイッチし,それに基づいて"週末"か"週日"かを返すものです。

先に起動したJshellセッションで,次のようなDayt列挙を定義しましょう。

jshell> enum Day{
   ...> NONE,
   ...> SAT,
   ...> SUN,
   ...> MON,
   ...> TUS,
   ...> WED,
   ...> THU,
   ...> FRI;
   ...> }
|  created enum Day

次にメソッド"String wahllsToday (Day day)"を定義して,dayの値でスイッチして(通常のswitchを使用します),次のように週日(working)または週末を返すようにします。

jshell> String whatIsToday(Day day){
   ...>   var today = "";
   ...>   switch(day){
   ...>	case SAT:
   ...>	case SUN: today = "Weekend day";
   ...>          	break;
   ...>	case MON:
   ...>	case TUS:
   ...>	case WED:
   ...>	case THU:
   ...>	case FRI: today = "Working day";
   ...>          	break;
   ...>	default:  today = "N/A";
   ...>  }
   ...>  return today;
   ...> }
|  created method whatIsToday(Day)
 
jshell> whatIsToday(Day.SUN)
$3 ==> "Weekend day"

Although this function works perfectly, and if we used the original version with the many break statements for each case, it introduces visual noise which often makes it hard to debug the errors. 同時に,コードが必要以上に冗長になっていて,break文を誤って忘れると,思いがけないフォールスルーが発生することになります。

上の例ではフォールスルー機構を使うことで,通常同じ処理を行うよりもbreakの使用を減らしていますが,それでもまだ非常に長くなっています。

従来のswitch文を拡張する

新たに"矢印"("switchラベル付きルール")形式を提案したJEP 325では,"case L->"と書くことで,ラベルにマッチした場合にラベルの右のコードのみを実行することを示すことができます。この記法は,switch文でもswitch式でも使用可能です。

同じように,従来の"コロン"構文("switchラベル付きステートメントグループ")も両方のcaseで使用できます。

コロンも新しい矢印の構文も,どちらのcaseでも使用できるので,コロン(:)が使用されているからswitch文である,あるいは"矢印"(->)が使用されているからswitch式である,という意味にはなりません。

それでは,従来の論理文の書き換えをしてみましょう。例えば,前述の"String whatIsToday(Day day)"メソッドは,次のようにすれば,もっと簡略化したものに書き換えることができます。

jshell> /edit whatIsToday

JShellの"/edit"コマンドを実行すると,JShell edit padが開いて,今回のメソッドのような,大きなコードスニペットを簡単に編集することができます。次に,JShell edit padで,以下のようにメソッドを修正してください。

String whatIsToday(Day day){
  var today = "";
  switch(day){
   case SAT, SUN -> today = "Weekend day";
   case MON, TUS, WED, THU, FRI -> today = "Working day";
   default -> throw new IllegalArgumentException("Invalid day: " + day.name());
  }
  return today;
}

修正が終わったらExitボタンをクリックして変更を登録し,現在の"JShell>"セッションに戻ります。値を指定して,もう一度メソッドを実行してみましょう。

jshell> /edit whatIsToday
|  modified method whatIsToday(Day)
 
jshell> whatIsToday(Day.SUN)
$5 ==> "Weekend day"
 
jshell> whatIsToday(Day.MON)
$6 ==> "Working day"
 
jshell> whatIsToday(Day.NONE)
|  Exception java.lang.IllegalArgumentException: Invalid day: NONE
|    	at whatIsToday (#11:6)
|    	at (#12:1)
 
jshell>

上記の新しいswicth文の構造を詳しく見ると,多くの点で違っていることが分かると思います。まず最初に,"break"がなくなって明確かつ簡潔になっています。

第2に,switch文で新しい"矢印"構文("ラベルルール")形式を使うことによって,明示的に"break"を指定する必要がなくなり,時に忌まわしいswitchの"フォールスルー"caseから私たちを救ってくれています。

"case L->"swicthラベルの右のコードが式,ブロック,または(便宜上)throw文に制限されている点にも注目してください。先程のメソッドでは,switch文のdefaultケースで,無効な日付を識別するために"IllegalArgumentException"をスローしています。

ひとつのcaseに複数のコンマ区切りラベルを指定する

従来の古いswitch文では、複数のcaseで同じステートメントセットを実行する必要がある場合には、 フォールスルーのメカニズムを使用していました。

しかし,ここで第3の注意点として,先程の例では"単一のswitch case内にコンマで区切られた複数のラベル"という構造を使用しています。これは単に,同じ文を実行する共通のcaseをコンマで区切ったものです。

switchラベル付きステートメントグループ

JEP325では,従来の"コロン"構文("switchラベル付きステートメントグループ")も普通に使うことができるので,先程のメソッドをもう一度,このコロン形式で次のように書き換えることが可能です。

String whatIsToday(Day day){
  var today = "";
  switch(day){
   case SAT, SUN: today = "Weekend day"; break;
   case MON, TUS, WED, THU, FRI: today = "Working day"; break;
   default: throw new IllegalArgumentException("Invalid day: " + day.name());
  }
  return today;
}

これはちょっとした練習として残しておきましょう。JShellコマンド"/edit whatIsToday"を実行して,矢印トークン構文を使用しているこのメソッドを以前のようなコロン/break構文に書き直した上で,いくつかの値でうまく動くことを確かめてください。

新しいスイッチ式

この話題に入る前に,として機能するようになる前のswitchがどのようなものだったのか,疑問に思う人もいるでしょう。Java SE 12以前のswitchは常に文でした。つまり,制御フローを指示する命令形の構文であり,指示先になることはできなかったのです。一方で,は常に,明確な値として評価されます。計算の最終的な目標は結果ですから,指示先に値を与えることが可能なのです。

文と式のち外について確認したので,新しいswitch式がどのように動作するのかを見てみましょう。実際に,日々のコードで使用している既存のswitch文の多くは,そして前述のコードも,それぞれのアームが共通の変数に値を設定する,あるいは値を返すという意味で,基本的にはswitch式のシミュレーションなのです。

新たにswitch式が使えるようになったことで,ターゲットとなる変数に値を直接返すことが可能になったので,"StringwhatIsToday(Day day)"メソッドを変更してみましょう。

String whatIsToday(Day day){
  var today = switch(day){
	case SAT, SUN -> "Weekend day";
	case MON, TUS, WED, THU, FRI -> "Working day";
	default -> throw new IllegalArgumentException("Invalid day: " + day.name());
  };
 
  return today;
}

修正したメソッドを詳しく見ると,switch文が式として書かれていることに気付くはずです。第1に,switchが等号の後にかかれています。第2に,文の一部であるため,これまでのswitch文とは違って,最後にセミコロンが必要です。第3に,矢印に続くものが戻り値になっています。

前述したように,新しいswitch式でも従来の"コロン"構文"case L:"を使用することができるので,breakを使っていた先程のメソッドを次のように書き換えることが可能です。

String whatIsToday(Day day){
  var today = switch(day){
	case SAT, SUN: break "Weekend day";
	case MON, TUS, WED, THU, FRI: break "Working day";
	default: throw new IllegalArgumentException("Invalid day: " + day.name());
  };
 
  return today;
}

breakの2つの記法(値ありと値なし)は,メソッドでのreturnの2形式に似ています。

ステートメントブロック

新しいswitch文/式を"コロン"あるいは"矢印"構文のどちらかを使って日々の作業で使用する時は,形式がどちらであっても,右辺には単一の式を使用する場合がほとんどでしょう。

しかしながら,複数の式を同時に評価することが必要な場合もあります。ブロック{}を使えば,これを簡単に行うことができます。また,次の例のように,ひとつのswitch文/式の中で同じ変数名を複数回生成して使用できる点も非常に便利です。

String whatIsToday(Day day){
  var today = switch(day){
	case SAT, SUN: break "Weekend day";
	case MON, TUS, WED, THU, FRI:{
   	 var kind = "Working day";
    	break kind;
	}
	default: {
    	var kind = day.name();
   	 System.out.println(kind);
   	 throw new IllegalArgumentException("Invalid day: " + kind);
	}
  };
 
  return today;
}

複合式

switch式は"複合式(poly expression)"です。ターゲット型が分かっていれば,その型が各caseアームにプッシュダウンされますが,不明な場合は,各caseアームを組み合わせた標準型が計算されます。

次のswitch式代入について考えてみましょう。

jshell> var day = Day.SUN
day ==> SUN
 
jshell> var today = switch(day){
   ...> 	case SAT, SUN -> "Weekend day";
   ...> 	case MON, TUS, WED, THU, FRI -> "Working day";
   ...> 	default -> {
   ...>          	var len = day.name().length();
   ...>          	break len;
   ...> 	}
   ...>   };
today ==> "Weekend day"
 
jshell> var day = Day.NONE
day ==> NONE
 
jshell> var today = switch(day){
   ...> 	case SAT, SUN -> "Weekend day";
   ...> 	case MON, TUS, WED, THU, FRI -> "Working day";
   ...> 	default -> {
   ...>          	var len = day.name().length();
   ...>          	break len;
   ...> 	}
   ...>   };
4
today ==> 4

上のswitch式をもう少し詳しく見ると,2つのアームはStringを,defaultアームはint変数のlenを返していることが分かります。ターゲット変数がvarなので,どちらも完全に動作するのです。

それでは,代入対象がvarではなくintであることを明示的に宣言してみましょう。これによってコンパイラは,"代入対象の型が既知である場合,これが各caseのアームにプッシュダウンされる"という条件を満足するようになります。

jshell> int today = switch(day){
   ...> 	case SAT, SUN -> "Weekend day";
   ...> 	case MON, TUS, WED, THU, FRI -> "Working day";
   ...> 	default -> {
   ...>          	var len = day.name().length();
   ...>          	System.out.println(len);
   ...>          	break len;
   ...> 	}
   ...>   };
|  Error:
|  incompatible types: bad type in switch expression
|      java.lang.String cannot be converted to int
|  	case SAT, SUN -> "Weekend day";
|                       ^-----------^
|  Error:
|  incompatible types: bad type in switch expression
|      java.lang.String cannot be converted to int
|  	case MON, TUS, WED, THU, FRI -> "Working day";
|                                      ^-----------^

新しいswitch機能ではできないこと

私たちの記述したswitch文/式に対して,コンパイラが怒って不満を言うような場合を検討してみましょう。そうすることで,このような状態を回避して,コンパイラを幸福にする方法を学ぶことができます :)。

switch文中のreturnでは値を返すことはできない

switch文に関連するbreakから値を返そうとすると,コンパイル時エラーになります。

jshell> String whatIsToday(Day day){
   ...>   var today = "";
   ...>   switch(day){
   ...>    case SAT, SUN: break "Weekend day";
   ...>    case MON, TUS, WED, THU, FRI: break "Working day";
   ...>    default: throw new IllegalArgumentException("Invalid day: " + day.name());
   ...>   }
   ...>   return today;
   ...> }
|  Error:
|  unexpected value break
| 	case SAT, SUN: break "Weekend day";
|    	            ^------------------^
|  Error:
|  unexpected value break
| 	case MON, TUS, WED, THU, FRI: break "Working day";
|                                   ^------------------^
|  Error:
|  unreachable statement
|	return today;
|	^-----------^
 
jshell>

矢印構文が示すのはswitch文内の文のみである

値を返すswitch文で矢印構文を使おうとすると,同じくコンパイル時エラーになります。この場合の矢印構文は値を返さずに,文を示すだけでなくてはなりません。

jshell> String whatIsToday(Day day){
   ...>   var today = "";
   ...>   switch(day){
   ...>    case SAT, SUN -> "Weekend day";
   ...>    case MON, TUS, WED, THU, FRI -> "Working day";
   ...>    default -> throw new IllegalArgumentException("Invalid day: " + day.name());
   ...>   }
   ...>   return today;
   ...> }
|  Error:
|  not a statement
| 	case SAT, SUN -> "Weekend day";
|                  	^-----------^
|  Error:
|  not a statement
| 	case MON, TUS, WED, THU, FRI -> "Working day";

矢印構文とコロン/break構文を混在することはできない

"矢印"と従来のコロン/break構文を混在しようとすると,コンパイラは非常に腹を立ててエラーを出すでしょう。

jshell> String whatIsToday(Day day){
   ...>   var today = "";
   ...>   switch(day){
   ...>    case SAT, SUN -> today = "Weekend day";
   ...>    case MON, TUS, WED, THU, FRI: today = "Working day";break;
   ...>    default -> throw new IllegalArgumentException("Invalid day: " + day.name());
   ...>   }
   ...>   return today;
   ...> }
|  Error:
|  different case kinds used in the switch
| 	case MON, TUS, WED, THU, FRI: today = "Working day";break;
|     ^--------------------------------------------------------^

switch式ではすべてのケースを指定しなければならない

そして最後に,switch式ですべてのswitchケースが指定されていない場合にも,同じくコンパイルエラーになります。指定されていないものがあると,次のようなエラーが発生します。

jshell> String whatIsToday(Day day){
   ...>   var today = switch(day){
   ...>    case SAT, SUN -> "Weekend day";
   ...>    case MON, TUS, WED, THU, FRI -> "Working day";
   ...>   };
   ...>   return today;
   ...> }
|  Error:
|  the switch expression does not cover all possible input values
|	var today = switch(day){
|         	   ^-----------...

switch式ですべてのケースを指定するのは容易ではありませんし,いらいらするのは確かですが,修正は簡単です -- 単にdefaultを追加するだけで,問題なくコンパイルすることができます。上記のコードを正しくコンパイルする方法は,読者にお任せしたいと思います。

switchの今後

古いswitchと同じく,新しく拡張されたswitch文swith式のどちらも,列挙型で問題なく動作します。では,他の型ではどうでしょうか?拡張された"従来型"も"式"形式も同じで,String,int,short,byte,charやそれぞれのラッパ型でも動作します。現時点では何も変わっていません。

将来のJavaバージョンでのswitchの拡張では,例えば,これまでは許可されていないfloat,doublelong(およびそれらのボックスタイプ)でのswitchを可能にすることが目標になるはずです。

まとめ

今回の記事では,Project Amberとパターンマッチング,JEP 325がswitch文を拡張してのいずれにも使えるようにすること,どちらの形式でも"従来型"および"簡略型"のスコープと制御構造が可能であることを学びました。

これらの変更は日々のコーディングを簡単にするとともに,適切なbreak文を付け忘れた場合の"フォールスルー"問題によって生じるバグからあなたを救ってくれます。さらに,新しいswitch文/式を使い始める時に開発者が犯しがちな,最も一般的な間違いについても挙げました。

さらに今回の言語変更は,switch文/式へのパターンマッチング(JEP 305)導入の準備であると同時に,現在のswitchがfloatやdouble,long,さらにはそれらのラッパクラスによるスイッチのサポートへと拡張されるようになります。

終わりになりましたが,今回の記事を執筆したことをとてもうれしく思うとともに,読者の皆さんが記事を楽しんで頂けたらと願っています!もしそうでしたら,"like"ボタンをクリックして,この言葉を友人やソーシャルメディアに広げてください!

リソース

著者について

Mohamed TamanContradeのディジタルサービスのシニアエンタープライズアーキテクトであり,Java Champion,Oracle Groundbraker Ambassador,Java SE.next()とJakartaEE.next()のAdopt,JCPメンバ,JCP Executive Committee元メンバ,JSR 354/363/373 Expert Groupメンバ,EGJUGリーダ,Oracle Egypt Architects Clubボートメンバです。氏はJavaを語り,モバイル,ビッグデータ,クラウド,ブロックチェーン,DevOpsを愛しています。国際的な講演者で,"JavaFX essentials"や"Getting Started with Clean Code, Java SE 9","Hands-On Java 10 Programming with JShell",および新刊"Secrets of a Java Champions"といった書籍やビデオの著者であり,Duke’s choice 2015と2014,JCP outstanding adopt-a-jar participant 2013といった賞を受賞しています。

この記事に星をつける

おすすめ度
スタイル

BT