(むかしのメモ)

どうしよう。むずかしい

ので私が解るようになるまでまとめようと思います。利己的ですね。


まずジェネリクスが一体何なのか掴むために、たくさんの一言でイメージを挙げてみましょう

  • クラス等に”型”の変数を持たせる > 型パラメータ
  • 異なる複数のクラスに対しても同じ動作をさせる
  • 任意のオブジェクトを所持できるデータ構造をつくる

ん~???


書き方

Javaにおいてジェネリクスは ダイヤモンド演算子 < > で記述します。

class Hoge <T> {
	  // 処理...
}
 
class Ex {
		public static void main(String[] args) {
				Hoge<Number> hoge = new Hoge<Number>();
		}
}

さっそく頭が痛くなってきた

さてと、この例ではHogeクラスにNumber型をセットしています。

(Number型はIntegerDoubleなどの数に関連したラッパークラスの基底、親クラス、抽象)

クラス宣言時は、クラス名の右に<型パラメータ名>を記述。 ここではTという名前の型パラメータを定義する。

インスタンス生成時はmain()に書いてあるとおり、クラス名の右に<型名>を記述…

なんだか長ったらしいですね。少し短くしましょうか。

// これが
Hoge<Number> hoge = new Hoge<Number>();
// こうなって
Fuga<Number> fuga = new Fuga<>();
// こう
var piyo = new Piyo<Number>();
// すべて同じ意味

急に出てきたvarは型推論というもの。
右辺の型が自明であった場合自動的にその型の参照変数になる


使用例

それでは、実際の使用例をば。

その前に一つ復習をしておきます。 Object型はプリミティブ型を除く全てのクラスの基底クラスです。
全てのクラスはObjectクラスを継承しています。
そしてこれから説明するジェネリクスはプリミティブ型を扱うことが出来ません。
型パラメータにはクラスのみが入ります。

それでは、実際の使用例をば。

class Hoge<T> {
		// フィールド
		private T t;
		
		// コンストラクタ
		Hoge(T t) {
				this.t = t;
		}
		
		// コンストラクタで代入した、現在持っているオブジェクトを返す
		T get() {
				return t;
		}
		
		// 現在持っているオブジェクトを表示
		void print() {
				System.out.print("current param : ");
				System.out.println(t);
		}
}
 
class Ex {
		public static void main(String[] args) {
				var hoge = new Hoge<Integer>(Integer.valueOf(123));
				System.out.println(hoge.get() + hoge.get());
				hoge.print();
				/*コンソール
						246
						current param : 123
				*/
		}
}

うわっ

丁寧に一つづつ説明します。

まずクラス宣言からのprivate T t;

これは設定した型パラメータの型の、オブジェクトのフィールド変数です。

次にコンストラクタ

引数として、インスタンス生成時< >に入れた型を取り、先程のフィールド変数に代入する。
これでフィールド変数tには< >で受け取った型のオブジェクトが入っていることになります。

get()メソッド

先程のコンストラクタで設定した型パラメータTの型のオブジェクトtを返す。

print()メソッド

まずSystem.out.print()メソッドはObjectを引数にとります。
復習の通り、Objectは全てクラスの基底クラスの為、Objectクラスが持つメソッドは全てのクラスが利用できます。
そしてSystem.out.print()メソッドはそのObjectクラスが持つtoString()メソッドを利用しているため、型パラメータにどんな型を設定したとしても実行することができます。
型パラメータにはプリミティブ型も設定できないので文字通り例外なく実行することができます。
長々と書きましたが、このように複数の型でも実行することが出来る仕組みを提供するのがジェネリクスなのです。
で、フィールド変数tをコンソールに表示する。

ふぅ

次にmainメソッドの中身を

まずインスタンスを生成。

型パラメータには型を設定。コンストラクタにはその型のオブジェクトを引数に。

次にget()メソッドで返された結果二つを+して表示する。

型パラメータにNumberのサブクラスを設定しているので、アンボクシングされ計算後の結果が表示されます。

最後にprint()メソッドで現在のフィールド変数を表示する。

ここではInteger型を受け取っているので、
Hogeクラスの中に記述してある型パラメータTを全てIntegerに置き換えて考えると分かりやすいかもしれませんの

んでー

なんで型パラメータにずっとTを使っているのかと言いますが、
型パラメータ名には命名の慣習があります。 いろいろな命名の種類を挙げると、

TTypeクラスのインスタンスなど
SSecond型パラメータが二つの場合
UTの次型パラメータが三つの場合
EElementコレクション(ListやSet)の要素
KKeyMap(連想配列)のキー
VValueMapの値
NNumber数値に関連するクラス

こんな感じ…


ジェネリクスメソッド

ジェネリクスはメソッドにも適用することができます。

class Ex{
		static <T> ArrayList<T> toArrayList(T t, int size) {
		    // ArrayListに同じ要素をsize分だけ敷き詰めるメソッド
				return Stream.generate(() -> t)
                .limit(size)
                .collect(Collectors.toCollection(ArrayList::new));
		}
		
		public static void main(String[] args) {
				var li = toArrayList("Hello", 5);
        System.out.println(li);
				/*コンソール
						[Hello, Hello, Hello, Hello, Hello]
				*/
		}
}

次継承と正規表現による制限comingsoon


型の制限

なんとなく基本の書き方が解ったかもですが、このままでは型パラメータにはどんな型も入るようになってしまいます。
これでは全てのクラスに対して実行出来るようにしなければならないので、場合により型パラメータに入る型を制限する必要があります。

例えば、数値に関する処理をしたい場合はNumber型のサブクラスに制限するなど、

class Hoge<T extends Number> {
		private T t;
		T multiplyTwo(){
				return t * t;
		}
}

結論

ならいいな