CLOVER🍀

That was when it all began.

JDK 7の新機能(主に言語仕様の変更)で遊ぶ

未だ仕事の現場ではJava 6を使っていますが、JDK 7もリリースされてupdate 6まで来たことですし、そろそろ1度触っておこうかなと思いまして。

今回は、言語仕様系の変更を扱ってみます。ダイアモンド演算子だけは、さらっと触ったことがあります。NIO.2とかクラスローダ、Fork/Join Frameworkとかもテーマとしてはありますが、とりあえずパス…。

若干、今更感はありますけどね。

ダイアモンド演算子

パラメータ化されたクラスを使用する時に、変数宣言時とインスタンス作成時で同じ型パラメータを指定しなくてはならないのが冗長だ、ということで楽に書けるようにしたそうな。

要は、JDK 6以前で

        // JDK 6 以前
        List<String> stringsOldStyle = new ArrayList<String>();

のように左辺と右辺で同じ型パラメータを書いていたものが

        // JDK 7
        List<String> strings = new ArrayList<>();

と書けると。これ、割と便利そう。ダイアモンド演算子という名前だそうです。まあ、JDK 6以前でもジェネリクスに関する型推論が全くなかったわけではないですが、もうちょい身近になりましたね。

型指定がネストしてても頑張れる。

        Map<String, List<String>> map = new HashMap<>();

宣言時に必ず代入しなくても、大丈夫そう。

        List<String> list;
        if (...) {
            list = new ArrayList<>();
        } else {
            list = new LinkedList<>();
        }

ただ、メソッド引数に直接適用はできない模様。例えば、こんなメソッドがあって

    public static void receiveList(List<String> strings) {
        for (String s : strings) {
            System.out.println(s);
        }
    }

こういうコードを書くと、コンパイルエラーになります。

        receiveList(new ArrayList<>());

エラーを見ると

         receiveList(new ArrayList<>());
         ^
   期待値: List<String>
   検出値: ArrayList<Object>
   理由: 実引数ArrayList<Object>はメソッド呼出変換によってList<String>に変換できません

と言われますので、型がわかってないってことですね。つまり、メソッドの引数にダイアモンド演算子は使えない、と。

よって、こういうケースは普通に書かざるをえません。これについては、JDK 6まででも使える型推論を使っても、同じ話ですね。

        receiveList(new ArrayList<String>());

あと、使う機会があるかどうかはかなり疑問ですが、匿名インナークラスでの使用も不可。以下のコードは、コンパイルエラーです。

        List<String> subList = new ArrayList<>() { };

switch文にStringが指定可能

これまで整数系の型とenumのみがswitch文で使用できていましたが、これにStringが加わりました。

        String s = "foo";

        switch (s) {
        case "foo":
            System.out.println("This is foo!");
            break;
        case "bar":
            System.out.println("This is bar!");
            break;
        default:
            System.out.println("others");
            break;
        }

注意点は

  • case節にnullは指定できない
  • switchに渡す値がnullだと、NullPointerExceptionになる

ですかね。

数値表現形式の追加

10進数表記、8進数表記(0)、16進数表記(0x)に加えて、バイナリ表記が加わりました。

        byte b = (byte)0b00101111;  // 0x2F

0bを付けると。

あと、数値表現中にアンダースコアを入れることができるようになったそうな。

        int number = 1000_000;

組み合わせOK。

        byte b2 = (byte)0b0010_1111;

try-with-resources

リソースの自動クローズができるようになった構文です。前提としては、java.lang.AutoCloseableインターフェースが実装されていることのようですが、java.io.Closeableをはじめとして既存のクローズ可能なインターフェースなどはけっこう実装しているっぽいので普通に使えるでしょう。

try-with-resourcesを使うと、これまで

        FileInputStream fis = null;
        InputStreamReader isr = null;
        BufferedReader reader = null;

        try {
            fis = new FileInputStream("src/main/java/TryWithResourcesTest.java");
            isr = new InputStreamReader(fis, "UTF-8");
            reader = new BufferedReader(isr);

            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) { }
            }

            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) { }
            }

            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) { }
            }
        }

