メソッドの戻り値の型をジェネリックにする方法を教えてください。


588

次の例を考えてみてください(OOPブックでは一般的です)。

私にはAnimalクラスがあり、それぞれにAnimal多くの友達がいます。
好きなサブクラスDogDuckMouseのような特定の動作を追加などbark()quack()など

ここにAnimalクラスがあります:

public class Animal {
    private Map<String,Animal> friends = new HashMap<>();

    public void addFriend(String name, Animal animal){
        friends.put(name,animal);
    }

    public Animal callFriend(String name){
        return friends.get(name);
    }
}

そして、ここにたくさんの型キャストがあるコードスニペットがあります:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

((Dog) jerry.callFriend("spike")).bark();
((Duck) jerry.callFriend("quacker")).quack();

型キャストを取り除くために戻り値の型にジェネリックスを使用できる方法はありますか?

jerry.callFriend("spike").bark();
jerry.callFriend("quacker").quack();

以下に、使用されないパラメーターとしてメソッドに返される戻り値の型を含む初期コードをいくつか示します。

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

追加のパラメーターを使用せずに、実行時に戻りの型を理解する方法はありますinstanceofか?または、少なくともダミーのインスタンスの代わりにそのタイプのクラスを渡すことによって。
ジェネリックはコンパイル時の型チェック用であると理解していますが、これに対する回避策はありますか?

回答:


903

callFriendこのように定義できます:

public <T extends Animal> T callFriend(String name, Class<T> type) {
    return type.cast(friends.get(name));
}

次に、そのように呼び出します。

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

このコードには、コンパイラ警告を生成しないという利点があります。もちろん、これは実際には、前一般的な時代のキャストのアップデートバージョンであり、安全性を高めるものではありません。


29
...しかし、callFriend()呼び出しのパラメータ間のコンパイル時の型チェックはまだありません。
デビッドシュミット

2
これが今のところ最良の答えです。ただし、同じ方法でaddFriendを変更する必要があります。両方の場所でそのクラスリテラルが必要になるため、バグの記述が難しくなります。
クレイグP.モトリン2009年

@Jaider、まったく同じではありませんが、これは機能します。} //呼び出しクラスjerry.CallFriend <Dog>( "spike")。Bark(); jerry.CallFriend <Duck>( "quacker")。Quack();
Nestor Ledon 2014年

124

いいえ。コンパイラはどのタイプjerry.callFriend("spike")が返されるかを知ることができません。また、実装では、型の安全性を追加することなく、メソッドのキャストを非表示にするだけです。このことを考慮:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast

この特定のケースでは、抽象talk()メソッドを作成し、サブクラスで適切にオーバーライドすることで、より適切に機能します。

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();

10
mmyersメソッドはうまくいくかもしれませんが、このメソッドはオブジェクト指向プログラミングの方が優れており、将来的にはいくつかの問題を解決できると思います。
James McMahon、

1
これは、同じ結果を得る正しい方法です。目的は、醜い型チェックとキャストを行うためのコードを自分で明示的に記述せずに、実行時に派生クラス固有の動作を取得することです。@lazによって提案された方法は機能しますが、タイプセーフをウィンドウの外に投げ出します。このメソッドではコード行数は少なくて済みます(メソッドの実装が実行時にバインドされ、実行時に検索されるため)。それでも、Animalの各サブクラスに一意の動作を簡単に定義できます。
dcow 2013

しかし、最初の質問は型安全性について尋ねているのではありません。私がそれを読む方法では、質問者は、ジェネリックを活用してキャストする必要がないようにする方法があるかどうかを知りたいだけです。
2013

2
@laz:はい、元の質問は(提起されたとおり)タイプセーフについてではありません。それを実装する安全な方法があるという事実は変わりません。クラスキャストの失敗を排除します。weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
デビッドシュミット

3
私はそれに同意しませんが、私たちはJavaとそのすべての設計上の決定/問題を扱っています。この質問は、再設計する必要があるxyproblem(meta.stackexchange.com/questions/66377/what-is-the-xy-problem)としてではなく、Javaジェネリックで何が可能かを学習しようとするものだと思っています。他のパターンやアプローチと同様に、私が提供したコードが適切な場合もあれば、(この回答で提案したような)まったく異なるものが必要な場合もあります。
2013

114

次のように実装できます。

