読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

JDK7正式版の新機能一覧(Java言語仕様に関して)

Java

2011-08-02追記 JDK7に致命的なバグ

重要なので先頭に追記しておきますが、JDK7のホットスポットコンパイラに致命的なバグが存在するそうです。
詳しくはコチラ。
http://d.hatena.ne.jp/takahashikzn/20110802
JDK7を使用する際はご注意下さい。




(Javadocのスタイルもガラっと変わりました!!)


http://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html
JDK7の正式版がリリースされています。


興味深い機能やAPIなどが追加されているわけですが、
その中でもみんな一番気になるところである、Java言語仕様の変更についてご紹介。


あとは、個人的にjava.lang.invokeなんてアヤシゲなパッケージが気になっているのですが…
APIを見る限り、動的言語サポート関連のパッケージっぽいですね、コレ。どなたか解説をお願いします(笑)



では、順次見ていきましょう。

※念のため記載:大半のサンプルコードはリリースノートからの引用(一部、翻訳のために改変)であり、著作権はOracleにあります。

Binary Literals (バイナリリテラル)


これまでは、バイナリ値のリテラルを使おうものなら

byte b = 0x42;

などと書く必要があったわけです。でもこれだと、ビットパターンをイメージしづらい。
(脳内に『16進数→2進数』変換ハードウェア回路を焼成済みの方を除く)


というわけで、JDK7からはこんな風に書くことが出来るようになりました。

byte b = 0b01000010;


byteだけじゃなくて、他の型のバイナリ表現もあります。

// An 8-bit 'byte' value:
byte aByte = (byte)0b00100001;

// A 16-bit 'short' value:
short aShort = (short)0b1010000101000101;

// Some 32-bit 'int' values:
int anInt1 = 0b10100001010001011010000101000101;
int anInt2 = 0b101;
int anInt3 = 0B101; // The B can be upper or lower case.

// A 64-bit 'long' value. Note the "L" suffix:
long aLong = 0b1010000101000101101000010100010110100001010001011010000101000101L;


もちろん、配列でも使えます。

public static final int[] phases = {
  0b00110001,
  0b01100010,
  0b11000100,
  0b10001001,
  0b00010011,
  0b00100110,
  0b01001100,
  0b10011000
};

整数型の定数を使える場所ならどこでも使えます。(例えばcaseラベル)

public State decodeInstruction(int instruction, State state) {
  if ((instruction & 0b11100000) == 0b00000000) {
    final int register = instruction & 0b00001111;
    switch (instruction & 0b11110000) {
      case 0b00000000: return state.nop();
      case 0b00010000: return state.copyAccumTo(register);
      case 0b00100000: return state.addToAccum(register);
      case 0b00110000: return state.subFromAccum(register);
      case 0b01000000: return state.multiplyAccumBy(register);
      case 0b01010000: return state.divideAccumBy(register);
      case 0b01100000: return state.setAccumFrom(register);
      case 0b01110000: return state.returnFromCall();
      default: throw new IllegalArgumentException();
    }
  } else {
    final int address = instruction & 0b00011111;
    switch (instruction & 0b11100000) {
      case 0b00100000: return state.jumpTo(address);
      case 0b01000000: return state.jumpIfAccumZeroTo(address);
      case 0b01000000: return state.jumpIfAccumNonzeroTo(address);
      case 0b01100000: return state.setAccumFromMemory(address);
      case 0b10100000: return state.writeAccumToMemory(address);
      case 0b11000000: return state.callTo(address);
      default: throw new IllegalArgumentException();
    }
  }
}

Underscores in Numeric Literals (アンダースコア入り数値リテラル)


数値リテラルに、可読性のためにアンダースコアを入れられるようになりました。

int i = 1_2_3;

は、

int i = 123;

と同じ意味です。


ただし、文法上そもそもアンダースコアを入れると別の意味になることがあるので、
紛らわしいケースでは注意が必要です。


例えば、

int i = _123;

int i = 123;

