Capítulo 9

Reflection e Annotations

Por ser uma linguagem compilada, Java permite que, enquanto escrevemos nosso código, tenhamos total controle sobre o que será executado, de tudo que faz parte do nosso sistema. Em tempo de desenvolvimento, olhando nosso código, sabemos quantos atributos uma classe tem, quais métodos ela tem, qual chamada de método está sendo feita e assim por diante.

Mas existem algumas raras situações onde essa garantia toda do compilador não nos ajuda. Isto é, existem situações em que precisamos de características dinâmicas. Por exemplo, imagine permitir que, dado um nome de método que o usuário passar, nós o invocaremos em um objeto. Ou ainda, que, ao recebermos o nome de uma classe enquanto executando o programa, possamos criar um objeto dela!

Nesse capítulo você vai conhecer um pouco desse recurso avançado e muito poderoso, mas que deve ser usado de forma sensata!

9.1 - Por que Reflection?

Um aluno que já cursou o FJ-21 pode notar uma incrível semelhança com a discussão em aula sobre MVC, onde, dado um parâmetro da requisição, criamos um objeto das classes de lógica. Outro exemplo já visto é o do XStream: dado um XML, ele consegue criar objetos para nós e colocar os dados nos atributos dele. Como ele faz isso? Será que no código-fonte do XStream acharíamos algo assim:

Negocio n = new Negocio(...);

O XStream foi construído para funcionar com qualquer tipo de XML. Com um pouco de ponderação fica óbvio que não há um new para cada objeto possível e imaginável dentro do código do XStream. Mas então... como ele consegue instanciar um objeto da minha classe e popular os atributos, tudo sem precisar ter new Negocio() escrito dentro dele?

O javax.reflection é um pacote do Java que permite criar chamadas em tempo de execução, sem precisar conhecer as classes e objetos envolvidos quando escrevemos nosso código (tempo de compilação). Esse dinamismo é necessário para resolvermos determinadas tarefas que nosso programa só descobre serem necessárias ao receber dados, em tempo de execução.

De volta ao exemplo do XStream, ele só descobre o nome da nossa classe Negocio quando rodamos o programa e selecionamos o XML a ser lido. Enquanto escreviam essa biblioteca, os desenvolvedores do XStream não tinham a menor idéia de que um dia o usaríamos com a classe Negocio.

Apenas para citar algumas possibilidades com reflection:

9.2 - Class, Field e Method

O ponto de partida de reflection é a classe Class. Esta, é uma classe da própria API do Java que representa cada modelo presente no sistema: nossas classes, as que estão em JARs e também as do próprio Java. Através da Class conseguimos obter informações sobre qualquer classe do sistema, como seus atributos, métodos, construtores, etc.

Todo objeto tem um jeito fácil pegar o Class dele:

Negocio n = new Negocio();

// chamamos o getClass de Object
Class<Negocio> classe = n.getClass();

Mas nem mesmo precisamos de um objeto para conseguir as informações da sua classe. Diretamente com o nome da classe também podemos fazer o seguinte:

Class<Negocio> classe = Negocio.class;

Java 5 e Generics

A partir do Java 5, a classe Class é tipada e recebe o tipo da classe que estamos trabalhando. Isso melhora alguns métodos, que antes recebiam Object e agora trabalham com um tipo T qualquer, parametrizado pela classe.

A partir de um Class podemos listar, por exemplo, os nomes e valores dos seus atributos:

Class<Negocio> classe = Negocio.class;
for (Field atributo : classe.getDeclaredFields()) {
  System.out.println(atributo.getName());      
}

A saída será:

preco
quantidade
data

Fazendo similarmente para métodos é possível conseguir a lista:

getPreco
getQuantidade
getData
getVolume
isMesmoDia

É possível fazer muito mais. Investigue a API de reflection usando ctrl + espaço no Eclipse e pelo JavaDoc.

Seus livros de tecnologia parecem do século passado?

Conheça a Casa do Código, uma nova editora, com autores de destaque no mercado, foco em ebooks (PDF, epub, mobi), preços imbatíveis e assuntos atuais.
Com a curadoria da Caelum e excelentes autores, é uma abordagem diferente para livros de tecnologia no Brasil. Conheça os títulos e a nova proposta, você vai gostar.

Casa do Código, livros para o programador.

9.3 - Usando anotações

Anotações (annotations) são uma novidade do Java 5 cujo objetivo é possibilitar a declaração de metadados nos nossos objetos, isto é, configurações de uma classe podem ficar dentro dela, em vez de em um XML à parte!

