7

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

26

Проблема с использованием обобщений в классе 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);        
}

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

5 ответ(ов)

10

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

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

1

Нет, компилятор не может определить, какой тип вернет jerry.callFriend("spike"). Кроме того, ваша реализация просто скрывает приведение типов в методе без какой-либо дополнительной безопасности типов. Рассмотрите следующий пример:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* неиспользуемый */ new Dog()); // приводит к ошибке при неверном приведении типов

В данном конкретном случае создание абстрактного метода talk() и его переопределение в подклассах было бы гораздо более эффективным решением:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
0

Вот упрощенная версия кода:

public <T> T callFriend(String name) {
    return (T) friends.get(name); // Приведение к T не обязательно в этом случае, но это хорошая практика
}

Полный рабочий код:

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("Я собака");
        }
    }

    public static class Duck extends Animal {
        public void quack() {
            System.out.println("Я утка");
        }
    }

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

В этом примере создается базовый класс Animal, который хранит своих друзей в виде карты (Map). Метод callFriend позволяет получить друга по имени и привести его к нужному типу. Обратите внимание на использование дженериков, что позволяет более безопасно работать с типами.

0

На основе той же идеи, что и 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("Отсутствует параметр типа.");
    }
    this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
  }
}

Тем не менее, это может свести к нулю первоначальную идею, так как теперь вам нужно создавать новые объекты идентификаторов для каждой строки и сохранять их (либо восстанавливать с правильной информацией о типе).

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.

0

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

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь