1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Javaのジェネリクス再考

Last updated at Posted at 2022-03-21

概要

Javaにジェネリクスが導入され,型引数,パラメータ境界,共変,反変,不変,ワイルドカード,などなど,様々な構文,機能が追加されました.ジェネリクスの構文や,これら新しい機能(もう新しくないけど)の解説は少し探せばわかり易く解説してくれるページがたくさん見つかります.が,ジェネリクスを使ったクラスライブラリを使うだけならまだしも,自分のクラス定義でジェネリクスを使うとなると,「で,どうすればいいの?」となってしまい,適当に使ってきましたが一念発起して真面目に考え,Tips的にまとめてみました.
ですので,ジェネリクスの構文や意味などの説明は割愛しています.そして,確実に正しいことを言っている保証はなく,あくまで個人の理解ですので,間違い等ありましたらご指摘いただけると幸いです.

長くなりそうだったので,一旦公開しますが,パターンなどは追記する予定です.


そもそも

ジェネリクスを使う目的は,クラスそのものではなく,クラスが内包する変数のためのもの,だと考えると良いと思います.
そしてその典型的な例がコレクションフレームワークです.ですので,それ以外(といっても結局は同じことなんですが)の境界とかワイルドカードとかの使い所を考えていきます.

以下の例で具体的に説明します.コピペで確認できるために,全部書きます

Human.java:便宜上使うクラス群
public class Human {
	public void hello() {
		System.out.println("Hello Human");
	}
}
class Man extends Human {
	@Override
	public void hello() {
		System.out.println("Hello Man");
	}
}
class Woman extends Human {
	@Override
	public void hello() {
		System.out.println("Hello Woman");
	}
}
House.java
public class House {
	public void hello() {
		System.out.println("Hello House");
	}
}

ここに4つのクラスがあり,これを内包するContainerクラスを考えます.

Container.java
public class Container {
	
	private Human human;
	
	public Container(Human human) {
		this.human = human;
	}	
	public Human get() {
		human.hello();
		return human;
	}	
	public static void main(String ...args) {
		Human human = new Human();
		Man man = new Man();
		Woman woman = new Woman();
			
		Container humanContainer = new Container(human);
		Container manContainer = new Container(man);
		Container womanContainer = new Container(woman);
		humanContainer.get();
		manContainer.get();
		womanContainer.get();
	}
}

説明するまでもないですが,これを実行すると以下の結果を得ます.

Hello Human
Hello Man
Hello Woman

Human, Man, Womanに継承関係があるのでhuman.hello()が実行できます.

ここで,ジェネリクスを導入してみます

GenContainer.java
public class GenContainer<T extends Human> {
	private T t;	
	public GenContainer(T t) {
		this.t = t;
	}
	
	public T get() {
		t.hello();
		return t;
	}
	
	public static void main(String ...args) {
		Human human = new Human();
		Man man = new Man();
		Woman woman = new Woman();
		
		GenContainer<Human> humanContainer = new GenContainer<>(human);
		GenContainer<Man>   manContainer = new GenContainer<>(man);
		GenContainer<Woman> womanContainer = new GenContainer<>(woman);
		
		humanContainer.get();
		manContainer.get();
		womanContainer.get();
	}		
}

これを実行すると,

Hello Human
Hello Man
Hello Woman

の結果を得ます.先程のContainerと何ら結果が変わりません.この例では,型パラメータTに対し,上限境界をHumanにすることでt.hello()の実行を可能にしていますが,TすくなくともHumanとして扱いたい,というのが目的ならジェネリクスは不要だと思います.

次に,以下のクラスを導入します

User.java
public class User<T extends Human> {
	
	protected T t;
	
	public void usebycon(Container container) { ---(1)
		//t = container.get(); // compile error     
	}
	
	public void strictUsebygen(GenContainer<T> gencontainer) { ---(2)
		t = gencontainer.get();
		t.hello();
	}
	