Até agora, usamos anotações para indicar que um método não deve mais ser usado (@Deprecated), que ele foi sobrescrito (@Override) e para configurar um método como teste do JUnit (@Test) e talvez já a tenhamos visto em outros lugares.

Em todas essas ocasiões, percebemos que a presença da anotação não influi no comportamento daquela classe, daqueles objetos. Não são códigos executáveis, que mudam o que é executado ou não. São metadados: informações (dados) que falam sobre nossa classe mas não fazem parte da classe em si.

Metadados são muito usados para configurações de funcionalidades anexas àquela classe. Por exemplo, usamos a anotação do Test para configurar que o JUnit entenda aquele método como um teste e rode ele quando usarmos o alt + shift + X T. Mas, se retirarmos a anotação, a nossa classe continua compilando normalmente - apenas, quando rodarmos o JUnit, esse método será ignorado.

Em geral, portanto, usamos anotações para criar configurações nos nossos artefatos com objetivo de que depois essas anotações sejam lidas e processadas por alguém interessado naquelas informações.

9.4 - Usar JTables é difícil

No capítulo de Swing, vimos que usar JTable é uma tarefa trabalhosa. Precisamos definir as características de renderização do componente e o modelo de dados que vai ser exibido (nosso TableModel).

Criamos anteriormente uma classe chamada NegocioTableModel para devolver os dados dos negócios que queremos exibir na tabela. Mas imagine se nosso cliente decidisse que precisa ver também as tabelas de listagem de candles, um para cada dia. Mais ainda, ele um dia pode querer mostrar uma tabela com as Séries Temporais do sistema todo! Em ambos os casos, provavelmente criaríamos um TableModel muito parecido com o de negócios, apenas substituindo as chamadas aos getters de uma classe pelos getters de outra.

Usando um pouco de reflection, será que conseguiríamos criar um único TableModel capaz de lidar com qualquer dos nossos modelos? Um TableModel que, dependendo da classe (Negocio / Candle) passada, consegue chamar os getters apropriados? Poderíamos colocar um monte de if's que avaliam o tipo do objeto e escolhem o getter a chamar, mas com um pouco mais de reflection teremos bem menos trabalho.

Vejamos o que será preciso para implementar um ArgentumTableModel que consiga lidar com todas as classes de modelo do Argentum. Lembre-se que para termos um TableModel é preciso que implementemos, no mínimo, os três métodos: getRowCount, getColumnCount e getValueAt.

O método getRowCount meramente devolve a quantidade de itens na lista que será mostrada na tabela, portanto, a sua implementação é igual. Agora, para sabermos o número de colunas da tabela é preciso que olhemos a classe em questão e contemos a quantidade de, por exemplo, atributos dela!

A partir do tipo dos elementos da lista que estamos manipulando, conseguimos pegar a Class desses elementos e iterar nos seus atributos (Field) para pegar seus valores ou, simplesmente o que precisamos: o número de atributos. Dessa forma, o método ficaria assim:

@Override
public int getColumnCount() {
  Class<?> classe = lista.get(0).getClass();
  Field[] atributos = classe.getDeclaredFields();
  return atributos.length;
}

9.5 - Usando bem anotações

Note, contudo, haverá muitas vezes em que não gostaríamos de mostrar todos os atributos na tabela ou ainda, haverá vezes em que precisaremos mostrar informações compostas em uma mesma célula da tabela.

Nesses casos, podemos criar um método que traz tal informação e, em vez de contar atributos, contaremos esses métodos. Apenas, tanto no caso de contarmos atributos quanto no caso de contarmos métodos, não queremos contar todos eles, mas apenas os que representarão colunas na nossa tabela.

Para configurar quais métodos trarão as colunas da tabela, podemos usar um recurso do Java 5 que você já viu diversas vezes durante seu aprendizado: anotações. Por exemplo, podemos marcar os métodos de Negocio que representam colunas assim:

public final class Negocio {

  // atributos e construtor

  @Coluna
  public double getPreco() {
    return preco;
  }

  @Coluna
  public int getQuantidade() {
    return quantidade;
  }

  @Coluna
  public Calendar getData() {
    return (Calendar) data.clone();
  }

  public double getVolume() {
    return this.preco * this.quantidade;
  }
}

Dessa forma, a contagem deverá levar em consideração apenas os métodos que estão anotados com @Coluna, isto é, temos que passar por cada método declarado nessa classe contando os que tiverem a anotação @Coluna. Modificando nosso getColumnCount temos:

