2010/12/18

Project Coin

このエントリーをはてなブックマークに追加

このエントリは Java Advent Calendar -ja 2010 の一環です。

さて、何を書こうかいろいろ悩んでいたのですが、やっぱり櫻庭といえば Java SE の新しめのところというイメージがあると思うので、新しいところを取りあげようと思います。

で、取りあげるのが Project Coin です。

Project Coin は Java 言語仕様の小さな変更を行なうためのプロジェクトです。昔の言い方であれば Ease of Development に相当して、もっと簡単に書けるようにすることが目的です。

この Project Coin は来年リリース予定の Java SE 7 で導入されます。ところが、Project Coin のほとんど機能はすでに OpenJDK に実装済みです。

ということで、すでに使える Project Coin を使ってみてみましょう。ここでは JDK 7 b121 を使用しています。

ちなみに安全な可変長引数は b123 で導入予定なので、ここでは取りあげません。

数値リテラル

数値リテラルに関して以下の 2 つの機能が追加されます。

  • 数値の区切り文字として _ (アンダースコア)
  • 2 進数リテラル

数字がずらずらと並ぶと見にくいので、_ で区切りましょうというのが 1 番目の機能です。2 番目は 2 進数をリテラルで表すために 0b を頭につけて書けるようにしましょうというものです。

実際に書いてみましょう。

public class Test {
    public static void main(String[] args) {
        int num = 1_000;
        int num2 = 0b1000_0000;
    }
}

何の意味もないプログラムですが、気にしない気にしない。

さて、これをコンパイルします。Java SE 6 だとコンパイルエラーが出ますが、7 だとすんなり通ります。

さて、これだけではつまらないので、これを javap でバイトコードを出力させてみましょう。javap は -c オプションでバイトコードを出力します。

Compiled from "Test.java"
public class Test extends java.lang.Object {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: sipush        1000
       3: istore_1
       4: sipush        128
       7: istore_2
       8: return
}

注目すべきは赤文字のところ。アンダースコアをつかったリテラルは、アンダースコアを抜いた数値になってます。

0b で宣言した 1000_000 は、10 進数で 128 と表されています。

つまり、コンパイラレベルだけで、バイトコードは従来と何も変わっていないことが分かります。

とはいえ、クラスファイルのバージョンが違うので、Java SE 6 では実行できません。でも、バイナリエディタでクラスバージョンの部分を書き換えれば実行できます。

String Switch

さて、次です。

switch 文で文字列をつかうことができるようになりました。

public class Test {
    public static void main(String[] args) {
        switch (args[0]) {
        case "m":
            System.out.println("Good Morning, World!");
            break;
        case "n":
            System.out.println("Good Night, World!");
            break;
        default:
            System.out.println("Hello, World!");
            break;
        }
    }
}

簡単ですね。

これもどうやっって実行されているか見てみましょう。javap でもいいのですが、ちょっと長くなってしまうので JD を使って逆コンパイルしてみました。

public class Test {
    public static void main(String[] paramArrayOfString) {
        String str = paramArrayOfString[0]; 
        int i = -1; 

        switch (str.hashCode()) {
        case 109:
            if (str.equals("m")) i = 0;
            break;
        case 110:
            if (str.equals("n")) i = 1; 
        }
        
        switch (i) {
        case 0:
            System.out.println("Good Morning, World!");
            break;
        case 1:
            System.out.println("Good Night, World!");
            break;
        default:
            System.out.println("Hello, World!");
        }
    }
}

櫻庭の予想では、switch 文が if 文で書き換えられるのだと思っていました。しかし、違いました。

なぜか、switch 文が 2 回になっています。

はじめの switch 文は文字列のハッシュコードで分岐をしています。109 が "m" のハッシュコード、110 が "n" のハッシュコードです。

その結果で変数 i に値を設定しています。そして、2 回目の switch で i による分岐を行なっています。

なぜ、こういう 2 階建てにしているんでしょうね。

ハッシュコードだけでいいような気がするのですが、こうする理由があるんでしょうね。