	public void relaxUsebygen(GenContainer<? extends T> genContainer) { ---(3)
		t = genContainer.get();
		t.hello();
	}
	
	public static void main(String ...args) {
		Human human = new Human();
		Man man = new Man();
		Woman woman = new Woman();
		House house = new House();
		GenContainer<Human> humanContainer = new GenContainer<>(human);
		GenContainer<Man>   manContainer = new GenContainer<>(man);
		GenContainer<Woman> womanContainer = new GenContainer<>(woman);
		//GenContainer<House> houseContainer = new GenContainer<>(house); // compile error --- (*)
		
		User<Human> user = new User<>();     --- (4)
		user.strictUsebygen(humanContainer);
		//user.strictUsebygen(manContainer); // compile error
		user.relaxUsebygen(manContainer);    --- (5)
				
		//user.strictUsebygen(houseContainer); // compile error ---(**)
		//user.relaxUsebygen(houseContainer);  // compile error
	}
}

実行結果は意味がないので割愛します.
まず,(1)ように,container.get()Humanを返し,T extends Humanとしているのに,t = container.get()はコンパイルエラーになります.なぜか,,分からないです(笑).が,少なくともコンパイルが通りません(理屈ではあってる気がするのに...).一方,(2)では,コンパイルが通ります.このように,ジェネリクスは,クラスが内包するオブジェクトをクラス間でやり取りするときに効いてきます.

さらに,(4)で,Userの型引数にHumanを与えているので,(2)の引数にhumanContainer(型引数にHumanを与えている)は呼び出せますが,manContainerを与えるとジェネリクスの不変性からコンパイルエラーになります.そこで,(3)のように型境界を使うことでmanContainerも許容します(5).ここでは上限境界(extends)を使っていますが,これはクラス間で受け渡すオブジェクトの型関係を考慮して決定します(PUT/GETとかPECSとか言います).が,個人的にはなかなか理解できなくて苦労しました...

ここで,(*)のコンパイルエラーについて考えます.これがエラーになるのは,

class GenContainer<T extends Human>

の定義から,Tの上限がHumanで,HouseHumanの派生クラスではないからです.そこで,

class GenContainer<T>

と定義を変えると,コンパイルが通るようになります(GenContainer内のt.hello()はエラーになりますが,ここでは省略).
しかし,(**)では依然としてコンパイルエラーになります(してくれます).これは,

class User<T extends Human>

としているからで,Tは上限をHumanにしているからです.

ここまでの話から,以下のことが言えるのかなと思います

  • コンテナとしてクラスを使いたい場合はジェネリクスを導入(コレクションがあるので,自作する機会は少ないかも)
  • 既存クラスで型引数を導入しているクラスに,オブジェクトを内包するクラスのインスタンスを渡す場合(e.g., GenContainerのような)
  • 既存クラスで型引数を導入しており,オブジェクトを内包するインスタンスを受け取る場合
  • オブジェクトを渡すときには,型境界を導入して制限を緩める(上限/下限とワイルドカードを使用)

そして,これらを行って得られる効果は,型安全です(そりゃそうですね).
今回の例では,(**)の部分は絶対にコンパイルが通りません.ですので,引数にGenContainer型のオブジェクトを受け取るという制限だけでなく,GenContainerが内包するオブジェクトの型まで制限できる,ということです.
当たり前のことなんですが...

そして,もう一つ.なんかトリッキーなことができそうな感じがしてましたが,そんなことはない(Effective Javaとかに少し紹介されています).あくまでも型安全のために存在する仕組みである,ので使う場面は限られると考えて良いと思っています(知らないだけの可能性も十分にありますのでご容赦下さい).


パターン

〇〇のときにはジェネリクスを導入するといいよ,的なパターンを挙げていきます(あくまで個人の考え).
まともなプログラムを書くときにはとりあえずインタフェース,(必要なら)抽象クラス,具象クラスをつくる,みたいな典型的なパターンがジェネリクスでどれほどあるのかわかりません..が,とりあえず思いついたものから.随時更新します(したいw).

