Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
190
Help us understand the problem. What is going on with this article?
@koher

Javaの "? extends" や "? super" の使い方をC#やScala風に考える

More than 5 years have passed since last update.

いきなりですが、次のコードの良くないところはどこでしょう?

interface Foo {
    Comparator<Integer> getComparator();
}

なお Comparatorjava.util パッケージに含まれるインタフェースで次のように宣言されています。

interface Comparator<T> {
    int compare(T o1, T o2);
}

答え

Foo をより良く書きなおすと次のようになります。

interface Foo {
    Comparator<? super Integer> getComparator();
}

? super? extends (下限または上限つきワイルドカード)は Java でジェネリクスを使う上で特に難しい部分です。比較的新しい言語( C# 、 Scala 、 Ceylon など)では同じ目的のためによりシンプルでわかりやすいアプローチがとられています。

Java が採用した方法は Use-site variance annotation と呼ばれ、 C# などが採用した方法は Declaration-site variance annotation と呼ばれます。本投稿では Declaration-site variance annotation の考え方をベースに Java でどのように ? extends? super を使えば良いか について書きます。

これは必ずしも Java で一般的な方法ではありません。 Java の ? super? extends の使い方については PECS が有名ですが、一部それと異なる結果になります。  PECS との違いも最後に述べます。

想定読者

前提知識

  • Java のコードが読める
  • 継承とポリモーフィズムについて理解している
  • ジェネリクスの基本について理解している( List<String> が意味することはわかる)

読むといいと思う人

  • ? super? extends って何?という人
  • ? super? extends をなんとなく使ったことはあるけどよくわかっていない人
  • 基本的な使い方はわかっているけど、ジェネリック型が入れ子になるなど複雑なケースでよくわからない人
  • Java しか知らず、他の言語での考え方について学びたい人
  • Variance(変性) って何?という人
  • Variance について聞いたことはあるけどよくわかっていない人
  • PECS って何?という人(比較するために PECS についても簡単に説明しています)

本投稿での表記について

コードのタイトル部

本投稿では Declaration-site variance annotationUse-site variance annotation の両方を扱うため、文中のコードがどちらを意図しているかわかりやすいように、タイトル部に Declaration-site または Use-site と表記しています。

Declaration-site
// Declaration-site variance annotation のコード
Use-site
// Use-site variance annotation のコード

何の表記もない場合は両者に共通のコードです。

Declaration-site variance annotation のコードは Java プログラマが理解しやすいように Java 風に書いていますが、 Java ではコンパイルできません。

Annotation

本投稿で Annotation と言った場合には、 Declaration-site variance annotation で使われる inoutUse-site variance annotation で使われる ? super? extends などのことを指します。Javaのアノテーション( @Override など)のことではないので注意して下さい。 Java のアノテーションについてはカタカナで「アノテーション」と書きます。

※(補足)

本文中に次のような形で補足を挿入しています。

※ これは補足の例です。

特に、厳密な説明をすることで話がわかりづらくなる箇所についてあえてやや不正確な説明をした上で、補足でより正確な説明をしている箇所があります。

Hello, Variance!!

まずは Variance(変性) の例について見てみましょう。

Animal クラスを継承した Cat クラスがあるとします。そこで、次のようなコードを考えてみます。

List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // List<Cat>をList<Animal>に代入
animals.forEach(System.out::println); // animalsの要素を表示( Java 8 の構文を使用)

一見、 CatAnimal の派生型なので List<Cat>List<Animal> に代入しても問題ないように思えます。しかし、このコードはコンパイルエラーになります。

いったい何が問題なのでしょう?次のコードを見ればその理由が分かります。

List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats;
animals.add(new Animal()); // 実体はList<Cat>なのにAnimalを追加!?

上記コードのように、もし catsanimals に代入できるなら List<Cat>Animal を追加するコードが書けることになり、コンパイル時の型チェックをすり抜けて実行時エラーになってしまいます。

とは言え、 List に変更を加えなければ問題ないのに、 List<Cat>List<Animal> に一切互換性がないも不便です。次のように書けば catsanimals に代入できます。

List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats;
animals.forEach(System.out::println);

Java では ? extends をつけることによって Variance を変更することができ、型パラメータの異なる List に互換性を持たせることができます。

次に Variance について詳しく説明します。

継承とVariance

ジェネリクスと Variance について考える準備として、まずは継承と Variance について説明します。

メソッドの戻り値

次のようなクラス A があるとします。

class A {
    Animal makeAnimal() { ... }
}

A を継承して makeAnimal メソッドをオーバーライドします。このとき、 makeAnimal の戻り値の型を Animal よりも狭い(より特化した) Cat に変更することができます。

class B extends A {
    @Override
    Cat makeAnimal() { ... }
}

これが許されるのは Animal 型の変数に Cat を代入できるのと同じ理由によります( CatAnimal を継承しているので Animal として振る舞える、 is-a 関係Cat is an Animal )。

AnimalCatAB の間にはそれぞれ継承による派生関係が成り立ちます。派生型と基本型の関係を 派生型 < 基本型 のように書くと、次のように書けます。

  • Cat < Animal
  • B < A

メソッドの戻り値の型( CatAnimal )とその持ち主の型( BA )について、派生の方向が一致(両方とも < )しています。このような状態を Covariant(共変) と呼びます。

上記のように、 継承におけるメソッドの戻り値の型については Covariant になります。

メソッドの引数

次はメソッドの引数について考えてみます。

class C {
    void useAnimal(Animal animal) { ... }
}

C を継承して useAnimal メソッドをオーバーライドします。同じように引数の型を狭めて Cat にするとどうなるでしょうか。

class D extends C {
    @Override
    void useAnimal(Cat cat) { ... }
}

これはコンパイルエラーになります。 DC を継承しており C として振る舞う必要があります。そのためには、 useAnimal が引数として Animal を受け取れなければなりません。しかし、 DuseAnimalAnimal よりも狭い Cat しか受け取れません。よって、 DC として振る舞うことができず、 is-a 関係 を満たすことができないのでエラーとなります。

逆に、引数の型を広めるとどうなるでしょうか。

class C {
    void useCat(Cat cat) { ... }
}

class D extends C {
    @Override
    void useCat(Animal animal) { ... }
}

DuseCat はより CuseCat よりも広い型( Animal )に対応しています。これなら、 DC として振る舞うことができそうです。

AnimalCatCD の関係を不等号で表すと次のようになります。

  • Cat < Animal
  • C > D

引数の時とは異なり、不等号の向きが逆転( < と > )しました。このような状態を Contravariant(反変) と呼びます。

一般的に、 継承におけるメソッドの引数については Contravariant となります。

実際には Java の型システムでは、オーバーライドするときにメソッド引数の型を変えることはできません(上記の useCat のコードはエラーになります)。不等号(と等号)で表すと

  • Cat = Cat (ここの型を変えることができない)
  • C > D

でなければなりません。そのような関係を Invariant(非変) と呼びます。一般的な型システムについて考えると継承時のメソッドの引数については Contravariant で安全なのですが、 Java の型システムでは Invariant となります。

※ Java の型システムではオーバーライドしてメソッドの引数の型を変えた場合は、オーバーライドではなくオーバーロードされた別のメソッドとして扱われます。そのため、 useCat のコードから @Override を取り除くとエラーでなくなります。引数の型を狭めた場合( useAnimal のコード)でも @Override をつけなければオーバーロードとなりエラーになりません。

CovariantContravariantInvariant かという性質のことを Variance と呼びます。

ジェネリクスとVariance

派生型を作るのは継承だけではありません。ジェネリクスも派生型を作る場合があります。本題に戻ってジェネリクスと Variance について考えてみましょう。

メソッドの戻り値

最もシンプルな例として、 T 型のオブジェクトを返す(供給する)だけの型 Supplier (供給者)を考えます。

Supplier は Java 8 で頻出のインタフェースです。

interface Supplier<T> {
    T get();
}

このとき、 Supplier<Cat>Supplier<Animal> の関係について考えてみましょう。 Supplier<Cat>Cat を供給します。供給された CatAnimal として振る舞うことができるので、 Supplier<Cat>Animal を供給しているとも考えられます。そのため、 Supplier<Cat>Supplier<Animal> として振る舞うことができ、 Supplier<Cat>Supplier<Animal> の派生型であると言うことができそうです。

この関係を < を使って書くと次のようになります。

  • Cat < Animal
  • Supplier<Cat> < Supplier<Animal>

不等号の向きがそろっているので、これは継承のときに見たのと同じ Covariant な関係です。

一般的に、 ジェネリックな型はメソッドの戻り値としてしか使われない型パラメータについて Covariant になります。

※ 実際には、上記のように宣言するだけでは Covariant として取り扱えません。後述の構文で明示的に Covariant であることを指定する必要があります。

メソッドの引数

同じく最もシンプルな例として、 T 型のオブジェクトを受け取る(消費する)だけの型 Consumer (消費者)を考えます。

Consumer も Java 8 で頻出のインタフェースです。

interface Consumer<T> {
    void accept(T t);
}

このとき、 Consumer<Cat>Consumer<Animal> について考えてみましょう。 Consumer<Cat>Cat を受け取って消費します。 Consumer<Animal> はより広い型 Animal を受け取ることができます。 Consumer<Animal>Consumer<Cat> として振る舞って Cat を受け取ることができるため、 Consumer<Animal>Consumer<Cat> の派生型と考えることができそうです。

逆はダメです。 Consumer<Cat>Consumer<Animal> として振る舞おうとしても、 Consumer<Cat>Animal を受け取れません。

つまり、次のような関係になります。

  • Cat < Animal
  • Consumer<Cat> > Consumer<Animal>

不等号の向きが反転しているので、これは継承の時に見たのと同じ Contravariant な関係です。

一般的に、 ジェネリックな型はメソッドの引数としてしか使われない型パラメータについて Contravariant になります。

※ 実際には、上記のように宣言するだけでは Contravariant として取り扱えません。後述の構文で明示的に Contravariant であることを指定する必要があります。

Declaration-site variance annotation

Declaration-site variance annotation を採用した言語では、ジェネリクスの型パラメータの宣言(Declaration)に Annotation を付与して Variance を指定することができます。

ここでは C# や Ceylon にならって、 out を付けたら Covariantin を付けたら Contravariant 、何も付与しなければ Invariant になるとします。 Covariant であるために out が付与された型は戻り値でしか使えません。同様に、 Contravariant であるために in が付与された型は引数でしか使えません。これに違反するとコンパイルエラーになります。

Declaration-site
interface Supplier<out T> { // outが付与された型は戻り値にしか使えない
    T get();
}

interface Consumer<in T> { // inが付与された型は引数にしか使えない
    void accept(T t);
}

このとき、 Supplier は Covariant、 Consumer は Contravariant なので、次のような代入が可能になります。

Declaration-site
Supplier<Cat> catSupplier = ...;
Supplier<Animal> animalAupplier = catSupplier; // Covariant なので Supplier<Cat> < Supplier<Animal>

Consumer<Animal> animalConsumer = ...;
Consumer<Cat> catConsumer = animalConsumer; // Contravariant なので Consumer<Animal> < Consumer<Cat>

また、複数の型パラメータを持つジェネリックな型では、片方について Covariant で、もう一方について Contravariant なケースも考えられます。

最もシンプルな例として、 T を受け取り R を返すだけの型 Function について考えます。 Functioninout を付与すると次のようになります。

Function も Java 8 で頻出のインタフェースです。

Declaration-site
interface Function<in T, out R> { // T は引数に、 R は戻り値にしか使えない
    R apply(T t);
}

FunctionT については Contravariant に、 R については Covariant になるので、次のような代入が可能です。

Declaration-site
Function<Animal, Cat> animalToCat = ...;
Function<Cat, Animal> catToAnimal = animalToCat; // TについてはContravariant、RについてはCovariant

in, out と引数・戻り値の型のより正確な関係

これまでは意図的に簡略化して次のように説明してきました。

  • out で型パラメータを宣言 → メソッドの戻り値でのみ利用可
  • in で型パラメータを宣言 → メソッドの引数でのみ利用可

しかし、これは正確な説明ではありません。次の話に進む前に、型パラメータと引数・戻り値の型のより正確な関係について説明します。

例えば次のコードでは、一見 out を付与された T が引数で使われているようですが問題ありません。

Declaration-site
interface Foo<out T> {
    void foo(Consumer<T> consumer); // Tが引数で使われているように見えるが問題なし
}

Foo<Cat>Foo<Animal> について考えてみましょう。 Consumer は前の例のように Consumer<in T> なので T について Contravariant です。そのため、次のような関係になります。

  • Cat < Animal
  • Consumer<Cat> > Consumer<Animal>

Consumer<Cat> > Consumer<Animal> ということは、 Foo<Cat>void foo(Consumer<Cat>)Foo<Animal>void foo(Consumer<Animal>) よりも広い型に対応できるので

  • Foo<Cat> < Foo<Animal>

となります。 Foo<Cat>Foo<Animal>CatAnimal で不等号と向きが同じであるため FooT について Covariant であることがわかります。よって、 FooTout に違反していないわけです。

このように、 out が付与された型パラメータなら戻り値、 in なら引数という単純なルールではなく、その型パラメータについて CovariantContravariant として扱っても安全かどうかによって outin に違反していないかが判定されます。

定式化された判定方法は参考文献1が詳しいです。

Use-site variance annotation

Use-site variance annotation を採用した言語では、宣言時ではなく利用(Use)時に付与する Annotation によってジェネリック型の Variance を決定します。

Java では上限・下限付きワイルドカードを Annotation として使い、 Supplier<? extends Animal> とすれば Covariant に、 Supplier<? super Animal> とすれば Contravariant になります。また、単に Supplier<Animal> とすれば Invariant になります。

Declaration-site variance annotation で例に上げた SupplierConsumerFunction のコードを Use-site variance annotation で書きなおすと次のようになります。

Use-site
Supplier<Cat> catSupplier = ...;
Supplier<? extends Animal> animalAupplier = catSupplier; // Covariant

Consumer<Animal> animalConsumer = ...;
Consumer<? super Cat> catConsumer = animalConsumer; // Contravariant

Function<Animal, Cat> animalToCat = ...;
Function<? super Cat, ? extends Animal> catToAnimal = animalToCat; // TについてはContravariant、RについてはCovariant

欠点

見ての通り、 Declaration-site variance annotation では SupplierConsumer などの型を宣言する際に一度だけ inout と記述すればいいところが、 Use-site variance annotation ではそれらを利用する際に毎回 ? extends? super と記述しなければなりません。これは、単に面倒だというだけでなく、 Supplier は Covariant だと想定されているから ? extends にするといったことを、 その型の設計者ではなく利用者が常に把握していなければならない ことを意味します。

利点

一方で、 Use-site variance annotation では一つの型の VarianceInvariant にも Covariant にも Contravariant にもできるので表現力の幅が広いという利点があります。

例えば、 AnimalList を受け取って要素を表示するメソッドを考えてみます。

void printAnimals(List<Animal> animals) { ... }

このメソッドに List<Cat> を渡したくても、 ListInvariant なのでこのままでは渡せません。

List<Cat> cats = ...;
printAnimals(cats); // コンパイルエラー( List<Cat> is NOT a List<Animal> )

printAnimals は単に渡された List から Animal を取り出して表示するだけなので、 List には何の変更も加えません。そのため、このメソッドに限っては ListCovantiant であると考えても問題はおこりません。そこで、 ? extends を使って Variance を変更することで、 List<Animal> も List<Cat> も受け取れるメソッドを作ることができます。

Use-site
void printAnimals(List<? extends Animal> animals) { ... } // Covariant
Use-site
List<Cat> cats = ...;
printAnimals(cats); // 問題なし( List<Cat> is a List<? extends Animal> )

コンパイル時の型チェック

ところで、 "Hello, Variance!!" で見たように、 ListCovariant だと危険なコードがコンパイル時の型チェックをすり抜けてしまう心配がありました。 Use-site variance annotationListCovariant にするとどうなるのでしょうか。

Use-site
List<Cat> cats = new ArrayList<>();
List<? extends Animal> animals = cats; // CovariantなのでこれはOK
animals.add(new Animal()); // コンパイルエラー

上記のコードでは add が(実行時ではなく)コンパイルエラーになります。 List<? extends Animal> のような CovariantList は、 addset など CovariantList が持っているとおかしいメソッドは使えないものとして型チェックが行われます。そのため、 Use-site variance annotation であっても危険なコードがコンパイルをすり抜けてしまうことはありません。

※ 実際には、 addset などのメソッド自体が利用不可になっているわけではありません。メソッドの引数の型が安全な範囲に限定されているだけで、 add メソッドであれば null を渡すことはできます。

Use-site variance annotation の問題点と対処法

これまでに見てきた Declaration-site variance annotation と Use-site variance annotation の利点・欠点についてまとめると次のようになります。

Declaration-site variance annotation Use-site variance annotation
利点 宣言時に一度だけ out, in を書けばいい より柔軟なケースに対応可能
欠点 対応可能なケースが限定されている 利用時に毎回 ? extends, ? super を書かなければならない

これだけを見ると一長一短で、どちらが望ましいとは言えないように思えます。しかし、 Use-site variance annotation で利用時に正しく Variance を指定するのは、その型の設計者ではない利用者にとってはとても難しいことです。

問題点

冒頭のコードを思い出して下さい。

Use-site(良くない例)
interface Foo {
    Comparator<Integer> getComparator();
}
Use-site(良い例)
interface Foo {
    Comparator<? super Integer> getComparator();
}

Variance についてきちんと理解していなかったり、注意が足りなかったりすると前者のように書いてしまうのではないでしょうか。

前者が良くないのは Foo を継承して次のような Bar を作ることができないからです。

Use-site
interface Bar extends Foo {
    Comparator<Number> getComparator(); // オーバーライドして Comparator<Number> を返すようにする。
}

Comparator<Number>Comparator<Integer> よりも広い型 Number に対応できます。もちろん、 Integer にも対応できるため、 getComparator メソッドの戻り値の型を Comparator<Number> でオーバーライドしても問題は起こらないはずです。不必要にそれを禁止してしまうのは良くありません。

これくらいのシンプルな例であれば少し考えれば ? super を付ければ良いとわかるかもしれません。しかし、より複雑な次の例ではどうでしょうか。次の qux メソッドの Animal にどのような Annotation を付ければ良いか考えてみて下さい( BazQux に意味はありません。純粋に型のみから考えて下さい)。

Use-site
interface Baz<T, U> {
    Supplier<? extends U> baz(Consumer<? super T> consumer);
}

interface Qux {
    Baz<Animal, Animal> qux(); // Baz<Animal, Animal> に適切に Annotation を付けたい
}

答えは↓です(説明は後述)。

Use-site
interface Qux {
    Baz<? extends Animal, ? super Animal> qux();
}

このように、 Use-site variance annotation で適切な Annotation を考えるのは複雑です。この複雑さこそが、比較的新しい言語で Use-site variance annotation が採用されていない理由だと思います。

対処法

とはいえ、 Java を使う以上 Use-site variance annotation と付き合っていかなければなりません。

ここからは僕の考えですが、 Use-site variance annotation を正しく使うには、 一度 Declaration-site variance annotation として考えてから Use-site variance annotation に変換すると良い と思います。

Declaration-site variance annotation で考えるのは難しくありませんし、 Use-site variance annotation に変換するのも簡単です。次の表のような単純なルールで変換できます。

Declaration-site variance annotation Use-site variance annotation
宣言時 <out T>, <in U> <T>, <U>
利用時 <Foo>, <Bar> <? extends Foo>, <? super Bar>

Variance がわかっている場合

前述の Comparator の例で考えてみます。 ComparatorTcompare メソッドの引数でしか使われていないので ComparatorContravariant だと考えられます。そのため、 T には in が付与されていると考えます。

Declaration-site
// 宣言時
interface Comparator<in T> {
    int compare(T o1, T o2);
}

// 利用時
interface Foo {
    Comparator<Integer> getComparator();
}

これを Use-site variance annotation に変換すると、「宣言時に in → 利用時に ? super 」なので、簡単に次のコードが得られます。

Use-site
// 宣言時
interface Comparator<T> {
    int compare(T o1, T o2);
}

// 利用時
interface Foo {
    Comparator<? super Integer> getComparator();
}

これなら、 inout かを知っていれば簡単に Use-site variance annotation に変換できます。次のような複雑な型であっても、

interface Foo {
    Supplier<Comparator<Cat>> foo(Function<Consumer<Animal>, List<Animal>> function);   
}

Declaration-site variance annotation での宣言を考えると

Declaration-site
interface Supplier<out T> { ... }
interface Consumer<in T> { ... }
interface Function<in T, out R> { ... }

interface Comparator<in T> { ... }

interface List<T> { ... } // T は引数でも戻り値でも使われているので in でも out でもない

なので、機械的に Use-site variance annotation に変換できます。

Use-site
interface Foo {
    Supplier<? extends Comparator<? super Cat>> foo(Function<? super Consumer<? super Animal>, ? extends List<Animal>> function);   
}

Variance がわかっていない場合

inout かどちらでもないかわからない場合は、まずは Variance について考えます。

例として、前述の Baz について考えてみましょう。

Use-site
interface Baz<T, U> {
    Supplier<? extends U> baz(Consumer<? super T> consumer);
}

まずはワイルドカードを外します。

Declaration-site(in、outを付ける前)
interface Baz<T, U> {
    Supplier<U> baz(Consumer<T> consumer);
}

一見 T は引数で、 U は戻り値で使われているように見えますが、 ConsumerContravariantSupplierCovariant です。

  • Consumer<Cat> > Consumer<Animal>
  • Supplier<Cat> < Supplier<Animal>

なので、 Baz<Cat, Animal> と Baz<Animal, Cat> で考えると前者の baz メソッドはより広い型を受けてより狭い型を返しているので、

  • Baz<Cat, Animal> < Baz<Animal, Cat>

と考えても問題なさそうです。 T については Cat < Animal と方向が同じなので CovariantU は方向が逆なので Contravariant です。つまり、 Annotation を付けると次のようになります。

Declaration-site(in、outを付けた後)
interface Baz<out T, in U> {
    Supplier<U> baz(Consumer<T> consumer);
}

ここまでわかれば、機械的に Use-site variance annotation に変換し、利用時には Baz<? extends Animal, ? super Animal> とすれば良いことがわかります。

難しいのは、 メソッドの型から Variance を考えただけでは不十分なケースがあることです。

例えば、 Java 8 で導入された Optional クラスの主な機能は次の通りで、 OptionalCovariant と考えると適切に思えます。

Use-site
class Optional<T> {
    T get() { ... }

    void ifPresent(Consumer<? super T> consumer) { ... }

    <U> Optional<U> map(Function<? super T, ? extends U> mapper) { ... }

    ...
}
  • Cat < Animal
  • Consumer<Cat> > Consumer<Animal>
  • Function<Cat, U> > Function<Animal, U>
  • Optional<Cat> < Optional<Animal>

しかし、 Optional クラスは次のようなメソッドも持っています。

Use-site
T orElse(T other) { ... }

このメソッドから考えると OptionalT について Invariant でなければなりません。しかし、 Optional の本質ではないこのメソッドに引きずられて Invariant になってしまうというのも微妙な話です。

僕の考えでは、本来は orElse は次のような static メソッドとして宣言されているべきだったと思います(もし Declaration-site variance annotationOptionalCovariant として実装するなら orElse をこうしなければならなかったはずです)。

static <T> T orElse(Optional<T> optional, T other) { ... }

このように、必ずしもその型の持つメソッドだけから既存の型の Variance を判断できるわけではありません。 Java は Declaration-site variance annotation ではないので、設計時にそこまで考慮されずにメソッドが実装されていることがあります。ですので、メソッドの型からだけではなく、その型の持つ本質的な機能まで考えて Variance を考える必要があります。

ただ、実際にはよく使うジェネリックなクラスやインタフェースは限られているので、それらの型パラメータが inout かを覚えておけば困ることは少ないと思います。

下に Java の主要なクラス、インタフェースの型パラメータに in, out を付与したリストを示します。

Declaration-site
Collection<E>
List<E>
Set<E>
SortedSet<E>
Map<K, V>
SortedMap<K, V>
Map.Entry<out K, V>

Iterable<out E>
Iterator<out E>

Comparable<in T>
Comparator<in T>

Optional<out T>

Supplier<out T>
Consumer<in T>
Predicate<in T>
Function<in T, out R>

BiConsumer<in T, in U>
BiPredicate<in T, in U>
BiFunction<in T, in U, out R>

UnaryOperator<T>
BinaryOperator<T>

Stream<out T>
Collector<in T, A, out R>

ジェネリックな型を自作する場合

主要な型については inout かを覚えてしまえばいいですが、自作した型についてはそういうわけにはいきません。

ジェネリックな型を自作する場合は、 Declaration-site variance annotation のように型パラメータが in なのか out なのかどちらでもないのかを意識して設計するのが良いと思います。メソッドの引数や戻り値が inout に違反していないか確認して下さい。

僕はわかりやすいように次のようにコメントを付けています。

Use-site
class Foo</*in*/ T, /*out*/ U, V> {
    ...
}

また、 Java 8 からは型パラメータにアノテーションを付けられるようになったので、自作ライブラリを使ってアノテーションを付けています。

Use-site
class Foo<@In T, @Out U, V> {
    ...
}

inout が決まっていれば使うときは簡単です。

Use-site
Foo<? super String, ? extends Number> getFoo();

ただ、宣言したクラスやインタフェースが正しく inout を守れているかを検証するのはなかなか難しいです。前述のライブラリでは C# と同様のアルゴリズムによって @In, @Out による型チェックを行いエラーを報告してくれます。

@In と @Out の例

? extends や ? super を書かなくても良い場合、書いた方が良い場合

利用時に out なら ? extends を、 in なら ? super を付与すると言っても、さすがにすべてのジェネリック型の変数にワイルドカードを使う必要はないと思います。

Optional<Animal> animal = Optional.ofNullable(...); // Optional<? extends Animal> でなくて良い

変数のスコープ内で CovariantContravariant であってほしいケースは稀だと思います。変数の型については必要なとき(そうしないとコンパイルが通らないとき)だけ ? extends? super をつければ良いでしょう。

しかし、メソッドの戻り値や引数、フィールドについては常に ? extends? super を付けた方が良いでしょう。前述の Comparator の例のように、 ? extends? super を書き忘れると本来問題ないことまで禁止してしまうことになります。

※ Java においては、"継承と Variance"で見たように継承してメソッドの引数の型を広げてもオーバーライドではなくオーバーロードとして扱われます。そのため、引数に対して ? extends? super が必要になるのは、ジェネリックな型がその型パラメータを別のジェネリックな型の型パラメータとして指定する場合だけです(例えば、 Foo<T> のメソッドで引数に Comparator などのジェネリックな型を指定し、その型パラメータとして T を使う場合など)。ただ、それ以外のケースでついていても特に害はないと思います。

List<? extends Foo> はダメ?

Declaration-site variance annotation 的に考えると ListInvariant なので List<? extends Foo> のように List に ? extends を付けて使うべきではないということになります。しかし、前述の

Use-site
void printAnimals(List<? extends Animal> animals) { ... }

みたいなメソッドを作りたくなったらどうすれば良いでしょうか。

Java にはイミュータブルな List を表す型がありません。イミュータブルな List もミュータブルな ListList を共用します。その上で、 addset のようなミュータブルな List 用のメソッドは optional operation なので、ミュータブルのときだけ実装して下さい(イミュータブルな List では UnsupportedOperationException をスローして下さい)という設計です。

この設計には苦々しい気持ちになりますが、そうなってしまっているものは仕方ありません。 addset が oprional operation と言うのであれば、それらがないイミュータブルな List を想定しているケースにおいては List<? extends Foo> などとしても良いと思います。

しかし、現実問題としてそういうケースでは大抵 Iterable を代わりに使えば事足りるのではないでしょうか。

Use-site
void printAnimals(Iterable<? extends Animal> animals) { ... }

IterableIterable<out T> と考えることができるので、 ? extends を付けて

Use-site
void printAnimals(Iterable<? extends Animal> animals) { ... }

としても問題ありません。

List<? extends Foo> を使うのは、すべてを理解した上で、イミュータブルな List として使いたい場合で、かつ Iterable ではダメなとき( get が使いたいときなど)に限定するのがいいんじゃないでしょうか。

PECSとの比較

PECS (producer-extends, consumer-super) は Java のワイルドカードの使い方に関する有名な戦略です。 Foo<T> から T を取得して利用する、つまり Foo が producer なら ? extends を、 Foo<T>T を渡して処理させる、つまり Foo が consumer なら ? super を付けるというものです。これは、 in が引数でしか使えず、 out が戻り値でしか使えないことと同じようなことを言っており、その結果はほぼ Declaration-site variance annotation で考えたときと同じになります。

ただ、 PECSUse-site variance annotation に基づいた考え方であり、一つの型の Variance を利用時に変更することに躊躇がないように思います。例えば、 Collections クラスの copy メソッドは次のように宣言されています。

Use-site
// src から dest へコピー
static <T> void copy(List<? super T> dest, List<? extends T> src)

dest は consumer なので ? super 、 src は producer なので ? extends というわけです。これは前節で述べた考え方とはマッチしません。 copy メソッドに関しては利用時に ListVariance を変更しなくても、次のような実装をすれば十分ではないかと思います。

// src から dest へコピー
// ただし、 dest が src より短い場合のエラー処理などは省く。
static <T, U extends T> void copy(List<T> dest, List<U> src) {
    ListIterator<T> destIterator = dest.listIterator();
    Iterator<U> srcIterator = src.listIterator();
    while (srcIterator.hasNext()) {
        destIterator.next();
        destIterator.set(srcIterator.next());
    }
}

また、 PECS はあくまで利用時の指針であり、ジェネリック型を設計するときには役に立ちません。ジェネリック型を作るときは Declaration-site variance annotation 的な考え方で、型パラメータごとに in にしたいのか out にしたいのかを考えないと、 Variance について設計者の意図がよくわからない型ができあがってしまいます。

まとめ

? super? extends は Java でジェネリクスを使う上で特に難しい部分です。 C# や Scala 、 Ceylon など比較的新しい言語では、よりシンプルでわかりやすいアプローチが採用されています。

本投稿では、それらの言語で採用された考え方を元に、 Java の ? super? extends を使う方法を紹介しました。また、そのための前提知識として Variance(変性) の考え方について説明しました。最後に、 Java でよく用いられる PECS という戦略との比較をしました。

色々と調べながら書いた部分も多いので、おかしなところがあればご指摘下さい。

参考文献

  1. Exact rules for variance validity - Eric Lippert's Blog
  2. Covariance and contravariance (computer science) - Wikipedia
190
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
koher
Swift は他の多くの言語と異なり値型中心の言語です。そんな Swift を学ぶ上で重要なポイントをまとめたオンライン書籍 "Heart of Swift" ( https://heart-of-swift.github.io ) を書きました。
qoncept
リアルタイム画像認識を専門にした会社です。近年はスポーツにおける認識技術の応用に力を入れています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
190
Help us understand the problem. What is going on with this article?