Как сделать метод возвращаемым типом generic?

Рассмотрим этот пример (типичный для книг ООП):

У меня есть класс Animal, где у каждого Animal может быть много друзей.
И подклассы типа Dog, Duck, Mouse и т.д., Которые добавляют определенное поведение, например 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();

Есть ли способ использовать generics для возвращаемого типа, чтобы избавиться от typecasting, так что я могу сказать

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

Здесь некоторый исходный код с возвращаемым типом передается методу в качестве параметра, который никогда не использовался.

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

Есть ли способ выяснить тип возврата во время выполнения без дополнительного параметра, используя instanceof? Или, по крайней мере, путем передачи класса типа вместо фиктивного экземпляра.
Я понимаю, что generics предназначены для проверки типа времени компиляции, но есть ли способ обхода этого пути?

Ответ 1

Вы можете определить 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();

Этот код имеет преимущество не генерировать никаких предупреждений компилятора. Конечно, это действительно просто обновленная версия кастинга с предродовых дней и не добавляет дополнительной безопасности.

Ответ 2

Нет. Компилятор не может знать, какой тип 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();

Ответ 3

Вы можете реализовать его следующим образом:

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

(Да, это юридический код, см. Java Generics: общий тип, определенный только как тип возврата.)

Тип возврата будет выведен из вызывающего. Однако обратите внимание на аннотацию @SuppressWarnings: это говорит о том, что этот код не является typafe. Вы должны убедиться в этом сами, или вы можете получить ClassCastExceptions во время выполнения.

К сожалению, способ, которым вы его используете (без присвоения возвращаемого значения временной переменной), единственный способ сделать компилятор счастливым - называть его следующим образом:

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

Хотя это может быть немного лучше, чем литье, вам, вероятно, лучше дать класс Animal абстрактному методу talk(), как сказал Дэвид Шмитт.

Ответ 4

Этот вопрос очень похож на Пункт 29 в Эффективной Java - "Рассмотрим типы гетерогенных контейнеров". Ответ Лаза наиболее близок к решению Блоха. Тем не менее, как 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);

Внутри обоих методов вы должны проверить, что параметры являются нормальными. См. "Эффективная Java" и Class javadoc для получения дополнительной информации.

Ответ 5

Как вы сказали, передача класса будет в порядке, вы можете написать это:

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) с использованием токенов Super Type, но у этого есть свои проблемы снова.

Ответ 6

Невозможно. Как Карта должна знать, какой подкласс Animal она собирается получить, учитывая только клавишу String?

Единственный способ, которым это было бы возможно, - это, если бы каждое Животное принимало только один тип друга (тогда он мог быть параметром класса Animal), или метод callFriend() получил параметр типа. Но действительно похоже, что вам не хватает точки наследования: это значит, что вы можете обрабатывать подклассы только при использовании исключительно методов суперкласса.

Ответ 7

Основываясь на той же идее, что и маркеры Super Type, вы можете создать типизированный идентификатор для использования вместо строки:

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, хотя это означает, что вы можете получить тип из идентификатора позже, если хотите.

Вам нужно будет также реализовать методы сравнения и хэширования TypedID, если вы хотите сравнить два идентичных экземпляра идентификатора.

Ответ 8

Кроме того, вы можете попросить метод вернуть значение в заданном типе таким образом

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

Другие примеры здесь в документации Oracle Java

Ответ 9

Я написал статью, содержащую доказательство концепции, классы поддержки и тестовый класс, который демонстрирует, как маркеры Super Type могут быть извлечены вашими классами во время выполнения. В двух словах он позволяет вам делегировать альтернативные реализации в зависимости от фактических общих параметров, передаваемых вызывающим. Пример:

  • TimeSeries<Double> делегирует частный внутренний класс, который использует double[]
  • TimeSeries<OHLC> делегирует частный внутренний класс, который использует ArrayList<OHLC>

См: Использование TypeTokens для извлечения общих параметров

Спасибо

Ричард Гомес - Blog

Ответ 10

"Есть ли способ выяснить тип возврата во время выполнения без дополнительного параметра с помощью instanceof?"

В качестве альтернативного решения вы можете использовать шаблон посетителя. Сделайте абстрактное животное и сделайте его реалистичным. 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);
  }
}

Visitable просто означает, что реализация 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); }
}

Трюк здесь заключается в том, что, поскольку Собака знает, какой тип она может вызвать соответствующий перегруженный метод посещения посетителя v, передавая "this" в качестве параметра. Другие подклассы реализуют 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 должен работать одинаково.

Ответ 11

Here is the simpler version

                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
                 }


    ======================

    Fully working code

        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

Не так, потому что, как вы говорите, компилятор знает, что callFriend() возвращает Animal, а не Dog или Duck.

Не можете ли вы добавить абстрактный метод makeNoise() в Animal, который будет реализован в виде коры или шарлата по его подклассам?

Ответ 13

То, что вы ищете, это абстракция. Код против интерфейсов больше, и вам нужно делать меньше кастинга.

Ниже приведен пример С#, но концепция остается прежней.

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

Ответ 14

Я знаю, что это совсем другая вещь, о которой спрашивал. Другим способом решения этой проблемы было бы отражение. Я имею в виду, что это не приносит пользы от 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");

    }

}

Ответ 15

как насчет

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

}

Ответ 16

Здесь много замечательных ответов, но это подход, который я принял для теста 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;
}

Ответ 17

В моем lib kontraktor я сделал следующее:

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

подклассов:

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

по крайней мере, это работает внутри текущего класса и при наличии сильной типизированной ссылки. Многократное наследование работает, но становится очень сложным:)

Ответ 18

Существует другой подход, вы можете сузить возвращаемый тип при переопределении метода. В каждом подклассе вам придется переопределить callFriend, чтобы вернуть этот подкласс. Стоимость будет заключаться в нескольких объявлениях callFriend, но вы можете выделить общие части для метода, называемого внутренне. Для меня это намного проще, чем упомянутые выше решения, и для определения возвращаемого типа не нужен дополнительный аргумент.

Ответ 19

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.

Это лучше всего, если вы хотите настроить любые другие записи вместо реальных курсоров.