継承関係があるクラス群でメソッドチェイン

以下のようなコードを継承関係のあるクラスに対して行いたい場合です.

class A {
  A do1() {..return this;}
  A do2() {..return this;}
  A do3() {..return this;}

  public static void main(...) {
      new A().do1().do2().do3();
  }
}

このクラスを継承し,メソッドを追加します

class B extends A {
   B do4() {..return this;}
   public static void main(...) {
     new B().do1().do2().do3().do4(); --> X
   }
}

これはコンパイルエラーです.なぜなら,do3()Aを返すので,Aにはdo4()が定義されていないからです.このような場合に使えるのが,simulated self-typeというイディオムです(by Effective Java).Effective Javaでは,Builderパターンを例に,抽象オブジェクトと抽象Builderと,それらを継承した具象オブジェクト,具象Builderで説明されているので,ここではインタフェースを使ってみます.

IBase.java
public interface IBase<T extends IBase<T>> {	
	public T firstName();	
	public T lastName();	
	public T setName(String first, String last);
}

インタフェース定義の,IBase<T extends IBase<T>>がself-typeのところです.この定義で,Tを制限しています.具体的には,
TIBaseを継承(または実装)し,更にその型引数もTである必要があります.
混乱しますが,具体的には以下のような定義です.

public class Something implements IBase<Something> {
   ......
}

この,Something<T extends IBase<T>>Tに当てはめてみると,まさしくSomethingのクラス定義になっています.
しかし,このクラスはなんの意味もありません.なぜならTSomethingに確定したので,本来の目的だった継承関係のメソッドチェインが実現できません.ですので,具象クラスの前に抽象クラスを導入します.

Base.java
@SuppressWarnings("unchecked")
public abstract class Base<T extends IBase<T>> implements IBase<T> {
	protected String firstName;
	protected String lastName;
	
	public T setName(String first, String last) {
		this.firstName = first;
		this.lastName = last;
		return (T)this;//絶対に成功する.TはIBaseを継承または実装する.this(Base)はIBaseを実装する.よって絶対成功する.
		               // ただし,コンパイラーの型検査ではチェックできない
	}
	
	public T firstName() {
		System.out.println(firstName);
		return (T)this;
	}
	
	public T lastName() {
		System.out.println(lastName);
		return (T)this;
	}
}

BaseIBase<T>を実装するため,素直にクラス定義をすると

class Base<T> implements IBase<T>

となりますが,書いてみると型引数Tでコンパイルエラーとなります.IBaseの型引数Tは,T extends IBase<T>となっているため,Base<T>Tにも同等の定義が必要になるためです.
実は,Base<T extends Base<T>としても,"TはBaseを上限とし,そのTをIBaseに適用"してもIBaseのT extends IBase<T>の制約を満たす(TはBaseの定義(implements IBase))ため,コンパイルは通ります.が,拡張性に問題が出てきます(後述).

次に,コードでのポイントは,return (T)this;の部分で,コメントにあるように,このキャストは絶対成功します.したがって,@SuppressWarnings("unchecked")でコンパイラの警告を無視します.この実装によって,実行時に型クラスに指定されたクラスに安全にダウンキャストできます.

最後に,具象クラスを実装します

Concrate.java
public class Concrete extends Base<Concrete> {
    public Concrete company() {
		System.out.println("Apple");
		return this;
	}
	
	public static void main(String ...args) {
		Concrete cobj = new Concrete();		
		cobj.setName("Jobs", "Steve");
		Object o = cobj.firstName().lastName().company();
		System.out.println(o.getClass().getName());
	}
}

これを実行すると,以下の結果を得ます

Steve
Jobs
Apple
Concrete

ここで新たにcompany()メソッドをサブクラスに追加していますが,メソッドチェインが成功します.
o.getClass().getName()の結果もConcrete,つまり型引数に与えたクラス,つまりサブクラスにキャストされて返されているのがわかります.

