Java の Generics にもの思い

  • 20
    いいね
  • 7
    コメント

私は背景を把握していませんが、最近 Go と Generics について話が盛り上がっているそうです。

なんとなくやや感情的な煽りになっているので私の立ち位置を書いておくと、最近は Go をよく書いています。昔は Java を書いていました。私は Go も Java も両方好きです。あとおまけで Python も好きです。それぞれ素晴らしい言語だと思います。

私自身 Generics に詳しくなく、ちょうど Generics のことを勉強し直そうと思っていたところなので元記事はとても勉強になりました。元記事で @mattn が書いていることは概ね正しいと思うので同意するところなのですが、いくつか説明が抜けているところがあったように思うので本稿では補足を書いてみます。

以下の記事は Java について触れていますが、Java を dis っている訳でもありませんし、冗長に見える例を意図的に使っています。

意図的にそういう例を選択しているとは書いてありますが、私からみると一方的過ぎるかなと感じた次第です。

境界のある型パラメーター

import java.util.List;
import java.util.Arrays;

public class Foo {
    public static <T> T sum(List<T> list) {
        int ret = 0;
        for (T i : list) {
            ret += i; // コンパイルエラー
        }
        return ret;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(5,2,3,1,4);
        System.out.println(sum<Integer>(list));
    }
}

Java もアドホック多相のまま演算子を処理するコードはコンパイル出来ないのです。そうなると「足し算する関数を持ったインタフェース」が必要になる訳です。

もちろんインタフェースを使っても実装できますが、この用途ならば私はインタフェースを使わずに以下のように実装します。

import java.util.Arrays;
import java.util.List;

public class MyNumberUtil<T extends Number> {
    public int sum(List<T> list) {
        int ret = 0;
        for (T i : list) {
            ret += i.intValue();
        }
        return ret;
    }
}

public class MyMain {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(5, 2, 3, 1, 4);
        MyNumberUtil<Integer> util = new MyNumberUtil<Integer>();
        System.out.println(util.sum(list));

        List<Double> doubleList = Arrays.asList(5.0, 2.0, 3.0, 1.0, 4.0);
        MyNumberUtil<Double> doubleUtil = new MyNumberUtil<Double>();
        System.out.println(doubleUtil.sum(doubleList));
    }
}

型変数の境界を書かない場合、Java ではその型変数が Object 型として扱われるため、元記事ではインタフェースを定義していたわけですが、sum の結果を int 型で返すことが分かっているのなら java.lang.Number を境界型として定義することで int 型の値を取得できます。

ret += i.intValue();

もちろん Double 型でも動きます。

List<Double> doubleList = Arrays.asList(5.0, 2.0, 3.0, 1.0, 4.0);
MyNumberUtil<Double> doubleUtil = new MyNumberUtil<Double>();
System.out.println(doubleUtil.sum(doubleList));

Java を dis る人はたいてい同じ型 (ここではDouble型) をたくさん書いてて冗長だよねと言います。そこは正にその通りです。そういう人は lombok を使えば幸せになれます。

インタフェースだけでは検出できないこと

元記事では Numeric なインタフェースをもつ複数の型が混在するコレクションを扱うときの Java と Go の実装例が紹介されています。

    list := []Numeric {
        Float(5),
        Int(2),
        Int(3),
        Float(1),
        Int(4),
    }
    fmt.Println(sum(list))

これを実装しようと思うと、実行時に型チェックを行う必要があるので Java でも Go でも似たようなコードになるのはその通りだと思います。

しかし、複数の型が混在するコレクションを扱う方が稀な事例であって、普通はこんなことをせずにコレクションに 複数の型が混在しない ことを保証したいはずです。ここが Java と Go で大きく異なります。

Go では実行時に型チェックするしかありませんが、Java では Generics を使ってコンパイル時に検出できます。

以下の例は型パラメーターに Integer を指定したときしか動かない中途半端なコードなのですが、型パラメーターを使うことで検出できるエラーの事例として紹介します。あとでまた他の型でもちゃんと動くように考えてみますが、どうしたら実装できるのかが分かる方がいましたら教えてください m(_ _)m

import java.util.Arrays;
import java.util.List;

public class MyMain {
    public static void main(String[] args) {
        List<MyNumber<Integer>> list = Arrays.asList(
                new MyNumberImpl<Integer>(5),
                new MyNumberImpl<Integer>(2),
                new MyNumberImpl<Integer>(3),
                new MyNumberImpl<Integer>(1),
                new MyNumberImpl<Integer>(4)
        );

        MyNumberUtil<Integer> util = new MyNumberUtil<Integer>();
        Integer r = util.sum(list).value();
        System.out.println(r.intValue());
    }
}

public interface MyNumber <T extends Number> {
    MyNumber<T> add(MyNumber<T> i);
    T value();
}

public class MyNumberImpl<T extends Number> implements MyNumber<T> {
    private T x;

    MyNumberImpl(T n) {
        this.x = n;
    }

    @Override
    @SuppressWarnings("unchecked")
    public MyNumber<T> add(MyNumber<T> rhs) {
        int value = this.x.intValue() + rhs.value().intValue();
        return (MyNumber<T>) new MyNumberImpl<Number>(value);
    }

    @Override
    public T value() {
        return this.x;
    }
}

public class MyNumberUtil<T extends Number> {
    public MyNumber<T> sum(List<MyNumber<T>> list) {
        MyNumber<T> ret = list.get(0);
        for (int i=1; i<list.size(); i++) {
            ret = ret.add(list.get(i));
        }
        return ret;
    }
}

List<MyNumber<Integer>> は Integer だと型パラメーターを指定しているため、以下のように Double のそれが紛れ込んだときにコンパイル時にエラーにしてくれます。これがインタフェースだけではコンパイル時に検出できないエラーだと思います。

        List<MyNumber<Integer>> list = Arrays.asList(
                new MyNumberImpl<Integer>(5),
                new MyNumberImpl<Integer>(2),
                new MyNumberImpl<Double>(3.0),
                new MyNumberImpl<Integer>(1),
                new MyNumberImpl<Integer>(4)
        );

まとめ

私にとって Go に Generics が必要かどうかの議論はどうでも良いことなのですが、Generics という概念そのものや型システムの仕組みには関心をもっています。

改訂2版 パーフェクトJava ではジェネリック型について以下のように説明しています。

形式的な定義をすると、ジェネリック型とは、フィールド、メソッドの引数、返り値の間の型の関係性 (どの型とどの型が同じ、あるいは異なる) を定義可能な言語機能、と説明できます。

あとおまけで Java と Go の Generics の議論に疲れたら、みんな Python を使うと良いです。こんな難しいことを考えずに適当にコードを書いても、それなりにうまく動いてくれると思います。但し、それなりに動いて嬉しいかどうかはまた別の問題です。

言語機能も言語の大きな特徴の1つだとは思いますが、機能の有無だけを語るのではなくなぜそうなっているのか、それぞれの言語の文化や哲学も学んでいくときっとおもしろい発見があると思います。

リファレンス