はじめに
自己紹介
皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。
いずれもJava EE(Jakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。
Udemy講座のご紹介
この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをQiita内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。
この講座は、以下のような皆様にお薦めします。
- Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
- 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
- フリーランスエンジニアとして付加価値の更なる向上を図っている方
- 「Oracle認定Javaプログラマ」の資格取得を目指している方
この記事を含むシリーズ全体像
この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。
7.1 総称型
チャプターの概要
このチャプターでは、型をパラメータ化することでクラスの汎用性を高める仕組みである、総称型について学びます。
総称型はコレクションフレームワークで多用されているため、多くの皆さんが馴染みがあると思いますが、このチャプターでは、総称型を使ったクラスを自分で作成したり、それをAPIとして提供するための方法を取り上げます。
7.1.1 総称型の基本
総称型とは
総称型とは、ジェネリクスと呼ばれることもありますが、何らかの汎用的なクラスに対して、型をパラメータとして指定するための機能です。
総称型の最も代表的な用途は、コレクションフレームワークにおける型の指定です。コレクションフレームワークに関する詳細は『Java Basic編』で取り上げていますが、例えばString型を格納するためのリストであれば、以下のように宣言します。
List<String> list = new ArrayList<>();
変数はList<String>
型ですが、これは「Stringを格納するためのList型」という意味です。もし「Integerを格納するためのList型」にするのであれば、List<Integer>
型になります。<>
はダイヤモンド演算子というもので、総称型のための記法です。
総称型のお陰で、このリストに対してパラメータとして指定された型(ここではString型)以外の値を追加しようとすると、コンパイルエラーになります。またこのリストから値を取り出すときも、その値がString型であることが、コンパイラによって保証されます。このようにコンパイラによって型の安全性が保証されることを、タイプセーフと呼びます。
なお総称型が利用可能なクラスであっても、総称型を使わずに変数を宣言することは可能です。このような型を、総称型に対して原型と呼びます。
原型を使うと、例えばList型の変数は以下のように宣言します。
List list = new ArrayList();
このように記述すると「どんな型でも格納可能なリスト」が生成されます。これはいかにも便利そうに聞こえますが、静的型付け言語であるJavaでは、このようなリストはほとんど用途がありません。仮にこのリストにString型の値を格納したにも関わらずInteger型として取り出そうとしても、コンパイラはそれを検知することができず、実行時にエラー(ClassCastException)が発生します。つまり、このコードはタイプセーフではない、ということになります。
コンパイラによる型の安全性保証はJavaの利点の1つでもあるので、総称型を利用可能なクラスでは、特別な事情がない限りは、総称型を利用するようにしてください。
ジェネリックタイプの宣言
コレクションのように、総称型が適用されたクラスやインタフェースのことを、ジェネリックタイプと呼びます。
ジェネリックタイプは、以下の構文のように宣言します。
class クラス名<型パラメータ名1, 型パラメータ名2, ....> { .... }
interface インタフェース名<型パラメータ名1, 型パラメータ名2, ....> { .... }
ジェネリックタイプでは、クラス名またはインタフェース名の直後にダイヤモンド演算子を記述し、その中に型パラメータを指定します。型パラメータは、カンマで区切って複数列挙することができます。
型パラメータ名には、Javaの識別子のルールに則ってさえいればどのような命名も可能ですが、一般的な命名規約として以下のようなものがあります。
- 可能な限り一文字の大文字を使用する。
- 一般的な型の場合は、typeの意味で
T
を使用する。 - コレクション内の要素型を表す場合は、elementの意味で
E
を使用する。 - マップ内のキー型と値型を表す場合は、keyとvalueの意味で、それぞれ
K
、V
を使用する。
Java SEのクラスライブラリの中でも、代表的なジェネリックタイプには、以下のようなものがあります。
- public final class Class<T>
- public interface Comparator<T>
- public interface Map<K,V>
Java SEクラスライブラリにおける総称型
Java SEのクラスライブラリには、ジェネリックタイプとして提供されるものが数多くあります。
その中でも、特に主要なものを以下に示します。これらはいずれも、これまでのレッスンで既出のものか、または『Java Basic編』で取り上げたものです。
【表7-1-1】Java SEクラスライブラリにおける総称型
仕組み | ジェネリックタイプ | 本コースにおける主な参照先 |
---|---|---|
コレクションフレームワーク | java.utilパッケージのList<E>、Set<E>、Map<K,V>、Deque<E>など | 『Java Basic編』チャプター18.1 |
自然順序付け(ソート) | java.lang.Comparable<T> | 『Java Basic編』チャプター18.2 |
コンパレータ(ソート) | java.util.Comparator<T> | 『Java Basic編』チャプター18.2 |
オプショナル | java.util.Optional<T> | 『Java Basic編』チャプター20.2 |
スレッドローカル | java.lang.ThreadLocal<T> | チャプター3.1 |
並行処理ユーティリティ (Executorフレームワーク) |
java.util.concurrentパッケージのCallable<V>、Future<V>、BlockingDeque<E>など | チャプター3.2~3.3 |
関数型インタフェース | java.util.functionパッケージのSupplier<T>、Consumer<T>、Function<T,R>など | チャプター4.1 |
ストリームAPI | java.util.streamパッケージのStream<T>、Collector<T,A,R>など | チャプター4.2 |
クラスのメタ情報 | java.lang.Class<T> | チャプター8.1 |
これらのクラスやインタフェースは、いずれも型を問わない何らかの汎用的な仕組みであり、総称型によって型をパラメータ化することで、タイプセーフを実現しているのです。
7.1.2 ジェネリックタイプの特徴
総称型を利用したライブラリ設計
このレッスン以降では、開発者自身がジェネリックタイプとなるクラスやインタフェースを作成し、それをライブラリ化するための設計手法について説明します。総称型は、型を問わない何らかの汎用的な仕組みを提供するために利用しますが、その代表的な用途には以下のようなものがあります。
- 汎用的なコンテナ(入れ物)
- 汎用的なユーティリティ
- 汎用的な機能実装
まず1つ目は「汎用的なコンテナ(入れ物)」としての用途です。クラスを「汎用的なコンテナ」として使う場合、格納する要素の型を指定するために、総称型を使います。このケースの代表はコレクションですが、その他にもjava.util.Optional<T>や、java.lang.ThreadLocal<T>なども、「汎用的なコンテナ」の一種と見なすことができます。ただしこの用途では、Java SEのクラスライブラリで充足できることがほとんどであり、開発者自身が作成するケースは限定的でしょう。
2つ目は「汎用的なユーティリティ」としての用途です。スタティックメソッドに総称型を持つ引数や戻り値を指定することで、型を問わない汎用的なユーティリティを提供することができます。例えばArraysクラスのasList()メソッドはstatic <T> List<T> asList(T...)
という宣言になっていますが、これはこの用途の典型です。
3つ目は「汎用的な機能実装」としての用途です。この用途の代表的な存在が、コンパレータです。コンパレータについては『Java Basic編』で取り上げていますが、コレクションの要素をソートするときなどに、ソートロジックを実装するための機能です。コンパレータでは、ソートロジックの実装を総称型によってタイプセーフにしています。開発者自身が総称型を利用してライブラリを設計するケースは、この用途が比較的多いのではないかと思います。
このような総称型の様々な用途については、この後詳しく説明していきます。
ジェネリックなクラスの作成方法
ここでは、ジェネリックなクラスの作成方法を説明します。ジェネリックなクラスは、主に前項で述べた「汎用的なコンテナ」を提供するために作成します。以下のコード(MyContainerクラス)を見てください。
public class MyContainer<T> { //【1】
private T property;
public MyContainer(T property) { //【2】
this.property = property;
}
public T getProperty() { //【3】
return property;
}
public void setProperty(T property) { //【4】
this.property = property;
}
}
ジェネリックなクラスでは、クラス宣言するときにダイヤモンド演算子と型パラメータ(ここではT)を指定します【1】。このクラスにはコンストラクタ【2】とゲッター【3】およびセッター【4】がありますが、いずれも引数や戻り値として型パラメータTを指定しています。
このクラスは以下のように使います。
MyContainer<Integer> container = new MyContainer<>(100); //【1】
int num = container.getProperty(); //【2】
コレクションと同じように、変数を宣言するときにダイヤモンド演算子に型を指定することで、Tの型が具体的に決まります。ここではInteger型を指定しています。従ってコンストラクタにInteger型以外の型を指定しようとすると、コンパイルエラーが発生します【1】。またgetProperty()メソッドで内容を取り出すときも、Integer型であることが保証されます【2】。
ジェネリックなクラスの特性
ジェネリックなクラスには、以下のような特性があります。
- クラスの中では、型パラメータに対してメソッドを呼ぶことはできません。そもそも型パラメータの具体的な型は、後から決まるため、必然的にメソッド呼び出しはできないことになります。
- クラスの中で、型パラメータをnew演算子に指定して、インスタンス生成することはできません。(
new T()
はできない) - 型パラメータは、コンパイルされると、すべてObject型に変換されます。このような変換処理のことを、イレイジャと呼びます。また型パラメータを返す処理では、コンパイルされると、Object型から型パラメータで指定された型にキャストするための処理が自動的に追加されます。
- ジェネリックタイプにおけるメソッドのオーバーロード可否は、イレイジャによる制約を受けます。例えば2つの型パラメータを持つクラスがあったときに、以下のような2つのメソッドは、型パラメータがObject型に変換されることによりシグネチャが同一になるため、宣言できません。
class MyContainer<T, S> { // コンパイルエラー
void add(T t) { .... }
void add(S s) { .... }
}
- クラスの型パラメータを、スタティックなメンバーに指定することはできません。もし、クラスの型パラメータをスタティックなメンバーに指定できてしまうとしたら、何が起きるでしょうか。例えばMyContainerクラスが、スタティックフィールドとして、List<T>型変数を持っていたとします。スタティックなメンバーは、インスタンス単位ではなく、クラス単位に存在します。従って、あるクラスではMyContainer<Integer>を、別のクラスではMyContainer<String>をそれぞれ指定したとすると、同一のフィールドに対して異なる型が指定されたことになり、型パラメータの競合が発生します。このような理由から、スタティックなメンバーには、クラスの型パラメータを指定することはできない仕様になっています。
class MyContainer<T> {
static List<T> list = new ArrayList<>(); // コンパイルエラー
....
}
ジェネリックなインタフェースの作成方法
ここでは、ジェネリックなインタフェースの作成方法を説明します。ジェネリックなインタフェースは、主に「汎用的な機能実装」のために作成します。何らかの汎用性の高い機能をインタフェースとして表した場合、型を「決め打ち」するのではなく、パラメータ化したくなるケースがあります。
その典型がコンパレータです。コンパレータはjava.util.Comparatorインタフェースをimplementsして作成し、ソートロジックはcompare()メソッドに実装します。compare()メソッドはcompare(比較対象1, 比較対象2)
といったシグネチャになりますが、ソート対象になるのは、String型かもしれませんし、Integer型かもしれません。要はこのソートロジックは、どんな型にも対応が必要な汎用的な機能のため、このメソッドの宣言において特定の型に「決め打ち」することはできないのです。
ではいっそのことcompare(Object, Object)
といった具合に、Object型によるシグネチャにしてしまえば良いのではないか、と考えるかもしれません。それでは試しに、compare()メソッドをObject型を使って実装してみます。例えば「String型を対象にした、文字列長に応じたソートロジック」を作りたい場合は、以下のようなコードになるでしょう。
public class StrLengthComparator1 implements Comparator {
@Override
public int compare(Object o1, Object o2) {
int length1 = ((String) o1).length();
int length2 = ((String) o2).length();
return length1 - length2;
}
}
ただしこのようにすると、StringクラスのAPIを呼び出すために、受け取ったObject型の引数をString型にダウンキャストしなければなりません。またString型を対象にしたコンパレータを作ったにも関わらず、Integer型など他の型を渡したとしても、これをコンパイラで検知することはできません。そのように想定外の型が指定された場合は、実行時に初めてエラー(ClassCastException例外)が発生するため、型の安全性は保証されません。
そこで総称型を使います。
Comparatorインタフェースのcompare()メソッドは、int compare(T, T)
という宣言になっています。このインタフェースには型パラメータとしてTが宣言されており、compare()メソッドの引数も、そこと連動する形でT型として宣言されます。
前述した「String型を対象とした、文字列長に応じたソートロジック」であれば、総称型を使うと以下のようなコードになります。
public class StrLengthComparator2 implements Comparator<String> {
@Override
public int compare(String str1, String str2) {
int length1 = str1.length();
int length2 = str2.length();
return length1 - length2;
}
}
インタフェースの宣言においてComparator<String>
と宣言することで、T=String型であることが決まるため、compare()メソッドの2つの引数も連動してString型になります。
既出のコードとは異なり、compare()メソッドはString型を受け取ることが保証されるため、キャストを行う必要はありません。このように型を問わない汎用的な機能であっても、総称型によって型の安全性を保証することが可能になります。
7.1.3 総称型における互換性
ジェネリックタイプの互換性
ジェネリックなクラスやインタフェース同士は、通常のクラスと同じような互換性を持ちます。例えばList<String>型は、ArrayList<String>型の上位にあたるため、以下のコードが成り立ちます。
List<String> list = new ArrayList<>();
開発者が作成するジェネリックなインタフェースやクラスについても同様です。
例えばMyContainerIF<T>インタフェースと、それをimplementsしたMyContainer<T>クラスと、さらにそれを継承したMySubContainer<T>クラス、という3つがあるものとします。具体的には以下のようなコードです。
interface MyContainerIF<T> {
T getProperty();
void setProperty(T property);
}
public class MyContainer<T> implements MyContainerIF<T> {
// 既出のMyContainerと同じコード
........
}
public class MySubContainer<T> extends MyContainer<T> {
public MySubContainer(T property) {
super(property);
}
}
このときMyContainerIFはMyContainerの上位になるため、以下は成り立ちます。
MyContainerIF<Integer> container = new MyContainer<>(100);
またMyContainerはMySubContainerの上位になるため、以下も成り立ちます。
MyContainer<String> container = new MySubContainer<>("Hello");
型パラメータの互換性
ジェネリックタイプの互換性については既出のとおりですが、ジェネリックタイプに指定された型パラメータ同士には互換性はありません。総称型におけるこのような特性を、非変(invariant)と呼びます。
非変の説明をするために、ここでは、java.lang.Number型を取り上げます。このクラスは、以下のような継承関係になっています。
例えば、Number型はInteger型の上位にあたります。ところがArrayList<Number>型とArrayList<Integer>型の間には互換性がなく、両者は異なる型と見なされるため、以下のコードは成り立ちません。
ArrayList<Number> list = new ArrayList<Integer>(); // コンパイルエラー
ジェネリックタイプへの要素の追加・取り出しと互換性
ここでは、ジェネリックタイプへの要素の追加や取り出しを、「型の互換性」という観点で整理します。ここで取り上げる話は、必ずしも総称型固有の話ではなく、Javaの型に関する仕様を踏まえると、ある意味当然の話かもしれません。
Javaの「型の互換性」には、次のようなルールがあることは、皆さんよくご存じかと思います。すなわち、「上位の型に対して、下位の値を代入することは(暗黙的なキャストによって)可能です。一方で、下位の型に対して、上位の値を代入することはできない」というものです。
具体的には、Number型変数に対して、下位であるInteger型やLong型の値を代入することはできますが、上位であるObject型の値を代入することはできません。また、Number型の値は、上位であるObject型の変数に代入することはできますが、下位であるInteger型の変数に代入することはできません。
この点はジェネリックタイプでも同様です。ここでも、List型の変数を例として取り上げ、この挙動を説明します。
以下のコードを見てください。
List<Number> numList = new ArrayList<>();
//// 追加
numList.add(100); // 【1】OK
numList.add(100L); // 【2】OK
// numList.add(new Object()); 【3】コンパイルエラー
//// 取り出し
Object val1 = numList.get(0); // 【4】OK
// Integer val2 = numList.get(0); 【5】コンパイルエラー
まずは要素の追加です。List型の変数に対してadd()メソッドによって値(インスタンス)を追加する、という操作は、「Number型変数に対して値を代入するとき」と同じ考え方です。
つまり【1、2】のように、Number型の下位にあたるInteger型やLong型の要素を追加することは可能です。一方で【3】のように、Number型の上位にあたるObject型を追加することはできません。List型の変数に格納可能な要素は、Number型のインスタンスである必要がありますが、上位にあたるObject型は、必ずしもNumber型とは限らない(例えばString型の可能性もある)ためです。
次に要素の取り出しです。List型の変数からget()メソッドによって値(インスタンス)を取り出す、という操作は、「Number型の値を変数に代入するとき」と同じ考え方です。つまり【4】のように、要素を、Number型の上位にあたるObject型として取り出すことは可能です。一方で【5】のように、要素を、Number型の下位にあたるInteger型として取り出すことはできません。List型の変数に格納された要素はNumber型であることは保証されていますが、Integer型である保証はない(例えばLong型の可能性もある)ためです。
7.1.4 ジェネリックなメソッドと型パラメータの継承
ジェネリックなメソッド
総称型は、クラス全体ではなく、個別のメソッドだけに適用することができます。このようなメソッドを、ジェネリックなメソッドと呼びます。
ジェネリックなメソッドは、このチャプターで前述した「汎用的なユーティリティ」を提供するために作成します。スタティックメソッドには、既出のとおりクラス単位の型パラメータは指定できませんが、メソッド単位の型パラメータであればスタティックメソッドでも問題ありません。
ジェネリックなメソッドでは、型パラメータが当該のメソッドにしか適用されないので、必然的に当該メソッド内で型パラメータによる処理を完結させる必要があります。従ってジェネリックなメソッドは、スタティックメソッドによるユーティリティという用途になるケースがほとんどです。
ジェネリックなメソッドは、以下のように宣言します。
<型パラメータ名> 戻り値型 メソッド名(....) { .... }
それでは、ジェネリックなメソッドのコードを具体的に見ていきましょう。このメソッドは、引数として渡された要素を繰り返しリストに追加し、出来上がったリストを返す、というユーティリティです。
public class MyUtil {
static <E> List<E> createSameElemList(E element, int count) { //【1】
List<E> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(element);
}
return list;
}
}
createSameElemList()メソッドはジェネリックなメソッドです。ジェネリックなメソッドはこのように、戻り値の前にダイヤモンド演算子と型パラメータを指定します【1】。第一引数に指定されたE型が、戻り値であるList<E>型と連動する、という点がポイントです。
このユーティリティは、以下のようにして呼び出します。
List<String> strList = MyUtil.createSameContentList("Hello", 10);
このコードでは、型パラメータEは引数"Hello"からString型に決まるため、戻り値はList<String>型で返されます。
型パラメータの制約
ジェネリックなメソッドでは、基本的には受け取った型パラメータのメソッドを呼び出すことはできません。
ただし型パラメータがあるクラスの子であること、またはあるインタフェースの実装クラスであることが確実な場合は、その上位にあたるクラスやインタフェースのメソッドを呼び出すことができます。
型パラメータが特定のクラスの子であること、または特定のインタフェースの実装クラスであること、という制約を設けるためには、型パラメータを以下のように指定します。
<型パラメータ名 extends クラス>
<型パラメータ名 extends インタフェース>
このように、extendsキーワードによって継承元クラスまたは実装元インタフェースを指定します。インタフェースであってもextendsキーワードを使う点に、注意してください。
それでは具体例を示します。以下のコードは、前項のユーティリティをベースに、型パラメータを<E extends Number>に修正したものです。
public class MyUtil {
static <E extends Number> List<E> createSameElemList(E element) { //【1】
List<E> list = new ArrayList<>();
int count = element.intValue(); //【2】
for (int i = 0; i < count; i++) {
list.add(element);
}
return list;
}
}
createSameElemList()メソッドはジェネリックなメソッドです。このメソッドでは、型パラメータとして<E extends Number>を宣言していますので、Number型の子クラスのインスタンスしか受け取ることができません【1】。メソッドの中では、E型の変数elementがNumber型であることが保証されているため、Numberクラスのメソッドを呼ぶことができます。ここではintValue()メソッドによってint型の値を取得し、その値でループ回数を決めています【2】。
このメソッドは、以下のように外部から呼び出します。
List<Long> longList = MyUtil.createSameElemList(10L);
List<Float> floatList = MyUtil.createSameElemList(3.5F);
引数には、Number型の子クラスであればどういった型でも渡すことが可能です。
なおこのユーティリティのように型パラメータに<E extends Number>と指定した場合は、コンパイルされるとイレイジャによってNumber型に変換されます。
ジェネリックなメソッドのオーバーロード
ジェネリックなメソッドでは、extendsキーワードによって型パラーメータを指定した場合、コンパイルされるとイレイジャによって継承元クラス、または実装元インタフェースに変換されます。
ここで例として、MyUtilクラスに以下の2つのprocess()メソッドがあるものとします。
public static <T extends CharSequence> void process(T param) { //【1】
........
}
public static <T extends Comparable<T>> void process(T param) { //【2】
........
}
型パラメータTは、1つ目のメソッド【1】ではCharSequence型へ、2つ目のメソッド【2】ではComparable型へと、それぞれコンパイラによって変換されます。これらの両メソッドは、シグネチャが異なるためオーバーロードが可能です。ただしコンパイル前の段階では、これらのメソッドのシグネチャはあいまいなので呼び分けることができません。例えばString型を渡そうとすると、String型はCharSequenceインタフェースとComparableインタフェースのいずれもimplementsしているため、どちらのメソッドを呼び出そうとしているのか特定できないのです。
このような場合は、メソッド呼び出し側のコードにおいて、以下のようにダイヤモンド演算子と型を指定します。
MyUtil.<CharSequence>process("Hello");
このようにして、どちらの型パラメータに対応するメソッドを呼び出そうとしているかを明示します。
なおprocess()メソッドにInteger型を渡す場合は、自動的に2つ目のメソッドであると決定できるため、型を明示する必要はありません。
7.1.5 総称型におけるワイルドカード型
ワイルドカード型とは
コレクションフレームワークの『APIドキュメント』を見ると、<?>
、<? extends E>
、<? super E>
といった具合に、?
を使った型パラメータが宣言されていることに気が付くと思います。このように?
を用いたジェネリックタイプを、ワイルドカード型と呼びます。
ワイルドカード型には、境界のないワイルドカード型(<?>
)や、上限境界ワイルドカード型(<? extends T>
)、下限境界ワイルドカード型(<? super T>
)といった種類があります。
このレッスンではまず、境界のないワイルドカード型を取り上げます。なお以降では、境界のないワイルドカード型を単にワイルドカード型と呼称します。
非境界ワイルドカード型によるAPI設計
ワイルドカードとは、任意の型を表す、型パラメータです。List<?>であれば、「任意の型を要素として持つリスト」という意味になります。このように任意の型をワイルドカードによって表したジェネリックタイプを、非境界ワイルドカード型と呼びます。
例えばList<Integer>とList<String>など、あらゆるリストに対して共通的な処理を行うメソッドを、非境界ワイルドカード型によって実装してみましょう。以下のコードを見てください。
public class MyUtil {
public static void printList(List<?> list) { //【1】
for (Object obj : list) {
System.out.println(obj);
}
}
}
このメソッドはList<?>型を受け取っています【1】が、この型はあらゆる型のリストと互換性があります。従ってこのメソッドには、List<Integer>やList<String>を、以下のようにそのまま渡すことができます。
List<Integer> intList = Arrays.asList(1, 2, 3);
MyUtil.printList(intList);
List<String> strList = Arrays.asList("foo", "bar", "baz");
MyUtil.printList(strList);
非境界ワイルドカード型の操作とタイプセーフ
List<?>型は、任意の型を要素として持つリストを表します。ただしこの話を聞くと、原型のList(総称型を使わないList)や、型パラメータにObjectクラスを指定したList(List<Object>)と、何が違うのかと疑問に感じるかもしれませんが、それぞれ特性が異なります。
まず原型のListです。原型のListは、任意の型を持つListと互換性があります。つまり以下のコードは成り立ちます。
List<Integer> list1 = new ArrayList<>();
List list2 = list1;
ただし原型のListには、あらゆる型の要素を追加したり取り出したりすることができるため、型の安全性が保証されないのは既出のとおりです。
次に型パラメータにObjectクラスを指定したListです。List<Object>と、List<Integer>の間に互換性はないため、以下はコンパイルエラーになります。
List<Integer> list1 = new ArrayList<>();
List<Object> list2 = list1; // コンパイルエラー
またList<Object>には、あらゆる型の要素を追加したり取り出したりすることができるため、型の安全性は保証されません。
最後にList<?>型です。この型は、前項でも触れたとおり、任意の型を持つListと互換性があります。つまり以下のコードが成り立ちます。
List<Integer> list1 = new ArrayList<>();
List<?> list2 = list1;
List<?>が、原型のListやList<Object>と異なるのは、型の安全性が保証される、という点です。変数がList<?>型として宣言されると、その変数はどのような型のリストなのかが不明なため、要素を追加することができません(例外としてnull値を渡すことは可能)。また要素を取り出すことはできますが、Object型で返されます。
このように操作に一定の制約は生じますが、型の安全性を保証しながら、同時に汎用性も確保するのが、ワイルドカード型の特徴です。
7.1.6 上限・下限境界ワイルドカード型
上限境界ワイルドカード型とは
前述したように、総称型には非変という特性があり、型パラメータ同士に互換性はありません。ただし型パラメータに互換性がないことによって、実装の効率性が損なわれてしまうケースがあります。
例えばNumber型のリストに対して、その合計値を計算して表示するユーティリティを作成するものとします。まず以下のコードを見てください。
public class MyNumberUtil_1 {
public static void printSum(List<Number> list) {
long total = 0;
for (Number num : list) {
total = total + num.longValue();
}
System.out.println(total);
}
}
このコードは一見すると要件を満たしているように見えますが、printSum()メソッドの引数であるList<Number>型は、List<Integer>型やList<Long>型とは、互換性がありません。従って、以下のコードはコンパイルエラーになります。
List<Integer> intList = Arrays.asList(1, 2, 3);
MyNumberUtil_1.printSum(intList); // コンパイルエラー
このように、非変であるが故の柔軟性の欠如を解決するための仕組みが、上限境界ワイルドカード型です。
先ほどのユーティリティを、以下のように修正します。
public class MyNumberUtil_2 {
public static void printSum(List<? extends Number> list) { //【1】
long total = 0;
for (Number num : list) {
total = total + num.longValue();
}
System.out.println(total);
}
}
printSum()メソッドの引数に注目してください。受け取る型が、List<? extends Number>になっています【1】。この型は「Number型を継承した任意のクラスのリスト」という意味なので、List<Integer>型や、List<Long>型との間には、互換性が確保されます。今度は、以下のコードはコンパイルエラーにはなりません。
List<Integer> intList = Arrays.asList(1, 2, 3);
MyNumberUtil_2.printSum(intList);
上限境界ワイルドカード型の特徴
ここでは、上限境界ワイルドカード型についてもう少し深掘りします。上限境界ワイルドカード型の特徴は、指定されたクラスへの下位互換性の保証にあります。
例えばList<? extends Number>型を受け取る、以下のようなメソッドがあるもとします。
public static void process(List<? extends Number> list) {
// 取り出しは可能
Number num = list.get(0); //【1】
// list.add(100);【2】
// list.add(new Object());【3】
}
List<? extends Number>型から取り出した要素は、Integer型かもしれないし、Long型かもしれません。いずれにしてもNumber型の子クラスであることが保証されているため、Number型として扱うことが可能です。従ってget()メソッドは問題ありません【1】。
ところが、このリストに対してadd()メソッドによってInteger型の要素を追加しようとすると、コンパイルエラーになります【2、3】。なぜならList<? extends Number>型は、List<Number>型かもしれないが、List<Integer>型、またはList<Long>型の可能性もあります。Number型の子クラスのリストである点が保証されているとはいえ、どのような要素を格納するためのリストなのかが分からない以上、追加ができてしまうと型の安全性が保証されなくなるためです。当然、Number型の上位にあたるクラスのインスタンス(Object型など)も、型の安全性の観点から追加はできません。
このように上限境界ワイルドカード型には、「当該の型を戻り値として返すメソッドは呼び出し可能だが、その反面、当該の型を引数に取るメソッドは呼び出しができない」という特徴があります。
下限境界ワイルドカード型の特徴
今度は、下限境界ワイルドカード型の特徴を見ていきます。下限境界ワイルドカード型の特徴は、指定されたクラスへの上位互換性の保証にあります。
例えばList<? super Integer>型を受け取る、以下のようなメソッドがあるもとします。
public static void process(List<? super Integer> list) {
// 以下はコンパイルエラー
// Integer num = list.get(0); 【1】
// Objectとしてしか取得できない
Object num = list.get(0); //【2】
// 追加は可能
list.add(100); //【3】
}
List<? super Number>型は、「Number型またはその上位にあたる任意のクラスのリスト」という意味です。したがって、List<Number>型はもちろん、List<Object>型の間に互換性が確保されます。つまりList<? super Number>には、List<Number>型なのかList<Object>型なのか分かりませんが、いずれにしても「Numberの上位クラスのリスト」を代入することが可能です。
逆に言うとNumber型リストである保証はない(Object型リストの可能性もある)ため、get()メソッドにより要素をNumber型として取り出すとコンパイルエラーになります【1】。つまり【2】のように、get()メソッドはObject型しか返せない、ということになります。
次にadd()メソッドです。add()メソッドによって、Integer型の要素を追加することは可能です。なぜならこのリストは、Integer型を含めてその上位に位置付けられるクラスのリストであることが、下限境界ワイルドカード型によって保証されているためです。
このように下限境界ワイルドカード型には、「当該の型を引数に取るメソッドは呼び出し可能だが、その反面、当該の型を戻り値として返すメソッドはObject型として返される」という特徴があります。
上限境界ワイルドカード型の特徴再整理
上限・下限境界ワイルドカード型の特徴は既出のとおりですが、理解するのは容易ではありません。ここでは、例を使ってこれらの仕様の特徴を、改めて整理してみます。
まず上限境界ワイルドカード型、List<? extends Number>に対する追加と取り出しです。List<? extends Number>には、List<Number>型、List<Integer>型、List<Long>型などを代入可能なのは、前述したとおりです。逆に言うと、List<Number>型、List<Integer>型、List<Long>型、いずれが代入されていたとしても、共通的な操作しか許容されないことになります。
それぞれの型に対して、Number型を中心に、上位であるObject型、下位であるInteger型の3つの型を追加しようとした場合と取り出そうとた場合、実施可能かどうかを以下の表にまとめます。
代入可能性のある型 | 追加 | 取り出し | ||||
---|---|---|---|---|---|---|
Object | Number | Integer | Object | Number | Integer | |
List<Number> | ✕ | 〇 | 〇 | 〇 | 〇 | ✕ |
List<Integer> | ✕ | ✕ | 〇 | 〇 | 〇 | 〇 |
List<Long> | ✕ | ✕ | ✕ | 〇 | 〇 | ✕ |
まとめ | ✕ | ✕ | ✕ | 〇 | 〇 | ✕ |
まず追加、つまりリストの場合はadd()メソッドです。
最初にList<Number>型の場合は、Object型は追加できませんが、Number型やInteger型は追加可能です。次にList<Integer>型の場合は、Object型やNumber型は追加できませんが、Integer型は追加可能です。最後にList<Long>型の場合は、Object型、Number型、Integer型、いずれも追加できません。List<? extends Number>が、List<Number>型、List<Integer>型、List<Long>型のいずれかにもなりうる、という可能性を考慮すると、一つでも×が付いたらその操作はNGです。つまりこの表にあるように、型の安全性の観点からは、追加は一切不可、ということになります。
次に取り出し、つまりリストの場合はget()メソッドです。
最初にList<Number>型の場合は、Object型やNumber型として取り出し可能ですが、Integer型として取り出しは不可です。次にList<Integer>型の場合は、Object型、Number型、Integer型、いずれの型でも取り出し可能です。最後にList<Long>型の場合は、Object型、Number型として取り出し可能ですが、Integer型として取り出すことはできません。このように整理すると、List<? extends Number>からは、Object型やNumber型として要素を取り出すことができる、ということが分かります。
下限境界ワイルドカード型の特徴再整理
ここでは下限境界ワイルドカード型、List<? super Number>に対する追加と取り出しを整理します。
List<? super Number>には、List<Object>型、List<Number>型などを代入可能なのは、前述したとおりです。逆に言うと、List<Object>型、List<Number>型、いずれが代入されていたとしても、共通的な操作しか許容されないことになります。
それぞれの型に対して、Number型を中心に、上位であるObject型、下位であるInteger型の3つの型を追加しようとした場合と取り出そうとした場合、実施可能かどうかを以下の表にまとめます。
代入可能性のある型 | 追加 | 取り出し | ||||
---|---|---|---|---|---|---|
Object | Number | Integer | Object | Number | Integer | |
List<Object> | 〇 | 〇 | 〇 | 〇 | ✕ | ✕ |
List<Number> | ✕ | 〇 | 〇 | 〇 | 〇 | ✕ |
まとめ | ✕ | 〇 | 〇 | 〇 | ✕ | ✕ |
まず追加、つまりリストの場合はadd()メソッドです。
最初にList<Object>型の場合は、Object型、Number型、Integer型、いずれも追加可能です。次にList<Number>型の場合は、Object型は追加できませんが、Number型やInteger型は追加可能です。List<? super Number>が、List<Object>型、List<Number>型のいずれかにもなりうる、という可能性を考慮すると、一つでも×が付いたらその操作はNGです。
つまりこの表にあるように、型の安全性の観点からはNumber型およびその子クラスに限って追加可能、ということになります。
次に取り出し、つまりリストの場合はget()メソッドです。
最初にList<Object>型の場合は、Object型としては取り出し可能ですが、Number型やInteger型として取り出しはできません。次にList<Number>型の場合は、Object型やNumber型としては取り出し可能ですが、Integer型として取り出しはできません。このように整理すると、List<? super Number>からは、Object型としてしか取り出すことができない、ということが分かります。
上限・下限境界ワイルドカード型の組み合わせ
上限・下限、それぞれの境界ワイルドカード型の特徴を踏まえて、両者を組み合わせた典型的な利用方法を説明します。
それは、コレクションにおける要素のコピーです。
ここでFoo、Bar、Bazという3つのクラスがあり、上位から下位に向かってFoo、Bar、Bazという順に継承関係があるものとします。
仮にBar型のリストから別のBar型リストへ、要素をコピーするためのユーティリティを作るとしたら、以下のようなコードになるでしょう。
public static void copy(List<Bar> src, List<Bar> dest) {
for (Bar bar : src) {
dest.add(bar);
}
}
このメソッドは2つのBar型のリストを引数に取り、第一引数のリストから、第二引数のリストへ、要素の詰め換えを行っています。ただしこのメソッドは、コピー元もコピー先もBar型リストにしか対応していないため、汎用性の観点で課題があります。
そこで境界ワイルドカード型を使って、このメソッドをより柔軟な仕様に変更してみましょう。まずコピー元は、Barだけではなく、その子クラス(Bazなど)のリストを対象にします。またコピー先は、Barだけではなく、その親クラス(Fooなど)のリストを対象にします。具体的には以下のコードを見てください。
public static void copy(List<? extends Bar> src, //【1】
List<? super Bar> dest) { //【2】
for (Bar bar : src) {
dest.add(bar);
}
}
このメソッドでは、第一引数、すなわちコピー元はList<? extends Bar>とし【1】、第二引数、すなわちコピー先をList<? super Bar>としています【2】。コピー元は上限境界ワイルドカード型のため、Bar型またはその下位クラスのリストであることが保証され、リストからはBar型として要素が取り出されます。またコピー先は下限境界ワイルドカード型のため、Bar型またはその上位クラスのリストであることが保証され、リストにはBar型として要素が追加されます。
このように上限・下限境界ワイルドカード型を組み合わせることで、コレクションに対する柔軟性の高いユーティリティを作成することが可能になります。
このチャプターで学んだこと
このチャプターでは、以下のことを学びました。
- 総称型のコレクションフレームワークにおける利用や、その恩恵について(復習)。
- ジェネリックタイプの宣言方法や、型パラメータの指定方法、命名規約について。
- Java SEクラスライブラリにおける総称型の全容について。
- 総称型を利用したAPI設計やジェネリックなクラスおよびインタフェースの作成方法について。
- ジェネリックなメソッドの作成方法や型パラメータの制約、オーバーロードメソッドの解決方法について。
- ワイルドカード型の特徴や、それを使ったAPI設計について。
- 上限境界ワイルドカード型および下限境界ワイルドカード型の特徴や、それらを使ったAPI設計について。