Entendendo Anotações: Neste artigo abordaremos as anotações, as quais foram introduzidas no revolucionário Java 5 – também chamado Tiger. Anotações é um recurso usado para anotar classes, campos e métodos, de tal maneira que essas marcações podem ser tratadas pelo compilador, ferramentas de desenvolvimento e bibliotecas. Três anotações, denominadas tipos anotação padrão, são parte do pacote java.lang, e podem ser usadas sem qualquer esforço adicional: @Override, @Deprecated e @SuppressWarnings. Além dessas, o texto trata das anotações de anotações – @Retention, @Documented, @Target e @Inherited – as quais são também parte da API juntamente com as anteriores, e da personalização de tipos anotação, mostrando como criar nossos próprios tipos. Para finalizar, é demonstrado um pequeno exemplo de reflexão de anotações, ou seja, a capacidade de observar dinamicamente esses tipos.


Em que situação o tema é útil:
Este tema é útil quando desejamos anotar código não simplesmente para documentação, mas de maneira que essas marcações possam ser verificadas em tempo de compilação, ou utilizadas por ferramentas, tais como analisadores de código, frameworks de persistência ou de testes unitários, entre outras.

Kent Beck, o criador da Programação Extrema e do Desenvolvimento Orientado a Testes, sugere que para criar um bom código deve-se seguir três valores básicos: comunicação, simplicidade e flexibilidade. Falando especificamente de comunicação, ele diz que os programas são lidos mais frequentemente do que escritos, e, por conseguinte deveriam comunicar claramente suas intenções. Beck completa afirmando que em um sistema empresarial, grande parte do código será modificada por vários programadores durante 5 a 10 anos, e, portanto estes deverão entendê-lo com facilidade.

Usamos estes ensinamentos de Kent Beck para reforçar a necessidade de que os programas sejam documentados. Em suma, documentação é importante, pois desenvolvedores que não fizeram parte do processo conseguem com facilidade entender o que foi escrito.

No entanto, Brett McLaughlin, um dos autores do livro “Java 5.0 Tiger – A Developer’s Notebook”, já afirmava em 2004 que o uso de metadados surgia com uma forte tendência na programação, particularmente em Java. Metadados consistem de dados que descrevem outros dados, e podem ser usados para criar documentação, para rastrear dependências no código fonte e até mesmo para fazer verificações rudimentares em tempo de compilação.

Antes do Java 5 (também denominado Tiger) ser lançado, o Javadoc – ferramenta usada para gerar documentação da API de Java em HTML – era o recurso usado para definir metadados. No entanto, sabe-se que é possível expressar algo de várias maneiras na documentação. Por exemplo, se desejarmos dizer que uma variável não pode ser nula, em Javadoc pode-se dizer “Não-nula”, “Esta variável não pode ser nula” ou ainda “Não atribua valor nulo a esta variável”, entre outras possibilidades todas válidas. Ou seja, Javadoc serve apenas para gerar a documentação, pois não existe uma ferramenta que possa analisá-lo de maneira a considerar todas as variadas formas que podemos usar para definir uma simples condição como a do exemplo anterior.

Em vista disso, a JSR (Java Specification Request) 175, denominada A Metadata Facility for the Java Programming Language, apresenta a justificativa e especificação oficiais para a incorporação de metadados na linguagem Java. JSR é um documento submetido ao PMO (Program Management Office) por um ou mais membros do JCP (Java Community Process) com a finalidade de propor o desenvolvimento de uma nova especificação ou revisão significativa de uma especificação Java existente. PMO, por sua vez, é o grupo dentro da Oracle encarregado de supervisionar a JCP, o qual é o mecanismo aberto para desenvolver e revisar especificações técnicas padrão para a tecnologia Java, onde qualquer pessoa pode tornar-se membro, desde que possua uma conexão com a internet.

