Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2011-07-29

Java7 ジェネリクスのダイアモンド・オペレータ

| 23:33 |  Java7 ジェネリクスのダイアモンド・オペレータを含むブックマーク

Java 7がリリースされました。もう随分前から7の話題はあったものの、ずっと延期されてたんで出た事自体が不思議な気がしますが、皆様いかがお過ごしでしょうか。Java 6が2006年12月ですからほぼ5年も経ってしまったのですね。

Java 7のリリースノートは

さて、本稿のターゲットはジェネリクスのダイアモンド・オペレータです。

これは以下のようにジェネリック変数にnewでインスタンスを生成して代入するというケースでの冗長表記を簡素化出来る機能です。

// Java6までの書き方
List<String> list = new ArrayList<String>();
// Java7でのダイアモンド・オペレータ
List<String> list = new ArrayList<>();

まぁStringぐらいだと可愛いものですが

List<List<Map<String, WeakReference<B>>>> list = new ArrayList<>();

みたいなケースでは実にシンプルで嬉しい。いや、コレクションをここまで多階層にするぐらいならclassを作れという話ではありますが。

ダイアモンド・オペレータを使用できる箇所

ダイアモンド・オペレータはnew演算子と対になるわけですが、これがnewできる箇所ならどこでも使えるという訳ではありません。

実用的な話を言えば、変数への代入でのみ利用可能と覚えておけばよいでしょう。

例えば以下のケースです。

public void hoge() {
  // 変数宣言の初期値として
  List<String> list = new ArrayList<>();
  // 単純な代入として
  list = new ArrayList<>();
  // 境界を持つ変数への代入
  List<? extends String> list2 = new ArrayList<>();
  List<? super String> list3 = new ArrayList<>();
  List<? extends List<? extends String>> list4 = new ArrayList<>();
  // 連鎖的な変数の初期値の例
  List<String> list4, list5 = list4 = new ArrayList<>();
}
// 型変数を使用したジェネリックな変数型
public <T> void hoge2() {
  List<T> list = new ArrayList<>();
}
// ジェネリックな配列の要素への代入
public void hoge3(List<String> ... listArray) {
    listArray[0] = new ArrayList<>();
}
// return値
public List<String> hoge4() {
  return new ArrayList<>();
}

これに対し、よくある使用できない例は以下のケースです。

public static void hoge(List<String> list){}
public static void main(String[] args) {
  hoge(new ArrayList<>()); // NG
}
boolean b = true;
List<String> l2 = b ? new ArrayList<>() : new LinkedList<>(); // NG

returnは例外的ですが、およそ変数への直接的な代入に限り利用出来ると覚えておけば大丈夫でしょう。実用的には。

境界をもつ変数へ代入した場合

ところで、さっきしれっと書いてますが

  // 境界を持つ変数への代入
  List<? extends String> list2 = new ArrayList<>();
  List<? super String> list3 = new ArrayList<>();
  List<? extends List<? extends String>> list4 = new ArrayList<>();

これ、不思議だと思いませんか。

何が不思議か分からない?では、以下をコンパイルしてみましょう。

  List<? extends String> list2 = new ArrayList<? extends String>();
  List<? super String> list3 = new ArrayList<? super String>();
  List<? extends List<? extends String>> list4 = new ArrayList<? extends List<? extends String>>();

コンパイルエラーになりましたね?

new演算子での型のバインドではワイルドカードは使えないんです。詳しくは Javaジェネリクス再入門 - プログラマーの脳みそあたりを見てもらうとして。

これ、どうなってるかというと、Javaジェネリクスはイレイジャ方式なのでなんか適当にnew ArrayList()しちゃってるようです。list2とかの変数に代入されてしまえば、あとは型の互換チェックはできてしまいますからね。

普段、僕らはlist4の宣言型 List<? extends List<? extends String>>に対してnewするときの型はどうすればいいの?って悩んだりするわけですが、コンパイラの実装側はそんな推論は実は全くする必要がなかったという話。

ちなみにダイアモンドを使わずにlist4のコンパイルを通したい場合はlist4 = new ArrayList<List<? extends String>>();とかですね。

2011.8.1修正 : Java 7以前もメソッドスコープの型変数の場合、型推論が行われていたのですが、その型推論が利用されているようです。詳細は別途エントリにまとめます。この件に関するtwitter上の議論はこちらのまとめを参考 Java7 のダイアモンドオペレータと型推論について。 - Togetterまとめ