ちなみにこれはリリース版ではないので、今後変わる可能性もあります。そこら辺はご了承ください。

ダイヤモンド

ダイヤモンドというのはジェネリクスを省略して書くための記法です。

        List<String> list = new ArrayList<>();
        list.add("a");

今までは、ジェネリクスのパラメータを定義とnewで 2 回書かなくてはいけなかったのが、1 回に済みます。

もちろん、定義と new を別々に書くこともできます。

        List<String> list;
  
        list = new ArrayList<>();
        list.add("a");

ジェネリクスはコンパイルしてしまうとパラメータが抜けてしまうので、これがどうなっているのかはよくわかりません。もちろん、クラスファイルをダンプして調べればいいのですが、ちょっとめんどうくさいので ^ ^;;

マルチキャッチ、再スロー

Java SE 6 までは複数の例外をまとめてキャッチすることはできませんでした。例外処理は同じ場合、同じような記述を何度も繰り返さないといけませんでした。

また、キャッチした例外を再びスローすることもできません。それをやる場合、他の例外にくるんでやる必要があります。

しかし、Java SE 7 では次のような書き方ができます。

    public String concat(String a, String b) 
        throws InstantiationException, IllegalAccessException {
 
        try {
            Class cls = StringBuilder.class;
            StringBuilder builder = (StringBuilder)cls.newInstance();

            builder.append(a);
            builder.append(b);

            return builder.toString();
        } catch (final InstantiationException
                     | IllegalAccessException ex) {
            log(ex);
            throw ex;
        }
    }

リフレクションを使う意味は何もないのですが、複数の例外を出したかったので。

複数の例外を一緒にキャッチするには例外のクラスを | で結びます。ようするに OR の意味ですね。final はついていなくてもコンパイル、実行はできるのですが、Project Coin のドキュメントを見るとなぜかみな書いているので。

いつか時間があるときに、ここら辺もちゃんと解明しておきます。

再スローもそのまま書いて大丈夫です。再スローをするときも final を書くみたいです。

では、これでどうういうバイトコードが生成されるかというと、なんと次のコードとまったく同じになります。

    public String concat(String a, String b) 
        throws InstantiationException, IllegalAccessException {
 
        try {
            Class cls = StringBuilder.class;
            StringBuilder builder = (StringBuilder)cls.newInstance();

            builder.append(a);
            builder.append(b);

            return builder.toString();
        } catch (final ClassNotFoundException ex) {
            log(ex);
            throw ex;
        } catch (final InstantiationException ex) {
            log(ex);
            throw ex;
        } catch (final IllegalAccessException ex) {
            log(ex);
            throw ex;
        }
    }

ようするに、マルチキャッチの部分を展開して、同じ例外処理を行なうコードに書き換えているわけです。

Try with Resources

最後に紹介するのが、ストリームなどのクローズを自動的にやってくれる Try with Resources です。

以前は Automatic Resource Management といっていたのですが、Try with Resources に名前が変更になりました。

どう書くかというと、try 文にクローズするオブジェクトの生成処理を記入します。

    private void fileCopy(String src, String dest) {
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dest)) {
 
            byte[] buf = new byte[1024];
            int n;

            while((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }
        } catch (IOException ex) {
            log(ex);
        }
    }

こうすると、クローズ処理を書かなくて済むわけです。いいですねぇ。

