ジェネリクス(generics、総称型)とは、クラスや関数が扱う型を決め打ちせず、「型そのもの」を引数として受け取れるようにする言語機能です。List<String>のStringのように、使う側が型を指定すると、1つの実装を複数の型で使い回しながら、型の誤りをコンパイル時(または型チェック時)に見つけられます。この記事では、同じ関数をJava・C#・TypeScript・Go・Rust・Python・PHPの7言語で書いて実行した結果をもとに、書き方と実装方式(型消去・具象化・単相化)の違い、使うべき場面を整理します。
まとめ:ジェネリクスの要点
- ジェネリクスは型を引数にする仕組み。
Tなどの型パラメータで書いておき、使う側がStringやintを型引数として渡す。 - 解決するのは、
anyやObjectで汎用化したときに型の誤りが実行時まで見つからない問題。ジェネリクスは型の関係を検査できるが、どこまで止まるかは言語・型の指定・型チェッカーの設定で変わる。 - 実装方式は3系統。Java・TypeScriptは型消去(実行時に型引数が消える)、C#は具象化(実行時も型引数が残る)、Rustは単相化(型ごとに機械語を生成)。Goは単相化と辞書の併用。
- Pythonの型ヒントとPHPDocの
@templateは実行時に何も強制しない。mypyやPHPStanを回して初めて誤りが見つかる。 - 型パラメータが入力と出力の型を結び付けていない関数(1か所にしか使われない)や、メソッドを呼ぶだけの関数はジェネリクスにしない。インターフェースや具体型で書いたほうが読みやすい。
ジェネリクスの意味:型を引数として受け取る仕組み
英語のgenericは「総称的な・特定の銘柄に限らない」という意味で、ジェネリック医薬品のジェネリックと同じ語です。プログラミングでは「特定の型に限らない」コードを指します。
関数の引数が「値」を外から受け取るのに対し、ジェネリクスは「型」を外から受け取ります。List<T>のTに何を入れるかは使う側が決めます。
型パラメータ・型引数・ジェネリック型の用語
似た用語が混ざりやすいので、Javaの書き方で整理します。
| 用語 | 指すもの | 例 |
|---|---|---|
| 型パラメータ(型変数) | 宣言側に置く型の仮引数 | class Box<T>のT |
| 型引数 | 使う側が渡す具体的な型 | Box<String>のString |
| ジェネリック型(ジェネリッククラス) | 型パラメータを持つクラス・インターフェース | Box<T>、List<E> |
| パラメータ化された型 | 型引数を当てはめた後の型 | List<String> |
| ジェネリックメソッド | メソッド単位で型パラメータを持つもの | <T> T first(List<T> xs) |
| 境界(制約) | 型パラメータに入れられる型の条件 | T extends Comparable<T> |
ジェネリックプログラミング・C++テンプレートとの関係
ジェネリックプログラミングは、型に依存しない形でアルゴリズムとデータ構造を書くという設計の考え方です。名前を広めたのはDavid MusserとAlexander StepanovがISSAC 1988で発表した論文「Generic Programming」で、Stepanovは後にC++の標準テンプレートライブラリ(STL)を設計しました。ジェネリクスは、この考え方を言語機能として実現する手段の1つです。
C++ではほぼ同じ役割をテンプレートが担います(C++98のISO/IEC 14882:1998第14章で標準化)。テンプレートは型ごとにコードを展開し、型のほかに整数などの値も非型テンプレート引数として取れるため、JavaやC#のジェネリクスより表現力は広い一方、エラーメッセージが長くなりがちです。
ジェネリクスが解決する問題:anyやObjectとの型情報の違い
ジェネリクスが無い時代、どんな型でも受け取る汎用コードは、すべてのクラスの親(JavaのObject)や型検査をやめる型(TypeScriptのany)などで書かれていました。共通の操作だけが必要なら、インターフェースを使う方法もあります。anyはその値への型検査をまるごと止めます。JavaのObjectはそのままStringの変数には代入できないものの、(String)のようなキャストを書けばコンパイルが通り、型が違えば実行時にClassCastExceptionになります。どちらも、戻り値の本当の型を確かめる仕事が実行時へ持ち越される点は同じです。TypeScript 7.0.2で確かめたコードです。
function largerAny(a: any, b: any): any {
return a >= b ? a : b;
}
const x = largerAny(3, 7);
x.toUpperCase();
function larger<T extends number | string>(a: T, b: T): T {
return a >= b ? a : b;
}
const n = larger(3, 7);
n.toUpperCase();
larger(3, "a");
tsc --strict --noEmitの結果、any版のx.toUpperCase()は素通りし、実行して初めてTypeError: x.toUpperCase is not a functionになります。ジェネリクス版は次の2件をコンパイル時に止めました。ただし、larger<number | string>(3, "a")のように型引数を共用体型で明示すると通るため、実行時の型が必ず一致するという保証ではありません。
larger.ts(11,3): error TS2339: Property 'toUpperCase' does not exist on type '3 | 7'.
larger.ts(12,11): error TS2345: Argument of type '"a"' is not assignable to parameter of type '3'.
ポイントは、ジェネリクス版の戻り値の型が「呼び出しごとに」決まることです。larger(3, 7)の戻り値は数値(リテラル型の3 | 7)、larger("apple", "banana")なら文字列として扱われます。anyは型の情報を捨て、ジェネリクスは型の情報を呼び出し元から戻り値まで運びます。TypeScriptの型推論と型注釈の書き分けはTypeScriptの型の解説で扱っています。
同じ関数を7言語で書く:2つの値の大きい方を返すlarger
「2つの値を比べて大きい方を返す」関数largerを7言語で書き、larger(3, 7)・larger("apple", "banana")・誤った呼び出しのlarger(3, "a")を試しました。比べられない型を渡されたら困るので、どの言語でも型パラメータに「比較できる型に限る」という境界(制約)を付けています。書き方の差を先に表で示します。
| 言語 | 型パラメータ | 比較できる型への制約 | 誤った呼び出しを止めるもの |
|---|---|---|---|
| Java | <T> |
T extends Comparable<T> |
javac |
| C# | <T> |
where T : IComparable<T> |
C#コンパイラ |
| TypeScript | <T> |
T extends number | string |
tsc |
| Go | [T] |
cmp.Ordered |
go build |
| Rust | <T> |
T: PartialOrd |
rustc |
| Python | [T] |
T: (int, str) |
mypy等(実行時は無し) |
| PHP | @template T |
@template T of ... |
PHPStan等(実行時は無し) |
表の右端が、言語ごとの一番大きな違いです。Pythonの型ヒントやPHPDocの型パラメータは、実行時の型検査を強制しません。宣言との不一致を実行前に検出するには型チェッカーが必要ですが、比較などの操作自体が実行時エラーになる場合はあります。
Java・C#:extendsとwhereによる型の境界
Javaはextends、C#はwhere句で境界を書きます。どちらもComparable/IComparableを実装した型に限ることで、関数の中でcompareTo/CompareToを呼べるようになります。
以下はJava用とC#用の別々のプログラムです。JavaはLarger.javaとして保存してjava Larger.java、C#はコンソールプロジェクトのProgram.csに保存してdotnet runで実行します。
public class Larger {
static <T extends Comparable<T>> T larger(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
public static void main(String[] args) {
System.out.println(larger(3, 7));
System.out.println(larger("apple", "banana"));
}
}
static T Larger<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
Console.WriteLine(Larger(3, 7));
Console.WriteLine(Larger("apple", "banana"));
Javaのlarger、C#のLargerとも、(3, 7)では7、("apple", "banana")ではbananaを返しました。Javaでlarger(3, "a")と書くと、javacはreason: inference variable T has incompatible bounds(型変数Tの境界が両立しない)で止まります。Javaのジェネリッククラスの定義、ワイルドカード、型消去による制限の詳細はJavaのジェネリクスの解説記事にまとめています。
Go:角括弧の型パラメータとインターフェースによる制約
Goは2022年3月15日公開のGo 1.18でジェネリクスを導入しました(Go公式ブログ「Go 1.18 is released!」)。型パラメータは角括弧で書き、制約はインターフェースで表します。標準パッケージcmpのcmp.Orderedは、<や>=で比較できる整数・浮動小数点数・文字列の型をまとめた制約です。cmpパッケージ自体はGo 1.21で追加されたため、次のコードはGo 1.21以降で動きます(Go 1.21リリースノート)。
package main
import (
"cmp"
"fmt"
)
func Larger[T cmp.Ordered](a, b T) T {
if a >= b {
return a
}
return b
}
func main() {
fmt.Println(Larger(3, 7))
fmt.Println(Larger("apple", "banana"))
}
Go 1.27.2でLarger(3, "a")をビルドすると、mismatched types untyped int and untyped string (cannot infer T)で止まりました。
Go 1.26まではメソッドに独自の型パラメータを付けられませんでしたが、2026年8月のGo 1.27でfunc (l List[T]) Map[U any](f func(T) U) List[U]のようなジェネリックメソッドが書けるようになりました(Go 1.27.2で実行を確認)。ただし、インターフェースのメソッドは型パラメータを持てず、ジェネリックメソッドでインターフェースを実装することもできません(Go 1.27リリースノート)。Goの導入と基本文法はGo言語の実装入門を参照してください。
Rust:PartialOrdのトレイト境界
Rustでは「TはPartialOrdトレイトを実装している」というトレイト境界をT: PartialOrdと書きます。
fn larger<T: PartialOrd>(a: T, b: T) -> T {
if a >= b { a } else { b }
}
fn main() {
println!("{}", larger(3, 7));
println!("{}", larger("apple", "banana"));
}
rustc 1.99.1でlarger(3, "a")を渡すとerror[E0308]: mismatched typesになり、「1つ目の引数が整数なので、2つ目も同じ整数型である必要がある」と指摘されました。なおPartialOrdは「比べられない組み合わせがありうる」比較で、浮動小数点数のNaNはどの値とも大小が決まりません。汎用の最大値関数にするなら、全順序を保証するOrdに絞るほうが安全です(PartialOrdのドキュメント)。Rustの所有権や基本文法はRust入門で解説しています。
Python:PEP 695の型パラメータ構文とmypyによる検査
Pythonは3.5で入ったtyping.TypeVarで型パラメータを表してきましたが、3.12からは関数・クラスの定義に直接書けます(PEP 695)。T: (int, str)は「Tはintかstrのどちらか」という制約です。
def larger[T: (int, str)](a: T, b: T) -> T:
return a if a >= b else b
print(larger(3, 7))
print(larger("apple", "banana"))
larger(3, "a")
mypy 2.4.0は最終行をValue of type variable "T" of "larger" cannot be "object"と指摘しました。一方、Python 3.14.6でそのまま実行すると、7とbananaを表示したあと、最終行でTypeError: '>=' not supported between instances of 'int' and 'str'になります。これは型パラメータの検査ではなく、>=の比較そのものの失敗です。b: Box[int] = Box("文字列")のような宣言違反は、実行時には何事もなく通りました。Pythonでジェネリクスを使うなら、mypyやPyrightをCIに組み込むところまでが1セットです。
PHP:PHPDocの@templateとPHPStanによる検査
PHP 8.5にもジェネリクスの構文はありません。PHP 8.5.8でclass Box<T> {}と書くとsyntax error, unexpected token "<"になります。実務では、PHPDocコメントの@templateで型パラメータを宣言し、PHPStanやPsalmといった静的解析ツールに検査させます(PHPStan「Generics in PHP using PHPDocs」)。
<?php
/**
* @template T of int|string
* @param T $a
* @param T $b
* @return T
*/
function larger(int|string $a, int|string $b): int|string
{
return $a >= $b ? $a : $b;
}
echo larger(3, 7), PHP_EOL;
echo larger('apple', 'banana'), PHP_EOL;
echo larger(3, 'a'), PHP_EOL;
PHP 8.5.8で実行すると7、bananaに続き、3行目も止まらずにaを出力しました。しかもPHPStan 2.3.3はレベル9でもこの呼び出しを指摘しませんでした。\PHPStan\dumpType()で確かめると、Tは3|'a'という和集合の型に推論されており、「2つの引数が同じ型であること」までは縛れていません。
PHPStanが型の不一致を検出する例として、要素の型を固定したコレクションがあります。@template T付きのBoxクラスで、Box<int>を受け取る関数にnew Box('21')を渡すと、レベル9でParameter #1 $box of function double expects Box<int>, Box<string> given.と指摘されました(実行時はエラーになりません)。PHPStanの導入とレベルの選び方はPHPStanのレベルと導入の解説にあります。
実装方式の違い:型消去・具象化・単相化
コンパイル後にジェネリクスをどう扱うかは言語で分かれ、それが「実行時に型引数を調べられるか」「型ごとにコードが増えるか」を決めます。
| 方式 | 言語 | 実行時の型引数 | 主な帰結 |
|---|---|---|---|
| 型消去 | Java、TypeScript | 残らない | new T()や型引数での分岐ができない |
| 具象化 | C#(.NET) | 残る | typeof(T)で型を取得できる |
| 単相化 | Rust、C++ | 型ごとに別の関数 | 具体型ごとに実体化しコード量が増えうる |
| 単相化+辞書 | Go | 辞書で実行時型記述子を渡す | 同じGC shapeの型で実装を共有 |
| 注釈のみ | Python、PHP | 検査されない | 型チェッカーが無ければ効果なし |
型消去と具象化の差は、同じクラスに別の型引数を当てたときの比較ではっきり出ます。
// Java(JDK 25):true
System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass());
// C#(.NET 10):False、Int32
Console.WriteLine(new List<string>().GetType() == new List<int>().GetType());
Console.WriteLine(NameOf<int>());
static string NameOf<T>() => typeof(T).Name;
JavaではArrayList<String>とArrayList<Integer>が実行時には同じArrayListクラスで、比較はtrueでした。C#はFalseを返し、関数の中でtypeof(T)からInt32という型名まで取れました。.NETのジェネリック型は実行時にも型引数の情報を持ち、リフレクションで確認できます(Microsoft Learn「Generics in the runtime」)。型ごとの分岐やインスタンス生成は、C#なら素直に書け、JavaではClass<T>を引数で渡すなどの回り道が要ります。実行時に型情報を扱う仕組みはリフレクションの言語横断の解説で掘り下げています。
Rustの単相化は、larger::<i32>とlarger::<&str>のように使われた型ごとに専用の関数を生成する方式で、型を抽象化したことによる実行時コストはかかりません(The Rust Programming Language 10.1節)。その代わり、使う型が増えるとコンパイル時間とバイナリサイズが増えることがあります(実際の増え方は最適化やインライン化で変わります)。この考え方はゼロコスト抽象化の解説で詳しく扱っています。Goは全面的な単相化を避け、Go 1.18で、基底型が同じ型どうし、またはポインタ型どうしを同じ「GC shape」として1つの実装を共有し、具体的な型引数は「辞書」(実行時型記述子の一覧)で渡す方式(GC shape stenciling)を採りました(Go 1.18の実装設計文書)。
ジェネリクスの歴史:MLの型多相からGo 1.27まで
ジェネリクスの源流は、Robin Milnerが1978年の論文「A Theory of Type Polymorphism in Programming」で報告した、MLの多相型の型検査です。研究言語で生まれたこの考え方が、40年以上かけて主流の言語に広がりました。
| 年 | 言語・出来事 |
|---|---|
| 1978年 | ML:Milnerが多相型の型検査を論文で報告 |
| 1983年 | Ada 83:generic unitsを標準化 |
| 1988年 | Musser・Stepanov「Generic Programming」 |
| 1998年 | C++98:テンプレートとSTLを標準化 |
| 2004年 | J2SE 5.0(9月30日):JSR 14のジェネリクス |
| 2005年 | C# 2.0(11月):具象化されたジェネリクス |
| 2013年 | TypeScript 0.9(6月18日):ジェネリクスを追加 |
| 2014年 | Swift 1.0:ジェネリクスを初版から搭載 |
| 2015年 | Rust 1.0/Python 3.5(PEP 484の型ヒント) |
| 2022年 | Go 1.18(3月15日):ジェネリクスを導入 |
| 2023年 | Python 3.12:PEP 695の型パラメータ構文 |
| 2024年 | Python 3.13:PEP 696の型パラメータ既定値 |
| 2026年 | Go 1.27:ジェネリックメソッド |
導入が遅れた理由は言語ごとに違います。Goの公式FAQは、当初は単純さを優先してジェネリクスを見送り、型システムと実行時の複雑さに見合う設計に至るまで時間がかかったと説明しています(Go FAQ「Why was Go initially released without generic types?」)。PHP Foundationは2024年8月の報告で、実行時に型を検査するジェネリクスの最大の難所を型推論(PHPのコンパイラはファイル単位でしかコードを見られない)とし、構文だけを入れて検査を静的解析ツールに任せる案も並べたうえで、どれが実現可能かは調査中だと結んでいます(PHP Foundation「State of Generics and Collections」)。
変性:List<String>とList<Object>の代入関係
StringはObjectの一種なのに、JavaではList<String>をList<Object>の変数に代入できません。JDK 25のjavacはincompatible types: List<String> cannot be converted to List<Object>で止めます。代入できると、List<Object>経由で整数を追加でき、元のリストに整数が混ざるからです。実際、この代入を許すJavaの配列では、Object[] arr = new String[1]; arr[0] = 1;がコンパイルを通り、実行時にjava.lang.ArrayStoreExceptionで落ちました。
取り出すだけなら安全なので、各言語は共変(out)・反変(in)を指定する方法を持っています。Javaは使う側でList<? extends T>/List<? super T>と書き、C#とKotlinは宣言側で型パラメータにout/inを付けます。ただし、C#で指定できるのはインターフェースとデリゲートで、変性による変換は参照型の型引数に限られます。C#のIEnumerable<out T>は共変なのでIEnumerable<object> objs = new List<string>();は通り、List<object>への代入だけがCS0029で止まりました。TypeScriptは4.7からin/outの変性注釈を書けます(TypeScript 4.7のリリースノート)。値を取り出すだけなら共変、入れるだけなら反変、両方なら不変、が判断の軸です。
ジェネリクスを使うべき場面・避けるべき場面
使うべき場面は、要素の型だけが違う同じ処理を書くときです。リスト・マップ・キャッシュ・結果型(成功値かエラーか)のようなコンテナ、ソートや検索のように要素の型に依存しないアルゴリズム、APIクライアントの「レスポンスの型を呼び出し側が指定する」関数がこれに当たります。Go公式ブログの「When To Use Generics」(2022年4月、Ian Lance Taylor)も、スライス・マップ・チャネルといった言語組み込みのコンテナを扱う関数と、汎用のデータ構造を代表例に挙げています(The Go Blog「When To Use Generics」)。
避けるべき場面は3つあります。
- 型パラメータが1回しか出てこない:
function greet<T extends string>(s: T): voidのTは引数にしか登場せず、stringと書くのと変わりません。TypeScriptハンドブックは「型パラメータは2回現れるべき」という指針を示しています(TypeScript Handbook「Guidelines for Writing Good Generic Functions」)。 - メソッドを呼ぶだけ:引数の
Writeメソッドを呼ぶだけの関数なら、Goではio.Writerのようなインターフェース型の引数で足ります。同ブログも「値のメソッドを呼ぶだけならインターフェース型を使う」と明言しています。インターフェースの考え方はインターフェースの意味と種類の解説を参照してください。 - 今は1〜2種類の型でしか使わない:将来の汎用化を見込んで最初から型パラメータを付けると、読み手は不要な抽象を追うことになります。Go公式ブログは「まったく同じコードを何度も書こうとしていると気づくまで型パラメータは避ける」と書いており、置き換えはそれからで間に合います。
既存システムがanyやObjectのキャストだらけになっている場合は、部分的なジェネリクス化より設計の作り直しが早いこともあります。型設計から見直す開発はフルスクラッチ開発として相談を受けています。
よくある質問
ジェネリクスの英語と読み方は?
英語ではgenerics(ジェネリクス)で、形容詞のgenericは「総称的な」という意味です。日本語では総称型とも訳され、「ジェネリックス」と表記されることもあります。型パラメータを持つ型は、英語ではgeneric type(ジェネリック型)と呼びます。
<T>のTは何の略ですか?
TはType(型)の頭文字です。型パラメータ名は1文字の大文字にする慣習があり、OracleのチュートリアルはE(Element=要素)、K(Key)、N(Number)、T(Type)、V(Value)を挙げています(Oracle「Generic Types」)。2つ目以降の型パラメータにはS、U、Vを順に使います。名前は文法上の決まりではないので、TResultのような長い名前も書けます。
ジェネリック関数をアロー関数で書くには?
TypeScriptではconst id = <T>(x: T): T => x;と書けます。ただし、Reactなどの.tsxファイルでは<T>がJSXの開始タグと解釈され、TypeScript 7.0.2ではerror TS17008: JSX element 'T' has no corresponding closing tag.になりました。.tsxでは<T,>と末尾にカンマを付けるか、<T extends unknown>と書くとコンパイルが通ります。
ジェネリクスを使うと実行速度は落ちますか?
実行速度への影響は言語の実装と扱う型によって異なり、具体型で書いた場合との差は一律には言えません。Rustは単相化で型の抽象化コストを消します。Javaでは型消去に伴うキャストに加え、プリミティブ型専用の処理との比較ではボクシング・アンボクシングのコストも検討が必要です。多くの型に展開するRust・C++では、速度よりビルド時間とバイナリサイズを測って判断してください。
ジェネリクスと継承(ポリモーフィズム)はどう使い分けますか?
継承は「型は違っても同じ操作ができる」ことを表し、ジェネリクスは「同じ処理をどの型にも適用できる」ことを表します。図形ごとに面積の計算方法が違うなら継承やインターフェース、要素の型によらず同じ順に並べ替えるならジェネリクスです。両者は組み合わせて使うのが普通で、T extends Comparable<T>のような境界は、ジェネリクスにインターフェースの条件を付けたものです。