@Override
public int getColumnCount() {
  Object objeto = lista.get(0);
  Class<?> classe = objeto.getClass();
  
  int colunas = 0;
  for (Method metodo : classe.getDeclaredMethods()) {
    if (metodo.isAnnotationPresent(Coluna.class)) {
      colunas++;
    }
  }
  return colunas;
}

Um outro problema ao montar a tabela dinamicamente aparece quando tentamos implementar o método getValueAt: precisamos saber a ordem em que queremos exibir as colunas. Agora, quando usamos reflection não sabemos exatamente a ordem em que os métodos são percorridos.

Como a posição das colunas em uma tabela é importante, precisamos adicionar essa configuração às colunas:

public final class Negocio {

  // atributos e construtores

  @Coluna(posicao=0)
  public double getPreco() {
    return preco;
  }

  @Coluna(posicao=1)
  public int getQuantidade() {
    return quantidade;
  }

  @Coluna(posicao=2)
  public Calendar getData() {
    return (Calendar) data.clone();
  }
}

Então, basta percorrer os atributos dessa classe, olhar para o valor dessas anotações e montar a tabela dinamicamente com essas posições - isto é, substituiremos aquele switch que escrevemos no NegocioTableModel por algumas linhas de reflection:

@Override
public Object getValueAt(int linha, int coluna) {
  try {
    Object objeto = lista.get(linha);
    Class<?> classe = objeto.getClass();
    for (Method metodo : classe.getDeclaredMethods()) {
      if (metodo.isAnnotationPresent(Coluna.class)) {
        Coluna anotacao = metodo.getAnnotation(Coluna.class);
        if (anotacao.posicao() == coluna) { 
          return metodo.invoke(objeto);
        }
      }
    }
    return "";
  } catch (Exception e) {
    return "Erro";
  }
}

Agora é a melhor hora de aprender algo novo

Se você gosta de estudar essa apostila aberta da Caelum, certamente vai gostar dos novos cursos online que lançamos na plataforma Alura. Você estuda a qualquer momento com a qualidade Caelum.

Conheça a Alura.

9.6 - Criando sua própria anotação

Para que nosso código compile, no entanto, precisamos descobrir como escrever uma anotação em Java! As anotações são tipos especiais declarados com o termo @interface, já que elas surgiram apenas no Java 5 e não quiseram criar uma nova palavra chave para elas.

public @interface Coluna {

}

Os parâmetros que desejamos passar à anotação, como posicao são declarados como métodos que servem tanto para setar o valor quanto para devolver. A sintaxe é meio estranha no começo:

public @interface Coluna {
  int posicao();
}

Nesse momento, já temos a anotação e um parâmetro obrigatório posicao, mas para que ela de fato faça seu trabalho, ainda é preciso informar duas configurações importantes para toda anotação: que tipo de estrutura ela configura (método, atributo, classe, ...) e também se ela serve apenas para ajudar o desenvolvedor enquanto ele está programando, tempo de compilação, ou se ela precisará ser lida inclusive durante a execução - por exemplo, a anotação @Override só precisa estar presente durante a compilação, enquanto a @Test precisa ser lida na hora de exeutar o teste em si.

Nossa annotation ficará como abaixo:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Coluna {
  int posicao();
}

Com a anotação compilando, a classe Negocio devidamente configurada e o ArgentumTableModel estamos preparados para mostrar tabelas de qualquer modelo que tenha seus getters anotados com @Coluna(posicao=...)!

9.7 - Exercícios: ArgentumTableModel

