Como faço para o método retornar tipo genérico?


588

Considere este exemplo (típico nos livros de POO):

Eu tenho uma Animalaula, onde cada um Animalpode ter muitos amigos.
E subclasses como Dog, etc Duck, Mouseque adicionam comportamento específico bark(), quack()etc.

Aqui está a Animalturma:

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

E aqui está um trecho de código com muita tipografia:

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

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

Existe alguma maneira de usar genéricos para o tipo de retorno para me livrar da conversão de texto, para que eu possa dizer

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

Aqui está um código inicial com o tipo de retorno transmitido ao método como um parâmetro que nunca é usado.

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

Existe uma maneira de descobrir o tipo de retorno em tempo de execução sem o parâmetro extra usando instanceof? Ou pelo menos passando uma classe do tipo em vez de uma instância fictícia.
Entendo que os genéricos são para verificação de tipo em tempo de compilação, mas existe uma solução alternativa para isso?

Respostas:


903

Você pode definir callFrienddesta maneira:

public <T extends Animal> T callFriend(String name, Class<T> type) {
    return type.cast(friends.get(name));
}

Em seguida, chame-o assim:

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

Este código tem o benefício de não gerar nenhum aviso do compilador. É claro que essa é apenas uma versão atualizada do elenco dos dias pré-genéricos e não adiciona nenhuma segurança adicional.


29
... mas ainda não tem verificação de tipo de tempo de compilação entre os parâmetros da chamada callFriend ().
David Schmitt

2
Esta é a melhor resposta até agora - mas você deve alterar o addFriend da mesma maneira. Isso torna mais difícil escrever bugs, pois você precisa dessa classe literal nos dois lugares.
Craig P. Motlin

@ Jaider, não exatamente o mesmo, mas isso funcionará: // Classe Animal public T CallFriend <T> (nome da string) em que T: Animal {retorna amigos [nome] como T; } // Chamando Classe jerry.CallFriend <Dog> ("spike"). Bark (); jerry.CallFriend <Duck> ("quacker"). Quack ();
Nestor Ledon

124

Não. O compilador não pode saber que tipo jerry.callFriend("spike")retornaria. Além disso, sua implementação apenas oculta a conversão no método sem nenhum tipo de segurança adicional. Considere isto:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast

Nesse caso específico, criar um talk()método abstrato e substituí-lo adequadamente nas subclasses ajudaria você muito melhor:

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

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

10
Embora o método mmyers possa funcionar, acho que esse método é melhor para programação OO e poupará alguns problemas no futuro.
James McMahon

1
Esta é a maneira correta de obter o mesmo resultado. Observe que o objetivo é obter o comportamento específico da classe derivada em tempo de execução sem escrever explicitamente o código para fazer a verificação e conversão de tipo feias. O método proposto por @laz funciona, mas lança a segurança de tipos pela janela. Esse método requer menos linhas de código (porque as implementações de métodos estão atrasadas e analisadas em tempo de execução de qualquer maneira), mas ainda assim vamos definir facilmente um comportamento exclusivo para cada subclasse de Animal.
Dcow

Mas a pergunta original não está perguntando sobre segurança de tipo. Da maneira que eu li, o autor da pergunta só quer saber se existe uma maneira de aproveitar os genéricos para evitar a transmissão.
Laz

2
@ Laz: Sim, a pergunta original - como colocada - não é sobre segurança de tipo. Isso não muda o fato de que existe uma maneira segura de implementar isso, eliminando falhas de conversão de classe. Veja também weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
David Schmitt

3
Eu não discordo disso, mas estamos lidando com Java e todas as suas decisões / falhas de design. Eu vejo essa pergunta apenas tentando aprender o que é possível nos genéricos Java, não como um xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ) que precisa ser reprojetado. Como qualquer padrão ou abordagem, há momentos em que o código que forneci é apropriado e momentos em que algo totalmente diferente (como o que você sugeriu nesta resposta) é necessário.
laz

114

Você poderia implementá-lo assim:

@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
    return (T)friends.get(name);
}

(Sim, este é um código legal; consulte Java Generics: tipo genérico definido apenas como tipo de retorno .)

O tipo de retorno será deduzido do chamador. No entanto, observe a @SuppressWarningsanotação: que informa que esse código não é seguro . Você precisa verificar por conta própria ou poderá obter ClassCastExceptionsem tempo de execução.

Infelizmente, do jeito que você o está usando (sem atribuir o valor de retorno a uma variável temporária), a única maneira de fazer o compilador feliz é chamá-lo assim:

jerry.<Dog>callFriend("spike").bark();

Embora isso possa ser um pouco melhor do que o elenco, é melhor você dar à Animalclasse um talk()método abstrato , como disse David Schmitt.


O encadeamento de métodos não era realmente uma intenção. Eu não me importo de atribuir o valor a uma variável Subtyped e usá-lo. Obrigado pela solução.
Sathish

isso funciona perfeitamente ao fazer o encadeamento de chamada de método!
Hartmut P.

Eu realmente gosto dessa sintaxe. Eu acho que em C # é o jerry.CallFriend<Dog>(...que acho melhor.
andho

