Java SE 7で新規追加されていた機能を振り返る
今更Java7の新機能をおさらいしながら振り返ってみる。
よく使うものもあまり使わないものも整理。改めて見ると全然使わんものもあるなという印象。
新機能の元ネタ
Oracle公式から引っ張ってみた。
プログラミング方式の変更の話
Java Programming Language Enhancements
New IOの話
Enhancements in Java I/O
必須で覚えておきたい機能
try-with-resources
try-with-resourcesって何?
ファイル扱うときなんかに「try-catchしてfinallyでcloseするのが当然だよね」を新しくしたもの。
専用のtry-catch記法を利用することで、finally書かなくても勝手にcloseが呼び出されるようになる。
java.io.Closeable
の実装であることが、この記法が利用できることの条件となっている。
この記法が出たからか、Commons HTTPあたりでfinallyでのリソース開放をするようなクラスにCloseable実装を追加したなんてこともあった。
使う場合のメリット
- 自分でcloseコード書く必要がなくて短くなる
- finallyでcloseするためにtry-catchの外側にリソースの変数スコープを置く必要がない
- finallyのリソース毎に行う冗長なnullチェックをやる必要もなくなる
- 後に生成したリソースからクローズする原則を勝手に守ってくれる
- close中に発生した例外も握り潰さず、Throwable#suppressedExceptions(Java7で新規追加)に保持してくれる
try-with-resources使わないコード例
比較用に、まずは新機能使わない場合のリソースクローズを含むコード例。*1
実際のコードでエラーハンドリングをe.printStackTrace()
で終わらせることはないと思うけど一旦これで簡略化。
package com.sample.io; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class ClosableTest { public static void main(String[] args) { // ファイルのデータをコピーする例 FileInputStream src = null; FileOutputStream dst = null; try { src = new FileInputStream("/var/log/messages"); dst = new FileOutputStream("/var/log/messages_copy"); // データをコピーする int c; while ((c = src.read()) != -1) { dst.write(c); } } catch (IOException e) { e.printStackTrace(); } finally { if (dst != null) { try { dst.close(); } catch (IOException e) { e.printStackTrace(); } } if (src != null) { try { src.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
try-with-resources使ったコード例
前述のコードを置き換えるとこんな形になる。
1つのtryの中でセミコロンで区切れば複数リソース宣言ができるところがミソだと思う。
package com.sample.io; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class ClosableTest { public static void main(String[] args) { // ファイルのデータをコピーする例 try (FileInputStream src = new FileInputStream("/var/log/messages"); FileOutputStream dst = new FileOutputStream("/var/log/messages_copy")) { // データをコピーする int c; while ((c = src.read()) != -1) { dst.write(c); } } catch (IOException e) { e.printStackTrace(); } } }
例外発生時に何がthrowされるのか
古い書き方におけるfinallyに相当する処理が隠蔽されているので、自動closeで例外発生した場合など挙動が読み取りにくいケースも存在する。
下記のコードを書いた場合の処理順や例外ハンドリングを検証してみた。
package com.sample.io; import java.io.Closeable; import java.io.IOException; public class ClosableTest2 { public static void main(String[] args) { try (Resource1 r1 = new Resource1(); Resource2 r2 = new Resource2()) { // テスト r1.IO例外するかもしれない処理(); } catch (IOException e) { e.printStackTrace(); } } }
この場合、正常処理順序は下記の通りになる。
- 処理1:Resource1の生成
- 処理2:Resource2の生成
- 処理3:Resource1のIO例外するかもしれない処理
- 処理4:Resource2のクローズ
- 処理5:Resource1のクローズ
検証は各ステップでIOException
/RuntimeException
がthrowされる可能性があると仮定。
各所で問題が発生した場合の動作を確認してみた。
処理1 (r1生成) |
処理2 (r2生成) |
処理3 (try内処理) |
処理4 (r2.close) |
処理5 (r1.close) |
結果 |
---|---|---|---|---|---|
IOException | - | - | - | - | IOExceptionがcatchされる。closeは実行されない。 |
成功 | IOException | - | - | 成功 | IOExceptionがcatchされる。リソース生成済のResource1はクローズされる。 |
成功 | IOException | - | - | IOException | IOException(処理2)がcatchされる。処理5の例外はSuppressedに入る。 |
成功 | RuntimeException | - | - | IOException | RuntimeException(処理2)がthrowされる。処理5の例外はSuppressedに入る。 |
成功 | 成功 | IOException | IOException | IOException | IOException(処理3)がcatchされる。処理4,5の例外がSuppressedに入る。close中に例外が発生しても必要なcloseは全て試みるようだ。 |
成功 | 成功 | RuntimeException | IOException | IOException | RuntimeException(処理3)がthrowされる。処理4,5の例外がSuppressedに入る。 |
成功 | 成功 | 成功 | IOException | IOException | IOException(処理4)がcatchされる。処理5の例外がSuppressedに入る。 |
成功 | 成功 | 成功 | RuntimeException | IOException | RuntimeException処理4)がthrowされる。処理5の例外がSuppressedに入る。 |
検証結果も踏まえて、処理と例外ハンドリングの仕様を細かく書いてみると下記のようになる。改めて見るとさすがに賢い。
- リソースの生成有無は勝手に判定し、生成済インスタンスのみclose処理が呼び出される。
- リソース生成-tryブロック中の処理-AutoCloseの中で最初に発生した例外がthrowされ、catch句の対象になる。
- 既に例外が発生した後にAutoClose中にさらに例外が発生した場合、最初に発生した例外のSuppressedに後発の例外が追加されていく。
- AutoClose中に例外が発生しても処理は継続し、closeが必要なリソースは全てclose処理を試みる。
ちなみに上記の検証パターンではAutoCloseでIOExceptionしか発生させていないが、その他例外が発生した場合も同じようにSuppressedへ追加される挙動となる。
ジェネリクスの省略(ダイヤモンド演算子)
ジェネリクスの指定が変数宣言などから明示的な場合に、ジェネリクス指定を省略することができるようになっている。
メリットは小さいものの、使わない理由が無いので使える場所では必須で使ってる。
使う場合のメリット
- わずかだが書く量が減る。
- 同じクラスの打鍵を繰り返さなくて良いので煩わしさダウン。
ジェネリクス省略を使ったコード例
public class GenericsTest { public static void main(String[] args) { // 古い書き方 List<String> oldList = new ArrayList<String>(); Map<String, Object> oldMap = new HashMap<String, Object>(); // 新しい書き方 List<String> newList = new ArrayList<>(); Map<String, Object> newMap = new HashMap<>(); } }
例外のマルチキャッチ
複数の例外を同じcatch句に複数定義することができる。
これまで複数例外種別のハンドリングを同じ挙動にしたかった場合、個別にcatch句を個別に書くか、大きくcatch(Exception e)
などと書いてしまうかの2択だった点が大きく改善された。
ぶっちゃけ5個も10個も検査例外をthrowするようなクソAPIでも、使う側としては大半が同じハンドリングに行き着くことが多いので、そういう場合は使ったほうが良い。
使う場合のメリット
- 個別に書く場合と比較して、いちいち同じ動作のcatch句コードを書かなくていい。
- 大きくExceptionなどでcatchした場合と比較して、想定外の例外までcatchされることが防げる。
マルチキャッチ使わないコード例
/** * マルチキャッチを使わない例 * @throws IllegalStateException どの例外が発生してもこの例外にラップしてthrowする */ public void notUseMultiCatch() throws IllegalStateException { try { this.複数例外をthrowする処理(); } catch (ExceptionA e) { // 同じ処理を3か所に書く必要がある throw new IllegalStateException("XXX処理で例外が発生しました", e); } catch (ExceptionB e) { throw new IllegalStateException("XXX処理で例外が発生しました", e); } catch (ExceptionC e) { throw new IllegalStateException("XXX処理で例外が発生しました", e); } }
マルチキャッチ使ったコード例
/** * マルチキャッチを使った例 * @throws IllegalStateException どの例外が発生してもこの例外にラップしてthrowする */ public void useMultiCatch() { try { this.複数例外をthrowする処理(); } catch (ExceptionA | ExceptionB | ExceptionC e) { // 同じハンドリングなら1つのcatch句で書ける throw new IllegalStateException("XXX処理で例外が発生しました", e); } }
使うときには使う機能
New IO(java.nio.file
あたりのお話) ※ 別途調査
各クラスの使い方含めて時間があったら別途詳細調査して書く。(優先度下がるかも)
印象としてはApache CommonsのFileUtilsやIOUtilsが便利なんであまりこっちに手を出してなかった節がある。
ざっとAPIリファレンス見た限りは大きく分けて下記の2方向でAPIが用意されている印象。
switchでStringが利用可能になった
switchの分岐条件にStringを利用することができるようになった。コード例は公式が出しているので参照されたし。
Strings in switch Statements
個人的にはこのレベルになると文字列で分岐させるよりもenum切るほうが好きだなー
(enumだとIDEで補完も効くようになるのが嬉しい)
java.util.Objects
Commonsのようなnullセーフのequalsなんかが実装されているユーティリティ。
それほど機能が多いわけじゃないけど、このクラスで完結できる内容ならこっちを使っておきたい。
あまり使ってるの見たこと無い機能
バイナリ定数
"0b"を先頭につけた0/1表現でバイナリリテラルを書くことができるようになった。
…といってもバイナリでリテラル書くことが有効な場面には出会ったことがない。
一応コード例を置いておくが、実際の使い所はこういうケースではない。
public static void main(String[] args) throws UnsupportedEncodingException { // バイナリ(2進数)で4を表現 int binaryFour = 0b0000000000000100; System.out.println(binaryFour); // "4"が出力 // ASCIIのバイナリ表現で文字列「HOGE」を表現 String binaryHOGE = new String(new byte[] { 0b01001000, // H 0b01001111, // O 0b01000111, // G 0b01000101, // E }, "UTF-8"); System.out.println(binaryHOGE); // "HOGE"が出力 }
公式曰く、マイクロプロセッサのシミュレータや、ビットマップを定数で見やすく定義するときなど、ガチガチにバイナリを意識するときに使えば嬉しいよという意図のようである。
Binary Literals
数値リテラルにアンダースコアが許可された
数値リテラルをアンダースコアで区切ることができるようになった。
int a = 123_456;
のような書き方ができるようになっている。この例だと変数の値は123456になる。
金額表記の","のように3桁区切りをするために使うのだろうか。アンダースコアで区切ったほうが本当に見やすくなるのかは疑問符。
非具象化可能な可変長引数の警告ポリシー変更と@SafeVarargsなどの抑止方法
単語だけ見るとなんのこっちゃとなるが、ジェネリクスと可変長引数を組み合わせた場合にトリッキーなコードを書くとジェネリクスの型安全をぶっ壊すことができるので警告が出る、という仕様が関連する。
このメソッドに対する警告の出し方の変更、及び@SafeVarargs
などの警告抑止方法が追加された。
型安全ぶっ壊しコードの例とアノテーションの使い方は他の方の記事に解説を任せてしまう。
可変長引数を持つメソッドに付与する、@SafeVarargsアノテーション - CLOVER🍀
実装ポリシーとして、Java7より前は「処理を呼び出す側が型安全が破壊される可能性がないか逐次チェックしろ」だったのがJava7以降「実装側が型安全を保証したプログラミングをする責任を持て」に変わったため、作った側が型安全であることを宣言する手法が取り入れられたと見受けられる。
このあたりのポリシー変更については公式のドキュメントに書いてある。(ちょっとわかりにくかったので日本語版で見た)
非具象化可能仮パラメータを可変長引数メソッドに使用する場合のコンパイラの警告の改善
注:Java SE 5 および 6 では、非具象化可能可変長引数仮パラメータを持つ可変長引数メソッドの呼び出しによってヒープ汚染が発生するかどうかは、その呼び出しを行うプログラマが判断する必要があります。しかし、このプログラマがそのメソッドの作成者でない場合は、これを簡単に判断することはできません。Java SE 7 では、このような可変長引数メソッドで可変長引数仮パラメータが適切に処理されること、およびヒープ汚染が発生しないことを保証するのは、メソッドを作成するプログラマの責任です。
なお、@SafeVarargs
はstaticメソッドかfinalなインスタンスメソッドに対して利用可能になっている。final付けないとダメなのはオーバライド後に保証が崩れるから限定したのかな。
*1:以降、Streamコピー処理の部分は1byteずつ処理するなどというひどい形なので真似しないように。あくまでclose例を示すためだけのコードです。