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




Нет. Компилятор не может знать, какой тип вернет 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();
Это правильный способ добиться того же результата. Обратите внимание, что цель состоит в том, чтобы получить специфичное для производного класса поведение во время выполнения без явного написания кода самостоятельно, чтобы выполнять некрасивую проверку типов и приведение типов. Метод, предложенный @laz, работает, но отбрасывает безопасность типов. Этот метод требует меньше строк кода (потому что реализации метода привязаны поздно и в любом случае просматриваются во время выполнения), но все же позволяет вам легко определять уникальное поведение для каждого подкласса Animal.
Но исходный вопрос не касается безопасности типов. Как я это читал, спрашивающий просто хочет знать, есть ли способ использовать дженерики, чтобы избежать приведения.
@laz: да, исходный вопрос - как он поставлен - не о безопасности типов. Это не меняет того факта, что существует безопасный способ реализовать это, устраняя сбои при приведении классов. См. Также weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
Я не согласен с этим, но мы имеем дело с Java и всеми ее дизайнерскими решениями / недостатками. Я рассматриваю этот вопрос как попытку узнать, что возможно в дженериках Java, а не как проблему xy (meta.stackexchange.com/questions/66377/what-is-the-xy-probl em), которую необходимо переработать. Как и любой шаблон или подход, бывают случаи, когда код, который я предоставил, уместен, и времена, когда требуется что-то совершенно иное (например, то, что вы предложили в этом ответе).
Не совсем, потому что, как вы говорите, компилятор знает только, что callFriend () возвращает Animal, а не Dog или Duck.
Разве вы не можете добавить в Animal абстрактный метод makeNoise (), который будет реализован его подклассами как лай или шарлатан?
что, если у животных есть несколько методов, которые даже не подпадают под общее действие, которое можно абстрагировать? Мне это нужно для связи между подклассами с разными действиями, где я могу передать Type, а не экземпляр.
Вы действительно только что ответили на свой вопрос - если у животного есть уникальное действие, вы должны применить его к этому конкретному животному. Если у животного есть действие, которое можно сгруппировать с другими животными, вы можете определить абстрактный или виртуальный метод в базовом классе и использовать его.
Вы можете реализовать это так:
@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
return (T)friends.get(name);
}
(Да, это законный код; см. Java Generics: общий тип, определенный только как возвращаемый тип.)
Тип возврата будет выведен от вызывающего. Однако обратите внимание на аннотацию @SuppressWarnings: в ней говорится, что этот код небезопасен. Вы должны проверить это сами, иначе вы можете получить ClassCastExceptions во время выполнения.
К сожалению, из-за того, как вы его используете (без присвоения возвращаемого значения временной переменной), единственный способ порадовать компилятор - это вызвать его так:
jerry.<Dog>callFriend("spike").bark();
Хотя это может быть немного лучше, чем приведение, вам, вероятно, лучше дать классу Animal абстрактный метод talk(), как сказал Дэвид Шмитт.
Цепочка методов на самом деле не была намерением. Я не против присвоить значение переменной подтипа и использовать ее. Спасибо за решение.
это отлично работает при создании цепочки вызовов методов!
Мне очень нравится этот синтаксис. Я думаю, что в C# это jerry.CallFriend<Dog>(..., который, на мой взгляд, выглядит лучше.
Интересно, что собственная функция JRE java.util.Collections.emptyList() реализована именно так, а ее javadoc рекламирует себя как типизированную.
@TiStrga: Это интересно! Причина, по которой Collections.emptyList() может сойти с рук, заключается в том, что для определения пустого списка нет объекта элемента типа T. Таким образом, нет риска привести объект к неправильному типу. Сами объекты списка могут работать с любыми типами, пока нет элементов.
Невозможно. Как карта должна знать, какой подкласс Animal она получит, учитывая только строковый ключ?
Единственный способ, которым это было бы возможно, - это если бы каждое животное принимало только один тип друга (тогда это мог бы быть параметр класса Animal), или метод callFriend () получил параметр типа. Но на самом деле похоже, что вы упускаете из виду точку наследования: вы можете обрабатывать подклассы единообразно только при использовании исключительно методов суперкласса.
Вы можете определить 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();
Преимущество этого кода в том, что он не генерирует никаких предупреждений компилятора. Конечно, это на самом деле просто обновленная версия кастинга из дообродовых дней и не добавляет никакой дополнительной безопасности.
... но по-прежнему не имеет проверки типа во время компиляции между параметрами вызова callFriend ().
На данный момент это лучший ответ, но вы должны точно так же изменить добавление друга. Это затрудняет написание ошибок, поскольку вам нужен литерал класса в обоих местах.
@Jaider, не совсем то же самое, но это будет работать: // Класс животных public T CallFriend <T> (строковое имя) где T: Animal {вернуть друзей [имя] как T; } // Вызов класса jerry.CallFriend <Dog> ("шип"). Bark (); jerry.CallFriend <Утка> ("шарлатан"). 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. Есть способ реализовать Типозащищенные гетерогенные контейнеры (THC) с использованием токенов супертипа, но здесь снова есть свои проблемы.
Извините, но это тот же ответ, что и у laz, так что вы либо копируете его, либо он копирует вас.
Это отличный способ передать Тип. Но все же это небезопасно, как сказал Шмитт. Я все еще могу пройти другой класс, и приведение типов будет бомбой. 2-й ответ mmyers для установки типа в возвращаемом типе кажется лучше
Немо, если вы проверите время публикации, вы увидите, что мы разместили их практически в один и тот же момент. Кроме того, они не совсем одинаковые, всего две строки.
@Fabian Я опубликовал аналогичный ответ, но есть важное различие между слайдами Блоха и тем, что было опубликовано в Effective Java. Он использует Class <T> вместо TypeRef <T>. Но это все равно отличный ответ.
Этот вопрос очень похож на Пункт 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 Класс для получения дополнительной информации.
Основываясь на той же идее, что и токены супертипа, вы можете создать типизированный идентификатор для использования вместо строки:
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, если вы хотите иметь возможность сравнивать два идентичных экземпляра идентификатора.
Я написал статью, которая содержит доказательство концепции, классы поддержки и тестовый класс, демонстрирующий, как ваши классы могут извлекать токены супертипа во время выполнения. Вкратце, это позволяет вам делегировать альтернативные реализации в зависимости от фактических общих параметров, переданных вызывающей стороной. Пример:
TimeSeries<Double> делегирует частный внутренний класс, который использует double[]TimeSeries<OHLC> делегирует частный внутренний класс, который использует ArrayList<OHLC>Видеть:
Спасибо
Ричард Гомес - Блог
Действительно, спасибо за то, что поделились своим мнением, ваша статья действительно все объясняет!
Оригинальное сообщение в блоге (что круто!) Доступно в Интернет-архиве здесь.
@ScottBabcock: Спасибо, что сообщили мне об этой неработающей ссылке. Я также разместил ссылку на свой новый блог.
Я знаю, что это совсем другое дело, о котором спрашивали. Другой способ решить эту проблему - размышление. Я имею в виду, что это не использует преимущества 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");
}
}
Здесь вы ищете абстракцию. Кодируйте больше интерфейсов, и вам придется меньше выполнять приведение.
Пример ниже написан на 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!");
}
}
}
Этот вопрос касается кодирования, а не концепции.
как насчет
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);
}
}
«Есть ли способ выяснить тип возвращаемого значения во время выполнения без дополнительного параметра с помощью 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 () точно так же.
Класс, который хочет вызвать методы, специфичные для подкласса, должен затем реализовать интерфейс посетителя следующим образом:
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(); }
}
Я знаю, что здесь гораздо больше интерфейсов и методов, чем вы рассчитывали, но это стандартный способ получить дескриптор для каждого конкретного подтипа с ровно нулевыми проверками instanceof и нулевым приведением типов. И все это делается в стандартной языковой независимой манере, поэтому не только для Java, но и любой объектно-ориентированный язык должен работать одинаково.
В моем lib kontraktor я сделал следующее:
public class Actor<SELF extends Actor> {
public SELF self() { return (SELF)_self; }
}
подклассы:
public class MyHttpAppSession extends Actor<MyHttpAppSession> {
...
}
по крайней мере, это работает внутри текущего класса и при наличии строго типизированной ссылки. Множественное наследование работает, но тогда становится действительно сложно :)
Кроме того, вы можете попросить метод вернуть значение данного типа таким образом
<T> T methodName(Class<T> var);
Дополнительные примеры здесь в документации Oracle Java
Есть другой подход: вы можете сузить возвращаемый тип при переопределении метода. В каждом подклассе вам придется переопределить callFriend, чтобы вернуть этот подкласс. Стоимость будет заключаться в нескольких объявлениях callFriend, но вы можете изолировать общие части до метода, вызываемого изнутри. Мне это кажется намного проще, чем решения, упомянутые выше, и не требует дополнительного аргумента для определения возвращаемого типа.
Я не уверен, что вы имеете в виду под «сужением возвращаемого типа». Afaik, Java и большинство типизированных языков не перегружают методы или функции на основе возвращаемого типа. Например, public int getValue(String name){} неотличим от public boolean getValue(String name){} с точки зрения компилятора. Вам нужно будет либо изменить тип параметра, либо добавить / удалить параметры, чтобы перегрузка была распознана. Может, я просто вас неправильно понимаю ...
в java вы можете переопределить метод в подклассе и указать более «узкий» (то есть более конкретный) тип возвращаемого значения. См. stackoverflow.com/questions/14694852/….
Здесь есть много отличных ответов, но это подход, который я применил для теста 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);
}
Если вы не хотите выдавать ошибки, вы можете поймать их так:
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;
}
Насколько я понимаю, это лучший и самый элегантный способ использования Generics.
Вот вариант попроще:
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();
}
}
Что значит Casting to T not needed in this case but it's a good practice to do. Я имею в виду, если об этом должным образом позаботятся во время выполнения, что означает «хорошая практика»?
Я хотел сказать, что явное приведение (T) не требуется, поскольку объявления типа возвращаемого значения <T> в объявлении метода должно быть достаточно
Поскольку вопрос основан на гипотетических данных, вот хороший пример, возвращающий общий, который расширяет интерфейс Comparable.
public class MaximumTest {
// find the max value using Comparable interface
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume that x is initially the largest
if (y.compareTo(max) > 0){
max = y; // y is the large now
}
if (z.compareTo(max) > 0){
max = z; // z is the large now
}
return max; // returns the maximum value
}
//testing with an ordinary main method
public static void main(String args[]) {
System.out.printf("Maximum of %d, %d and %d is %d\n\n", 3, 4, 5, maximum(3, 4, 5));
System.out.printf("Maximum of %.1f, %.1f and %.1f is %.1f\n\n", 6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7));
System.out.printf("Maximum of %s, %s and %s is %s\n", "strawberry", "apple", "orange",
maximum("strawberry", "apple", "orange"));
}
}
Хотя метод mmyers может работать, я думаю, что этот метод лучше подходит для объектно-ориентированного программирования и избавит вас от некоторых проблем в будущем.