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



рассмотрим этот пример (типичный в книгах ООП):



у меня есть Animal класса, где каждый элемент Animal может быть много друзей.

И подклассы вроде Dog,Duck,Mouse etc которые добавляют специфическое поведение как bark(),quack() etc.



здесь 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? Или, по крайней мере, передавая класс типа вместо фиктивного экземпляра.

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

628   19  

19 ответов:

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

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

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

вы могли бы реализовать это так:

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

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

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

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

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

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

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

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

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

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

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

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

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 generics. Есть способ реализовать Типобезопасные гетерогенные контейнеры (THC) с использованием маркеров Супертипа, но это имеет свои собственные проблемы.

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

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

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

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

не возможно. Как карта должна знать, какой подкласс животных она получит, учитывая только строковый ключ?

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

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

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

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 просто означает, что реализация животного готова принять посетителя:

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

трюк здесь заключается в том, что, поскольку собака знает, какой тип она может вызвать соответствующий перегруженный метод посещения посетителя v, передав "это" в качестве параметра. Другие подклассы будут реализовывать accept () точно так же.

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

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 должен работать одинаково.

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

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

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

спасибо

Ричард Гомес - блог

вот более простая версия:

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

        }
    }

не совсем, потому что, как вы говорите, компилятор знает только, что callFriend() возвращает животное, а не собаку или утку.

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

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

пример в 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!");
    }
}
}

здесь есть много отличных ответов, но это подход, который я взял для теста 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)
  • тип.getConstructor(Param.класса и т. д.) позволяет взаимодействовать с конструктор типа. Этот конструктор должен быть одинаковым между всеми ожидаемыми классами.
  • 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;
}

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

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

    }

}

а как же

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

}

Я сделал следующее в своем lib контракторе:

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

подклассы:

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

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

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

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.

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

Comments

    Ничего не найдено.