検索

キーワード


【Java】ジェネリクスの役割・使い方解説!コレクション以外での使用方法

  • 公開日:2020-10-06 14:58:16
  • 最終更新日:2021-01-25 18:58:58
【Java】ジェネリクスの役割・使い方解説!コレクション以外での使用方法

こんにちは、駆け出しプログラマーの若江です!

ここでは初学者として学習を終えた私からアウトプットの意味も込めて、

ジェネリクスの基礎知識について紹介させていただきます。

できる限り初学者が理解しやすい内容として紹介させていただくので、参考となれば幸いです!


関連記事リンク:参照型とは? / thisとsuperの使い方 / 継承の方法 / Listの使い方 / Iteratorの使い方 / ジェネリクスとは? / クラスの基礎 / インスタンスとは? / メソッドの作成方法 / 変数とは?

ジェネリクスの基本と使い方

ジェネリクスはクラスやインスタンス、メソッドに使われる型の宣言に関わる機能です。

ここではジェネリクスの役割や使い方について紹介させていただきます。


ジェネリクスとは?

ジェネリクスとは「<>(山カッコ)」で記述される型宣言の機能を指し、

コレクションの List などでよく使われるため見たことがあるかもしれません。

ジェネリクスを使った代表的なものとして List が挙げられますが、

それ以外にも自作のクラスやメソッドに付与することもできます。


ジェネリクスの紹介にあたりまずは「不変」「共変」「反変」という3つの変性を説明します。

これら3つを知ることでジェネリクスのことがより理解しやすくなります。


不変

ジェネリクスの基本的な仕組みは「不変」です。

ジェネリクスは扱う型の種類を必要に応じて指定することができる機能であり、

インスタンスを生成するタイミングまで、型の種類を決めないでおくことができます。

指定した型以外は、たとえ継承関係にあるクラスだったとしても扱うことができません。


 例えば、Number クラスを親に持つ Integer クラスの型であっても、

 ジェネリクスで Number クラスの型を指定したなら、Number クラスの型しか扱えません。

 よって Number クラスの型に Integer クラスの 型を代入することなどができません。


このように、指定した型以外を扱わないため不変と言われます。


共変

継承関係にあるクラスの型を扱うことができます。


 例えば通常の継承関係の仕組みと同じように、

 Number クラスの型に Integer クラスの型を代入して扱うことができます。



反変

子クラスの型に親クラスの型を代入するなどをして扱うことができます。


 共変とは逆の仕組みを取ることができ、

 Integer の型に Number 型を代入することなどができます。



ジェネリクスは不変の変性以外も、

ワイルドカードという機能を使うことで共変や反変を行うことができます。


ジェネリクスを使うメリット

型の指定を受けるまで、具体的に扱う型が決まっていない List のような型のことを「原型」と呼びます。

List などにはジェネリクスが使われており、ジェネリクスのお陰で扱う型を自在に指定することができます。


原型のように型が決定していないものは、様々なデータの型を扱うことができるということを意味ます。

ジェネリクスの機能が実装される以前は、

List などに Integer 型のデータや String 型のデータ、Double 型のデータなど様々な型を

保持させることができてしまうため実行時に取り出すまでどのような型を扱っているかわからず、

もし意図しないデータの型が含まれていたときに例外が発生するといった事態に陥ることが予想されました。

昔、実際にジェネリクスが導入される前までは頻繁にこのようなエラーが起こっては、

プログラマーを悩ませていたようです。


 例えば List は幅広いデータを保有できる機能を持っているため、型の指定はされていません。

 そうすると List の中に Integer型や String型、Boolean型といった様々な型のデータを保有できてしまいます。

 一方 List のデータを取り出す際はキャストなどを行い型の指定をしなくてはなりません。

 そのため String 型を指定すれば、Integer型と Boolean型では型の不一致が起こるということです。

  ※コレクションの List は別記事で紹介させていただいています。



List に限らず例のような問題が発生する可能性が様々な場面でありましたが、

それらをジェネリクスの機能実装により解決することができるようになりました。