@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
    return (T)friends.get(name);
}

(はい、これは正当なコードです。JavaGenerics:戻り値の型としてのみ定義された汎用型を参照してください。)

戻り値の型は呼び出し元から推測されます。ただし、@SuppressWarnings注釈に注意してください。このコードはタイプセーフではないことを示しています。自分で確認する必要がありClassCastExceptionsます。確認しないと、実行時に取得できます。

残念ながら、(一時的な変数に戻り値を割り当てずに)それを使用する方法は、コンパイラーを満足させる唯一の方法は、次のように呼び出すことです。

jerry.<Dog>callFriend("spike").bark();

これはキャストより少し良いかもしれませんが、David Schmittが言ったように、Animalクラスに抽象talk()メソッドを与える方が良いでしょう。


メソッドの連鎖は、実際には意図されていませんでした。値をサブタイプ化された変数に割り当てて使用してもかまいません。解決策をありがとう。
サティシュ2009年

これは、メソッド呼び出しの連鎖を行うときに完全に機能します。
Hartmut P.

私はこの構文が本当に好きです。C#ではjerry.CallFriend<Dog>(...見た目が良いと思います。
andho

興味深いことに、JRE自体のjava.util.Collections.emptyList()関数はこのように実装されており、そのjavadocはタイプセーフであることをアドバタイズします。
Ti Strga 2018年

31

この質問は、Effective Javaのアイテム29「タイプセーフな異種コンテナを検討する」とよく似ています。ラズの答えはブロッホの解決策に最も近いものです。ただし、安全のため、putとgetの両方でクラスリテラルを使用する必要があります。署名は次のようになります。

public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> T callFriend(String name, Class<T> type);

両方のメソッド内で、パラメーターが正常であることを確認する必要があります。詳細については、Effective JavaとClass javadocを参照してください。



17

これはより簡単なバージョンです:

public <T> T callFriend(String name) {
    return (T) friends.get(name); //Casting to T not needed in this case but its a good practice to do
}

完全に機能するコード:

    public class Test {
        public static class Animal {
            private Map<String,Animal> friends = new HashMap<>();

            public void addFriend(String name, Animal animal){
                friends.put(name,animal);
            }

            public <T> T callFriend(String name){
                return (T) friends.get(name);
            }
        }

        public static class Dog extends Animal {

            public void bark() {
                System.out.println("i am dog");
            }
        }

        public static class Duck extends Animal {

            public void quack() {
                System.out.println("i am duck");
            }
        }

        public static void main(String [] args) {
            Animal animals = new Animal();
            animals.addFriend("dog", new Dog());
            animals.addFriend("duck", new Duck());

            Dog dog = animals.callFriend("dog");
            dog.bark();

            Duck duck = animals.callFriend("duck");
            duck.quack();

        }
    }

1
何だCasting to T not needed in this case but it's a good practice to do。実行時に適切に処理されている場合、「良い習慣」とはどういう意味ですか?
ファリド

メソッド宣言の戻り型宣言<T>で十分なはずなので、明示的なキャスト(T)は必要ないということを意味しました
webjockey

9

クラスを渡すのは問題ないと言ったので、次のように書くことができます。

public <T extends Animal> T callFriend(String name, Class<T> clazz) {
   return (T) friends.get(name);
}

そして、次のように使用します。

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

完璧ではありませんが、これはJavaジェネリックを使用する場合に限ります。Super Type Tokenを使用してTypesafe Heterogenous Containers(THC)を実装する方法はありますが、これにも固有の問題があります。


申し訳ありませんが、これはlazの回答とまったく同じなので、あなたは彼をコピーするか、彼があなたをコピーしています。
James McMahon、

Typeを渡すための素晴らしい方法です。しかし、それでもシュミットが言ったようにタイプセーフではありません。私はまだ別のクラスに合格することができ、タイプキャスティングが爆破されます。タイプインリターンタイプを設定するmmyersの2番目の応答はより良いようです
Sathish

4
ニモ、投稿時刻を確認すると、ほぼ同時に投稿されたことがわかります。また、それらは完全に同じではなく、2行のみです。
Fabian Steeg、

@Fabian私も同様の回答を投稿しましたが、BlochのスライドとEffective Javaで公開されたものとの間には重要な違いがあります。TypeRef <T>の代わりにClass <T>を使用します。しかし、これはまだ素晴らしい答えです。
クレイグP.モトリン2009年