とは違います。このとき、_123はただの変数名として扱われるため

int _123 = 999;
int i = _123;
// i == 999

という意味として解釈されます(これまでどおりの仕様)。

Strings in switch Statements (switch文のcaseラベルに文字列を使える)


caseラベルに文字列リテラルを使えるようになりました。

public String getTypeOfDayWithSwitchStatement(String dayOfWeekArg) {
     String typeOfDay;
     switch (dayOfWeekArg) {
         case "Monday":
             typeOfDay = "Start of work week";
             break;
         case "Tuesday":
         case "Wednesday":
         case "Thursday":
             typeOfDay = "Midweek";
             break;
         case "Friday":
             typeOfDay = "End of work week";
             break;
         case "Saturday":
         case "Sunday":
             typeOfDay = "Weekend";
             break;
         default:
             throw new IllegalArgumentException("Invalid day of the week: " + dayOfWeekArg);
     }
     return typeOfDay;
}

ラベルのマッチングはString#equalsで行われるため、文字列はケース依存です。


で、これの使いどころですが、

if("Monday".equals(dayOfWeekArg))
    typeOfDay = "Start of work week";
else if ("Tuesday".equals(dayOfWeekArg))
    typeOfday = "Wednesday";
else if...

と書くよりは、多少なりとも効率的なコードへとコンパイルしてくれるそうです。

Type Inference for Generic Instance Creation (コンストラクタ呼び出し時の型推論)


待望の型推論が実装されました。と言ってもコンストラクタ呼び出しの時だけですが。


例えば、こんなコードを

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

こんな風に書くことが出来るようになりました。コレ、超ラクチン。

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


ちなみに、コレはダメです。単にrawタイプ警告が出ます。(従来通りの仕様)

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


万能ではないので、このようなケースはコンパイルエラーです。

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

// 引数をCollection<? extends String>と解釈してくれないのでコンパイルエラー
list.addAll(new ArrayList<>()); 

Improved Compiler Warnings and Errors When Using Non-Reifiable Formal Parameters with Varargs Methods (型パラメータの可変長引数におけるコンパイラ警告の改善)


型パラメータの可変長引数を使っているところで、コンパイラ警告の発生具合が変わりました。

ヒープ汚染とは

例えば、このようなコードを書くことが可能です。

List l = new ArrayList<Number>();
List<String> ls = l;       // 未チェック警告(その1)
l.add(0, new Integer(42)); // 未チェック警告(その2)
String s = ls.get(0);      // ClassCastExceptionが発生

これは何をしているかというと、要するにこういう事です。

List l = new ArrayList();
List ls = l;
l.add(0, new Integer(42));
String s = (String) ls.get(0); // ClassCastExceptionが発生

これをヒープ汚染と呼びます。ジェネリクスを使って型の安全性を
保証しているように見せかけて、爆弾を仕込むことが出来るということです。

可変長引数と型パラメータ

以下のようなコードを考えてみます。
ジェネリクスの配列にヒープ汚染を仕込んでいる例です。

import java.util.*;

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l; // ここまでは正しい
    objectArray[0] = Arrays.asList(new Integer(42)); // ヒープ汚染させる
    String s = l[0].get(0);    // ClassCastExceptionが発生
  }

}
import java.util.*;

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListA, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists = new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}


JDK7からは、コンパイラがArrayBuilder.addToListで以下のような警告を出すようになりました。

warning: [varargs] Possible heap pollution from parameterized vararg type T
警告: [可変長引数] 型パラメータTのヒープ汚染が発生する可能性があります。

(訳注: 実際にJDK7のコンパイラでこのコードをコンパイルしてみたわけではないので、コンパイラ警告の和訳は異なるかも)


で、この警告を出さないようにするためには、以下のいずれかの手段を使うことになります。

  • 新しく追加された@SafeVarargsを使う
    • 但しstaticメソッドでしか使えません
  • @SuppressWarnings({"unchecked", "varargs"})を使う
    • 但しメソッド呼び出し側の警告を消すことは出来ません
  • コンパイラのオプション"-Xlint:varargs."を使う


