Как сделать метод, возвращающий тип, универсальным? - PullRequest
526 голосов
/ 16 января 2009

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

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

Ответы [ 19 ]

811 голосов
/ 16 января 2009

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

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

111 голосов
/ 16 января 2009

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

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

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

26 голосов
/ 16 января 2009

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

14 голосов
/ 24 июня 2016

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

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

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

12 голосов
/ 15 июля 2017

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

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

        }
    }
10 голосов
/ 16 января 2009

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

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

7 голосов
/ 23 января 2009

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

6 голосов
/ 24 мая 2015

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

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

5 голосов
/ 16 января 2009

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

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

...