2005 年初頭に公開された Java 言語仕様第 3 版 (開発実行環境は 2004 年秋に先立って公開されていた) と 2005 年 6 月に ECMA に承認された C# 言語仕様第 3 版 (開発実行環境は 2005 年末に公開された) では、共にジェネリック (ジェネリックス、ジェネリクス、総称: generics) という新しい概念が導入され、文法もそれに伴って変化している。 Java と C# はどちらも静的な型付けを行うオブジェクト指向プログラミング言語であり、C 言語を基にした 似たような文法をしている。
この記事では、Java 言語のジェネリックと C# 言語のジェネリックを、その機能や文法の違いを中心に比較する。
注意: C# の新しい言語仕様ではジェネリックの共変性・反変性を可能にする拡張が予定されているが、 この文書ではまだそれについては説明していない。
ジェネリックな型は、どちらの言語でも山型の括弧を使って表される。ジェネリックなクラスを宣言する構文は、次のようになる。
public class ClassName<T> { }public class ClassName<T> { }この例では、山型括弧の中にある T が型パラメータ (type
parameter) である。(注: type parameter
は型引数
とも訳し得るが、type argument
との混同を避けるために、この記事では型引数
という日本語は使用しない。) どちらの言語も、ClassName<T,U> のようにコンマで区切ることで、複数の型パラメータを指定することが可能である。
Java では、同じ名前で型パラメータの有無のみが異なる二つのクラスを同時に宣言することはできない。
例えば、MyClass と MyClass<T> を同じパッケージの中に共存させることはできない。
同じ名前で型パラメータの数だけが異なる場合も不可である。
これに対し、C# では同じ名前でも型パラメータの数が異なるならば全く別の型として扱われる。
ゆえに、MyClass と MyClass<T> と MyClass<T,U>
は同じ名前空間内に共存できる。
どちらの言語も、ジェネリックなクラスのほかにジェネリックなインタフェースを宣言できる。 また C# ではジェネリックなデリゲートおよびジェネリックな構造体も宣言できる。
型パラメータの一般的な命名規則を比較してみよう。
Java では、型パラメータの名前は原則として常に一文字の大文字である。例えば、List<E> や Map<K,V> のように。
型パラメータが表しているものの頭文字が型パラメータの名前になる。例えば Map<K,V> の一つ目の型パラメータはマップのキー (key) を表し、二つ目はマップの値 (value)
を表しているので、それぞれの頭文字 k と v が型パラメータの名前になっている。
C# では、型パラメータの名前は常に T
で始まる。型パラメータが一つだけのときは、単に
T 一文字がそのまま型パラメータの名前になることが多い。
複数の型パラメータがあるときは、例えば Dictionary<TKey,TValue>
のように T
を接頭辞として使う。
どちらの言語も、型パラメータを親クラスにすることはできない。つまり、例えば次のようなことはできない。
public class MyClass<T> extends T { }public class MyClass<T> : T { }Java では、java.lang.Throwable の子孫クラスをジェネリックにすることはできない。
C# では、System.Attribute の子孫クラスをジェネリックにすることはできない。
ジェネリックなクラスをインスタンス化するには、型パラメータに当てはまる実際の型を指定して コンストラクタを呼び出さねばならない。例えば次のようになる。
new MyClass<String>()new MyClass<string>()上の例で、山型括弧の中にある String/string がパラメータの実際の型すなわち型アーギュメント
(type argument) である。(注:
type argument
は型引数
とも訳し得るが、type
parameter
との混同を避けるために、この記事では型引数
という日本語は使用しない。)
Java では、new MyClass() のように型アーギュメントを指定せずに
インスタンス化することも出来る。これにより、ジェネリックなクラスをジェネリックでないクラスとして扱うことが出来る。
ただし、これはジェネリックが導入される前のコードがそのままコンパイルできるようにするための処置であって、
これから書くコードには型アーギュメントを省略せずに書くべきである。
どちらの言語も、型アーギュメントの継承関係に伴うジェネリックな型の共変性はない。 例えば、文字列の配列はオブジェクトの配列として振舞うことができるが、ジェネリックな型ではそれはできない。
Object[] ary = new String[0]; // OK
ArrayList<Object> list = new ArrayList<String>(); // 不可object[] ary = new string[0]; // OK
List<object> list = new List<string>(); // 不可ジェネリックなメソッドの宣言は、例えば次のようになる。型パラメータの位置の違いに注意。
public <T> void method(T t) { }public void Method<T>(T t) { }Java では、型パラメータの有無や数はシグネチャに含まれない。よって、次のようなメソッドのオーバーロードはできない。
public class MyClass {
public void method() { }
public <T> void method() { }
public <T,U> void method() { }
}
C# では、型パラメータの有無や数はシグネチャに含まれる。よって、次のようなメソッドのオーバーロードが可能。
public class MyClass {
public void Method() { }
public void Method<T>() { }
public void Method<T1,T2>() { }
}
ジェネリックなメソッドを型アーギュメントを明示して呼び出すのは例えば次のようになる。
String s = obj.<String>get();string s = obj.Get<string>();この例では、山型の括弧の中にある String/string が型アーギュメントである。
Java では、型アーギュメントを表す山型括弧の前に必ずピリオドがなければならない。例えば、this.<String>get(); は問題ないが単に <String>get(); と書くのは不可。これは、<
記号が型アーギュメントを表す括弧なのか不等号演算子なのかを明確に区別するための構文上の制約であると思われる。
もしこの制約がない場合、例えば式 F((G)<A,B>H(7))
は次の二通りに解釈できてしまう:
C# では型アーギュメントを書く位置が異なるため、Java とは少し事情が異なる。例えば式 F(G<A,B>(7)) は次の二通りに解釈できる:
C# では、このような場合には後者の解釈が採用される。
多くの場合、メソッドの型アーギュメントは省略できる。というのも、コンパイラが型アーギュメントを推論するからだ。
型アーギュメントの推論は、メソッドの引数の型に基づいて行われる。Java と C# とでは推論の方法が異なるが、 非常に複雑なため (特に Java) ここでは取上げない。
Java では、引数の型から型アーギュメントが推論できなかった場合、 メソッドの戻り値の代入先の変数の型によっても推論が行われる。例えば次の例では型アーギュメントとして String が推論される。
class MyClass {
<T> T getNull() {
return null;
}
public static void main(String[] args) {
String s = getNull();
}
}
C# では、代入式におけるこのような推論は行われない。
Java では、ジェネリックなコンストラクタを宣言できる。例えば以下のようになる。
class MyClass {
<T> MyClass() {
}
public static void main(String[] args) {
new <String>MyClass();
}
}
この例では、String を型アーギュメントとしてジェネリックなコンストラクタを呼び出している。 なお、ジェネリックなコンストラクタは、ジェネリックなクラスのコンストラクタとは独立した概念であることに注意すべし。 上の例では、コンストラクタがジェネリックなのであってクラスはジェネリックではない。 以下のように、ジェネリックなクラスにジェネリックなコンストラクタを宣言することも出来る。
class MyClass<T> {
<U> MyClass() {
}
public static void main(String[] args) {
new <String>MyClass<Integer>();
}
}
この例では、String はコンストラクタに対する型アーギュメントであり、Integer はクラスに対する型アーギュメントである。
コンストラクタに対する型アーギュメントは、メソッドの場合と同様に推論が可能なので、省略できる。 クラスに対する型アーギュメントは、もちろん省略できない。
一方、C# ではジェネリックなコンストラクタは宣言できない。
Java では、ジェネリックなクラスはあくまでも一つのクラスとして扱われる。 そのため、ジェネリックなクラスの静的メンバ (フィールド・メソッド) にアクセスする際には、 クラスの型アーギュメントを指定する必要はない (してはいけない)。また、Java ではジェネリックなクラスの中にプログラムの開始点である main メソッドがあってもよい。
class MyClass<T> {
static Object staticField = new Object();
public static void main(String[] args) {
System.out.println(MyClass.staticField);
}
}
つまり、ジェネリックなクラスの静的なメンバでそのクラスの型パラメータを扱うことはできないということである。
class MyClass<T> {
static T staticField; // 不可
}
class MyClass<T> {
static class MyNestedClass<U extends T> { // 不可
}
}
また、ジェネリックなクラスの静的初期化子 (static initializer: C# の静的コンストラクタに相当) は一つのクラスに対して一回だけ実行される。
class MyClass<T> {
static {
System.out.println("Initialized.");
// "Initialized." は一回だけ出力される。
}
public static void main(String[] args) {
new MyClass<Object>();
new MyClass<String>();
}
}
C# では、ジェネリックな型はそれぞれの型アーギュメントごとに異なる型として扱われる。
例えば、ジェネリックなクラス List<T> から List<object> や List<string> などの具体的なクラス (閉じた構築型 (closed constructed type)) が構築される。
ジェネリックなクラスの静的メンバは、それぞれの構築された型ごとにそれぞれ存在することになる。
そのため、静的メンバでそのクラスの型パラメータを扱うことが出来る。
class MyClass<T> {
static T t;
static object o;
}
上の例では、MyClass<object> の静的フィールド
t の型は object であり、MyClass<string>
の静的フィールド t の型は string である。もちろん、これらは別々のフィールドである。
また、MyClass<object> の静的フィールド o と
MyClass<string> の静的フィールド o
も別々のフィールドである。
従って、ジェネリックなクラスの静的メンバにアクセスする際には、 必ずクラスの型アーギュメントを指定して具体的なクラスを特定しなければならない。 型アーギュメントの指定なしで静的メソッドを呼び出すことはできないので、プログラムの開始点である Main メソッドをジェネリックな型の中に置くことはできない。
静的コンストラクタ (Java の静的初期化子に相当) もそれぞれの構築された型ごとにそれぞれ実行される。
class MyClass<T> {
static MyClass() {
System.Console.WriteLine("Initialized: {0}", typeof(T));
}
}
class Program {
public static void Main() {
new MyClass<object>();
new MyClass<string>();
}
}
このプログラムを実行すると、次の出力が得られる。異なる型アーギュメントごとに静的コンストラクタが実行されていることと、 静的コンストラクタ内で型アーギュメントの Type オブジェクトを typeof 演算子で取得できる点に注意。
Initialized: System.Object Initialized: System.String
どちらの言語も、ジェネリックな型のメンバへのアクセス制御は、 ジェネリックでない型のメンバへのアクセス制御と同様に扱われる。 型アーギュメントはアクセス制御に影響しない。
型パラメータを宣言する際に制約 (constraint) を加えることによって、型パラメータに型アーギュメントとして当てはめることが出来る型を制限できる。 (Java 言語仕様書では「制約」という言葉を用いず、専ら境界 (bound) と呼んでいる。) どちらの言語も、型パラメータに対して型アーギュメントが継承すべきクラスまたは実装すべきインタフェースを指定できる。
Java では、型パラメータの境界を extends 句によって表す。 C# では、型パラメータの制約を where 句によって表す。
以下の例では、どちらも MyGenericClass クラスの型パラメータ T に対して、T に当てはめられる型アーギュメントが MyClass であるか、MyClass の子孫クラスであることを指定している。
class MyClass { }
class MyGenericClass<T extends MyClass> extends Object { }class MyClass { }
class MyGenericClass<T> : object where T : MyClass { }Java でも C# でもクラスの多重継承はできないので 一つの型パラメータに対して複数のクラスの制約をかける事はできないが、複数のインタフェースの制約をかける事は可能である。 また、クラスとインタフェースの両方の制約をかけることもできる (クラスの制約を最初に指定しなければならない)。
class MyClass { }
interface MyInterface1 { }
interface MyInterface2 { }
class MyGenericClass
<T extends MyClass & MyInterface1 & MyInterface2> { }
// インタフェースの制約でも implements ではなく extends と書くclass MyClass { }
interface MyInterface1 { }
interface MyInterface2 { }
class MyGenericClass<T>
where T : MyClass, MyInterface1, MyInterface2 { }また、複数の型パラメータに対して制約をかける場合は以下のようになる。
class MyGenericClass
<T extends MyClass, U extends T> { }class MyGenericClass<T, U>
where T : MyClass
where U : T { }ジェネリックなメソッドの型パラメータに対する制約は、以下のように指定する。
<T extends MyClass> T method() { }T Method<T>() where T : MyClass { }C# では、型は参照型 (reference type)と値型 (value type)の二つに大別される。C# で型パラメータに一切制約をかけない場合、 型アーギュメントは参照型でも値型でもよい。しかし型アーギュメントを参照型または値型に限定する制約をかけることもできる。
class MyClass1<T> where T : class { }
class MyClass2<T> where T : struct { }
Java では、型は参照型と原始型 (primitive type) の二つに大別される。Java では、型パラメータに一切制約がない場合でも 型アーギュメントに指定できるのは参照型のみであり、int や double などの原始型はそのままでは扱えないので Integer や Double などのラッパークラスを使う必要がある。
Java にはない C# の機能として、型パラメータにコンストラクタに関する制約を加えることが出来る。 これは、型アーギュメントに指定した型が引数なしのパブリックなコンストラクタを持っていることを要求するものである。
void Init<T>(T[] array) where T : new() {
for (int i = 0; i < array.Length; i++)
array[i] = new T();
}
List<String> のように、型アーギュメントを具体的に指定した型 (C# では閉じた構築型という) の変数はどちらの言語でも使用できる。
List<String> list = null;List<string> list = null;ジェネリックなクラスやメソッドの中では、そのクラスやメソッドの型パラメータを実在する型に見立てて使用できる。
Java では、型アーギュメントにおいて ? 記号をワイルドカードとして使用できる。例えば、以下のような具合である。
List<?> list = new ArrayList<String>();
型アーギュメントとしてワイルドカードを指定すると、任意の型アーギュメントのオブジェクトを受け入れることが出来る。 上の例では、変数 list には ArrayList<Object> も LinkedList<Number> も ArrayList<ArrayList<Object>> も代入できる。これにより、任意の型アーギュメントの List を同じように扱うことが出来る。
ワイルドカードに対しても境界 (つまり制約) を設定することが出来る。ワイルドカードの境界の指定には、 extends のほかに super が使える。これは extends とは逆の意味を持ち、実際の型アーギュメントが 指定した境界と同じであるかそのスーパークラスであることを指定する。
List<? extends Number> list1 = new ArrayList<Integer>();
List<? super Number> list2 = new ArrayList<Object>();
C# では、Java のようにワイルドカードを型アーギュメントとして使用することはできない。これにより、Java とはいくらか異なる実装を求められることになる。例えば、以下の Java の二つのメソッドは どちらもリストの内容を全て出力するものであるが、 前者はジェネリックなメソッドであるのに対し、後者は (ジェネリックな型の変数を使ってはいるが) ジェネリックなメソッドではない。
<T> void printAll(List<T> list) {
for (T t : list)
System.out.println(t);
}
void printAll(List<?> list) {
for (Object o : list)
System.out.println(o);
}
これと同じような動作をするメソッドを C# で書く場合、 ワイルドカードは使えないため、メソッドをジェネリックにせずに済ますことはできない。つまり、選択肢は一つしかない。
void PrintAll<T>(List<T> list) {
foreach (T t in list)
System.Console.PrintLine(t);
}
ここで問題となるのは、Java の ? super X に相当する制約が
C# では使えないことである。例えば次の Java のクラスを考えてみよう。メソッド copy
は、list に含まれる内容を list2 にコピーするものである。
class MyClass<T> {
List<T> list;
void copy(List<? super T> list2) {
for (T t : list)
list2.add(t);
}
}
ここで、list と list2 の型アーギュメントは同じである必要はなく、 型 T から明示的なキャストなしで代入できる型であればよいことに注意する。list2 の型は List<T> でなくとも List<? super T> でよいのである。
C# でこれと同じことをするためには、 メソッドの引数の型のワイルドカードをジェネリックなメソッドの型パラメータに置き換えなくてはならない。 ところが、C# には Java の super に相当する制約の設定はできないのである。 従って、List<T> の内容を List<T> にコピーすることしかできない。
class MyClass<T> {
List<T> list;
void Copy(List<T> list2) {
foreach (T t in list)
list2.Add(t);
}
}
なお、void Copy<U>(List<U>
list2) where T : U { ... }
と書けばうまく出来るように思うかもしれないが、これは出来ない。というのも、ここで制約を課すことができるのは
メソッドの型パラメータ U だけだからだ。この場合はクラスの型パラメータ T
に対してメソッド宣言内で制約をかけようとしているのでコンパイル時エラーになる。
この問題に関しては C# プログラミングガイドのジェネリック型の分散が詳しい。
ジェネリックな型の配列はどうだろうか。
Java では、型アーギュメントとしてワイルドカードを指定した型の配列のインスタンス化 (例: new List<?>[3]) は可能であるが、
型アーギュメントとして具体的な型を指定した型の配列のインスタンス化 (例: new List<String>[3]) は出来ない。
型アーギュメントの情報を配列のインスタンスに保持できないからである。(詳細は後述)
List[] a1 = new List[3]; // OK (非推奨)
List[] a2 = new List<?>[3]; // OK (非推奨)
List[] a3 = new List<String>[3]; // エラー
List<?>[] a4 = new List[3]; // OK (非推奨)
List<?>[] a5 = new List<?>[3]; // OK
List<?>[] a6 = new List<String>[3]; // エラー
List<String>[] a7 = new List[3]; // 警告 (非推奨)
List<String>[] a8 = new List<?>[3]; // エラー
List<String>[] a9 = new List<String>[3]; // エラー
List<String>[] a10 = (List<String>[]) new List<?>[3]; // 警告
@SuppressWarnings("unchecked")
List<String>[] a11 = (List<String>[]) new List<?>[3]; // OK
List<String>[] のような具体的な型アーギュメントを指定した配列を用意したいときは、 上の最後の例のようにワイルドカードを使用して配列をインスタンス化した後にキャストしなければならない。 (このとき @SuppressWarnings 注釈を付けないと、警告が出る)
C# ではワイルドカードは使えないので、常に具体的な型アーギュメントを指定する必要がある。
List<string>[] a = new List<string>[3]; // OK
プログラムが実際に実行されるとき、実行環境はジェネリックな型をどのように扱うのか。
Java では、ジェネリックはコンパイル時に型をより厳密に取り扱うための構文でしかない。 すなわち、実行時にはジェネリックに関する特別な処理は特に行われていない。
端的にいえば、Java においてジェネリックとはコンパイラがより厳密な型チェックを行うための糖衣構文 (文法的な置き換え) に過ぎない。例えば、次の二つのメソッドはコンパイルすると全く同一のバイトコードに変換される。 (Sun のコンパイラ javac 1.5.0 の場合)
void method1() {
List<String> list = new ArrayList<String>();
list.add("string");
String string = list.get(0);
}
void method2() {
List list = new ArrayList();
list.add("string");
String string = (String) list.get(0);
}
List<Object> も List<String> も List<Integer> も結局は単なる List として扱われることになる。 ジェネリックによって文法上はリストから文字列オブジェクトを取り出す際に明示的なキャストが不要となったが、 実行時には (ジェネリックによって不要となったはずの) 型のチェックが行われている。
実行時には型アーギュメントに関する情報は一切扱われていないため、 リフレクションにおいて実際のオブジェクトの型アーギュメントを扱うことはできない。 また instanceof 演算子で型アーギュメントを判別することも出来ない。
配列は、new T[10] のように型パラメータを
使用して作成することはできない。(T[]) new Object[10]
のようにキャストすることは可能だが、コンパイル時に厳密な型のチェックが行えないため実行時に例外が発生する要因になる。
C# では、ジェネリックはコンパイル時のみならず実行時にも意味を持つ。
型アーギュメントに応じて、List<object> や List<string> や List<int>
などのクラスが実行時に動的に作られると考えてもよい。
例えば、型パラメータ T に対して new
T[10] で配列を作成する場合でも、実際の
T に応じて string の配列や int の配列など異なる型の配列が作成される。
リフレクションでジェネリックな型を扱うときには、型アーギュメントの違いは厳密にチェックされる。 また is 演算子や as 演算子で型アーギュメントを判別したり、型パラメータに対して typeof 演算子を適用して型アーギュメントの Type オブジェクトを取得したり出来る。
Type GetListTypeArgument<T>(object obj) {
if (obj is List<T>) {
return typeof(T);
} else {
return null;
}
}
なお、is 演算子・as 演算子では型アーギュメントは常に指定しなければならない。例えば
(obj is List<?>)
のようなワイルドカードを使用して、オブジェクトが「任意の型アーギュメントの List」であるかを調べることはできない。また
try-catch 文でジェネリックなクラスの例外をキャッチする場合も、同様に型アーギュメントを指定しなければならない。
静的な型付けを行う言語においてより厳密な型のチェックを行うための手段としては、Java も C# もほぼ十分なジェネリック機能を備えている。しかし両者のジェネリック機能に細かな差異が見られるのは、 ジェネリックによるプログラムをどのように実行するかについて両者が異なる方針を採ったからである。
C# は、ジェネリックの導入による言語仕様の変更と共に実行環境も合わせて変更した。 そのため、コンパイル時だけでなく実行時にもジェネリックの機能を使用できる。
一方の Java は、言語仕様を変更しても実行環境には手を加えなかった。 マイクロソフトは、これを次のように批判している。
Sun Microsystems® では、次のバージョンの Java 言語 (コード ネームは "Tiger") でジェネリクスを追加する予定です。Sun は、Java 仮想マシンの変更を必要としない実装を選択しました。 このため、変更されていない仮想マシン上にジェネリクスを実装するという問題に直面しています。
計画されている Java の実装は、型パラメータや制約も含め、C++ のテンプレートや C# のジェネリクスに似た構文を使用しています。しかし、値型の扱いが参照型と異なるため、変更されていない Java 仮想マシンでは、値型のジェネリクスをサポートすることはできないでしょう。このため、Java のジェネリクスでは、実行効率の向上は得られません。実際 Java コンパイラは、データを返す必要が生じるたびに、 指定された制約から自動的にダウンキャストを挿入するか (制約が宣言されている場合)、Object 型を挿入します (制約が宣言されていない場合)。さらに、Java コンパイラは、コンパイル時に特殊化された型を 1 つ生成し、構築されるすべての型をそれを使ってインスタンス化します。最後に、Java 仮想マシンはジェネリクスをネイティブでサポートしていないため、ジェネリック型のインスタンスの型パラメータを 実行時に確認する手段はなく、リフレクションのその他の利用も大きく制限されます。
実行時にジェネリックの情報を扱えない点で、Java は C# より実行時のパフォーマンスの面でたしかに不利である。
一方 C# では型アーギュメントにワイルドカードを使用できないため、 型アーギュメントの汎用的な扱いが求められる場面では Java よりも実装が困難になることがあるかもしれない。
また、どちらの言語も配列の扱いはほぼ従来どおりであることに注意したい。コレクションモデルとしては、配列は 固定長のリストとして見做すことができ、実際に C# では配列は IList インタフェースを実装している。 しかし文字列の配列をオブジェクトの配列として扱うというような共変性は依然として残っている。 そのため配列における型のチェックはジェネリックなリストほどには厳密でないということになる。
全体としては、両者共に有利な点と不利な点があるが、実際にジェネリックを活用したプログラミングを行う際には、 どちらの言語が優れているかということはほとんど問題にならないと思われる。