ジェネリクスのメリット

・コレクションのような様々な型を許容するものに対して型の指定ができる

・型の指定をすることによって、どのクラス型を扱っているかすぐにがわかる

・コンパイラが型の一致/不一致を判断してくれる

・型を指定するため、データを取り出す際にキャストの記述を省略できる


次項のジェネリクスの書き方編でこれらメリットを確認してみましょう。



ジェネリクスの書き方

ジェネリクスは「<>(山カッコ)」でクラスやメソッドなどに記述されます。

ジェネリクスには付与する側と、型を指定する側があります。


ジェネリクスの付与

ジェネリクスはクラスやインターフェース、メソッドなどに付与することで、

インスタンスの生成やメソッドの処理の際にし指定された型を受け取ることができます。

型を受け取ることができるだけで、具体的な型は持っていません。


例えば List インターフェースは「List< E >」と設定されています。

この「< E >」は「型変数」や「型パラメーター」と呼ばれ、

「様々な属性の型を受け取れます」という意味になります。

名前の通り型を変動する値のように扱うことができるものが型変数です。


型変数の名前は大文字でなくても、また一文字でなくても設定はできますが、

一般的に「< E >」のように大文字の一文字で表現され、

受け取る型の目的に合わせて E と記述されたり T と記述されています。


型の指定

設定されたジェネリクスに対して、

先述の「< E >」のようなものへ型の指定をすることができます。

List インターフェースを例に型を指定するサンプルコードを見てみましょう。


◆ List を使ったジェネリクスの使用例

import java.util.ArrayList;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		List<String> list = new ArrayList<String>();
	}
}


上記例では List< E > の E に対して String の型を指定しました。

このようにジェネリクスの中で型を指定します。

ちなみに E を「仮型パラメータ」、String などで指定したものを「実型パラメー」と呼びます。


記述する型はクラス型(参照型)となっており、

int 型や boolean 型などのプリミティブ型を使うことができないため注意しましょう。


先ほど紹介したメリットも部分ごとにサンプルコードで確認しましょう。

ここでも List を例に使ってみます。


◆ジェネリクスのメリット

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		// List が扱っている型が一目でわかる
		List<Integer> list = new ArrayList<Integer>();

		// コンパイラが型のチェックを行ってくれる
		list.add(1);
		list.add("a"); // 型の不一致でエラー
		list.add(2);


		Iterator<Integer> iterator = list.iterator();
		while(iterator.hasNext()) {
			int num = iterator.next(); // 「(int)iterator.next();」といったキャストが不要
			System.out.println(num);
		}
	}
}


・「List<Integer> list = new ArrayList<Integer>();

 一目で Integer 型のデータを扱う List であることがわかります。

・「list.add(1);」「list.add("a");」「list.add(2);

 String 型の a が混じっていることをコンパイラーが判断して「"a"」に対してコンパイルエラーを出します。

・「int num = iterator.next();

 キャストで型の指定をしなくても処理が行われています。

 今回のようにキャストの回数が1回程度であればメリットというほどにはなりませんが、

 キャスト回数が増えるほどに負担が軽減されます。


ダイヤモンド演算子

ジェネリクスの型指定は左側と右側で必ず同じ型が書かれてなければいけません。

 例1: List< 左側の型 > list = new ArrayList< 右側の型 >();

そのため、左側の型が指定されれば右側の型に何が書かれるか決まります。

これは一種の手間となるため Java7 から右側の型記述を省略するすることが可能となりました。

 例2: List< 左側の型 > list = new ArrayList<>();

右側の型記述を省略したことで「<>」だけが残り、

その見た目からダイヤモンド演算子と呼ばれるようになりました。



ワイルドカードとは?

型変数で設定した型変数に対して、ワイルドカードを利用することで

冒頭で紹介した「共変」や「反変」をジェネリクスで実現させることができます。


ワイルドカードとは「?」で表記されるもので、

プログラマーに対して「実行時までどんな型でも受け取ることができます」という意味を持ちます。

また型変数に対して「具体的な型の受け渡しはまだ待ってください」と言っているようなイメージです。