Na descrição de sua finalidade, a JSR 175 afirma que tem havido uma tendência crescente no sentido de se anotar campos, métodos e classes, os quais devem ter atributos particulares que indicam que eles devem ser processados de forma especial por ferramentas de desenvolvimento, de implantação, ou bibliotecas runtime. O parágrafo da especificação conclui que tais anotações – ou annotations – são chamadas metadados.

Dessa forma, as anotações buscam resolver a situação apresentada anteriormente – que enfrentamos ao usar Javadoc – oferecendo um mecanismo bem definido de criar metadados. Deduz-se da especificação JSR 175 que anotações são modificadores que podem ser aplicados a pacotes, declarações de tipos, construtores, métodos, campos, parâmetros e variáveis, as quais serão estudadas a partir de agora.

A importância dos metadados

A utilização mais comum de metadados é na documentação de código. No entanto, como vimos anteriormente, o Javadoc já cumpre com eficiência e simplicidade esse papel. Portanto, estamos interessados em outras justificativas para a aplicação de metadados ou anotações. Para fins deste artigo, vamos considerar as anotações como informação para o compilador e para utilização em testes de unidade. Entretanto elas podem ser empregadas em ferramentas de persistência, a exemplo do Hibernate, e de análise de código, tal como o Findbugs.

Vejamos então a primeira das características, a de que o compilador Java pode usar metadados para fazer uma verificação durante o processo de compilação. Por exemplo, veremos mais adiante a anotação @Override, a qual permite especificar que um método sobrescreve outro método da superclasse. Dessa forma, o compilador tem como assegurar que o comportamento indicado na anotação realmente acontece no código. Se o leitor considera sem sentido essa utilização de anotação, vamos analisar o exemplo mostrado na Listagem 1.

Listagem 1. Hierarquia de classes implementando erradamente a sobrescrita.


    class Funcionario {
     protected double salario;
     
     public double getSalarioTotal(double bonus) {
         return this.salario + bonus;
     }
 }
 
 class Auxiliar extends Funcionario {
     protected double extra;
     
     public double getSalarioTotal(float bonus) {
         return this.salario + this.extra + bonus;
     }
 }
 

A nossa intenção é sobrescrever o método getSalarioTotal(). No entanto, como já sabemos de nosso estudo sobre orientação a objetos, um método sobrescreve outro apenas se as assinaturas de ambos forem exatamente iguais. Ou seja, além do nome, quantidade e tipos dos parâmetros serem iguais, o tipo de retorno também deve ser o mesmo.

O compilador não possui mecanismos para checar isso e, provavelmente, a aplicação que usar essas classes deverá produzir algum resultado inesperado, visto que os tipos de dados do parâmetro usado nos dois métodos são diferentes, descaracterizando a sobrescrita. Agora, acrescente acima do método sobrescritor na classe Auxiliar, a linha @Override. Se você estiver usando NetBeans ou Eclipse, imediatamente o IDE irá sinalizar um erro. Ou seja, a inclusão dessa anotação irá alertar para uma falha caso o método da subclasse não sobrescreva o método da superclasse.

Outra característica de metadados é a capacidade de usar anotações para uso em testes de unidade. Como exemplo, vejamos o caso do JUnit, um framework de código aberto usado para escrever e executar testes repetíveis bastante explorado por desenvolvedores Java. Entendamos que a finalidade dos testes é executar um programa utilizando algumas entradas específicas e verificar se seu comportamento está de acordo com o esperado.

Listagem 2. Uso de JUnit com anotações.


 import org.junit.*;
 import static org.junit.Assert.*;
 import java.util.*;
 
 public class TesteSimples {
     
     @Test
     public void testeConjuntoVazio() {
         Collection conjunto = new ArrayList();
         assertTrue(conjunto.isEmpty());
     }
     
     public static void main(String args[]) {
           org.junit.runner.JUnitCore.main
        ("TesteSimples");
     }
 }
 