これも JD で逆コンパイルしたら、よく分からないコードになってました。

    private void fileCopy(String paramString1, String paramString2) {
        try {
            FileInputStream localFileInputStream
                = new FileInputStream(paramString1);
            Object localObject1 = null;
            
            try { 
                FileOutputStream localFileOutputStream 
                    = new FileOutputStream(paramString2);
                Object localObject2 = null;
                
                try {
                    byte[] arrayOfByte = new byte[1024];
                    
                    while ((i = localFileInputStream.read(arrayOfByte)) >= 0) {
                        int i;
                        localFileOutputStream.write(arrayOfByte, 0, i);
                    }
                } catch (Throwable localThrowable4) {
                } finally {
                    if (localObject2 != null) {
                        try {
                            localFileOutputStream.close();
                        } catch (Throwable localThrowable5) {
                            localObject2.addSuppressed(localThrowable5);
                        }
                    } else { 
                        localFileOutputStream.close(); 
                    }
                }
            } catch (Throwable localThrowable2) {
            } finally {
                if (localObject1 != null) {
                    try { 
                        localFileInputStream.close();
                    } catch (Throwable localThrowable6) {
                        localObject1.addSuppressed(localThrowable6); 
                    } else {
                        localFileInputStream.close(); 
                    }
                }
            }
        } catch (IOException localIOException) {
            log(localIOException);
        }
    }

よく分からないのが、localObject1 と localObject2。null しか代入していないのに、null じゃない場合の処理が記述してあります。

しかも、addSuppressed なんていうメソッドまでコールしているし。このメソッドはなんなんだろう。まぁ、意味はだいたい分かりますが。

まとめ

というわけで、Project Coin の書き方と、そこでどういうバイトコードが生成されるかというところを追ってみました。

基本的にはバイトコードの追加や、クラスファイルの変更をせずに、言語仕様だけを変更していることがわかります。Java は互換性を非常に重んじる言語なので、おいそれとは変更できないのでしょう。

そういう制約があるので、javac コンパイラががんばっているわけです。

まぁ、あまりそういうことを考えずに、「Project Coin でいろいろと簡単に書けるようになったのがうれしい」だけでもいいと思いますけどね。

5 件のコメント:

nminoru さんのコメント...

String Switch を中間変数 i を使わずに1段の switch で書くと、

case "m":
case "n":
System.out.println("m or n");
break;

のような時に、Sysmte.out.println が複数に展開されるからじゃないでしょうか?

Yuichi Sakuraba さんのコメント...

コメントありがとうございます!!
確かにcase が複数ある場合を考えると、これがよさそうですね。そこまで考え付きませんでした。

fukai_yas さんのコメント...

再スローなんですが、単純な再スローなら現状でも普通に出来ますよね?
たぶん今回の変更は、スーパークラスでcatchしても、throwする際にはソース上からわかるサブクラスのみがthrowされる扱いになるという変更じゃないでしょうか。
この記事のサンプルだと、マルチキャッチを単にExceptionのキャッチにしても再スローできる(InstantiationException、IllegalAccessException、RuntimeExceptionしか発生しないのでコンパイラがそれらしかチェックしない)ということかと。(私は今Java7環境無いので試せませんが)
また、final指定は上記のケースで必要で、final付けないと別の例外が代入できてしまうので、その場合は新しい再スローのチェック機能が発動しないということだと思います。

Yuichi Sakuraba さんのコメント...

fukai_yas さん、コメントとありがとうございます。
現状で、再スローは可能なのですが、危険な処理です。最悪デッドロックを引き起こすこともあるので、やらないほうがいいと思います。

また、スーパークラスでキャッチ、サブクラスを再スローという扱いではないようです。
catchやthrowsで列挙した例外はクラスファイルのException Tableに列挙されます。スーパークラスでキャッチするようなコードが生成されていたとしたら、このTableにスーパークラスも列挙されるはずです。
しかし実際にはスーパークラスは列挙されていません。ということはスーパークラスでキャッチしているのではないと思われます。

Project Coinのコードは見ていないので、確証できないのですが... ^ ^;; 今度、時間があるときにでもCoinのコードを見てみます。

匿名 さんのコメント...

例外の再スローに関してですが要するに concat(String,String)の、

} catch (final InstantiationException
| IllegalAccessException ex) {

の部分を

} catch (final Exception ex) {

で置き換えてもコンパイルエラーは出ない、というだけのものではないかと。RuntimeExceptionのサブクラスもキャッチしてログを取ってしまうので挙動は変わってしまいますが。

安全性のために再スローでなく例外連鎖を使うべきという文脈からいうと、final付けて再スローしても安全性は上がらなかったと思います。