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

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

У меня есть class Animal , где у каждого Animal может быть много друзей.
И подclassы, как Dog , Duck , Mouse т. Д., Mouse добавляют определенное поведение, такое как bark() , quack() и т. Д.

Вот class Animal :

 public class Animal { private Map friends = new HashMap(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public Animal callFriend(String name){ return friends.get(name); } } 

И вот fragment кода с большим количеством типов:

 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 callFriend(String name, T unusedTypeObj){ return (T)friends.get(name); } 

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

Вы можете определить callFriend таким образом:

 public  T callFriend(String name, Class 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() и его переопределение в подclassах будет намного лучше:

 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 callFriend(String name) { return (T)friends.get(name); } 

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

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

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

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

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

Этот вопрос очень похож на пункт 29 в «Эффективной Java» – «Рассмотрим типы гетерогенных контейнеров». Ответ Лаза наиболее близок к решению Блоха. Тем не менее, как put, так и get должны использовать литерал Class для обеспечения безопасности. Подписи стали бы:

 public  void addFriend(String name, Class type, T animal); public  T callFriend(String name, Class type); 

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

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

  T methodName(Class var); 

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

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

 public  T callFriend(String name, Class clazz) { return (T) friends.get(name); } 

И затем используйте его так:

 jerry.callFriend("spike", Dog.class).bark(); jerry.callFriend("quacker", Duck.class).quack(); 

Не идеально, но это в значительной степени зависит от Java-дженериков. Существует способ реализовать Typesafe Heterogenous Containers (THC) с использованием токенов Super Type , но у этого есть свои проблемы снова.

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

 public abstract class TypedID { 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 spike = new TypedID("spike") {}; TypedID quacker = new TypedID("quacker") {}; jerry.addFriend(spike, new Dog()); jerry.addFriend(quacker, new Duck()); 

Но теперь вы можете использовать class так, как вы хотели, без приведения.

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

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

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

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

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

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

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

 abstract public class Animal implements Visitable { private Map friends = new HashMap(); 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); } 

И реализация посетителя может посещать все подclassы животного:

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

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

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

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

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

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

благодаря

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

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

 public  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 friends = new HashMap<>(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public  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 () возвращает Animal, а не Dog или Duck.

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

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

Пример ниже приведен в 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(string name) where T : _Animal; } interface _Animal { void Speak(); } abstract class AnimalBase : _Animal, _HasFriends { private Dictionary friends = new Dictionary(); public abstract void Speak(); public void AddFriend(string name, _Animal animal) { friends.Add(name, animal); } public T CallFriend(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 tapSignInButton(Class type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //signInButton.click(); return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } 
  • MobilePage – это суперclass, который расширяет его, что позволяет использовать любой из его дочерних элементов (duh)
  • type.getConstructor (Param.class и т. д.) позволяет вам взаимодействовать с конструктором типа. Этот конструктор должен быть одинаковым между всеми ожидаемыми classами.
  • newInstance принимает объявленную переменную, которую вы хотите передать новому конструктору объектов

Если вы не хотите бросать ошибки, вы можете их поймать так:

 public  T tapSignInButton(Class type) { // signInButton.click(); T returnValue = null; try { returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } catch (Exception e) { e.printStackTrace(); } return returnValue; } 

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

 import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; abstract class AnimalExample { private Map> friends = new HashMap>(); private Map theFriends = new HashMap(); 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> friends = new HashMap>(); public  void addFriend(String name, T animal){ friends.put(name,animal); } public  T callFriend(String name){ return friends.get(name); } 

}

В моем lib kontraktor я сделал следующее:

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

подclassы:

 public class MyHttpAppSession extends Actor { ... } 

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

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

 public  X nextRow(Y cursor) { return (X) getRow(cursor); } private  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. 

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

Interesting Posts

Команды клавиш окна, не работающие в Windows 7

Новые текстовые файлы создаются с заглавными расширениями

Изменение порта по умолчанию (т.е. 5037), на котором запускается сервер adb

Прикрепить отладчик в C # к другому процессу

Как добиться анимации пульсации с помощью библиотеки поддержки?

Как мне переместить медиатеку iTunes?

CSS3 – Как стилизовать выделенный текст в текстовых областях и входах в Chrome?

Планировщик заданий не позволит повторно использовать имена задач из старых, автоматически удаленных задач, которые были созданы с помощью / Z-переключателя

Значение, переданное с request.setAttribute (), недоступно request.getParameter ()

Android Webview POST

Преобразование внешнего жесткого диска в NTFS

Force GCC уведомлять о неопределенных ссылках в разделяемых библиотеках

Как отключить прокрутку RecyclerView?

Тестирование, если объект имеет общий тип в C #

Сохраните файлы трейлеров QuickTime на компьютер без Pro

Давайте будем гением компьютера.