Atenção: os imports necessários para esse exercício devem ser java.lang.annotation

  1. Anote os getters da classe Negocio com @Coluna, já passando o atributo posicao e sem se preocupar com os erros de compilação ainda.

    Adicione em cada método apenas a anotação, passando posições diferentes:

    public final class Negocio {
      
      // atributos e construtores
    
      @Coluna(posicao=0)
      public double getPreco() {
        return preco;
      }
    
      @Coluna(posicao=1)
      public int getQuantidade() {
        return quantidade;
      }
    
      @Coluna(posicao=2)
      public Calendar getData() {
        return (Calendar) data.clone();
      }
    }
    
  2. Usando o ctrl + 1 mande o Eclipse create new annotation para você e lembre-se de alterar o pacote dela para br.com.caelum.argentum.ui, já que essa anotação só existe para que a JTable funcione com qualquer modelo nosso.

    create-annotation.png

    Voltando na classe Negocio ela ainda não compila porque o atributo posicao ainda não existe na anotação nova! Use o ctrl + 1 novamente para criar o atributo. Confira! Sua anotação deve estar assim:

    public @interface Coluna {
    
      int posicao();
    
    }
    
  3. Para que essa anotação seja guardada para ser lida em tempo de execução é preciso configurá-la para tal. Além disso, como queremos que o desenvolvedor anote métodos (e não classes ou atributos, por exemplo) também é preciso fazer essa configuração.

    Adicione as anotações à sua annotation:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Coluna {
    
      int posicao();
    
    }
    
  4. Crie a classe ArgentumTableModel no pacote br.com.caelum.argentum.ui, e utilize o código aprendido durante o capítulo:

     1 public class ArgentumTableModel extends AbstractTableModel {
     2 
     3   private final List<?> lista;
     4   private Class<?> classe;
     5 
     6   public ArgentumTableModel(List<?> lista) {
     7     this.lista = lista;
     8     this.classe = lista.get(0).getClass();
     9   }
    10 
    11   @Override
    12   public int getRowCount() {
    13     return lista.size();
    14   }
    15 
    16   @Override
    17   public int getColumnCount() {
    18     int colunas = 0;
    19     for (Method metodo : classe.getDeclaredMethods()) {
    20       if (metodo.isAnnotationPresent(Coluna.class))
    21         colunas++;
    22     }
    23     return colunas;
    24   }
    25 
    26   @Override
    27   public Object getValueAt(int linha, int coluna) {
    28     try {
    29       Object objeto = lista.get(linha);
    30       for (Method metodo : classe.getDeclaredMethods()) {
    31         if (metodo.isAnnotationPresent(Coluna.class)) {
    32           Coluna anotacao = metodo.getAnnotation(Coluna.class);
    33           if (anotacao.posicao() == coluna) 
    34             return metodo.invoke(objeto);
    35         }
    36       }
    37     } catch (Exception e) {
    38       return "Erro";
    39     }
    40     return "";
    41   }
    42 }
    
  5. Altere o método carregaDados da classe ArgentumUI para usar o nosso novo TableModel. Onde tínhamos:

    NegociosTableModel model = new NegociosTableModel(negocios);
    tabela.setModel(model);
    

    Substitua por:

    ArgentumTableModel model = new ArgentumTableModel(negocios);
    tabela.setModel(model);
    

    Rode novamente e deveremos ter a tabela montada dinamicamente.

    (Note que perdemos os formatos. Vamos adicioná-los em seguida.)

  6. Alguns frameworks usam bastante reflection para facilitar a criação de suas telas. Pesquise a respeito.

9.8 - Exercícios opcionais: nomes das colunas

  1. Queremos possibilitar também a customização dos títulos das colunas. Para isso, vamos alterar as chamadas na classe Negocio para usar o novo parâmetro:

    @Coluna(nome="Preço", posicao=0)
    // ...
    
    @Coluna(nome="Quantidade", posicao=1)
    // ...
    
    @Coluna(nome="Data", posicao=2)
    // ...
    
  2. Para fazer esse código compilar, basta usar novamente o ctrl + 1 e pedir que Eclipse crie esse atributo para você! Ele mesmo vai adicionar tal parâmetro à anotação:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Coluna {
    
      String nome();
      int posicao();
    }
    
  3. Na classe ArgentumTableModel, adicione o método getColumnName. Ele é muito parecido com os códigos que escrevemos antes:

     1 @Override
     2 public String getColumnName(int coluna) {
     3   for (Method metodo : classe.getDeclaredMethods()) {
     4     if (metodo.isAnnotationPresent(Coluna.class)) {
     5       Coluna anotacao = metodo.getAnnotation(Coluna.class);
     6       if (anotacao.posicao() == coluna)
     7         return anotacao.nome();
     8     }
     9   }
    10   return "";
    11 }
    

    Rode novamente e observe os nomes das colunas na tabela.

Você pode também fazer o curso FJ-16 dessa apostila na Caelum

Querendo aprender ainda mais sobre boas práticas de Java, testes e design patterns? Esclarecer dúvidas dos exercícios? Ouvir explicações detalhadas com um instrutor?
A Caelum oferece o curso FJ-16 presencial nas cidades de São Paulo, Rio de Janeiro e Brasília, além de turmas incompany.

Consulte as vantagens do curso Laboratório Java com Testes, XML e Design Patterns.

9.9 - Para saber mais: Formatter, printf e String.format

Nossa tabela, apesar de bem mais flexível, perdeu todas as formatações que tínhamos feito antes. Note que a data está de volta ao toString padrão do Calendar e, se você havia feito o opcional de formatação dos valores, ela também se perdeu.

