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

StackOverflow https://stackoverflow.com/questions/450807

  •  19-08-2019
  •  | 
  •  

Вопрос

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

У меня есть 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();

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

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

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

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

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

Это было полезно?

Решение

Вы можете определить 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) : Общий тип определен только как возвращаемый тип .)

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

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

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

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

Этот вопрос очень похож на пункт 29 в Effective 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 и класс javadoc. Информация.

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

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

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

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

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

        }
    }

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

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. Существует способ реализации Typesafe гетерогенных контейнеров (THC) с использованием Супер-жетоны типа , но у него опять свои проблемы.

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

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

" есть ли способ выяснить тип возвращаемого значения во время выполнения без дополнительного параметра, используя 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);
  }
}

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

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

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

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

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

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

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

Спасибо

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

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

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

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

Здесь вы ищете абстракцию. Кодируйте больше интерфейсов, и вам придется меньше кастовать.

Пример ниже приведен на 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!");
    }
}
}

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

    }

}

о чем

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

}

Я сделал следующее в своей библиотеке kontraktor:

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.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top