web-dev-qa-db-ja.com

メソッドの戻り型をジェネリックにするにはどうすればよいですか?

この例を考えてみましょう(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を使用して追加のパラメータなしで実行時に戻り型を把握する方法はありますか?または少なくともダミーインスタンスの代わりにその型のクラスを渡すことによって。
総称はコンパイル時の型チェックを目的としていることを理解していますが、これに対する回避策はありますか?

522
Sathish

このように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();

このコードには、コンパイラの警告を生成しないという利点があります。もちろん、これは実際には一般的な時代のキャスティングの単なる更新版であり、追加の安全性を追加するものではありません。

805
laz

いいえ、コンパイラは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();
111
David Schmitt

あなたはこのようにそれを実装することができます:

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

(はい、これは有効なコードです。 Java Generics:戻り型としてのみ定義されている汎用型 を参照してください。)

戻り型は呼び出し元から推測されます。ただし、@SuppressWarningsアノテーションに注意してください。これは、 このコードは型保証がない ではないことを示しています。あなたはそれをあなた自身で確かめなければなりません、さもなければあなたは実行時にClassCastExceptionsを手に入れることができます。

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

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

これはキャストするより少し良いかもしれませんが、David Schmittが言ったように、おそらくAnimalクラスに抽象的なtalk()メソッドを与えるほうが得策です。

99
Michael Myers

この質問はEffective Javaの Item 29に非常によく似ています - "型安全な異種コンテナを考えてください。 Lazの答えは、Blochのソリューションに最も近いものです。ただし、putとgetはどちらも安全のためにClassリテラルを使用する必要があります。署名は次のようになります。

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を参照してください。

25
Craig P. Motlin

さらに、このようにしてメソッドに特定の型の値を返すように依頼することもできます。

<T> T methodName(Class<T> var);

その他の例 ここ Oracle Javaドキュメント

14

これがより単純なバージョンです。

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();

        }
    }
12
webjockey

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

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の総称を使って得られる限りのことです。 スーパータイプトークンを使用してタイプセーフなヘテロコンテナ(THC) を実装する方法がありますが、それもまた独自の問題を抱えています。

10
Fabian Steeg

スーパータイプトークンと同じ考え方に基づいて、文字列の代わりに使用する型付き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の内側にtypeパラメータを隠しているだけですが、必要に応じて後で識別子からタイプを取得できるという意味です。

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

7
Mike Houston

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

代わりの解決策として 訪問者パターン のように利用することができます。 Animalを抽象化し、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);
}

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

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

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

ここでのトリックは、犬がそれがどんなタイプであるかを知っているのでそれがパラメータとして "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(); }
}

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

6
Antti Siiskonen

概念実証、サポートクラス、および実行時にスーパータイプトークンをクラスから取得する方法を示すテストクラスを含む記事を書きました。一言で言えば、それはあなたが呼び出し側によって渡された実際の一般的なパラメータに応じて代替の実装に委任することを可能にします。例:

  • TimeSeries<Double>は、double[]を使用するプライベート内部クラスに委任します
  • TimeSeries<OHLC>は、ArrayList<OHLC>を使用するプライベート内部クラスに委任します

TypeTokensを使って一般的なパラメータを取得する

ありがとう

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

5
Richard Gomes

ありえない。 Stringキーだけが与えられた場合、MapはどのようにしてAnimalのどのサブクラスを取得しようとしているのかを知ることになっていますか?

これが可能な唯一の方法は、各Animalが1種類のフレンドしか受け入れない場合(それはAnimalクラスのパラメータになる可能性がある)、またはcallFriend()メソッドがtypeパラメータを取得した場合です。しかし、継承のポイントを逃しているように見えます。スーパークラスのメソッドを排他的に使用するときは、サブクラスを一様に扱うことしかできません。

5

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

以下の例は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
Nestor Ledon

ご存知のように、コンパイラはcallFriend()がAnimalを返し、DogやDuckを返さないことだけを認識しているためです。

あなたは、そのサブクラスによって吠え声やackとして実装される抽象makeNoise()メソッドをAnimalに追加することはできませんか?

2
sk.

ここにはたくさんの素晴らしい答えがありますが、これは私が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は、その型が拡張されたスーパークラスであり、その子のいずれかを使用できるという意味です。
  • 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;
}
2
MTen

これは全く違うことだと私は知っています。これを解決するもう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
user743489

どうですか

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);
}

}

1
gafadr

私は私のlib kontraktorで以下をしました:

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

サブクラス化:

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

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

1
R.Moeller

別の方法があります。メソッドをオーバーライドするときに戻り型を絞り込むことができます。各サブクラスでは、そのサブクラスを返すようにcallFriendをオーバーライドする必要があります。コストはcallFriendの複数回の宣言になりますが、共通部分は内部的に呼び出されるメソッドに分離できます。これは上で述べた解決策よりもずっと簡単に思えますし、戻り型を決定するために追加の引数を必要としません。

0
FeralWhippet