8

スーパータイプトークンと同じアイデアに基づいて、文字列の代わりに使用する型付きIDを作成できます。

public abstract class TypedID<T extends Animal> {
  public final Type type;
  public final String id;

  protected TypedID(String id) {
    this.id = id;
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
  }
}

しかし、これは目的を無効にする可能性があると思います。各文字列に対して新しいidオブジェクトを作成して保持する必要があるためです(または正しいタイプ情報で再構築する必要があります)。

Mouse jerry = new Mouse();
TypedID<Dog> spike = new TypedID<Dog>("spike") {};
TypedID<Duck> quacker = new TypedID<Duck>("quacker") {};

jerry.addFriend(spike, new Dog());
jerry.addFriend(quacker, new Duck());

ただし、キャストを行わなくても、本来の方法でクラスを使用できます。

jerry.callFriend(spike).bark();
jerry.callFriend(quacker).quack();

これは、ID内のタイプパラメータを非表示にするだけですが、必要に応じて、後で識別子からタイプを取得できることを意味します。

IDの2つの同一のインスタンスを比較できるようにする場合は、TypedIDの比較およびハッシュメソッドも実装する必要があります。


8

「instanceofを使用して、追加のパラメーターなしで実行時に戻り値の型を把握する方法はありますか?」

代替ソリューションとして、次のようなVisitorパターンを利用できます。動物を抽象化し、Visitableを実装します。

abstract public class Animal implements Visitable {
  private Map<String,Animal> friends = new HashMap<String,Animal>();

  public void addFriend(String name, Animal animal){
      friends.put(name,animal);
  }

  public Animal callFriend(String name){
      return friends.get(name);
  }
}

訪問可能とは、Animalの実装が訪問者を受け入れる用意があることを意味します。

public interface Visitable {
    void accept(Visitor v);
}

そしてビジターの実装は動物のすべてのサブクラスを訪問することができます:

public interface Visitor {
    void visit(Dog d);
    void visit(Duck d);
    void visit(Mouse m);
}

したがって、たとえば、犬の実装は次のようになります。

public class Dog extends Animal {
    public void bark() {}

    @Override
    public void accept(Visitor v) { v.visit(this); }
}

ここでのトリックは、Dogがどのタイプかを知っているので、「this」をパラメーターとして渡すことで、訪問者vの関連するオーバーロードされた訪問メソッドをトリガーできることです。他のサブクラスもまったく同じ方法でaccept()を実装します。

次に、サブクラス固有のメソッドを呼び出すクラスは、次のようにVisitorインターフェースを実装する必要があります。

public class Example implements Visitor {

    public void main() {
        Mouse jerry = new Mouse();
        jerry.addFriend("spike", new Dog());
        jerry.addFriend("quacker", new Duck());

        // Used to be: ((Dog) jerry.callFriend("spike")).bark();
        jerry.callFriend("spike").accept(this);

        // Used to be: ((Duck) jerry.callFriend("quacker")).quack();
        jerry.callFriend("quacker").accept(this);
    }

    // This would fire on callFriend("spike").accept(this)
    @Override
    public void visit(Dog d) { d.bark(); }

    // This would fire on callFriend("quacker").accept(this)
    @Override
    public void visit(Duck d) { d.quack(); }

    @Override
    public void visit(Mouse m) { m.squeak(); }
}

あなたが交渉したよりもはるかに多くのインターフェイスとメソッドであることは知っていますが、インスタンスのチェックと型キャストがまったくゼロである特定のサブタイプごとにハンドルを取得する標準的な方法です。そして、それはすべて標準言語にとらわれない方法で行われるので、それはJavaだけでなく、あらゆるOO言語が同じように機能するはずです。


6

ありえない。文字列キーのみが与えられた場合、マップはどのようにして、Animalのどのサブクラスを取得するかを知っているはずですか?

これが可能になる唯一の方法は、各Animalが1種類の友達しか受け入れなかった場合(その後、Animalクラスのパラメーターである可能性があります)、またはcallFriend()メソッドが型パラメーターを取得した場合です。しかし、継承のポイントが欠けているように見えます。つまり、スーパークラスのメソッドのみを使用する場合は、サブクラスを均一にしか処理できないということです。


5