これでうまくいくのですが,ちょっとだけ違和感があります.
class Concrete extends Base<Concrete>ですが,Baseの型引数の定義は,Base<T extends IBase<T>>です.このTConcreteを素直に適用すると,Concrete extends IBase<Concrete>であるべきで,class Concrete extends Base<Concrete>でいいのか?と.
BaseIBaseを実装するし,コンパイルも通るので間違いでないことは確実なんだけど..
これをスッキリさせるには,Baseの定義を

abstract class Base<T extends Base<T>

とすればよく,これで何ら問題はありません.このように,インタフェース->抽象クラス->具象クラスのような関係ならば,このほうが気持ち良いです.

さて,ここで更にIBaseを継承したインタフェースを導入します.

IConcrete.java
public interface IConcrete extends IBase<IConcrete> {	
	public String greeting();
}

IConcreteも,IBaseの型引数が要求するT extends IBaseを満たしています.
そして,次にBaseを継承し,このIConcreteを実装するConcrate2を定義してみます.

Concrete2.java
public class Concrete2 extends Base<Concrete2> implements IConcrete {	// コンパイルエラー
	@Override
	public String greeting() {	
		return "SayHello";
	}
}

Concreteと同じように(class Concrete2 extends Base<Concrete2>),定義しようとすると,
"The interface IBase cannot be implemented more than one with different arguments: IBase and IBase"というエラーが出ます.これは,Base<Concrete2>ということは,

abstract class Base<Concrete2 extends IBase<Concrete2>> implements IBase<Concrete2>

としていることであり,これが

interface IConcrete extends IBase<IConcrete>

とコンフリクトを起こす,ということのようです.

というわけで,正解は

Concrete2.java
public class Concrete2 extends Base<IConcrete> implements IConcrete {
	@Override
	public String greeting() {	
		return "SayHello";
	}
}

のように,Base<IConcrete>とすればよいです.ちょっと頭が混乱しますね...

おまけ

インタフェースを導入した状態で,以下のようにBaseの定義を変更する(T extends Base<T>)と,