Interessante que a java.util.Collections.emptyList()função do JRE seja implementada exatamente dessa maneira, e seu javadoc se anuncia como sendo tipicamente seguro.
Ti Strga

31

Essa pergunta é muito semelhante ao item 29 do Java efetivo - "Considere contêineres heterogêneos com segurança de tipo". A resposta de Laz é a mais próxima da solução de Bloch. No entanto, tanto o put quanto o get devem usar o literal de classe por segurança. As assinaturas se tornariam:

public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> T callFriend(String name, Class<T> type);

Dentro dos dois métodos, você deve verificar se os parâmetros estão corretos. Consulte Java efetivo e javadoc de classe para obter mais informações.


17

Além disso, você pode solicitar ao método que retorne o valor em um determinado tipo dessa maneira

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

Mais exemplos aqui na documentação do Oracle Java


17

Aqui está a versão mais simples:

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
}

Código totalmente funcional:

    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
O que faz Casting to T not needed in this case but it's a good practice to do. Quero dizer, se foi resolvido adequadamente durante o tempo de execução, o que significa "boa prática"?
Farid

Eu quis dizer que a conversão explícita (T) não é necessária, pois a declaração do tipo de retorno <T> na declaração do método deve ser suficiente
webjockey

9

Como você disse que aprovar uma aula seria bom, você poderia escrever o seguinte:

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

E então use-o assim:

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

Não é perfeito, mas isso é praticamente o máximo possível com os genéricos Java. Existe uma maneira de implementar os Contentores Heterogêneos Typesafe (THC) usando Super Type Tokens , mas isso tem seus próprios problemas novamente.


Sinto muito, mas esta é exatamente a mesma resposta que Laz tem, então você está copiando ele ou ele está copiando você.
James McMahon

Essa é uma maneira elegante de passar o Type. Mas ainda não é seguro como Schmitt disse. Eu ainda poderia passar por uma classe diferente e a transmissão de texto será bombardeada. mmyers segunda resposta para definir o tipo em Tipo de retorno parece melhor
Sathish

4
Nemo, se você verificar a hora da publicação, verá que as publicamos exatamente no mesmo momento. Além disso, eles não são exatamente iguais, apenas duas linhas.
Fabian Steeg

@Fabian Publiquei uma resposta semelhante, mas há uma diferença importante entre os slides de Bloch e o que foi publicado no Effective Java. Ele usa a classe <T> em vez de TypeRef <T>. Mas essa ainda é uma ótima resposta.
Craig P. Motlin

8

Com base na mesma idéia que os Super Type Tokens, você pode criar um ID digitado para usar em vez de uma string:

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

Mas acho que isso pode anular o objetivo, já que agora você precisa criar novos objetos de identificação para cada string e segurá-los (ou reconstruí-los com as informações de tipo corretas).

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

Mas agora você pode usar a classe da maneira que queria originalmente, sem os elencos.

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

Isso está apenas ocultando o parâmetro type dentro do id, embora isso signifique que você pode recuperar o tipo do identificador posteriormente, se desejar.

Você também precisará implementar os métodos de comparação e hash de TypedID, se quiser comparar duas instâncias idênticas de um ID.


8

"Existe uma maneira de descobrir o tipo de retorno em tempo de execução sem o parâmetro extra usando instanceof?"

Como uma solução alternativa, você pode utilizar o padrão Visitor assim. Tornar o Animal abstrato e implementá-lo Visível:

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

Visível significa apenas que uma implementação Animal está disposta a aceitar um visitante:

public interface Visitable {
    void accept(Visitor v);
}

E uma implementação de visitante pode visitar todas as subclasses de um animal:

public interface Visitor {
    void visit(Dog d);
    void visit(Duck d);
    void visit(Mouse m);
}

Então, por exemplo, uma implementação Dog seria assim:

public class Dog extends Animal {
    public void bark() {}

    @Override
    public void accept(Visitor v) { v.visit(this); }
}

O truque aqui é que, como o cão sabe que tipo é, ele pode acionar o método de visita sobrecarregado relevante do visitante v, passando "this" como parâmetro. Outras subclasses implementariam accept () exatamente da mesma maneira.

A classe que deseja chamar métodos específicos da subclasse deve implementar a interface do Visitante desta forma:

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

Sei que são muito mais interfaces e métodos do que você esperava, mas é uma maneira padrão de lidar com cada subtipo específico com exatamente zero instância de verificações e zero tipos de conversão. E tudo é feito de uma maneira independente de linguagem padrão, portanto não é apenas para Java, mas qualquer linguagem OO deve funcionar da mesma maneira.


6

Não é possivel. Como o Mapa deve saber qual subclasse de Animal será obtida, considerando apenas uma chave String?

A única maneira de isso ser possível é se cada Animal aceitar apenas um tipo de amigo (então pode ser um parâmetro da classe Animal) ou o método callFriend () obter um parâmetro de tipo. Mas realmente parece que você está perdendo o ponto de herança: é que você só pode tratar subclasses de maneira uniforme ao usar exclusivamente os métodos da superclasse.


