Hatena::ブログ(Diary)

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

2017-12-24

Java Generics Hell - new T ()

| 23:52 |  Java Generics Hell - new T()を含むブックマーク

Java Generics Hell アドベントカレンダー 24 日目。

読者の推奨スキルとしては OCJP Silver ぐらいを想定している。

今回は Java のジェネリクスでは型変数を用いて new T () できないけど特に問題ないという話。

コンストラクタの形

コンストラクタは、その内部で this が使えるしインスタンスフィールドへのアクセスも インスタンスメソッド へのアクセスも出来る。そこからするとインスタンススコープのメソッドのように見えるが、インスタンスに所属しているわけではない。


インスタンスメソッドを呼び出すにはインスタンスを指定するか、自分のインスタンスのメソッド呼び出しで this. を省略できる、というシチュエーションでなくてはならない。逆に言えば通常の classのnew はインスタンスなしで呼び出せるわけで、所属としては classのstatic に相当する。クラス名を指定する必要があるが、インスタンスを指定する必要はない、という独特のポジションである。


なので、継承を用いて interface や親クラスによってコンストラクタの引数の形を規定することは出来ない。通常の継承の埒外にいるわけである。コンストラクタはリスコフの置換原則の範囲外で、親クラスのコンストラクタと同様に子クラスのコンストラクタが呼べる必要はない。


こうした前提を踏まえると、型変数のような任意の型に対して、統一的な new をしたいというのは Java の型システムの範疇で考えると無理であるし、無茶な要求と言える。 Java あたりの世代のプログラム言語ではコンストラクタの引数がどのようなものか、指定することが出来ないし、指定したければ継承の範囲を超えた別の プログラミングパラダイム が必要になる。


デフォルトコンストラクタ

さて、そんなわけで汎用にコンストラクタの形状を定めるというのは無理で、どうしてもやりたければ、インスタンスを生成するクラスというものを用意するという迂遠なやり方をする必要がある。迂遠というか面倒くさいわけだが、こうした生成を行うクラスを別途用意する ( ビルダークラスとかファクトリークラスという呼ばれ方をする ) ことで対処できるといえば出来る問題である。いわゆるボイラープレートというやつで、定型句がわさわさ出てきて煩わしいという話はあるにせよ、だ。


しかし、特定のシチュエーションでは決まったコンストラクタの型をしているという前提を置くことが、ある程度妥当性を持つことがある。 デフォルトコンストラクタ である。

インスタンスの生成を行うメソッド

デフォルトコンストラクタとは要するに引数なしのコンストラクタで、単なるデータを保持するようなクラスの場合、概ね デフォルトコンストラクタ を持つという前提を想定してよいケースがある。例えば O/R マッパーのようなケースで、所定の型の デフォルトコンストラクタ でインスタンスを作ってリフレクションでデータを詰めて返したい、そのメソッドがジェネリクスで具象型を指定したい、というわけである。

public class ORMapper {
 public <T> T select(String query) { ... } // これでは生成できない
}

この場合、 Java でやるなら先に挙げたようにビルダークラスを使う必要がある。そして、 Java8 以降を前提とするならば、ビルダークラスはラムダ式ないしメソッド参照でよい。引数に java.util.function. Supplier <T> を用いよう。

public class ORMapper {
 public <T> T select(Supplier<T> builder, String query) {
  T ret = builder.get();
  // (略) 詰め込む処理
  return ret;
 }
}

こうした場合、呼び出し側は

ORMapper mapper = new ORMapper();
Hoge hoge = mapper.select(Hoge::new, "select * from hoge");

といった具合である。今後の Java のリリースで 変数宣言 時の型推論の導入が検討されている。それが導入されると次のように書けるようになるだろう。

ORMapper mapper = new ORMapper();
var hoge = mapper.select(Hoge::new, "select * from hoge");

C # の場合

さて、引き合いによく出される C # では実行時にバインドされた型変数の型が引き回される。そして、 new 制約という デフォルトコンストラクタ を持つことを型変数の制約とする特殊機能を使って ( 型クラスのような汎用機能ではないので過渡期の対処法だと思っておいた方が良いと思う ) 型変数での new を実現している。

https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/new-constraint

class ItemFactory<T> where T : new()
{
    public T GetNewItem()
    {
        return new T();
    }
}

ここでさっきの JavaのORMapper の例を C # にしてみると

public class ORMapper {
 public T Select<T>(String query) where T : new()
 {
  T ret = new T();
  // (略) 詰め込む処理
  return ret;
 }  
}

といった形になるだろう ( 筆者は C # は不案内なので誤りがあればご指摘願いたい )

この時、呼び出し側は

ORMapper mapper = new ORMapper();
var hoge = mapper.select<Hoge>("select * from hoge");

といった形になるだろう。

Java版変数宣言時の型推論あり ) と比較してみよう。

ORMapper mapper = new ORMapper();
var hoge = mapper.select(Hoge::new, "select * from hoge");

違いとしては、 C # 側ではメソッドスコープの型変数に対するバインドを明記する必要があり、代わりに new 制約を使うので引数にビルダークラスを取らなくて良い。 Java 側ではメソッドスコープの型変数は型推論で済ませる代わりに new 制約がないため引数に Hoge:: new を渡す必要がある。概ね、 Hoge の位置が変わる程度で大差がない。

まとめ

イレイジャのせいにする言説もしばしば見かけるが、いささか見当違いと言えるだろう。何を問題視しているか冷静に検討したいところだ。

トラックバック - http://d.hatena.ne.jp/Nagise/20171224/1514127133