みたいに書いていたものを

        try (FileInputStream fis = new FileInputStream("src/main/java/TryWithResourcesTest.java");
             InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
             BufferedReader reader = new BufferedReader(isr)) {

            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

と一気に短くできます。

リソースを複数使う場合は、セミコロンで区切るんですと。

        try (FileInputStream fis = new FileInputStream("src/main/java/TryWithResourcesTest.java");
             InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
             BufferedReader reader = new BufferedReader(isr)) {

まあ、ひとつならセミコロン不要ということで。

try-with-resources文でキャッチせずに、そのまま呼び出し元にスローするならこんな感じ。

        try (FileInputStream fis = new FileInputStream("src/main/java/TryWithResourcesTest.java");
             InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
             BufferedReader reader = new BufferedReader(isr)) {

            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }

なお、本体の処理で例外発生後、さらにcloseメソッドで例外が発生するとThrowable#addSuppressed()が呼び出され、元の例外に情報が付加されるとの記述を見たので、ちょっと試してみました。

こんなコードを用意して

public class AutoCloseableTest implements AutoCloseable {
    public static void main(String[] args) {
        try {
            run();
        } catch (Exception e) {
            System.out.println(e);
            
            for (Throwable th : e.getSuppressed()) {
                System.out.println("suppressed => " + th);
            }
        }
    }

    public static void run() throws Exception {
        try (AutoCloseableTest act = new AutoCloseableTest()) {
             act.execute();
        }
    }

    public void execute() throws Exception {
        throw new Exception("execute error");
        //System.out.println("execute");
    }

    public void close() throws Exception {
        throw new Exception("close error");
    }
}

実行。

java.lang.Exception: execute error
suppressed => java.lang.Exception: close error

なるほど、確かにクローズ時の例外情報がくっついてますね。

では、本体の処理を

    public void execute() throws Exception {
        //throw new Exception("execute error");
        System.out.println("execute");
    }

と変更して例外を投げないようにすると

execute
java.lang.Exception: close error

当然のことながら、クローズ時に投げられた例外がcatchでひっかかるようになりますね。

あと、閉じる順は

public class AutoCloseableTest2 implements AutoCloseable {
    private String name;

    public AutoCloseableTest2(String name) {
        this.name = name;
    }

    public void close() {
        System.out.printf("close[%s]%n", name);
    }

    public static void main(String[] args) {
        try (AutoCloseableTest2 o1 = new AutoCloseableTest2("o1");
             AutoCloseableTest2 o2 = new AutoCloseableTest2("o2");
             AutoCloseableTest2 o3 = new AutoCloseableTest2("o3")) {
        }
    }
}

後ろから閉じていくようです。

close[o3]
close[o2]
close[o1]

まあ、そりゃそうですよね。

tryの中でインスタンスをnewする必要はないようですが

public class AutoCloseableTest3 implements AutoCloseable {
    public void close() {
        System.out.println("close");
    }

    public static void main(String[] args) {
        AutoCloseableTest3 act = new AutoCloseableTest3();
        try (AutoCloseableTest3 a = act) {
        }
    }
}

try内で宣言した変数に、代入する必要があるみたいです。

public class AutoCloseableTest3 implements AutoCloseable {
    public void close() {
        System.out.println("close");
    }

    public static void main(String[] args) {
        AutoCloseableTest3 act = new AutoCloseableTest3();
        try (act) {  // <- これはコンパイルエラー
        }
    }
}

このサンプルで変数actにfinalを付与してもダメでした。try内で宣言した変数への代入も許されていないようです。

最後、try-with-resourcesにはcatchとfinallyも付けることができますが、これで例外が発生する場合は

public class AutoCloseableTest2 implements AutoCloseable {
    private String name;

    public AutoCloseableTest2(String name) {
        this.name = name;
    }

    public void execute() {
        throw new RuntimeException("runtime error");
    }

    public void close() {
        System.out.printf("close[%s]%n", name);
    }

    public static void main(String[] args) {
        try (AutoCloseableTest2 o1 = new AutoCloseableTest2("o1");
             AutoCloseableTest2 o2 = new AutoCloseableTest2("o2");
             AutoCloseableTest2 o3 = new AutoCloseableTest2("o3")) {
            o1.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("execute finally");
        }
    }
}

どうなるかというと

close[o3]
close[o2]
close[o1]
java.lang.RuntimeException: runtime error
	at AutoCloseableTest2.execute(AutoCloseableTest2.java:9)
	at AutoCloseableTest2.main(AutoCloseableTest2.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:601)
	at sbt.Run.invokeMain(Run.scala:68)
	at sbt.Run.run0(Run.scala:61)
	at sbt.Run.execute$1(Run.scala:50)
	at sbt.Run$$anonfun$run$1.apply$mcV$sp(Run.scala:54)
	at sbt.TrapExit$.executeMain$1(TrapExit.scala:33)
	at sbt.TrapExit$$anon$1.run(TrapExit.scala:42)
execute finally

というように、close実行→catch実行→finally実行、となるようです。

例外のマルチキャッチ

これまでのcatchは、例外ひとつずつに対してのみ処理が記述できたので、いくつかの例外がスローされる場合には

try {
} catch (Exception1 e) {
  // code
} catch (Exception2 e) {
  // code
}

という風にcatchを並べて書いていたものですが、捕捉している例外が異なっても記述処理が同じであれば割と面倒…。

JDK 7からは、この状況が変わり、ひとつのcatchで複数の例外を捕捉できるようになりました。以下、サンプル。

import java.io.*;
import java.sql.*;

public class MultiCatchExceptionTest {
    public static void main(String[] args) {
        try {
            throwIOException();
            throwSQLException();
        } catch (IOException | SQLException e) {
            e.printStackTrace();
        }
    }

    public static void throwIOException() throws IOException {
        throw new IOException("io error");
    }

    public static void throwSQLException() throws SQLException {
        throw new SQLException("sql error");
    }
}

こんな感じで、「|」で区切って例外クラスをつなげます。これをマルチキャッチと言うそうな。

        try {
            throwIOException();
            throwSQLException();
        } catch (IOException | SQLException e) {
            e.printStackTrace();
        }

この時の変数eの型は、「|」でつなげた例外の共通の親クラスになるようです。この場合は共通の親クラスはExceptionになるので

        } catch (IOException | SQLException e) {
            e.printStackTrace();

            e.getSQLState();  // <- SQLExceptionのみで使える
        }

みたいなことを書くと、コンパイルエラーです。

なお、継承関係のある例外クラスは、マルチキャッチで捕捉することはできないようです。

        try (FileInputStream fis = new FileInputStream("src/main/java/MultiCatchExceptionTest.java");
             InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
             BufferedReader reader = new BufferedReader(isr)) {
        } catch (FileNotFoundException | IOException e) {  // <- ココ
            e.printStackTrace();
        }

こういう記述をすると、コンパイルエラーになります。

エラー: 複数catch文の代替をサブクラス化によって関連付けることはできません
         } catch (FileNotFoundException | IOException e) {
                                          ^
   代替FileNotFoundExceptionは代替IOExceptionのサブクラスです

例外の再送

catchで捕まえた例外を、再度送出する際のthrowsの付与とcatch内の書き方についての話です。

JDK 6以前だと、例外を一気にExceptionやThrowableで受けてしまうと、それを再送する場合はそのメソッドにthrows Exceptionやthrows Throwableが必要でした。

これが、JDK 7だとキャッチした例外変数を再代入しない限りは、元々捕まえる予定の例外以上のものをthrowsに付与しなくてもよくなったみたいです。以下、サンプル。

import java.io.IOException;

public class RethrowTest {
    public static void main(String[] args) {
        try {
            execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void execute() throws IOException {
        try {
            throwIOException();
        } catch (Exception e) {
            throw e;
        //} catch (Throwable th) {  // <- こちらでもOK
        //    throw th;
        }
    }

    public static void throwIOException() throws IOException {
        throw new IOException("io error");
    }
}

注目ポイントは、ここですね。

    public static void execute() throws IOException {
        try {
            throwIOException();
        } catch (Exception e) {
            throw e;
        //} catch (Throwable th) {  // <- こちらでもOK
        //    throw th;
        }
    }

Exceptionで捕まえてスローしているのに、このメソッドのthrows節はIOExceptionでもよしとなっています。
*コメントでも書いているように、Throwableで捕まえて再スローしてもOKです

ただ、これを

    public static void execute() throws IOException {
        try {
            throwIOException();
        } catch (Exception e) {
            e = new RuntimeException(); // <- eに代入
            throw e;
        }
    }

みたいなことをすると、コンパイルエラーになります。

スタイルとしては、final指定で書くのがよいらしい?

    public static void execute() throws IOException {
        try {
            throwIOException();
        } catch (final Exception e) {
            throw e;
        //} catch (final Throwable th) {  // <- こちらでもOK
        //    throw th;
        }
    }

「再送するので、throws宣言をしなくてもよい」ではないので、注意です。もちろん、RuntimeExceptionのサブクラスのみが送出されるということであれば(チェック例外を使うコードがない)、再送する場合のthrowsは不要ですが。

ネット上でさらさらと見ていただけで、なんとなくわかった気になっていましたが、実際に触るといろいろ気になるところが出てきますね。ま、そんなもんですよね。

参考)
http://itpro.nikkeibp.co.jp/article/COLUMN/20120417/391316/
http://yoshio3.com/2011/07/29/java-se-7-officially-released/
http://www.ne.jp/asahi/hishidama/home/tech/java/uptodate.html