Não está no escopo deste artigo estudar o uso do JUnit, portanto não cabe aqui explicar o código da Listagem 2. Por ora, é suficiente sabermos que a anotação @Test foi empregada para identificar que testeConjuntoVazio() é um método de testes. Nesse método chamamos assertTrue() passando para ele uma expressão booleana que deve ser true se o teste for bem sucedido.

Introdução às anotações

Agora que sabemos o quanto as anotações podem ser úteis, vamos estudar como elas funcionam em Java. Para definir uma anotação no código Java, usamos o símbolo arroba (@) seguido do nome da mesma. Dependendo da categoria da annotation, pode ser necessário incluir dados a ela, no formato de pares nome=valor. São três as categorias de anotações:

  • Anotações marcadoras – são aquelas que não possuem membros. São identificadas apenas pelo nome, sem dados adicionais. Por exemplo, @Test na Listagem 2 é uma anotação marcadora;
  • Anotações de valor único – são similares às anteriores, no entanto, possuem um único membro, chamado valor. Elas são representadas pelo nome da anotação e um par nome=valor, ou simplesmente com o valor, entre parênteses. Em outras palavras, quando a anotação possui um único membro, só é necessário informar o valor, além do nome da anotação. Por exemplo, @MinhaAnotacao(“valor”) é um exemplo de sintaxe deste tipo de anotação;
  • Anotações completas – são aquelas que possuem múltiplos membros. Portanto, neste tipo, devemos usar a sintaxe completa para cada par nome=valor. Neste caso, cada par é informado separado do outro por uma vírgula. Por exemplo, @Version(major=1, minor=0, micro=0) é um caso de anotação completa.

Em uma classe podemos ter várias anotações, cada uma delas correspondente a algum tipo anotação, tal como @Override, que se utiliza para marcar os métodos sobrescritos, por exemplo. O tipo anotação é a definição da anotação e a anotação propriamente dita é um caso específico deste tipo. Explicando de uma maneira mais simples, e usando uma analogia, vamos imaginar os conceitos com os quais estamos familiarizados. Em uma aplicação temos uma classe – Funcionario, por exemplo – mas podemos ter várias instâncias – objetos – dela ao mesmo tempo. De maneira semelhante acontece com tipo anotação e anotação. Tipo anotação é como se fosse uma classe e a anotação é como se fosse uma instância da classe. Mais adiante, quando aprendermos a criar tipos anotação, mostraremos exemplos que tornarão mais claro esse conceito.

O desenvolvedor pode então definir suas próprias anotações a serem usadas em uma aplicação. Além disso, Java apresenta algumas anotações predefinidas, que veremos a seguir.

Tipos anotação padrão

Os tipos anotação padrão são aquelas providas como parte do Java no pacote java.lang. Essas anotações podem ser utilizadas sem qualquer esforço adicional em suas aplicações, pois, visto que são parte de java.lang, nem mesmo precisam ser importadas.

@Override

É uma anotação marcadora que deve ser usada apenas com métodos. Serve para indicar que o método anotado está sobrescrevendo um método da superclasse, da maneira que se observa na Listagem 3.

Listagem 3. Classe Funcionario usando Override.


    public class Funcionario {
     protected double salario;
     
     public double getSalarioTotal(double bonus) {
         return this.salario + bonus;
     }
 
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append("Funcionario [salario=");
         builder.append(salario);
         builder.append("]");
         return builder.toString();
     }
 
     @Override
     public int hashCode() {
         final int prime = 31;
         int result = 1;
         long temp;
         temp = Double.doubleToLongBits(salario);
         result = prime * result + (int) 
   (temp ^ (temp >>> 32));
         return result;
     }
 
 }
 

Como já sabemos de artigos anteriores, a classe da Listagem 3 é derivada de Object, visto que ela explicitamente não estende qualquer outra classe. Sabemos também que as classes em Java deveriam sobrescrever métodos definidos em Object, tais como toString(), hashCode() e equals(). Dessa forma, no código, @Override anota que os métodos toString() e hashCode() sobrescrevem os métodos da sua superclasse. Tais métodos foram gerados automaticamente pelo Eclipse, o qual insere também a anotação, e o código da classe compila sem qualquer problema.

