概要
Javaにジェネリクスが導入され,型引数,パラメータ境界,共変,反変,不変,ワイルドカード,などなど,様々な構文,機能が追加されました.ジェネリクスの構文や,これら新しい機能(もう新しくないけど)の解説は少し探せばわかり易く解説してくれるページがたくさん見つかります.が,ジェネリクスを使ったクラスライブラリを使うだけならまだしも,自分のクラス定義でジェネリクスを使うとなると,「で,どうすればいいの?」となってしまい,適当に使ってきましたが一念発起して真面目に考え,Tips的にまとめてみました.
ですので,ジェネリクスの構文や意味などの説明は割愛しています.そして,確実に正しいことを言っている保証はなく,あくまで個人の理解ですので,間違い等ありましたらご指摘いただけると幸いです.
長くなりそうだったので,一旦公開しますが,パターンなどは追記する予定です.
そもそも
ジェネリクスを使う目的は,クラスそのものではなく,クラスが内包する変数のためのもの,だと考えると良いと思います.
そしてその典型的な例がコレクションフレームワークです.ですので,それ以外(といっても結局は同じことなんですが)の境界とかワイルドカードとかの使い所を考えていきます.
以下の例で具体的に説明します.コピペで確認できるために,全部書きます
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");
}
}
public class House {
public void hello() {
System.out.println("Hello House");
}
}
ここに4つのクラスがあり,これを内包するContainerクラスを考えます.
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()
が実行できます.
ここで,ジェネリクスを導入してみます
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
として扱いたい,というのが目的ならジェネリクスは不要だと思います.
次に,以下のクラスを導入します
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
で,House
はHuman
の派生クラスではないからです.そこで,
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で説明されているので,ここではインタフェースを使ってみます.
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
を制限しています.具体的には,
T
はIBase
を継承(または実装)し,更にその型引数もT
である必要があります.
混乱しますが,具体的には以下のような定義です.
public class Something implements IBase<Something> {
......
}
この,Something
を<T extends IBase<T>>
のT
に当てはめてみると,まさしくSomething
のクラス定義になっています.
しかし,このクラスはなんの意味もありません.なぜならT
はSomething
に確定したので,本来の目的だった継承関係のメソッドチェインが実現できません.ですので,具象クラスの前に抽象クラスを導入します.
@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;
}
}
Base
はIBase<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")
でコンパイラの警告を無視します.この実装によって,実行時に型クラスに指定されたクラスに安全にダウンキャストできます.
最後に,具象クラスを実装します
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>>
です.このT
にConcrete
を素直に適用すると,Concrete extends IBase<Concrete>
であるべきで,class Concrete extends Base<Concrete>
でいいのか?と.
Base
はIBase
を実装するし,コンパイルも通るので間違いでないことは確実なんだけど..
これをスッキリさせるには,Base
の定義を
abstract class Base<T extends Base<T>
とすればよく,これで何ら問題はありません.このように,インタフェース->抽象クラス->具象クラスのような関係ならば,このほうが気持ち良いです.
さて,ここで更にIBase
を継承したインタフェースを導入します.
public interface IConcrete extends IBase<IConcrete> {
public String greeting();
}
IConcrete
も,IBase
の型引数が要求するT extends IBase
を満たしています.
そして,次にBase
を継承し,このIConcrete
を実装するConcrate2
を定義してみます.
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>
とコンフリクトを起こす,ということのようです.
というわけで,正解は
public class Concrete2 extends Base<IConcrete> implements IConcrete {
@Override
public String greeting() {
return "SayHello";
}
}
のように,Base<IConcrete>
とすればよいです.ちょっと頭が混乱しますね...
おまけ
インタフェースを導入した状態で,以下のようにBase
の定義を変更する(T extends Base<T>
)と,
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)
既に「そもそも」のところで出したクラス群を再掲します.
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");
}
}
次に,これらを内包するクラス
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
を導入します.
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)のように,User
とGenContainer
間でHuman
を受け渡して処理します.こういう処理のため,単なる継承ではなく型引数が必要なのでした.そして,(4)では,Human
を内包するGenContainer
を引数にメソッドを呼び出し,処理が成功します.このように,上限を決めることで,型引数のオブジェクトに対して何らかの処理(メソッド呼び出し),ができるようになる,というのが上限のメリットです.
一方,(5)は,Human
継承したMan
を内包するGenContainer
を引数にメソッド呼び出しをしようとしますが,コンパイルエラーになります.これは,ジェネリクスが不変(非変)だからです.
ここで登場するのが ワイルドカード(?) です(この辺からややこしくなります).
使う側からすると,"ManはHumanを継承しているんだから,hello
の呼び出しは絶対成功する"と分かっています.なので,GenContainer
が内包するオブジェクト,つまりT
型の成約を緩めたのが(3)の? extends T
になります.これは,"引数はGenContainerで内包するオブジェクトはT
またはT
を継承した型"なら許可する,と解釈します.この結果,(6)の呼び出しは成功します.
この,上限とワイルドカードを知ると,柔軟性を上げるために色々なことを書いてみたくなります,が,これが混乱の原因です.
考えうるダメな例を挙げてみます.
class Sample<? extends T> {...}
コンパイルが通りません.T
をString
など,具象クラスにしても同じです.
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))を呼び出すのではなく,コンテナから値を取り出し,上限の型として扱って処理をする.
User
のrelaxUsebygen(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にしかメソッド呼べないし...のように,存在意義があるとは思えません.が,一応あります.
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();
}
}
public class House {
public int getHight() {
return 10;
}
}
次に,型引数を導入したインタフェースと,具象クラスを定義します
public interface Builder<T> {
public void build(T t);
}
public class HumanBuilder implements Builder<Human>{
public void build(Human human) {
System.out.println("HouseBuilder.build is invoked");
System.out.println("Name: " + human.getName());
}
}
public class ManBuilder implements Builder<Man> {
public void build(Man man) {
System.out.println("ManBuilder.build is invoked");
System.out.println("Name: " + man.getName());
}
}
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
を使う,型引数を持つクラスを考えます.
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
を指定),さらにHouseBuilder
とHumanBuilder
を作ります.その後,それぞれのBuilderUser
のメソッドに2つのBuilder
を適用する例から,下限境界の役割を考えます.
houseBuildUser.strictuse(houseBuilder);
BuilderとBuilderUserの型引数が両方House
であるためコンパイルが通ります.
humanBuildUser.strictuse(humanBuilder);
BuilderとBuilderUserの型引数が両方Human
であるためコンパイルが通ります.
manBuildUser.strictuse(humanBuilder); // compile error
BuilderUser
の型引数はMan
,Builder
の型引数はHuman
で,ジェネリクスが不変(非変)であることから,コンパイルエラーとなります.
manBuildUser.relaxuse(humanBuilder); // compile OK
BuilderUser
の型引数はMan
,Builder
の型引数はHuman
で,ジェネリクスは不変(非変)ではあるものの,(1)のように 下限を指定しているから コンパイルが通ります.後で詳しく考えます
houseBuildUser.strictuse(humanBuilder); // compile error
houseBuildUser.relaxuse(humanBuilder); // compile error
BuilderUser
とBuilder
で,House
とHuman
という,全く関係ないクラス同士を型引数にとるため,コンパイルエラーとなります.
パターン4のケースが下限を使うケースです.ここで,Human > Manという関係(Humanが親クラス)であり,BuilderUserに子であるMan
が型引数として与えられています.つまり,ManBuilderUser
のT
はMan
ということです.一方,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インタフェースがありますが,それについてはまた後日書くかもしれません.