コンストラクタにおける定義記述に関する改善(JEP482 - Second Preview)
コンストラクタで、親クラスのコンストラクタであるsuper()を実行する前に何らかの処理をしたい場合があります。例えば、リスト5のように引数の型などを変更したい場合です。
public class Jep482 extends ParentClass{
// 省略
// (1) コンストラクタ内で使用する為のstaticメソッドを定義
private static InputStream fileToStream(File file) throws FileNotFoundException {
FileInputStream fis = new FileInputStream(file);
return new BufferedInputStream(fis);
}
public Jep482(File file) throws FileNotFoundException {
super(fileToStream(file)); // (2) コンストラクタでの引数で利用する
}
// 省略
}
これまでのJavaでは、superを実行する前に処理ができなかったために、(1)(2)のようにメソッドをわけるなどのテクニックが必要でした。しかし、この処理をsuperの前に実行しても本来は何も問題ないはずで、むしろ、この制約により直感的なコード記述を阻害していました。今回の修正では、リスト6のように開発者にとってよりわかりやすい記述が可能になります。
public class Jep482 extends ParentClass{
public Jep482(File file) throws FileNotFoundException {
if(!file.exists()){
throw new FileNotFoundException();
}
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bos = new BufferedInputStream(fis);
super(bos);
}
}
この例では、superを呼び出す前にthisに一切アクセスしていないので何も問題が生じないことは明白ですが、場合によってはthisにアクセスしたい場合もあります。例えば、リスト7のように自分自身のクラスのメンバー変数に値を設定したい場合です。
public class Jep482 extends ParentClass{
private String childName;
public Jep482(String name) {
this.childName = name;
super(name);
}
}
このような使い方であれば、実際にはsuperの前に処理を記述しても、後に記述しても実質的な違いはほとんどありません。同じ意味なら、どちらの書き方も許してほしいと思う方もいるはずです(Javaに慣れた開発者であればsuperの後に書くのが制限と覚えてしまっている方も多いと思いますが)。このような無意味な制限がなくなるのは、一般的には望ましいことです。
もちろん、superを最初に記述する意味があるのは、このクラスのインスタンスを安全に作成するためです。例えば、インスタンスができあがっていないのに、親クラスで定義している変数や、各インスタンスメソッドなどが使えないなどの制限は、これまでと同様に課されます(エラーとなります)。
プリミティブ型におけるswitchやinstanceofなどを使った記述形式の改善(JEP455 - Preview)
Javaではプリミティブ型とオブジェクト型でさまざまな違いがあり、プリミティブ型では記述可能であるのに、オブジェクト型だと記述できない場合があります。
switchの条件分岐処理におけるプリミティブ型への対応
Java21からはリスト8のようにswitchにおいてより柔軟な条件分岐ルールが記述できます。
public void sample1_java21(Integer value){
switch (value){
case Integer i when i > 10 -> System.out.println("value is greater than 10");
case Integer i when i < 10 -> System.out.println("value is less than 10");
default -> System.out.println("default case");
}
}
しかし、これとほぼ同じ意味にもかかわらず、リスト9のように記述するとコンパイルエラーになります。
public void sample1_after_java21(int value){
switch (value){
case int i when i > 10 -> System.out.println("value is greater than 10");
case int i when i < 10 ->System.out.println("value is less than 10");
default -> System.out.println("default case");
}
}
// 表示されるエラー内容
// error: unexpected type
// case int i when i > 10 -> System.out.println("value is greater than 10");
// ^
// required: class or array
// found: int
分かりにくいエラーですが、Java21までは条件分岐においてプリミティブ型に対応していなかったわけです。しかし、今回の対応によってどちらでも使えるようになるので、こういったわかりにくさが解消されます。
instanceofにおけるプリミティブ型への対応
同じく、プリミティブ型ではinstanceofの記述ができませんでした。個人的には、"instance of"という名称から、インスタンスという概念ではないはずのプリミティブ型では使わないのでは、とも思います。しかし、オブジェクト型と同じような意味で、安全なcastのために使いたいといった動機もわかります。
例えば、int型をbyte型や、double型をint型に安全にcastしたい場合などです。その場合、リスト10のように利用できます。
int val = 120;
// 値が変わってしまう値の場合には、 instanceof でチェックが出来るようになった
// -128 〜 127は trueになる
if(val instanceof byte b1){
System.out.println("value is " + b1);
}
else{
System.out.println("different value: " + val + " -> " + (byte)val);
}
double val2 = 5.1;
// 5.0 はtrue , 5.1 はfalseなど、値が変わってしまうケースなど
if(val2 instanceof int f1){
System.out.println("value is " + f1);
}
else{
System.out.println("different value: " + val2 + " -> " + (int)val2);
}
byte型は-128~127の範囲まで扱えるので、この範囲のint型であればbyte型に変換しても値の意味は変わりません。この場合にはinstanceofでの値はtrueになります。しかし、それ以外の範囲ではfalseになります。
同様の考え方として、doubleでの5.1をintに変換しようとすると5のように小数点情報が抜け落ち、値の意味が変わってしまいます。この場合にはfalseになります。一方、5であれば、doubleからintに安全に同じ値としてcastが可能なのでtrueになります。
また、Java16から使えるようになったinstanceofの中での変数定義を使えば、castする必要もないため、より安全なコードが書けます。一方、double型から小数点以下の情報を取り除くためにint型へcastする形の使い方をしている場合には、instanceofを使った型変換はできません。
無名変数と無名パターン(JEP456)
この機能は「無名」と訳されることが多いですが、より意味的に近い日本語に訳すなら「使用しないの変数のための特別変数名」としたほうが分かりやすいと思います。
例えば、リスト11のような記述では、同じ変数名を使いがちになる場合があります。
try{
// 処理の実行
}
catch(Exception ex){ // (1) exと命名
try{
Thread.sleep(2000);
// 時間をおいて再度処理をする
}
catch(Exception ex){ // (2) ここで変数名の重複によりコンパイルエラーになる
// エラー処理
}
}
最初の処理では何らかの想定できないエラーがあり得ます。エラーが生じた場合には、時間をおいてから再度処理したいケースがあるでしょう。
この場合、最初のエラーはどのような理由であれcatchするのですが、特に原因までは考慮しない場合があります。このとき、処理の記述はしないため変数名は何でもよいのですが、(1)のようによく使われる変数名であるexとつけてしまいがちです。
当然この後ではexという変数名は使えなくなるため、このままでは、(2)でコンパイルエラーが生じてしまいます。こういった意味のない処理であっても便宜上、変数を定義しなくてはならない場合として「_(アンダースコア)」のみの変数が使えます。これを使った場合、リスト12のように書き換えることができます。
catch(Exception _){
try{
Thread.sleep(2000);
// 時間をおいて再度処理をする
}
catch(Exception ex){
// エラー処理
}
}
このように、使わない変数のために考慮する手間が少なくなります。
戻り値を利用しない場合の無名変数
通常の変数は、有効なスコープに1つしか定義できませんが、無名変数はリスト13のように同じスコープ内でも何度も利用することが可能です。
Queuewords = new ArrayDeque<>(List.of("dummy1","dummy2","apple","banana","cherry")); // (1) 最初の2つは必ず意味がないので、取得しても棄てる var _ = words.poll(); var _ = words.poll(); var item = words.poll();
(1)では、キューにある意味のない最初の2つの値を取り除く処理をしています。このような処理はプロトコル処理や特定のフォーマット処理などではよくあります。その場合には、リスト14のようにこれまで記述しているケースがありました。
words.poll(); words.poll();
例では、Queueクラスのpoll()なので、コメントがなくても想像しやすいです。しかし、これが独自に定義したクラスの場合、このような値を返すメソッドを使っていながら値を使わないのは、書き間違いなのか意図したものなのかわかりにくくなります。
もちろん、その旨のコメントを書けばよいのですが、この無名変数を使えば、例えコメントがなくても「値を取得しているが使わない」といった意味が明確にわかるようになります。
無名パターン
続いて、instanceofやswitchなどのパターン定義内で利用しない変数を明示的にする場合にも利用できます。
record Dish(String name,int calories){}
record Dinner(String name,Dish dish1,Dish dish2){};
Object todayDinner = new Dinner("スペシャルディナー",new Dish("メイン",600),new Dish("前菜",300));
// 使わない引数は "_"にする
if( todayDinner instanceof Dinner(String name,Dish(String d1name,int _),Dish(String d2name,int _))){
System.out.println("今日のメニューは「" + name + "(" + d1name + "," + d2name + ")」" );
}
このようにパターン内で使う場合、使う定義と使わない定義が明確になるため、意図がわかりやすいだけではなく、コーディングのむだも減らせます。