概念実証、サポートクラス、および実行時にスーパータイプトークンをクラスが取得する方法を示すテストクラスを含む記事を書きました。簡単に言うと、呼び出し元から渡された実際の汎用パラメーターに応じて、代替実装に委任できます。例:

  • TimeSeries<Double> を使用するプライベート内部クラスにデリゲート double[]
  • TimeSeries<OHLC> を使用するプライベート内部クラスにデリゲート ArrayList<OHLC>

参照: TypeTokensを使用してジェネリックパラメーターを取得する

ありがとう

リチャードゴメス- ブログ


確かに、あなたの洞察を共有してくれてありがとう、あなたの記事は本当にそれをすべて説明しています!
ヤン・ガエル・Guéhéneuc

3

ここには多くの素晴らしい答えがありますが、これは私がAppiumテストで採用したアプローチであり、単一の要素に作用すると、ユーザーの設定に基づいてさまざまなアプリケーション状態に移行する可能性があります。OPの例の慣例には従いませんが、誰かを助けることを願っています。

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePageは、タイプが拡張するスーパークラスであり、その子を使用できることを意味します(duh)
  • type.getConstructor(Param.classなど)を使用すると、型のコンストラクターを操作できます。このコンストラクタは、予想されるすべてのクラスで同じである必要があります。
  • newInstanceは、新しいオブジェクトのコンストラクタに渡す宣言された変数を受け取ります

エラーをスローしたくない場合は、次のようにしてエラーをキャッチできます。

public <T extends MobilePage> T tapSignInButton(Class<T> type) {
    // signInButton.click();
    T returnValue = null;
    try {
       returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return returnValue;
}

私の理解では、これはジェネリックスを使用するための最良かつ最もエレガントな方法です。
cbaldan 2017年

2

あなたが言うように、コンパイラは、callFriend()がDogまたはDuckではなく、Animalを返すことだけを知っているので、実際にはそうではありません。

サブクラスによって樹皮またはいんちきとして実装されるであろう、抽象的なmakeNoise()メソッドをAnimalに追加できませんか?


1
動物が抽象化できる共通のアクションに該当しない複数のメソッドを持っている場合はどうなりますか?インスタンスではなく、Typeを渡しても問題がない場合に、アクションが異なるサブクラス間の通信にこれが必要です。
Sathish、2009年

2
あなたは本当にあなた自身の質問に答えました-動物がユニークなアクションを持っているなら、あなたはその特定の動物にキャストしなければなりません。動物に他の動物とグループ化できるアクションがある場合は、基本クラスで抽象メソッドまたは仮想メソッドを定義して使用できます。
Matt Jordan、

2

ここで探しているのは抽象化です。インターフェイスに対するコードを増やすと、キャストを少なくする必要があります。

以下の例はC#で記述されていますが、概念は同じです。

using System;
using System.Collections.Generic;
using System.Reflection;

namespace GenericsTest
{
class MainClass
{
    public static void Main (string[] args)
    {
        _HasFriends jerry = new Mouse();
        jerry.AddFriend("spike", new Dog());
        jerry.AddFriend("quacker", new Duck());

        jerry.CallFriend<_Animal>("spike").Speak();
        jerry.CallFriend<_Animal>("quacker").Speak();
    }
}

interface _HasFriends
{
    void AddFriend(string name, _Animal animal);

    T CallFriend<T>(string name) where T : _Animal;
}

interface _Animal
{
    void Speak();
}

abstract class AnimalBase : _Animal, _HasFriends
{
    private Dictionary<string, _Animal> friends = new Dictionary<string, _Animal>();


    public abstract void Speak();

    public void AddFriend(string name, _Animal animal)
    {
        friends.Add(name, animal);
    }   

    public T CallFriend<T>(string name) where T : _Animal
    {
        return (T) friends[name];
    }
}

class Mouse : AnimalBase
{
    public override void Speak() { Squeek(); }

    private void Squeek()
    {
        Console.WriteLine ("Squeek! Squeek!");
    }
}

class Dog : AnimalBase
{
    public override void Speak() { Bark(); }

    private void Bark()
    {
        Console.WriteLine ("Woof!");
    }
}

class Duck : AnimalBase
{
    public override void Speak() { Quack(); }

    private void Quack()
    {
        Console.WriteLine ("Quack! Quack!");
    }
}
}

この質問は、概念ではなくコーディングに関係しています。

2

私のlib kontraktorで次のことを行いました:

public class Actor<SELF extends Actor> {
    public SELF self() { return (SELF)_self; }
}

サブクラス化:

public class MyHttpAppSession extends Actor<MyHttpAppSession> {
   ...
}

少なくとも、これは現在のクラス内で機能し、強い型付き参照がある場合に機能します。多重継承は機能しますが、そのとき本当にトリッキーになります:)