そのため、ワイルドカードの使いどころは型の指定時であるインスタンス生成時やメソッドの処理時などとなります。

一方型変数はクラス作成時やメソッド作成時などで使われる違いがあります。


実行時までどんな型でも受け取るワイルドカードですが、

言い換えると実行時までどんな型のデータを扱うか決定していないということです。

そのため、ワイルドカードの型は Object 型であることが特徴です。

このようにジェネリクスで受け取る型の許容度を緩めることができるのがワイルドカードです。



非境界ワイルドカード

「?」で表したどんな型でも受け取れるワイルドカードは「非境界ワイルドカード」と呼ばれます。

非境界ワイルドカードを付与した原型やメソッドなどは、

null を除くすべての具体的な型を持つデータの処理に関わる操作ができません。

これは非境界ワイルドカードが実行時までどのような型を扱うか決まっていないためです。

例えばワイルドカードを使った List で add メソッドを呼ぼうとしてもエラーとなります。


◆ List< E > で非境界ワイルドカードを使った例

import java.util.ArrayList;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		List<?> list = new ArrayList<Integer>();
		list.add(1); // コンパイルエラー
	}
}


ちなみに非境界ワイルドカードを付与したものが保持するデータは、

どのような型にも成り得るため、Object 型のデータを扱います。

import java.util.ArrayList;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		List<?> list = new ArrayList<Integer>();
		Object object = list.get(0);
		Integer object2 = list.get(0); // コンパイルエラー
	}
}


Object 型のデータを扱うため、

変数 object には非境界ワイルドカードを付与した list の代入ができるが、

Integer 型の object2 には list の代入ができません。



上限境界ワイルドカード

ワイルドカードの型許容範囲を指定した親クラスを上限として、

親クラスから派生するクラスまでに絞ったものを「上限境界ワイルドカード」と呼びます。

上限境界ワイルドカードの書き方は、ワイルドカードを継承する形で記述されます。

・上限境界ワイルドカードの基本構文: <? extends 親クラスの型>


原型を List とした Number 型と Integer 型、Long 型の例を見てみましょう。


◆上限境界ワイルドカードの例

import java.util.ArrayList;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		List<? extends Number> list = new ArrayList<Integer>();
		List<Integer> listInt = new ArrayList<>();
		List<Long> listLong = new ArrayList<>();
		List<String> listStr = new ArrayList<>();

		list = listInt; // Integer 型の代入を許容
		list = listLong; // Long 型の代入を許容
		list = listStr; // コンパイルエラー
	}
}


List (原型) の 変数(list) の型である <? extends Number> には、

Number 型に属する Integer 型・Long 型の代入ができますが、

Number 型に属さない String 型の代入はできませんでした。

冒頭で紹介したジェネリクスを使った共変がこの形にあたります。


また非境界ワイルドカードと同じく、

null を除いて具体的な型を持つデータの処理に関わる操作ができません。

import java.util.ArrayList;
import java.util.List;
public class GenericsTest {
	public static void main(String[] args) {

		List<? extends Number> list = new ArrayList<Integer>();
		list.add(1); // コンパイルエラー		
	}
}


1 は Number クラスに属するクラス( Integer )ではあるものの、

add する型と一致することが保証できない( Long や Double かもしれない)ためエラーとなります。



下限境界ワイルドカード

ワイルドカードの型許容範囲の下限を、

指定した子クラスまでに限定したものを「下限境界ワイルドカード」と呼びます。

下限境界ワイルドカードの書き方は、ワイルドカードに super を付与する形で記述されます。

・下限境界ワイルドカードの基本構文: <? super クラスの型>


指定したクラスの型以上の継承元クラスが型の許容範囲です。

こちらも List を使ったサンプルコードで確認してみましょう。

ここでは「親クラス」「子クラス」「子を継承したクラス」の3つの自作クラスを作っています。


◆下限境界ワイルドカードの例

【親クラス】

public class ClassParent {
}

【子クラス】 ClassParent を継承

public class ClassChild extends ClassParent {
}