A partir do Java 5 a API de Formatter nos ajuda em casos assim. Ela provê uma forma bastante robusta de se trabalhar formatação de dados para impressão e é fortemente baseada nas ideias do printf do C.

Basicamente, fazemos uma String de formato que descreve em uma sintaxe especial o formato de saída dos dados que passamos como argumento depois.

É possível formatar números, definindo por exemplo a quantidade de casas decimais, ou formatar a saída de um Calendar usando seus campos, etc. Para imprimirmos diretamente no console, podemos usar essa sintaxe com o método System.out.printf, que também surgiu no Java 5. Alguns usos:

// usar printf é mais facil que concatenar Strings
String nome = "Manoel"; 
String sobrenome = "Silva";
System.out.printf("Meu nome é %s e meu sobrenome é %s\n", nome, sobrenome);
// saída: Meu nome é Manoel e meu sobrenome é Silva

// formatando casas decimais
System.out.printf("PI com 4 casas decimais: %.4f\n", Math.PI);
//saída: PI com 4 casas decimais: 3.1416

// a data de hoje em dia/mes/ano
System.out.printf("Hoje é %1$td/%1$tm/%1$tY", Calendar.getInstance());
// saída: Hoje é 04/05/2012

Caso precisemos apenas da String formatada em vez de imprimí-la, podemos usar:

String msg = String.format("PI com 4 casas decimais: %.4f\n", Math.PI);

9.10 - Para saber mais: parâmetros opcionais

Da forma como criamos atibutos na anotação, eles são obrigatórios: isto é, dá erro de compilação quando anotamos um método com @Coluna mas não preenchemos sua posição (e seu nome, se você tiver feito o exercício opcional).

Contudo, há diversas situações em que passar uma informação para a anotação é opcional. Um exemplo disso é o atributo expected da anotação @Test: só é preciso passá-lo se esperamos receber uma exceção.

No caso da nossa formatação, não precisaremos fazer nada se recebermos uma String como parâmetro, mas precisaremos modificar a formatação se recebermos datas ou valores, por exemplo.

É justo fazer um paralelo entre atributos de uma anotação e getters, na hora de recuperar o valor. Assim, é fácil notar que, para funcionar sempre, a anotação precisa que você tenha um valor para devolver. Assim, se queremos criar um atributo opcional é preciso ter um valor padrão para ser devolvido se não passarmos nada nesse parâmetro.

Para indicar esse valor padrão a ser adotado se não passarmos nada como, no nosso exemplo, máscara de formatação usamos a palavra chave default:

//...
public @interface Coluna {
  //...
  String formato() default "%s";
}

9.11 - Exercícios opcionais: formatações na tabela

As datas e valores monetários não estão sendo exibidos corretamente. Para solucionar isso, vamos usar formatadores baseados na classe Formatter e o método String.format.

  1. Adicione na anotação Coluna mais um parâmetro. Será a String com o formato desejado.

    String formato() default "%s";
    

    Repare no uso do default para indicar que, se alguém não passar o formato, temos um formato padrão. E esse formato é o mais simples (%s) possível: ele simplesmente imprime a String.

  2. Altere o método getValueAt na classe ArgentumTableModel para usar o formato de cada coluna na hora de devolver o objeto. Felizmente, é muito simples fazer essa modificação! Altere apenas a linha do retorno:

    @Override
    public Object getValueAt(int linha, int coluna) {
      // outras linhas...
      if (anotacao.posicao() == coluna)
        return String.format(anotacao.formato(), 
                      metodo.invoke(objeto));
      // mais linhas...
    }
    
  3. Altere a classe Negocio passando agora nas anotações o formato desejado. Para a coluna de preço, usaremos:

    @Coluna(nome="Preço", posicao=0, formato="R$ %,#.2f")
    

    E, para a coluna data, usamos:

    @Coluna(nome="Data", posicao=2, formato="%1$td/%1$tm/%1$tY")
    

    Rode novamente e observe a formatação nas células.

  4. Faça testes de unidade para o nosso ArgentumTableModel.

  5. Faça todos os passos necessários para adicionar uma quarta coluna na tabela de negócios mostrando o volume do Negocio.

Tire suas dúvidas no novo GUJ Respostas

O GUJ é um dos principais fóruns brasileiros de computação e o maior em português sobre Java. A nova versão do GUJ é baseada em uma ferramenta de perguntas e respostas (QA) e tem uma comunidade muito forte. São mais de 150 mil usuários pra ajudar você a esclarecer suas dúvidas.

Faça sua pergunta.

9.12 - Discussão em sala de aula: quando usar reflection, anotações e interfaces