以下のコードをコンパイルした時、


public class ArrayBuilder {

public static void addToList (List listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}

@SuppressWarnings({"unchecked", "varargs"})
public static void addToList2 (List listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}

@SafeVarargs
public static void addToList3 (List listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}

// ...

}
public class HeapPollutionExample {

// ...

public static void main(String[] args) {

// ...

ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);
ArrayBuilder.addToList2(listOfStringLists, stringListA, stringListB);
ArrayBuilder.addToList3(listOfStringLists, stringListA, stringListB);

// ...

}
}


コンパイル結果は以下のとおりです。

  • addToList
    • メソッド宣言で『[unchecked] Possible heap pollution from parameterized vararg type T』
    • メソッド呼び出しで『[unchecked] unchecked generic array creation for varargs parameter of type List[]』
  • addToList2
    • メソッド呼び出しで『[unchecked] unchecked generic array creation for varargs parameter of type List[]』
  • addToList3
    • (一切警告を出しません)

The try-with-resources Statement (リソース管理用構文の追加)

必ずクローズしなければならないリソースを使う場合、try-finally構文を使うのがお約束だったわけですが、
ちょっと便利な構文が追加されました。

static String readFirstLineFromFile(String path) throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
  }
}


複数のリソースを使う場合はこんな感じ。宣言とは逆順にcloseが呼ばれます。

try (
  java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFileName);
  java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {

  // Enumerate each entry

  for (java.util.Enumeration entries = zf.entries(); entries.hasMoreElements();) {

    // Get the entry name and write it to the output file

    String newLine = System.getProperty("line.separator");
    String zipEntryName = ((java.util.zip.ZipEntry)entries.nextElement()).getName() + newLine;
    writer.write(zipEntryName, 0, zipEntryName.length());
  }
}


この構文で指定するリソースは、java.lang.AutoCloseableインタフェースを実装する必要があります。
これは、java.io.Closeableのスーパーインタフェースです。
だからCloseableを実装しているクラスは、標準でこの構文を使用可能です。


Catching Multiple Exception Types and Rethrowing Exceptions with Improved Type Checking (複数のcatch節、例外の再スローについての改善)


複数のcatch節を書くようなケースが改善されました。

例えば、このようなコードが

try {
    ...
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

こんな風に書けるようになります。

try {
    ...
} catch (IOException|SQLException ex) {
     logger.log(ex);
     throw ex;
}

ほとんどのケースでは、例外をキャッチしても出来ることは限られているわけで、
結果としてcatch節の中身は同じようなコードになりがちです。
だからこの構文があると、冗長なコードを減らすことが出来るわけ。
ついでに、コンパイラも専用のコードを生成してくれるようで、catch節を並べて書くよりも処理が効率的になるそうです。

例外再スロー時の型推論

JDK7から、例外再スロー時に型推論が働くようになりました。

// FirstException, SecondException共にチェック例外
public void rethrowException(String exceptionName)
  throws FirstException, SecondException {
    try {
      // FirstExceptionかSecondExceptionだけが発生しうる何らかのコード
    }
    catch (Exception e) {
      throw e;
    }
  }

このコード、JDK6までは当然コンパイルエラーですが、JDK7からはコンパイル可能。
要するに、try節とそれに対応するcatch節の中身を見て、throws宣言として適切な型を判断してくれるということです。


以下のケースでこの型推論が機能します。

  • try節が投げる可能性のある例外は、thorws節で宣言している例外だけである
  • 型推論を適用したいcatch節の前に、先に例外をキャッチしてしまうcatch節が無い
  • throws節で宣言する例外は、catch節で宣言する例外のサブクラスである


へぇ、何だか面白い機能ではありますが。。。
そもそもtry-catchを使わなきゃいいんじゃね?という気がしないでもないです。
逆に混乱を招いたりしないか、コレ?