6

Javaの総称型について教えてください。

以下のようなコードを書きましたが、コンパイルエラーとなりました。

class Hoge {}

class Test<T> {
    Test() {
        // error: incompatible types: Hoge cannot be converted to T
        T hoge = new Hoge();
    }
}

そこで、総称型TをHogeという具体的なクラスに限定すればよいのでは、と考えて、以下のように修正しました。

class Test<T extends Hoge> {

しかし、同じコンパイルエラーが出てしまいます。

どのようにすれば実現できますでしょうか。(そもそも総称型にしている意味が無い、というご指摘はあるかと思いますが)


質問者追記:質問の発端となった具体的なコード

文章でうまく表現できている自信がありませんが、やりたいことは以下になります。

  • 複数のアプリケーションに共通する処理は親クラスで共通部品として用意する
  • それを継承する各アプリケーションは、ユーザAまたはユーザBというクラスを使う
  • ユーザAとユーザBのどちらを使うのかは、アプリケーションによって事前に決まっている
  • ユーザAとユーザBが持つメソッドは異なる
  • ユーザのインスタンスは親クラスのメンバで持ちたい
  • そのインスタンスを生成するのも、親クラスでやりたい(ここで躓いた)

具体的なコードは以下になります。

共通部品としてあらかじめ用意しておくクラス

// アプリケーションの流れを Template Method パターンで用意
abstract class AppBase<T extends UserBase> {
    // ユーザの種類はサブクラスが決める
    T user;
}

// ユーザの基底クラス
class UserBase {}

// ユーザA
class UserA extends UserBase {
    // ユーザAは独自のメソッドmethodAを持っている
    void methodA() {}
}

// ユーザB
class UserB extends UserBase {
    // ユーザBは独自のメソッドmethodBを持っている
    void methodB() {}
}

上記部品を使って実装するイメージ

// ユーザA向けのアプリケーションA
class AppForUserA extends AppBase<UserA> {
    public AppForUserA() {
        // ユーザAだけが持っている、user.methodA() を使った処理
    }
}

// ユーザA向けのアプリケーションB
class AppForUserB extends AppBase<UserB> {
    public AppForUserB() {
        // ユーザBだけが持っている、user.methodB() を使った処理
    }
}

ここで、AppBaseuser メンバにユーザクラスのインスタンスを代入したいと思い、

abstract class AppBase<T extends UserBase> {
    T user;
    public AppBase() {
        T user = new T();
        // -> error: unexpected type
    }
}

としようとしたところ無理であることを知り、そこで試しに、それ自体に意味はありませんが

abstract class AppBase<T extends UserBase> {
    protected T user;
        public AppBase() {
        T user = new UserA();
        // -> error: incompatible types: UserA cannot be converted to T
    }
}

としてみてもエラーとなったため、質問した次第です。

andou
  • 689
  • 1
  • 4
  • 14

1 Answers1

3

代入の場合は、 extends ではなく super を用いる必要があるはずです。


そして、もう少し調べました。

例えば、今回のケースで言えば、以下のようなことが、ひとまずやれたらいいな、と思うことかもしれません:

class Hoge {}

class Test<T super Hoge> {
    Test() {
        T hoge = new Hoge();
    }
}

しかし、その実、これはコンパイルが通りません。参考: https://stackoverflow.com/questions/37411256/why-super-keyword-in-generics-is-not-allowed-at-class-level

これは、 java の generics は、コンパイルした直後に generics 情報が消えることと関係していると思います。このコードをコンパイルしようとしたとき、その型的な制約は、「T は任意の Hoge の上位クラスたちすべてに対して成立するようなコードのみを許可する。つまり、メソッドコールであれば Hoge の上位クラスがすべて持っているようなメソッドに限るし、代入であれば Hoge の上位クラスすべてが代入可能な型に対してのみ代入されなければならない」です。

これは、結果として何がおこるかというと、それって T はひとまず Object として取り扱ってこの file をコンパイルしてしまうのと、できることはまったくかわらなくなります。なので、 class に対するジェネリクスを定義する際に、 super 制約は付ける意味がない、だからいらない。禁止。という論理だと思っています。


追記された条件でしたら、私だったら、以下のようにすると思います。

  • AppBase に abstract な T を返す createUser メソッドを追加
  • 各 個別 App で createUser 実装
Yuki Inoue
  • 16,805
  • 19
  • 80
  • 196
  • ありがとうございます。ジェネリクスについて自分の理解がかなり足りていないことが分かりました。 – andou May 30 '18 at 03:05
  • @andou generics でできることは、型推論によるサポートでしかなくって、わかりやすいのは、 Q. その型パラメータをすべて端折って、T をとりだしているところにはキャストを書いたような場合に、プログラムは問題なく動くか? を考えることだと思います。事実、イレージャによって、 java の JV 上ではそう動いているはずなので。 – Yuki Inoue May 30 '18 at 04:11
  • 1
    ご提案頂いた方法や AppBase のコンストラクタの引数でインスタンスを渡す方法が解決方法かと思うのですが、サブクラスでの継承時に「UserAクラスを使いたい(extends AppBase<UserA>)」と親クラスに伝えており、 せっかくなのでその情報を使ってインスタンス生成を親クラス側で行うことができれば、サブクラス実装者は少し楽ができるかな、と考えた次第です。ただ、いま気付いたのですが、それが可能なのはUserAやUserBのコンストラクタのI/Fが同じである必要がありますね…。 – andou May 30 '18 at 05:08
  • 1
    楽だからという理由だけでそのような実装を行う人を見かけますが、単一責任の原則に違反したコードになり、大体のケースで不幸になります(コードの依存解決が難しくなる、テストしづらくなるなど)。逆にAppがUserを作る振る舞いを持つドメインと断言できるのであれば、createUser()というようなメソッドを持つべきでしょう。通常はそのような振る舞いや責務を持たないので、コンストラクタで渡し、テスト時にはUserをmockしたオブジェクトを渡すかと思います。 – harry0000 May 30 '18 at 15:30
  • 1
    @harry0000 なるほど、テストしやすさの視点が抜けていました。「責務」についてはまだピンときていませんので、勉強します。この質問への回答としては「そもそも質問がXY問題に陥っており、コンストラクタからインスタンスを渡す等すべき」で解決済みにしたいと思います。 – andou May 31 '18 at 01:24