5

Escrevi um artigo que contém uma prova de conceito, classes de suporte e uma classe de teste que demonstra como os Super Type Tokens podem ser recuperados por suas classes em tempo de execução. Em poucas palavras, permite delegar a implementações alternativas, dependendo dos parâmetros genéricos reais passados ​​pelo chamador. Exemplo:

  • TimeSeries<Double> delega a uma classe interna privada que usa double[]
  • TimeSeries<OHLC> delega a uma classe interna privada que usa ArrayList<OHLC>

Consulte: Usando TypeTokens para recuperar parâmetros genéricos

obrigado

Richard Gomes - Blog


De fato, obrigado por compartilhar sua visão, seu artigo realmente explica tudo!
Yann-Gaël Guéhéneuc 12/04

3

Há muitas ótimas respostas aqui, mas essa é a abordagem que eu adotei para um teste do Appium em que agir em um único elemento pode resultar em diferentes estados de aplicativos com base nas configurações do usuário. Embora não siga as convenções do exemplo do OP, espero que ajude alguém.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage é a super classe que o tipo estende, o que significa que você pode usar qualquer um dos filhos (duh)
  • type.getConstructor (Param.class, etc) permite que você interaja com o construtor do tipo. Esse construtor deve ser o mesmo entre todas as classes esperadas.
  • newInstance usa uma variável declarada que você deseja passar para o novo construtor de objetos

Se você não deseja lançar os erros, pode pegá-los da seguinte maneira:

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

Para mim, esta é a melhor e mais elegante maneira de usar os genéricos.
cbaldan

2

Na verdade, não, porque, como você diz, o compilador sabe apenas que callFriend () está retornando um Animal, não um Cachorro ou Pato.

Você não pode adicionar um método abstrato makeNoise () ao Animal que seria implementado como uma casca ou charlatão por suas subclasses?


1
e se os animais tiverem vários métodos que nem se enquadram em uma ação comum que possa ser abstraída? Eu preciso disso para comunicação entre subclasses com ações diferentes, onde eu estou bem em passar o Type, não uma instância.
Sathish

2
Você realmente acabou de responder sua própria pergunta - se um animal tem uma ação única, você deve fazer a conversão para esse animal específico. Se um animal tiver uma ação que possa ser agrupada com outros animais, você poderá definir um método abstrato ou virtual em uma classe base e usá-lo.
Matt Jordan

2

O que você está procurando aqui é abstração. Código contra interfaces mais e você deve ter que fazer menos transmissões.

O exemplo abaixo está em C #, mas o conceito permanece o mesmo.

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

Esta questão tem a ver com codificação, não com conceito.

2

Eu fiz o seguinte no meu kontraktor lib:

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

subclasse:

public class MyHttpAppSession extends Actor<MyHttpAppSession> {
   ...
}

pelo menos isso funciona dentro da classe atual e ao ter uma referência digitada forte. Herança múltipla funciona, mas fica realmente complicado :)


1

Eu sei que isso é uma coisa completamente diferente que a pergunta. Outra maneira de resolver isso seria a reflexão. Quero dizer, isso não tira o benefício da Generics, mas permite imitar, de alguma forma, o comportamento que você deseja executar (fazer um cachorro latir, fazer um pato grasnar, etc.) sem cuidar do tipo de conversão:

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

A respeito

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

Há outra abordagem: você pode restringir o tipo de retorno ao substituir um método. Em cada subclasse, você teria que substituir o callFriend para retornar essa subclasse. O custo seria as várias declarações de callFriend, mas você poderia isolar as partes comuns em um método chamado internamente. Isso me parece muito mais simples do que as soluções mencionadas acima e não precisa de um argumento extra para determinar o tipo de retorno.


Não sei ao certo o que você quer dizer com "restringir o tipo de retorno". Afaik, Java e a maioria das linguagens digitadas não sobrecarregam métodos ou funções com base no tipo de retorno. Por exemplo, public int getValue(String name){}é indistinguível do public boolean getValue(String name){}ponto de vista dos compiladores. Você precisaria alterar o tipo de parâmetro ou adicionar / remover parâmetros para que a sobrecarga fosse reconhecida. Talvez eu esteja apenas mal-entendido que você embora ...
O One True Colter

em java, você pode substituir um método em uma subclasse e especificar um tipo de retorno mais "estreito" (isto é, mais específico). Consulte 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;
}

Você pode retornar qualquer tipo e receber diretamente como. Não precisa digitar.

Person p = nextRow(cursor); // cursor is real database cursor.

É melhor se você deseja personalizar qualquer outro tipo de registro em vez de cursores reais.


2
Você diz que "não há necessidade de digitar", mas claramente há uma digitação envolvida: (Cursor) cursorpor exemplo.
Sami Laine

Este é um uso totalmente inapropriado de genéricos. Esse código precisa cursorser um Cursore sempre retornará um Person(ou nulo). O uso de genéricos remove a verificação dessas restrições, tornando o código inseguro.
Andy Turner
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.