【子クラスを継承した子クラス】 ClassChild を継承

public class ClassGrandson extends ClassChild {
}

【インスタンス生成クラス】

import java.util.ArrayList;
import java.util.List;
public class Test {
	public static void main(String[] args) {

		List<? super ClassGrandson> list = new ArrayList<>();
		List<ClassChild> listC = new ArrayList<>();
		List<ClassParent> listP = new ArrayList<>();
		
		list = listP; // ClassGrandson 以上の継承元であるため代入できる
		list = listC; // ClassGrandson 以上の継承元であるため代入できる
	}
}


このように ClassGrandson の型に ClassChild や ClassParent を代入することができるため、

冒頭で紹介したジェネリクス使った反変が可能とります。


非境界ワイルドカードと上限境界ワイルドカードでは add などのメソッドが利用できませんでしたが、

下限境界ワイルドカードでは、指定した下限クラスに限り add などの型の操作が可能です。

これは下限クラスが必ずそのクラス以上のクラスを継承していることが保証されるためです。


先ほどの「親」「子」「子を継承したクラス」の3つを使ったサンプルコードを見てみましょう。


◆下限境界ワイルドカードの型操作例

import java.util.ArrayList;
import java.util.List;
public class Test {
	public static void main(String[] args) {

		List<? super ClassGrandson> list = new ArrayList<>();
		list.add(new ClassGrandson());
	}
}


コンパイルエラーなく add メソッドを利用できました。

ちなみに ClassChild を add しようとすると、

<? super ClassGrandson> が ClassParent なのか ClassGrandson なのか、

または ClassParent にも更に親クラスが存在するかもしれず判断できないためエラーとなります。



上限境界ワイルドカード・下限境界ワイルドカードの実用例

一般的には List の型指定で利用するなどといったローカル変数での利用は稀で、

メソッドの引数で利用されることがほとんどです。

メソッドの処理にあたり受け取る引数の型を <? extends 型> のように幅を持たせるか、

<? super 型> のように型の幅に制限をかけるといった使い方をします。


サンプルコードでは継承関係にあるクラスを3つと、

メソッド引数にジェネリクスを付与したクラスを1つ用意しています。

※クラスやメソッドへのジェネリクス付与方法は別記事で紹介させていただきます。


◆メソッドの引数に上限境界ワイルドカードを利用した例

【親クラス】

public class ClassParent{
}

【子クラス】

public class ClassChild extends ClassParent {
}

【子クラスを継承したクラス】

public class ClassGrandson extends ClassChild {
}

【ジェネリクスを付与したメソッドを持つクラス】 継承関係にあるクラスを上限境界ワイルドカードで指定

import java.util.List;
public class WildCardObject {
	public void wildcard(List<? extends ClassParent> list) {
		System.out.println(list);
	}
}

【実行クラス】 継承関係にあるインスタンスを保持した List型インスタンスを WildCardObject のメソッドの引数へ渡す

import java.util.ArrayList;
import java.util.List;
public class Test {
	public static void main(String[] args) {

		WildCardObject test = new WildCardObject();
		List<ClassParent> list = new ArrayList<>();

		list.add(new ClassParent());
		list.add(new ClassChild());
		list.add(new ClassGrandson());

		test.wildcard(list);
	}
}


上記の継承クラスを利用して下限境界ワイルドカードの利用例も見てみましょう。


◆メソッドの引数に下限境界ワイルドカードを利用した例

【ジェネリクスを付与したメソッドを持つクラス】 継承関係にあるクラスを下限境界ワイルドカードで指定

public class WildCardObject {

	public void wildcard(List<? super ClassGrandson> list) {
		System.out.println(list);
	}
}

【実行クラス】 ClassGrandson のインスタンスを保持した List型インスタンスを WildCardObject のメソッドの引数へ渡す

import java.util.ArrayList;
import java.util.List;
public class Test {
	public static void main(String[] args) {

		WildCardObject test = new WildCardObject();
		List<ClassGrandson> list = new ArrayList<>();

		list.add(new ClassGrandson());

		test.wildcard(list);
	}
}