Base.java
public abstract class Base<T extends Base<T>> implements IBase<T> {

今度は
"IConcrete is not a valid a substitute for bounded parameter T extends Base"といわれます.
IConcreteは,"IConcete extends IBase"ですからね...

というわけで,具象クラスが他のインタフェースを実装しないなら,`Base>のほうがスッキリわかりやすく,
実装する場合には今回の正解例のようにするのがよいのかもしれません.


上限/下限の使い道と考え方

ジェネリクスでの上限とは

T extends Number

のように,extendsを使って型引数の上限(クラス階層の上の限界)を決めることで,

下限とは

T super Integer

のように,superを使って型引数の下限(クラス階層の下の限界)を決めることです.

直感的に考えると,上限はなんとなく意味がわかりますが(クラス継承に慣れているので),下限の存在意義がわかりにくいです.
まず,わかりやすい上限境界から考えていきます.

上限境界(extend)

既に「そもそも」のところで出したクラス群を再掲します.

Human.java:便宜上使うクラス群
public class Human {
	public void hello() {
		System.out.println("Hello Human");
	}
}
class Man extends Human {
	@Override
	public void hello() {
		System.out.println("Hello Man");
	}
}
class Woman extends Human {
	@Override
	public void hello() {
		System.out.println("Hello Woman");
	}
}

次に,これらを内包するクラス

GenContainer.java
public class GenContainer<T extends Human> {
	private T t;	
	public GenContainer(T t) {
		this.t = t;
	}

    public void add(T t) {
        this.t = t;
    }
	
	public T get() {
		t.hello(); ---(1)
		return t;
	}
}

上限を決めない場合,T型は不定なので,tに対してメソッドを呼ぶことはできません(ただし,どんな型もObjectを継承するので,Objectに定義されたメソッドは呼ぶことができます,があまり有意義なことはできません).ここで(1)に注目すると,'hello'を呼び出しています.これができるのはT extends Humanのように,上限を決めているためで,Tは"少なくともHuman"のサブクラスであることが保証されているからです.
ただ,このような使い方をするだけであれば,単なる継承でよいので,型引数など必要ない(と思う)のは前の議論のとおりです.

ここで,Userを導入します.

User.java
public class User<T extends Human> {
	
	protected T t;
	
	public void strictUsebygen(GenContainer<T> gencontainer) {   ----- (2)
		t = gencontainer.get();
		t.hello();                                               ----- (1)
	}
	
	public void relaxUsebygen(GenContainer<? extends T> genContainer) { -- (3)
		t = genContainer.get();
		t.hello();                                               ----- (1)
	}
	
	public static void main(String ...args) {
		Human human = new Human();
		Man man = new Man();
		Woman woman = new Woman();
		House house = new House();
		GenContainer<Human> humanContainer = new GenContainer<>(human);
		GenContainer<Man>   manContainer = new GenContainer<>(man);
		GenContainer<Woman> womanContainer = new GenContainer<>(woman);
		
		User<Human> user = new User<>();     
		user.strictUsebygen(humanContainer); --- (4)
		//user.strictUsebygen(manContainer); // compile error -- (5)
		user.relaxUsebygen(manContainer);    --- (6)
	}
}

Userも,上限をHumanとしているので,(1)のようにhelloが呼び出せます.そして,(4)のように,UserGenContainer間でHumanを受け渡して処理します.こういう処理のため,単なる継承ではなく型引数が必要なのでした.そして,(4)では,Humanを内包するGenContainerを引数にメソッドを呼び出し,処理が成功します.このように,上限を決めることで,型引数のオブジェクトに対して何らかの処理(メソッド呼び出し),ができるようになる,というのが上限のメリットです.
一方,(5)は,Human継承したManを内包するGenContainerを引数にメソッド呼び出しをしようとしますが,コンパイルエラーになります.これは,ジェネリクスが不変(非変)だからです.
ここで登場するのが ワイルドカード(?) です(この辺からややこしくなります).
使う側からすると,"ManはHumanを継承しているんだから,helloの呼び出しは絶対成功する"と分かっています.なので,GenContainerが内包するオブジェクト,つまりT型の成約を緩めたのが(3)の? extends Tになります.これは,"引数はGenContainerで内包するオブジェクトはTまたはTを継承した型"なら許可する,と解釈します.この結果,(6)の呼び出しは成功します.

この,上限とワイルドカードを知ると,柔軟性を上げるために色々なことを書いてみたくなります,が,これが混乱の原因です.
考えうるダメな例を挙げてみます.

class Sample<? extends T> {...}

コンパイルが通りません.TStringなど,具象クラスにしても同じです.

GenContainer<? extends Human> container = new GenCongainer<>(human);

意味がありません.こう書きたくなるのは,

GenContainer<? extends Human> container = new GenCongainer<>(human);
container.add(man) // コンパイルエラー

などのように,Humanを継承したクラスを引数にメソッドを呼び出したいから,だと思いますが,逆効果でこのaddはコンパイルエラーになります.このコードだけならエラーなど起きませんが,これをコンパイルエラーにしなければいけない理由は複雑なので後述します.
ワイルドカードは,T型の継承関係をそのものを考えて導入するもの ではなく, T型(に継承関係のある)を 内包するオブジェクト同士 を代入可能にすること,が目的です.ですので,先程の例では

GenContainer<Human> container = new GenCongainer<>(woman);//コンパイルOK
container.add(man) // コンパイルOK

これでOKで,ワイルドカードで実現したいことは

GenContainer<Man> manContainer = new GenContainer<>(man)
GenContainer<Human> humanContainer1 = manContainer            // (1)コンパイルエラー
GenContainer<? extends Human> humanContainer2 = manContainer; // (2)コンテナの代入

この(2)で,内包するオブジェクトの継承関係を考慮して コンテナの代入 を許可しています.(1)だとコンパイルエラーになります.しかし,(2)だと,addが呼び出せませんorz..この辺でパニックになりそうです.

これが分かったところで,addが呼び出せない理由がわかるコードを示します.

GenContainer<Man> manContainer = new GenContainer<>(man)
GenContainer<? extends Human> humanContainer = manContainer; ---(1)
humanContainer.add(woman)---(2)

(1)でmanContanerの代入を許可し,(2)でwomanを許可できたとします.このとき,humanContainerの実体はmanContainerなので,manに特有の処理を内部でするかもしれません.そこにwomanの代入を許可すると,実行時エラーになる可能性があります.こういった理由でコンパイルエラーにしているのだと思います.

ここまでの議論から,上限に関しては以下の教訓が導けると思います

  • 型パラメータを指定してクラスを生成するときには,型を厳密に決めて生成
  • 型パラメータ付きのコンテナを受け取るメソッドでは,ワイルドカードを使用して型成約を緩めて受け取る
  • メソッドの中では,コンテナに型パラメータ型の何かを引数に取るようなメソッド(container.add(t))を呼び出すのではなく,コンテナから値を取り出し,上限の型として扱って処理をする.

UserrelaxUsebygen(GenContainer<? extends T> genContainer)はまさしくこの形になっています.

下限境界(super)

下限境界は上限境界の反対で,T super Numberのように書くのですが,そのまま理解するとNumberを下限とする型Tとなり,それって何?Object?と考えると,ただのTで良いし,何が嬉しいの?と思ってしまいます.しかも,(私の理解が足りてないせいかもしれませんが),上限と比べて使える範囲が限られます.まず,

class Something<T super Number> {...}

こんな定義はできません.しかも,仮にこれができたとして


class Something<T super Number> {
    T t;
    void m(T t) {
      // t....何をかけというの?
    }
}

このように,Numberの親クラスであることは分かるけど,Numberじゃないし,親クラスが何だかわからないし,Objectにしかメソッド呼べないし...のように,存在意義があるとは思えません.が,一応あります.

Human.java(便宜クラス)
public class Human {
	protected String name;
	
	public Human(String name) {
		this.name = name;
	}	
	public String getName() {
		return name;
	}
}

class Man extends Human {	
	public Man(String name) {
		super(name);
	}	
	public String getName() {
		return "Mr. " + super.getName();
	}
}

class Woman extends Human {
	public Woman(String name) {
		super(name);
	}	
	public String getName() {
		return "Ms. " + super.getName();
	}
}
House.java(便宜クラス)
public class House {
	public int getHight() {
		return 10;
	}
}

次に,型引数を導入したインタフェースと,具象クラスを定義します

Builder.java
public interface Builder<T> {	
	public void build(T t);
}
HumanBuilder.java
public class HumanBuilder implements Builder<Human>{	
	public void build(Human human) {
		System.out.println("HouseBuilder.build is invoked");
		System.out.println("Name: " + human.getName());
	}
}

ManBuilder.java
public class ManBuilder implements Builder<Man> {	
	public void build(Man man) {
		System.out.println("ManBuilder.build is invoked");
		System.out.println("Name: " + man.getName());
	}
}
HouseBuilder.java
public class HouseBuilder implements Builder<House>{
	
	public void build(House house) {
		System.out.println("HouseBuilder.build is invoked");
		System.out.println("height = " + house.getHight());		
	}
}

ここで,Builderを使う,型引数を持つクラスを考えます.

GeneralBuilderUser.java
public class GeneralBuilderUser<T> {
	
	private T t;
	
	public GeneralBuilderUser(T t) {
		this.t = t;
	}
	
	public void strictuse(Builder<T> builder) {
		builder.build(t);
	}
	
	public void relaxuse(Builder<? super T> builder) {  --- (1)
		builder.build(t);
	}
	
	public static void main(String ...args) {
		GeneralBuilderUser<House> houseBuildUser = new GeneralBuilderUser<>(new House());
		GeneralBuilderUser<Human> humanBuildUser = new GeneralBuilderUser<>(new Human("abc")); // Man is also OK
		GeneralBuilderUser<Man>   manBuildUser   = new GeneralBuilderUser<>(new Man("xyz"));   // Woman and Human are not applicable
		
		
		HouseBuilder houseBuilder = new HouseBuilder();
		HumanBuilder humanBuilder = new HumanBuilder();
		
		houseBuildUser.strictuse(houseBuilder);
		humanBuildUser.strictuse(humanBuilder);
		//manBuildUser.strictuse(humanBuilder); // compile error
		manBuildUser.relaxuse(humanBuilder);    // compile OK
		
		//houseBuildUser.strictuse(humanBuilder); // compile error
		//houseBuildUser.relaxuse(humanBuilder);  // compile error
		
	}
}

mainでは,まず3つのGeneralBuilderUserを作り(型引数にそれぞれ,House, Main, Humanを指定),さらにHouseBuilderHumanBuilderを作ります.その後,それぞれのBuilderUserのメソッドに2つのBuilderを適用する例から,下限境界の役割を考えます.

パターン1
houseBuildUser.strictuse(houseBuilder);

BuilderとBuilderUserの型引数が両方Houseであるためコンパイルが通ります.

パターン2
humanBuildUser.strictuse(humanBuilder);

BuilderとBuilderUserの型引数が両方Humanであるためコンパイルが通ります.

パターン3
manBuildUser.strictuse(humanBuilder); // compile error

BuilderUserの型引数はManBuilderの型引数はHumanで,ジェネリクスが不変(非変)であることから,コンパイルエラーとなります.

パターン4
manBuildUser.relaxuse(humanBuilder);    // compile OK

BuilderUserの型引数はManBuilderの型引数はHumanで,ジェネリクスは不変(非変)ではあるものの,(1)のように 下限を指定しているから コンパイルが通ります.後で詳しく考えます

パターン5
houseBuildUser.strictuse(humanBuilder); // compile error
houseBuildUser.relaxuse(humanBuilder);  // compile error

BuilderUserBuilderで,HouseHumanという,全く関係ないクラス同士を型引数にとるため,コンパイルエラーとなります.

パターン4のケースが下限を使うケースです.ここで,Human > Manという関係(Humanが親クラス)であり,BuilderUserに子であるManが型引数として与えられています.つまり,ManBuilderUserTManということです.一方,Builderの型引数には親であるHumanが型引数として与えられています.つまり,build(T t)Tは,Humanです.
ここで,メソッドを見てみます.

public void relaxuse(Builder<? super T> builder) { 
		builder.build(t);
}

このように,builder.build(t)builderは今Humanの型引数を想定しており,一方で実引数tは,BuilderUserのメンバ変数のT型,すなわちManです.言い換えると,引数にHumanを期待するメソッドにManを与える,ということになります.Human > Manなので,これは成立します.
この例から,次のことが言えると思います.

  • 下限境界を適用するケースは限られる.例えばクラス定義には使えない.
  • 下限境界を適用するクラスのメソッド(e.g., build)と,そこに与える引数(t)の型の関係を考え,下限境界を適用するクラスが引数の親クラスになっている場合に適用する.

これがいわゆる,PECSという法則の,Consumerになる場合にはsuperを用いる,ということですが..Consumerと言われても私にはピンときませんでした.それよりも,メソッドの呼び出し関係で考えた方がわかりやすい気がします.
念の為付け加えると,このメソッドのTはBuilderUserに与えられたManで,builderはこのTではなく,

public class HumanBuilder implements Builder<Human>

クラス定義で与えられたHumanです.Tとかtが出てくると,非常に混乱します.

結局は,下限境界も,2つ以上の型引数を取るクラス同士でオブジェクトをやり取りするときに制限を緩めるために使う,ということです.

また,下限境界の説明でよく用いられる,Comparableインタフェースがありますが,それについてはまた後日書くかもしれません.

1
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?