No entanto, a força desta anotação é percebida quando o programador faz algo errado como na Listagem 4.

Listagem 4. Classe Funcionario sobrescrevendo incorretamente um método.


    public class Funcionario {
     protected double salario;
     
     public double getSalarioTotal(double bonus) {
         return this.salario + bonus;
     }
 
     @Override
     public int hasCode() {
         final int prime = 31;
         int result = 1;
         long temp;
         temp = Double.doubleToLongBits(salario);
         result = prime * result + (int) 
   (temp ^ (temp >>> 32));
         return result;
     }
 
 }
 

Observe que a anotação indica que o método hasCode() deveria sobrescrever um método, mas durante a compilação será detectado que a classe Object não possui um método hasCode() que possa ser sobrescrito. Isso resulta em um erro, indicando que hasCode() deve implementar ou sobrescrever um método do supertipo.

Conclui-se então que é uma boa prática anotarmos os métodos sobrescritos para assegurar que estamos fazendo a coisa certa em nossas aplicações.

@Deprecated

@Deprecated, assim como @Override, é também uma anotação marcadora. Esta anotação é utilizada quando é necessário indicar que um método não deveria mais ser usado, ou seja, informa que o método está obsoleto. Diferente de @Override, @Deprecated deve ser colocada na assinatura do método, como podemos ver na Listagem 5.

Listagem 5. Uso de @Deprecated.


    public class Funcionario {
     protected double salario;
     
     @Deprecated public double getSalarioTotal
  (double bonus) {
         return this.salario + bonus;
     }
 
     public double getTotalSalario
  (double bonus) {
         return this.salario + bonus;
     }
 }
 

O leitor pode perguntar por que simplesmente não removemos o método obsoleto. E a resposta é que precisamos manter a compatibilidade com sistemas legados que usam a classe. Ou seja, mesmo implementando uma nova versão de um método, os sistemas que já existem precisam continuar funcionando. Os programadores serão então alertados da mudança e gradativamente irão modificando suas aplicações para se adaptar à nova versão da classe.

O uso dessa anotação não provoca qualquer erro durante a compilação, quando utilizamos um método marcado com ela, seja sobrescrevendo-o ou fazendo uma chamada a ele. Se desejarmos, o compilador apenas emitirá um alerta de que o método está obsoleto. Para que esse alerta seja feito, é necessário configurar adequadamente o compilador com a opção -Xlint:deprecated. No Eclipse isso é feito no menu Window | Preferences. Depois disso, escolha a opção Java > Compiler > Errors/Warnings e marque as duas caixas em Deprecated and restricted API > Deprecated API. Observe a Figura 1.

Configuração de alertas sobre métodos obsoletos durante a compilação

Figura 1. Configuração de alertas sobre métodos obsoletos durante a compilação.

@SuppressWarnings

Aplicações criadas antes do advento do Java 5 podem ter algum código que gera alertas (warnings) durante a compilação com esta versão ou posteriores. É o caso das collections, nas quais o Java 5 permite o uso de generics para a especificação de tipos. Por exemplo, antes do Tiger podíamos usar a instrução ArrayList a = new ArrayList(), mas atualmente é necessário especificar um tipo para a collection empregando generics, tal como ArrayList a = new ArrayList(). Com isso, o código que usa collections criado até o Java 1.4 e que funcionava bem até então, gera warnings quando compilado com o Java 5. Entretanto, se o desenvolvedor pode assegurar a funcionalidade do código implementado por ele, então não deve haver problemas em ignorar os warnings correspondentes. Contudo, ignorar todos os alertas em uma aplicação pode não ser uma boa decisão. Sendo assim, como devemos tratar essa situação?

