_generics Coisas que você não sabia sobre Generics Veja neste artigo como tirar todo proveito da utilização de tipos genéricos na linguagem Java. Eduardo Guerra | [email protected] é desenvolvedor de frameworks, editor-chefe da revista MundoJ e pesquisador do Instituto Nacional de Pesquisas Espaciais (INPE). Foi professor do Instituto Tecnológico de Aeronáutica (ITA), onde concluiu sua graduação, mestrado e doutorado. Suas pesquisas se concentram nas áreas de design, arquitetura e teste de software. Possui diversas certificações da plataforma Java e experiência como arquiteto de software. Participa de projetos de frameworks open-source, como SwingBean, Esfinge e ClassMock. É autor do livro “Design Patterns com Java: projeto orientado a objetos guiado por padrões” lançado pela editora Casa do Código. Ele acredita que um bom software se faz mais com criatividade do que com código e vive em busca de melhores formas para seu desenvolvimento. O suporte a tipos genéricos foi adicionado na linguagem Java a partir do JDK 1.5. A princípio ela foi anunciada como uma funcionalidade “opcional“, que poderia ou não ser utilizada pelos desenvolvedores. É verdade que todo código legado continuou compilando sem o uso de tipos genéricos, porém na minha opinião qualquer funcionalidade adicionada como parte da linguagem está longe de ser opcional para os desenvolvedores. Acredito que na época a ideia era não causar alarde, porém, hoje, o conhecimento de tipos genéricos é essencial a qualquer programador Java. O uso de tipos genéricos é muito conhecido pela sua utilização na API de coleções, que disponibiliza um conjunto de classes cujo uso é essencial a qualquer desenvolvedor. Quando se cria uma coleção, seja ela uma lista (List) ou um conjunto (Set), normalmente se cria a coleção para armazenar alguma coisa, ou seja, algum tipo específico de classe. É aí que entram os tipos genéricos, para dizer se aquela lista é uma lista de pessoas (List<Pessoa>) ou se o conjunto é um conjunto de produtos (Set<Produto>). Indo na contramão de algumas linguagens dinâmicas, onde a ideia é ter o menor número de definições possível para tornar o código mais simples de ser criado, os tipos genéricos adicionam certa verbosidade na linguagem em troca de uma maior segurança de código. Por exemplo, antes dos tipos genéricos, qualquer objeto poderia ser adicionado em uma lista, e isso criava a possibilidade que se tentasse realizar o cast desse objeto para a classe errada, causando erros em tempo de execução. Com o uso de tipos genéricos, pelo fato do tipo da coleção ser explicitamente definido, esse tipo de erro na maioria dos casos é detectado em tempo de compilação. A ideia deste artigo é mostrar algumas coisas sobre os tipos genéricos que a maioria das pessoas não sabe. Você sabia, por exemplo, que em alguns casos você consegue recuperar o tipo genérico utilizando reflexão? Você sabia que usando tipos genéricos você pode definir o tipo da exceção que será lançada por um método através da classe que o invoca? Consegui despertar sua curiosidade? Então vamos começar o artigo com aquela revisada de tipos genéricos para aqueles que estão um pouco enferrujados e em seguida vamos direto para a parte mais divertida. Tipos Genéricos em Java A melhor forma de entender um tipo genérico é como um parâmetro para um tipo. Muitas vezes você quer definir uma classe, mas quer que o retorno de certos métodos ou que certos parâmetros possam ser 5\ O suporte a tipos genéricos é uma funcionalidade de linguagem que muitas vezes é subutilizada pelos desenvolvedores. Eu percebo que muitos acabam se limitando a utilizar classes com tipos genéricos, mas não consideram inclui-los em suas próprias classes. Este artigo mergulha nas questões mais avançadas e curiosas dos tipos genéricos, mostrando como eles podem ser utilizados de uma forma que você nunca tinha imaginado. definidos por quem está instanciando um objeto daquela classe. A lista é um ótimo exemplo, pois quem a instancia para armazenar um determinado tipo vai querer que o método de adição só aceite aquele tipo e que o método de recuperação retorne aquele tipo. A Listagem 1 mostra um exemplo de como uma classe define um tipo genérico e como essa classe é instanciada. Repare que na definição da classe Grafo, o tipo genérico <E> é definido em sua declaração e utilizado na definição dos métodos, no lugar de parâmetros e/ou retornos. Dessa forma, quando um objeto instancia a classe e define um tipo genérico, é como se para aquela instância todos os lugares onde foi definido E fossem substituídos pelo tipo configurado. Listagem 1. Exemplo de uma classe que define um tipo genérico. //Definição de classe com tipo genérico public class Grafo<E> { public void adicionarNo(E no){ ... } public void adicionarLigacao(E noA, E noB) { ... } public List<E> recuperaVizinhos(E no) { ... } } //Instanciando a classe definindo tipo genérico Grafo<Cidade> grafo = new Grafo<Cidade>(); Outro caso de utilização de tipos genéricos é quando se deseja definir uma interface e quer que a classe que vai implementá-la possa definir o tipo de alguns retornos e parâmetros. Dessa forma, podese definir uma interface de propósito mais geral e inserir parâmetros de acordo com a classe que está a implementando. De forma alternativa, a classe pode “propagar“ o tipo genérico para sua definição, deixando assim para que ele seja definido no momento de instanciar os objetos, como mostrado na Listagem 1. /6 A Listagem 2 apresenta o exemplo da interface DAO<E> que define um tipo genérico e da classe ProdutoDAO que implementa essa interface fixando o tipo genérico que será utilizado. Dessa forma, na própria classe, os retornos e parâmetros definidos utilizando o tipo genérico na interface, serão fixados de acordo com o tipo definido. Dessa forma, quem instanciar a classe ProdutoDAO nem saberá que ela implementa uma interface com um tipo genérico. Listagem 2. Exemplo de uma interface que define um tipo genérico. //Definição de interface com tipo genérico public interface DAO<E> { public void salvar(E obj); public E recuperar(int id); public List<E> listarTodos(); } //Definindo uma classe que fixa o tipo genérico public class ProdutoDAO implements DAO<Produto> { public void salvar(Produto obj) { ... } public Produto recuperar(int id) { ... } public List<Produto> listarTodos(){ ... } } Além de poder ser definido em classes em interfaces, o tipo genérico também pode ser definido no contexto de um método. O principal objetivo desse uso é amarrar os tipos dos parâmetros passados ou permitir que o retorno seja inferido de acordo com o parâmetro passado. A Listagem 3 apresenta o exemplo de um método que define um tipo genérico para amarrar os parâmetros. Observe que ele recebe como parâmetro uma List<E> e um E, então a lista precisa ser do mesmo tipo do elemento, senão será apontado um erro em tempo de compilação. Por exemplo, um código que passar um List<Integer> e uma String como parâmetro não será válido. Listagem 3. Exemplo de método que usa tipos genéricos para amarrar os parâmetros. public static <E> void colocarNaFrente(List<E> lista, E elemento) { ... } A Listagem 4 apresenta outro exemplo, onde o tipo genérico é utilizado para que o tipo do retorno seja inferido de acordo com o parâmetro passado. Por exemplo, se for passada uma lista de String, então o retorno será do tipo String. Isso evita que casts desnecessários sejam feitos e evita erros como a atribuição do retorno para uma variável de tipo incompatível com o objeto retornado. Listagem 4. Exemplo de método que usa tipos genéricos para inferência de retorno. //Definição do método public static <E> E elementoDoMeio(List<E> lista) { ... } //Uso do método List<String> lista = //recupera lista String meio = elementoDoMeio(lista) ou interface é uma escolha válida. Outro fato importante no exemplo da Listagem 5 é que, nesse caso, como o parâmetro não tem um tipo genérico definido, nenhum método que recebe o tipo genérico como parâmetro pode ser invocado, a não ser passando “null“. De forma mais concreta, o método add(), que recebe o tipo genérico da lista, não poderia ser invocado nesse caso. Sendo assim, seria como se a lista fosse read-only. Outro caso em que isso pode ser muito útil é para restringir o tipo de classe que pode ser configurada em uma anotação. Por mais que possa parecer estranho, a classe Class possui um tipo genérico que é o tipo da própria classe que está representando. Sendo assim, a classe String retorna no método getClass() uma instância de Class<String>. Sendo assim, para tanto um método ou uma anotação que recebe uma classe como parâmetro, é possível definir apenas um subconjunto de classes que podem ser passadas como parâmetro. Imagine, por exemplo, uma anotação chamada @RelatedDAO que configura qual a classe DAO que precisa ser utilizada para uma determinada entidade. Devido a própria descrição, só faz sentido a configuração de classes que implementem uma interface chamada DAO. A Listagem 6 mostra como seria essa anotação. De forma bem rápida e resumida essa pequena introdução mostrou um pouco do funcionamento dos tipos genéricos. As próximas seções irão focar em coisas interessantes que podem ser feitas com tipos genéricos. Listagem 6. Anotação que restringe a Class configurada em uma propriedade. Restringindo Tipos Genéricos @Retention(RetentionPolicy.RUNTIME) Uma coisa que poucos sabem sobre os tipos gené@Target(ElementType.TYPE) ricos é que é possível restringir quais tipos podem ser public @interface RelatedDAO { passados como parâmetro. Para ilustrar essa questão Class<? extends DAO> value(); considere um método que grave em um arquivo uma } lista de objetos. Como os objetos precisarão ser serializados, não faz sentido receber uma lista em que os Para finalizar essa parte de restrição de tipos, uma objetos não implementem Serializable. A Listagem 5 classe também pode restringir os tipos genéricos que podem ser utilizados para parametrizá-la. Imagine mostra como esse método poderia ser definido. que ao invés de criar um método para guardar uma Listagem 5. Exemplo de método que restringe o tipo lista em um arquivo, eu decida criar uma lista serialigenérico do parâmetro. zável. Observe na Listagem 7 que é possível, mesmo implementando outra interface, restringir na declapublic void gravarEmArquivo(List<? Extends Serializable> lista) { ... } ração do tipo genérico qual classe ou interface ele precisa ter como supertipo. Nesse caso, ao invés do Quando a restrição é utilizada em parâmetros de mé- wildcard “?“, utiliza-se o próprio parâmetro genérico. todos, o “?“, chamado de wildcard, pode ser utilizado para definir um tipo que obedece certa regra de tipo. Listagem 7. Exemplo de método que usa tipos genéSe o wildcard for acompanhado de “extends“ significa ricos para inferência de retorno. que ele pode ser qualquer classe que estenda a classe public class SerializableList<E extends Serializable> ou implemente a interface. Se ele for acompanhado implements List<E> { ... } de “super“ significa que pode ser qualquer supertipo do configurado. Em ambos os casos a própria classe Uma das vantagens desse tipo de restrição é que se 7\ você define que um tipo genérico deve implementar uma interface, então é possível invocar os métodos daquela interface mesmo que o tipo não esteja definido. Esse tipo de definição também é útil para evitar que erros de tipagem ocorram na definição dos tipos genéricos. Ok! Eu sei que essa seção não impressionou tanto quem conhecia um pouco mais sobre tipos genéricos! Mas prepare-se que na próxima seção as coisas ficam mais interessantes! Definindo o Tipo da Exceção pelo Cliente Qualquer desenvolvedor Java aprende ao lidar com exceções que um método pode declarar as exceções que podem ser lançadas durante a sua execução. Mas poderia um cliente definir o tipo da exceção que será lançada por um método que ele invoca? Surpreendentemente utilizando tipos genéricos sim! Muitas vezes acabamos capturando a exceção lançada por um componente para a encapsularmos dentro de uma exceção definida pela aplicação. Nesse cenário seria muito interessante que fosse possível definir para esse componente qual exceção ele deve lançar, pois dessa forma não seria preciso mapear as suas exceções para as da aplicação. O exemplo dessa seção considera uma classe que autentica usuários a partir de definições em um arquivo de propriedades com login e senha. Um dos requisitos é que esse componente lance uma exceção toda vez que um usuário não for autenticado. Para permitir seu reúso, deseja-se então que o cliente possa definir a classe da exceção que será lançada. O segredo dessa solução está na definição de uma interface com tipo genérico para representar uma fábrica de exceções, como a classe FabricaExcecoes na Listagem 8. O tipo genérico dessa interface representa a exceção que é criada por ela. A intenção é que as classes ao implementarem essa interface definam o tipo da exceção que irão criar. Listagem 8. Interface com tipo genérica para criação de exceções. public interface FabricaExcecoes<E extends Exception> { public E criarExcecao(String msg); } A Listagem 9 apresenta a classe Autenticador, a qual é responsável pela autenticação dos usuários. No construtor é feito o carregamento do arquivo de propriedades “users.prop” onde os usuários e suas senhas estão armazenadas (a ideia aqui é apenas ilustrar a técnica, não armazene senhas assim “em casa“). O método autenticar() define um tipo genérico E, que é utilizado para um parâmetro do tipo FabricaExcecoes<E> e para o tipo de exceção lançada /8 Cuidado! Os tipos genéricos são invariantes! Uma coisa que confunde muitas pessoas que começam a trabalhar com tipos genéricos é como funciona a herança e o polimorfismo. Os arrays são tipos covariantes, isso significa que um array de um tipo A é considerado por polimorfismo um array do tipo B, se B for um supertipo de A. Por exemplo, se eu tenho uma variável do tipo Integer[] então ela pode ser atribuída para uma do tipo Number[] via polimorfismo. Com os tipos genéricos as regras são diferentes, pois eles são invariantes. Nesse caso, uma classe nunca pode ser convertida por polimorfismo para uma classe com um tipo diferente. Isso significa que uma variável do tipo List<Integer> não pode ser atribuída para uma do tipo List<Number>. Se você quiser receber variáveis com mais de um tipo genérico, é preciso utilizar o wildcard com as palavras super e extends de acordo com a restrição desejada. No caso de métodos que utilizarem um wildcard com extends em um parâmetro, existe a restrição de não ser possível invocar nessa instância métodos que recebem o tipo genérico. De forma equivalente, quando o wildcard com super for utilizado, não pode ser feita a inferência do tipo do retorno, que deverá ser sempre recebido como Object. por ele. Dessa forma, o tipo da exceção lançada irá depender do tipo genérico da fábrica, a qual é definida pelo cliente. Observe que a exceção lançada é criada pela fábrica. Listagem 9. Classe que faz a autenticação de um usuário e declara uma exceção genérica. public class Autenticador { private Properties p; public Autenticador(){ try { p = new Properties(); p.load(new FileInputStream(“users.prop”)); } catch (Exception e) { throw new RuntimeException(“Usuarios não encontrados”); } } public <E extends Exception> void autenticar(String login, String senha, FabricaExcecoes<E> f) throws E { } } if(!(p.contains(login) && p.get(login).equals(senha))){ throw f.criarExcecao(“Login e senha incorretos!”); } } Para entender melhor como isso funciona, será mostrado um exemplo concreto. A Listagem 10 apresenta a exceção SecurityException, que no exemplo faz o papel de uma exceção que foi definida para a aplicação. Já na Listagem 11, a classe FabricaSecurityException implementa a interface FabricaExcecoes com o tipo genérico igual a exceção definida, no caso SecurityException. Listagem 10. Exceção definida para ser lançada pelo método. public class SecurityException extends Exception { } public SecurityException(String message) { super(message); } Listagem 11. Implementação da fábrica de exceções para a exceção definida. public class FabricaSecurityException implements FabricaExcecoes<SecurityException> { } @Override public SecurityException criarExcecao(String msg) { return new SecurityException(msg); } A Listagem 12 apresenta um código cliente que utiliza a classe Autenticador. Observe que uma instância da classe FabricaSecurityException é criada para que o tipo da exceção possa ser definido. Quando essa classe é passada como parâmetro, o tipo da exceção é inferido pelo tipo genérico da fábrica. Sendo assim, observe que o bloco try/catch captura a exceção que é definida pela aplicação. Listagem 12. Código que invoca a classe Autenticador definindo a exceção que ela irá lançar. public class Principal { public static void main(String[] args) { Autenticador a = new Autenticador(); FabricaSecurityException f = new FabricaSecurityException(); try { a.autenticar(“admin”, “admin”, f); } System.out.println(“Autenticação com sucesso”); } catch (SecurityException e) { System.out.println(e.getMessage()); } Outra abordagem nessa solução seria a utilização de um tipo genérico de classe ao invés de um tipo genérico de método. Dessa forma, a fábrica de exceções precisa ser configurada apenas uma vez no construtor e não precisa ser passada todas as vezes como parâmetro. Da outra forma, a mesma instância poderia lançar diferentes exceções de acordo com o parâmetro do método, e nessa solução a exceção é definida no momento da criação. A Listagem 13 mostra como ficaria essa implementação. Listagem 13. Utilizando tipo genérico de classe no Autenticador. public class Autenticador<E extends Exception> { private Properties p; private FabricaExcecoes<E> f; public Autenticador(FabricaExcecoes<E> f){ this.f = f; try { p = new Properties(); p.load(new FileInputStream(“users.prop”)); } catch (Exception e) { throw new RuntimeException(“Usuarios não encontrados”); } } public void autenticar(String login, String senha) throws E{ if(!(p.contains(login) && p.get(login).equals(senha))){ throw f.criarExcecao(“Login e senha incorretos!”); } } } Recuperando o Tipo Genérico com Reflexão Uma coisa que sempre é comentada sobre os tipos genéricos, é que é são validações realizadas em tempo de compilação e que essa informação não é mantida em tempo de execução. Sendo assim, imagino que talvez você esteja se perguntando: como um tipo genérico pode ser recuperado por reflexão se ele não é mantido pela máquina virtual? Calma! Dado um objeto, não é possível saber o tipo genérico desse objeto em tempo de execução. Porém, em declarações de métodos, atributos ou de classe que utilizam tipos genéricos, é sim possível saber qual o tipo genérico declarado. 9\ Considere, por exemplo, a classe Pessoa apresentada na Listagem 14. Imagine que fosse necessário criar um algoritmo que recuperasse todas as entidades relacionadas com uma determinada classe. Nesse contexto, uma entidade seria toda classe com a anotação @Entity. Nesse caso, seria necessário varrer os atributos dessa classe e procurar pelos tipos que possuem essa anotação. Porém, por exemplo, o atributo telefones possui uma entidade declarada como um tipo genérico. Como fazer para buscar essa informação? ParameterizedType tipoGenerico = (ParameterizedType) f.getGenericType(); Class parametroGenerico = (Class) tipoGenerico.getActualTypeArguments()[0]; if(parametroGenerico.isAnnotationPresent( Entity.class)){ list.add(parametroGenerico); } } } return list; } Listagem 14. Classe que possui tipos relacionados } declarados em tipos genéricos. @Entity public class Pessoa { private private private private private String nome; int idade; Endereco endereco; List<Telefone> telefones; Set<String> cargos; A Listagem 16 apresenta um exemplo de como esse método seria utilizado para retornar as entidades relacionadas na classe Pessoa e imprimir no console. No caso, será impresso o nome das classes Telefone e Endereco que possuem a anotação. Listagem 16. Classe que possui tipos relacionados declarados em tipos genéricos. public class PrincipalReflection { //getters e setters omitidos } Na API de reflexão, a classe Field possui um método chamado getGenericType(), que não retorna os parâmetros de tipo, mas uma instância de ParameterizedType (apesar do retorno ser do tipo Type e ser necessário fazer o cast). Essa classe possui um método chamado getActualTypeArguments() que retorna um array com os parâmetros genéricos configurados. A Listagem 15 mostra o método getEntidadesRelacionadas() que recebe uma classe e retorna uma lista com as entidades relacionadas a ela. Primeiramente esse método verifica se a classe do atributo possui a anotação @Entity e, em caso positivo, a adiciona a lista que será retornada. Em caso negativo, é verificado se o tipo implementa a interface Collection e, em caso positivo, é recuperado o tipo genérico e no qual também é verificado se possui a anotação @Entity. Listagem 15. Método que retorna as entidades relacionadas, procurando nos tipos genéricos. public class Relacoes { public static List<Class> getEntidadesRelacionadas(Class c){ List<Class> list = new ArrayList<>(); for(Field f : c.getDeclaredFields()){ Class tipo = f.getType(); if(tipo.isAnnotationPresent(Entity.class)){ list.add(tipo); }else if(Collection.class.isAssignableFrom(tipo)){ / 10 } public static void main(String[] args) { List<Class> lista = Relacoes.getEntidadesRelacionadas(Pessoa.class); for(Class c : lista){ System.out.println(c.getName()); } } Outro cenário onde a recuperação do parâmetro genérico por reflexão pode ser muito útil é em relação ao tipo utilizado na superclasse. Imagine o exemplo de uma abstração para a representação de um DAO, como anteriormente apresentado na Listagem 2 deste artigo. Muitas vezes, é preciso saber qual é a classe que será persistida para implementação dos métodos, principalmente se a implementação for JPA. Dessa forma, a superclasse pode disponibilizar um método para a recuperação do parâmetro genérico utilizado na subclasse. A Listagem 17 apresenta como esse método pode ser implementado. A partir da classe do objeto, é feita uma busca nas superclasses até que seja encontrada a superclasse DAO. Então, o método getGenericSuperclass() é utilizado para recuperar uma instância de ParameterizedType e a partir dele serem recuperados os parâmetros genéricos. Listagem 17. Exemplo de uma superclasse com tipo genérico que fornece método para recuperação do parâmetro genérico configurado. public abstract class DAO<E> { public abstract void salvar(E obj); public abstract E recuperar(int id); public abstract List<E> listarTodos(); } public Class getGenericParameter(){ Class c = this.getClass(); while(!c.getSuperclass().equals(DAO.class)){ c = c.getSuperclass(); } ParameterizedType tipoGenerico = (ParameterizedType) c.getGenericSuperclass(); return (Class) tipoGenerico.getActualTypeArguments()[0]; } Para completar o exemplo, a Listagem 18 mostra a classe DAOPessoa que estende a classe DAO com o tipo genérico Pessoa. O método main() cria uma instância classe e invoca o método getGenericParameter() criado. Com a execução desse código, o nome da classe Pessoa será impresso no console. Listagem 18. Classe que estende a superclasse genérica e retorna a classe do parâmetro genérico. public class PessoaDAO extends DAO<Pessoa> { public static void main(String[] args) { PessoaDAO dao = new PessoaDAO(); System.out.println(dao.getGenericParameter(). getName()); } //implementações dos métodos abstratos omitidas } Considerações finais Este artigo teve como objetivo fazer uma revisão dos tipos genéricos e apresentar algumas utilizações desse recurso que são pouco exploradas pelos desenvolvedores. Dentre as técnicas apresentadas está a restrição no parâmetro genérico de uma classe ou método, utilização de exceções genéricas e a recuperação do tipo genérico em declarações, como de atributos, de métodos e de classe. Além de ser interessante e curioso a utilização de um recurso de linguagem pouco conhecido, ou mesmo de uma forma que foge do convencional, este artigo também discutiu os cenários em que cada uma dessas técnicas poderiam ser utilizadas. Através dos exemplos, foram mostrados cenários cotidianos de aplicações que ilustraram o contexto para o seu uso. Design Patterns com Java: projeto orientado a objetos guiado por padrões Se você se interessou por este artigo e gosta de estudar sobre técnicas de modelagem de software, acabou de ser lançado pela editora Casa do Código o livro “Design Patterns com Java: Projeto orientado a objetos guiado por padrões”. Esse livro apresenta, de uma forma didática e moderna, como utilizar os padrões de projeto para um bom design orientado a objetos em Java. Além dos padrões em si, o livro traz exemplos de APIs e frameworks Java que os utilizam. Adicionalmente, também são abordadas técnicas de design modernas, como o uso de interface fluente, componentes plugáveis e criação de frameworks. Dentro do contexto deste artigo, é abordado no último capítulo como os tipos genéricos podem ser utilizados na implementação dos padrões. 11 \