1

これはまったく違うことだと私は聞いた。これを解決する別の方法は、リフレクションです。つまり、これはGenericsの利点を活用していませんが、型キャストを気にせずに、実行したい動作(犬の鳴き声、カモの鳴き声など)をエミュレートできます。

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

abstract class AnimalExample {
    private Map<String,Class<?>> friends = new HashMap<String,Class<?>>();
    private Map<String,Object> theFriends = new HashMap<String,Object>();

    public void addFriend(String name, Object friend){
        friends.put(name,friend.getClass());
        theFriends.put(name, friend);
    }

    public void makeMyFriendSpeak(String name){
        try {
            friends.get(name).getMethod("speak").invoke(theFriends.get(name));
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    } 

    public abstract void speak ();
};

class Dog extends Animal {
    public void speak () {
        System.out.println("woof!");
    }
}

class Duck extends Animal {
    public void speak () {
        System.out.println("quack!");
    }
}

class Cat extends Animal {
    public void speak () {
        System.out.println("miauu!");
    }
}

public class AnimalExample {

    public static void main (String [] args) {

        Cat felix = new Cat ();
        felix.addFriend("Spike", new Dog());
        felix.addFriend("Donald", new Duck());
        felix.makeMyFriendSpeak("Spike");
        felix.makeMyFriendSpeak("Donald");

    }

}

1

どうですか

public class Animal {
private Map<String,<T extends Animal>> friends = new HashMap<String,<T extends Animal>>();

public <T extends Animal> void addFriend(String name, T animal){
    friends.put(name,animal);
}

public <T extends Animal> T callFriend(String name){
    return friends.get(name);
}

}


0

別のアプローチがあります。メソッドをオーバーライドするときに戻り値の型を絞り込むことができます。各サブクラスでは、そのサブクラスを返すためにcallFriendをオーバーライドする必要があります。コストはcallFriendの複数の宣言になりますが、内部で呼び出されるメソッドに共通の部分を分離できます。これは、上記の解決策よりもはるかに単純で、戻り値の型を決定するための追加の引数は必要ありません。


「戻り値の型を狭める」という意味がわかりません。Afaik、Java、およびほとんどの型付き言語は、戻り値の型に基づいてメソッドや関数をオーバーロードしません。たとえば、コンパイラの観点からpublic int getValue(String name){}見分けがつかないpublic boolean getValue(String name){}。オーバーロードが認識されるようにするには、パラメータータイプを変更するか、パラメーターを追加/削除する必要があります。多分私はあなたを誤解しているだけかもしれません...
The One True Colter '18

Javaでは、サブクラスのメソッドをオーバーライドして、より「狭い」(つまり、より具体的な)戻り値の型を指定できます。stackoverflow.com/questions/14694852/…を参照してください。
FeralWhippet 2017

-3
public <X,Y> X nextRow(Y cursor) {
    return (X) getRow(cursor);
}

private <T> Person getRow(T cursor) {
    Cursor c = (Cursor) cursor;
    Person s = null;
    if (!c.moveToNext()) {
        c.close();
    } else {
        String id = c.getString(c.getColumnIndex("id"));
        String name = c.getString(c.getColumnIndex("name"));
        s = new Person();
        s.setId(id);
        s.setName(name);
    }
    return s;
}

どんなタイプでも返して直接受け取ることができます。型キャストする必要はありません。

Person p = nextRow(cursor); // cursor is real database cursor.

これは、実際のカーソルではなく他の種類のレコードをカスタマイズする場合に最適です。


2
あなたは「型キャストする必要はない」と言っていますが、明らかに型キャストが含まれています:(Cursor) cursorたとえば。
2016年

これは、ジェネリックの完全に不適切な使用です。このコードはである必要cursorがあり、Cursor常にPerson(またはnull)を返します。ジェネリックスを使用すると、これらの制約のチェックが削除され、コードが安全でなくなります。
アンディターナー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.