A resposta está no uso da anotação @SuppressWarnings, que permite desligar os alertas de uma parte do código da aplicação – classe, método ou inicialização de variável ou campo – e os warnings do restante do código permanecem inalterados.

Diferente das duas anotações anteriores, @SuppressWarnings é uma anotação de valor único, onde o valor é um array de String. Neste array, cada elemento é um tipo de alerta a ser suprimido. Na Listagem 6 temos um exemplo de uso dessa anotação.

Listagem 6. Uso de @SuppressWarnings.


 import java.util.*;
 
 public class Teste {
     
     @SuppressWarnings(value={"unchecked", "rawtypes"})
     public static void main(String[] args) {
         List lista = new ArrayList();
         lista.add("dado");
     }
 
 }
 

Antes de prosseguirmos estudando a anotação @SuppressWarnings, precisamos entender porque o compilador emite alertas no código da Listagem 6. Antes do Java 5, uma collection podia ser declarada como na primeira linha do método main(), ou seja, não havia uma especificação do tipo de dado que a collection poderia tratar. Assim, o objeto lista poderia armazenar qualquer tipo e o programador é quem deveria se preocupar com a segurança do seu código, de maneira a garantir que o conjunto iria receber apenas os objetos do tipo pretendido. Nessa situação, a responsabilidade é do desenvolvedor, pois o compilador não teria mecanismos de verificar a validade da chamada ao método add(), mostrado na segunda linha. Com o surgimento de generics, devemos especificar o tipo de dado na declaração da collection, e assim, a chamada a add() pode ser checada durante a compilação. No entanto, para manter o funcionamento de códigos legado, Java não impede que a classe da Listagem 6 seja compilada, porém, emite avisos de que o código está usando operações não seguras e não verificadas.

Portanto, no exemplo da Listagem 6, foram usados em @SuppressWarnings os valores unchecked, que é empregado para suprimir alertas referentes a operações não verificadas, tais como no caso de lista.add(“dado”) e rawtypes, que suprime os warnings referentes à ausência de tipos, tais como em List lista = new ArrayList().

Existem algumas dezenas de possíveis valores que podem ser usados com @SuppressWarnings, dos quais, alguns são apresentados a seguir:

  • all – suprime todos os alertas;
  • deprecation – suprime alertas referentes ao uso de código obsoleto;
  • fallthrough – suprime alertas referentes à falta de break em comandos switch.

Além das anotações padrão descritas aqui, Java define mais quatro anotações embutidas na sua API: @Retention, @Documented, @Target e @Inherited. Estas são meta-anotações, visto que são empregadas para marcar tipos anotação e fazem parte do pacote java.lang.annotation. A seguir são descritas estas meta-anotações:

  • @Retention – as anotações podem estar presentes apenas no código fonte ou no binário de classes ou interfaces. @Retention é usada para escolher entre essas possibilidades. Ela suporta três valores: SOURCE, para indicar que as anotações marcadas não estarão no código binário; CLASS, para gravar as anotações no arquivo .class, mas não estarão disponíveis em tempo de execução; e RUNTIME, para indicar que as anotações estarão disponíveis em tempo de execução;
  • @Documented – é uma anotação marcadora usada para indicar que os tipos anotação anotados com ela serão incluídos na documentação Javadoc;
  • @Target – ao criar um tipo anotação é possível estabelecer que elementos (construtor, variável local, parâmetro de método e método) de uma classe podem ser anotados com ele. Para obter esse efeito, usamos @Target, a qual suporta os seguintes valores (cada um destinado a definir o elemento que se pretende anotar): CONSTRUCTOR, LOCAL_VARIABLE, PARAMETER e METHOD;
  • @Inherited – por padrão anotações declaradas em uma classe não são herdadas pelas subclasses. Mas, se for necessário que essa herança ocorra, então o tipo anotação que desejamos que seja herdado deve ser anotado com @Inherited. É importante destacar que a utilização desta meta-anotação restringe-se apenas a classes. Por exemplo, anotações em interfaces não são herdadas pelas classes que as implementam.