このようにメソッドの引数が受け取る型の範囲を調整することが可能となります。



まとめ

ジェネリクスを利用することでクラスやメソッドなどで幅広い型の扱い方ができるようになります。

事前に用意されたクラスやメソッドでもよく使われる機能なので、

ジェネリクスの知識を押さえておくことで様々なクラスやメソッドの役割が理解しやすくなるはずです。


関連記事リンク

参照型とは? / thisとsuperの使い方 / 継承の方法 / Listの使い方 / Iteratorの使い方 / ジェネリクスとは? / クラスの基礎 / インスタンスとは? / メソッドの作成方法 / 変数とは?



【著者】

若江

30代で異業種となるIT業界へ転職した駆け出しのプログラマです。これまで主に Java や Ruby、HTML/CSS を使って学習を目的としたショップサイトや掲示板サイトの作成を行いました。プログラマとしての経験が浅いからこそ、未経験者の目線に近い形で基礎の紹介をしていきたいと思います。

よく読まれている記事
【Java】JSPでタグライブラリを使う(JSTL)

【Java】JSPでタグライブラリを使う(JSTL)

こんにちは。エンジニアの新田です!ここでは、システムエンジニアとして働いている私が、システム開発手法や開発言語について紹介していこうと思います。今回は、JSPの標準タグライブラリ「JSTL」について紹介します。Javaについて勉強している方、Webアプリケーションを構築したいと思っている方の参考になれば幸いです!関連記事リンク: 【Java】JSPの基本的な構文/【Java】JSPのアクションタグ

【Java】Stringクラス文字列を操作するメソッドの使い方まとめ!実例も紹介!

【Java】Stringクラス文字列を操作するメソッドの使い方まとめ!実例も紹介!

こんにちは。新人エンジニアのサトウです。システムエンジニアとして駆け出したばかりですが、初心者なりの視点でわかりやすい記事を心がけていますので参考になればうれしいです。プログラム初心者✅にも、プログラムに興味がある人✨も、短い時間で簡単にできますのでぜひこの記事を読んで試してみてください!そもそもStringとは何?『 String 』... Java言語において文字列のデータ型を指します。基本デ

【Java】文字列の置き換え(String#format)!エスケープシーケンスのまとめも!!

【Java】文字列の置き換え(String#format)!エスケープシーケンスのまとめも!!

こんにちは。新人エンジニアのサトウです。システムエンジニアとして駆け出したばかりですが、初心者なりの視点でわかりやすい記事を心がけていますので参考になればうれしいです。プログラム初心者✅にも、プログラムに興味がある人✨も、短い時間で簡単にできますのでぜひこの記事を読んで試してみてください!Stringクラスformatメソッドの文字列整形【java.utilパッケージ】Formatterクラスfo

【Java】文字列格納後に変更可能!?StringBufferクラスとStringBuilderクラス!

【Java】文字列格納後に変更可能!?StringBufferクラスとStringBuilderクラス!

こんにちは。新人エンジニアのサトウです。システムエンジニアとして駆け出したばかりですが、初心者なりの視点でわかりやすい記事を心がけていますので参考になればうれしいです。プログラム初心者にも✅、プログラムに興味がある人✨も、短い時間で簡単にできますのでぜひこの記事を読んで試してみてください!文字列を扱う3つのクラス【java.langパッケージ】java.langパッケージの文字列を扱うクラスにはS

【Java】値?変数?型??しっかり解説!『データ型(プリミティブ型と参照型)』

【Java】値?変数?型??しっかり解説!『データ型(プリミティブ型と参照型)』

こんにちは。新人エンジニアのサトウです。システムエンジニアとして駆け出したばかりですが、初心者なりの視点でわかりやすい記事を心がけていますので参考になればうれしいです。プログラム初心者✅にも、プログラムに興味がある人✨も、短い時間で簡単にできますのでぜひこの記事を読んで試してみてください!プリミティブ型と参照型プログラム開発では型を持った変数を使ってデータのやり取りをしますが、データ型によって仕様