型推論に失敗する例

このダイアモンド、推論可能な場所、つまり前述の変数への代入というシチュエーションではかなり強固なのですが、ひとつ失敗する例を見つけました。

public class Sample<T> {
    class X {}
    class Y<Z extends X> {}
    void hoge() {
        Y<X> y = new Y<>(); // OK
    }
    static void hoge2() {
        Sample<String> s = new Sample<>();
        Sample<String>.Y<Sample<String>.X> y1 = s.new Y<>(); // NG
        Sample<String>.Y<Sample<String>.X> y2 = s.new Y<Sample<String>.X>(); // OK
    }
}

ジェネリックな外部クラスの内部クラスを用いた場合で、外部からinstance.new で初期化するケースです。

コメントでNGとしている行は、その下の行のように推論されることを期待するわけですが

エラー: Sample.Y<>の型引数を推定できません。

Sample<String>.Y<Sample<String>.X> y1 = s.new Y<>(); // NG

理由: 型変数Zのインスタンスが存在しないので、Sample<String>.Y<Z>はSample<String>.Y<Sample<String>.X>に適合しません

Z,Tが型変数の場合:

クラス Sample.Yで宣言されているZ extends Sample<T>.X

クラス Sampleで宣言されているT extends Object

エラー1個

といったエラーが出ます。コンパイル環境はOracle純正JDKでバージョン1.7.0です。ちなみにNetBeansIDE上ではこれはコンパイルエラーになりません。コンパイラバグの疑いが強いですね。

リリースノートの解説

の下の方のサンプル、分かりにくいと思うので解説しておきます。

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

というクラスがあった場合の話ですが、このMyClassのコンストラクタには<T>というコンストラクタスコープの型変数を持っています。

このコンストラクタスコープの型変数は、メソッドスコープの型変数の場合と同等に型推論が効きます。これはJava7以前からある機能です。半端感はありますけども。

んで、このコンストラクタスコープの型変数を省略してTを型推論させた場合が

MyClass<Integer> myObject = new MyClass<>("");

であって、正書法で省略せずに書いた場合が

MyClass<Integer> myObject = new <String> MyClass<>("");

というわけです。コンストラクタスコープの型変数型推論は今までも出来たけど、ダイアモンドだとXの方を型推論できるようになったよ!という話。

ところで、この下のコードなんですが、コンパイルしようとすると

エラー: MyClassの型引数を推定できません。

MyClass<Integer> myObject = new <String> MyClass<>("");

理由: コンストラクタの明示的な型パラメータでは'<>'を使用できません

と怒られます。上のコードだと通るのに…。微妙なオチがついてしまいました。

実用的な話

お待たせしました。スーパー変態タイムです。いや、あまりスーパーじゃないですけども。

実は変数への代入じゃなくてもダイアモンド・オペレータをコンパイル通すことはできるんですよね。

// newするだけ
new ArrayList<>();
// newしたインスタンスを直接使う
new ArrayList<>().add("hoge");
// newしたインスタンスを式中で使う
if (new ArrayList<>() == null) {}
// instanceof演算子に直接あてる
if (new ArrayList<>() instanceof List){}
// Object型への代入
Object o = new ArrayList<>();
// raw型への代入
List l = new ArrayList<>();
// Object型の配列の初期化子
Object[] oarray = {new ArrayList<>()};
// raw型の配列の初期化子
List[] larray = {new ArrayList<>()};
// 3項演算子 Object型の場合
boolean b = true;
Object o = b ? new ArrayList<>() : new ArrayList<>();
// 3項演算子 raw型の場合
List l2 = b ? new ArrayList<>() : new ArrayList<>();

ただまあ、ごらんの通り実用性はないので忘れていいです。「ダイアモンドオペレータは代入でのnewで使える」というシンプルなルールで覚えておくほうが混乱しなくていい。

上記の例は非ジェネリック変数に代入するケースです。この他、メソッド引数に渡す場合でも非ジェネリックであれば利用可能です。実用上、あまり意味はないですが。

public void hoge0(Object o) {}
public void hoge1(List l) {}
public void hoge2(List<String> l) {}

public void hoge() {
    hoge0(new ArrayList<>()); // OK
    hoge1(new ArrayList<>()); // OK
    hoge2(new ArrayList<>()); // NG
}

それでは皆さん、楽しいJava7ライフを!