Criando Tipos Anotação

Se você começar a usar anotações com frequência, pode ser que sinta a necessidade de criar seus próprios tipos, pois os tipos anotação embutidos de Java dificilmente irão atender a todas as necessidades do desenvolvedor. Esta é uma tarefa relativamente simples em Java. Para isso deve-se utilizar a palavra-chave @interface, juntamente com alguns outros elementos ou estruturas introduzidos em Java 5, tais como enum, por exemplo.

Uma declaração de tipo anotação é na verdade uma espécie de declaração de interface. Apenas para distinguir uma declaração da outra, a palavra-chave interface é precedida por um arroba (@) quando queremos declarar um tipo anotação.

Todo tipo anotação é uma subinterface (especialização) de java.lang.annotation.Annotation, não pode ser genérica e não permite o uso da cláusula extends, ou seja, um tipo anotação não pode explicitamente declarar uma superclasse ou superinterface.

De forma genérica, um tipo anotação pode ser declarado assim:

modificadores @interface identificador {
 
       declarações de elementos
 
       }

Nesta sintaxe os modificadores e os elementos são opcionais e o identificador especifica o nome do tipo anotação. Aqui vale também a mesma restrição para a formação de nomes em Java: o nome do tipo anotação não pode ser o mesmo de uma classe ou interface usadas na aplicação.

Objetivando um melhor entendimento, vamos então apresentar casos de aplicação dessa sintaxe. O primeiro caso é um exemplo bem simples de uma anotação marcadora, que não possui membros, conforme estudado anteriormente. Para isso, observe o código da Listagem 7.

Listagem 7. Declaração de um tipo anotação marcadora.


 /**
 * Anotação marcadora para indicar que um método ou classe
 * está ainda em desenvolvimento
 */
 
 public @interface InProgress {
 
 }
 

Na Listagem 8 apresentamos um exemplo de uso do tipo anotação criado. Esta anotação indica que o método getTotalSalario(double bonus) ainda está sendo desenvolvido.

Listagem 8. Usando a anotação marcadora criada.


    public class Funcionario {
     protected double salario;
     
     @InProgress
     public double getTotalSalario(double bonus) {
         // Necessita ser implementado mais tarde
         return 0;
     }
 }
 

A maneira de criar e utilizar um tipo anotação é semelhante ao que se dá para uma classe ou interface. Ou seja, o tipo anotação pode ser criado em um arquivo fonte exclusivo e, ao usá-lo, este pode ser especificado em uma declaração import, se for necessário, ou ainda pode-se usar o nome completo. Por exemplo, suponha que o tipo anotação da Listagem 7 tenha sido criado no pacote br.com.nomeempresa.nomeprojeto, então seu nome completo seria @br.com.nomeempresa.nomeprojeto.InProgress.

Adicionando membros ao tipo anotação

Vimos na seção de introdução que os tipos anotação podem possuir membros – ou elementos. Isto é, além do caso apresentado na Listagem 7, que não possui membro, os tipos anotação podem ter um ou mais elementos.

Um elemento em um tipo anotação é definido por uma declaração de método, tal como pode ser visto na Listagem 9.

Listagem 9. Declaração de um tipo anotação com um elemento.


 /**
 * Tipo anotação para indicar que uma
 * tarefa precisa ser concluída
 */
 public @interface TODO {
     String value();
 }
 

Note que a declaração String value(); é o que define o membro do tipo anotação. Dessa maneira indica-se que a anotação @TODO possui o membro value do tipo String. A utilização dessas declarações nos tipos anotação seguem algumas regras:

  • Por convenção, o nome do único membro em um tipo anotação com um único elemento deve ser value;
  • A declaração de um método em um tipo anotação não pode ter qualquer parâmetro e nem uma cláusula throws – que indica um lançamento de exceção;
  • O método não deve possuir corpo – ele é especificado como um método abstrato;
  • O tipo de retorno do método será o tipo de dado do elemento;
  • O tipo de retorno deve ser um dos seguintes: primitivos, String, Class, enum ou um array cujo tipo seja um dos precedentes.

