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


589

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

У меня есть 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? Или, по крайней мере, передав класс типа вместо фиктивного экземпляра.
Я понимаю, что дженерики предназначены для проверки типов во время компиляции, но есть ли обходной путь для этого?

Ответы:


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 таким же образом. Это затрудняет написание ошибок, так как вам нужен этот литерал класса в обоих местах.
Крейг П. Мотлин,

@Jaider, не совсем то же самое, но это будет работать: // Класс животных public T CallFriend <T> (имя строки), где T: Animal {вернуть друзей [name] как T; } // Вызов класса jerry.CallFriend <Dog> ("spike"). Bark (); jerry.CallFriend <Утка> ( "quacker") Квак ().
Нестор Ледон

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

1
Это правильный способ добиться того же результата. Обратите внимание, что цель состоит в том, чтобы получить поведение, специфичное для производного класса, во время выполнения без явного написания кода для выполнения уродливой проверки типов и приведения типов. Метод, предложенный @laz, работает, но выбрасывает безопасность типов в окно. Этот метод требует меньше строк кода (поскольку реализации метода в любом случае связаны с поздним связыванием и проверяются во время выполнения), но все же позволяет легко определить уникальное поведение для каждого подкласса Animal.
13

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

2
@laz: да, первоначальный вопрос - как он задан - не о безопасности типов. Это не меняет того факта, что есть безопасный способ реализовать это, устраняя сбои приведения классов. См. Также weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
Дэвид Шмитт

3
Я не согласен с этим, но мы имеем дело с Java и всеми его проектными решениями / недостатками. Я рассматриваю этот вопрос как попытку понять, что возможно в обобщениях Java, а не как проблему xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ), которую необходимо переработать. Как и в случае с любым шаблоном или подходом, бывают случаи, когда приведенный мною код является подходящим, и иногда требуется нечто совершенно иное (например, то, что вы предложили в этом ответе).
лаз

114

Вы можете реализовать это так:

@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()метод, как сказал Дэвид Шмитт.


Метод цепочки не был на самом деле намерением. я не против присвоить значение переменной Subtyped и использовать его. Спасибо за решение.
Сатиш

это отлично работает при выполнении цепочки вызовов методов!
Хартмут П.

Мне очень нравится этот синтаксис. Я думаю, что в C # это jerry.CallFriend<Dog>(...выглядит лучше.
andho

Интересно, что собственная java.util.Collections.emptyList()функция JRE реализована именно так, и ее Javadoc объявляет себя безопасным для типов.
Ti Strga

31

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


17

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

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

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


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


Извините, но это тот же самый ответ, который есть у Лаз, так что вы либо копируете его, либо он копирует вас.
Джеймс МакМахон

Это отличный способ передать Тип. Но все же это не так безопасно, как сказал Шмитт. Я все еще мог бы пройти другой класс, и типографская бомба будет бомбить. Второй ответ mmyers для установки типа Type in Return кажется лучше
Sathish

4
Немо, если вы проверите время публикации, то увидите, что мы разместили их в одно и то же время. Также они не совсем одинаковые, только две строчки.
Фабиан Стиг

@Fabian Я опубликовал аналогичный ответ, но есть важное различие между слайдами Блоха и тем, что было опубликовано в Effective Java. Он использует Class <T> вместо TypeRef <T>. Но это все еще отличный ответ.
Крейг П. Мотлин

8

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


8

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

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

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, но любой ОО-язык должен работать так же.


6

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

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


5

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

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

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

Спасибо

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


Действительно, спасибо, что поделились своим пониманием, ваша статья действительно объясняет все это!
Янн-Гаэль Геенек

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

2

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

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


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

2
Вы действительно только что ответили на свой собственный вопрос - если у животного есть уникальное действие, тогда вы должны бросить это конкретное животное. Если у животного есть действие, которое можно сгруппировать с другими животными, вы можете определить абстрактный или виртуальный метод в базовом классе и использовать его.
Мэтт Джордан

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){}с точки зрения компиляторов. Вам нужно будет либо изменить тип параметра, либо добавить / удалить параметры для распознавания перегрузки. Может быть, я просто неправильно понимаю тебя ...
Единственный настоящий

в Java вы можете переопределить метод в подклассе и указать более «узкий» (то есть более конкретный) тип возвращаемого значения. См. Stackoverflow.com/questions/14694852/… .
FeralWhippet

-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например.
Сами Лэйн

Это совершенно неуместное использование дженериков. Этот код cursorдолжен быть a Cursorи всегда будет возвращать Person(или ноль). Использование обобщений снимает проверку этих ограничений, делая код небезопасным.
Энди Тернер
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.