Apresentamos na Listagem 10 um exemplo de utilização dessa anotação.

Listagem 10. Utilizando a anotação de valor único.


    public class Funcionario {
     protected double salario;
     
     @TODO("O salário total do funcionário = 
   salário + bonus")
     public double getTotalSalario(double bonus) {
         return 0;
     }
 }
 

Observe na Listagem 10 que foi adotada a sintaxe curta, permitida quando se usa a anotação de valor único, pois @TODO foi empregado sem a especificação do nome da variável membro.

Entretanto, pode-se optar por usar a sintaxe completa, tal como foi feito na Listagem 11 ao escrever a mesma classe anterior.

Listagem 11. Anotação de valor único com a sintaxe completa.


    public class Funcionario {
     protected double salario;
     
     @TODO(value="O salário total do funcionário = 
   salário + bonus")
     public double getTotalSalario(double bonus) {
         return 0;
     }
 }
 

Adicionar mais membros ao tipo é igualmente simples tal qual o exemplo anterior. O exemplo da Listagem 12 demonstra isso.

Listagem 12. Tipo anotação com mais de um elemento.


 /**
 * Um TODO voltado para grupos 
 * de desenvolvedores, onde pode-se
 * especificar a pessoa a quem se destina o item 
 */
 public @interface groupTODO {
     public enum Severity 
  { CRITICAL, IMPORTANT, TRIVIAL, DOCUMENTATION };
     Severity severity( ) default Severity.IMPORTANT;
     String item( );
     String assignedTo( );
 }
 

No exemplo da Listagem 12, uma versão melhorada do tipo @TODO criado antes, foi declarado um enum – tipo enumerado – que estudamos na série “Orientação a Objetos – uma abordagem com Java” desta revista. Neste caso o enum foi usado para definir a severidade (severity) da tarefa a ser executada. Já entre os demais elementos, item é responsável por definir o que deve ser feito, e assignedTo informa o desenvolvedor encarregado da tarefa descrita em item. Na Listagem 13 demonstra-se a utilização deste tipo.

Listagem 13. Utilização do tipo anotação completo.


    public class Funcionario {
     protected double salario;
     
     @groupTODO
     (severity=groupTODO.Severity.TRIVIAL,
      item="O salário total do funcionário = 
   salário + bonus",
      assignedTo="Carlos Araújo"
     )
     public double getTotalSalario(double bonus) {
         return 0;
     }
 }
 

Parece estar claro pelo exemplo da Listagem 12 que é possível estabelecer um valor padrão (default) para um membro de um tipo anotação. Para tanto, note a cláusula default na declaração do elemento severity. Neste caso, se não for especificado um valor para severity, será assumido o padrão IMPORTANT.

Segundo a especificação da linguagem, valores default são aplicados dinamicamente, isto é, não são compilados com a anotação. Deste modo, mudar os valores default afetam as anotações mesmo nas classes que foram compiladas antes que as mudanças tenham sido feitas. Visto que o valor default deve ser atribuído ao membro, então o tipo do valor padrão deve ser compatível com o tipo do membro. Assim, se um elemento é do tipo String, então seu valor padrão – se houver – deve ser String.

Refletindo anotações

Até aqui as discussões sobre anotações giraram em torno de ter informações que são úteis para os programadores ou como verificação de código para o compilador. No entanto, anotações podem ser utilizadas por ferramentas como depuradores, navegadores de classes dos IDEs, entre outras, as quais leem essas informações em tempo de execução.

Como exemplos de ferramentas que empregam anotações temos o JUnit, sobre o qual apresentamos um exemplo nesta mesma matéria, e o Hibernate. Hibernate é um framework de persistência, onde as anotações são utilizadas para representar o mapeamento nas classes persistentes. Para obter mais informações sobre este framework, leia a série de artigos “Introdução à Persistência com Hibernate” na Easy Java Magazine nº 4 e nº 5.

Portanto, é importante falar sobre o uso de reflexão para determinar quais são as anotações usadas em uma classe, campo ou método. Consideramos importante, pois, além de entendermos como as ferramentas citadas fazem uso desse mecanismo, por meio de reflexão pode-se estender a funcionalidade das aplicações. Nesse aspecto o pacote java.lang.reflect deve ser utilizado, pois oferece classes e interfaces com o objetivo de obter informações dinâmicas – em tempo de execução – sobre classes e objetos. Na Listagem 14 apresentamos um exemplo bastante simples que demonstra o uso deste pacote e do método isAnnotationPresent(), para verificar a presença de uma anotação em um campo de uma classe.

Reflexão: é um mecanismo usado em algumas linguagens de programação para examinar e modificar a estrutura e o comportamento de um programa em tempo de execução. Especificamente em Java, é possível descobrir informações sobre campos, métodos e construtores de uma classe. Isto também é chamado de programação dinâmica.

A reflexão (reflection) é conseguida usando a API que consiste de classes e interfaces dos pacotes java.lang e java.lang.reflect. Utilizando esta API pode-se, entre outras coisas:

  • Determinar a classe de um objeto;
  • Descobrir constantes e declarações de métodos de uma interface;
  • Criar uma instância de uma classe, cujo nome só saberemos em tempo de execução;
  • Obter e definir o valor do campo de um objeto;
  • Invocar um método de um objeto.

Listagem 14. Simples exemplo de reflexão de anotação.


 import java.lang.reflect.Field;
 
 public class TesteAnotacao {
 
     @Deprecated
     public static int value = 1;
 
     public static void main(String[] args) 
     throws Exception {
         Field field = TesteAnotacao.class.getField
         ("value");
         if (field.isAnnotationPresent
         (Deprecated.class)) {
           System.out.println
          ("Campo anotado com Deprecated");
         } else {
               System.out.println
          ("Campo não anotado com Deprecated");
         }
     }
 }
 

No exemplo, inicialmente pegamos o campo value da classe TesteAnotacao. Em seguida fazemos uma chamada a isAnnotationPresent() passando como parâmetro Deprecated.class – pois o método recebe como argumento o objeto Class correspondente ao tipo anotação. Assim, o método irá retornar true se Deprecated estiver anotando o campo, ou false, caso contrário. Importante observar que a reflexão funciona apenas naquelas anotações que tenham sido marcadas com @Retention.RUNTIME.

Conclusões

Espera-se com este texto que tenha ficado claro ao leitor a importância e as maneiras de utilizar anotações, as quais devem ser empregadas com moderação, sob pena de se ver códigos sobrecarregados de metadados, inclusive em lugares onde são desnecessários, e até mesmo sem qualquer sentido.

Considerando-se principalmente o emprego de @Override, julgamos ser uma boa prática usar as anotações padrão (@Override, @Deprecated e @SuppressWarnings), pois seu comportamento é facilmente entendido e todo compilador Java as suporta. No entanto, quanto às anotações personalizadas, torna-se mais difícil garantir que os tipos criados pelo desenvolvedor tenham qualquer significado fora do contexto do seu próprio ambiente de desenvolvimento.

Vimos também que as anotações são úteis em outras ferramentas através do uso de reflexão, tais como JUnit e Hibernate. Por esse motivo incluímos também um tópico que demonstra como podemos determinar que anotações uma classe, campo ou método possuem. A isto denominamos reflexão de anotações.

E como sempre costumamos finalizar nossos artigos, queremos reforçar que não tivemos a pretensão de esgotar o assunto. Portanto, sugere-se fortemente que o leitor vá em busca de informações mais aprofundadas, principalmente nos tópicos referentes às meta-anotações e reflexão.