Java Anotações

Uma anotação é uma etiqueta que começa com @ e fica grudada numa classe, num método ou num campo: @Override, @Deprecated, @Test. Ela não muda o que o código faz; ela informa algo sobre o código para quem vai lê-lo: o compilador, uma ferramenta ou um framework.

Você vai aprender
  • O que é uma anotação e onde ela pode ficar.
  • As quatro que você mais vai ver: @Override, @FunctionalInterface, @Deprecated e @SuppressWarnings, com os erros e avisos reais do javac.
  • Para que servem as anotações dos frameworks (@Test do JUnit, @GetMapping do Spring…).
  • Como criar uma anotação sua, com elementos e valores padrão, e lê-la com reflexão.

Você já usou @Override na aula de sobrescrita e talvez @FunctionalInterface em interfaces funcionais. Agora vamos entender o que esse @ é. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.


A ideia antes do código

Pense nas etiquetas que a Ana cola nas caixas da loja: "FRÁGIL", "ESTE LADO PARA CIMA", "PROMOÇÃO ATÉ SEXTA". A etiqueta não muda o que tem dentro da caixa. Mas quem pega a caixa lê a etiqueta e age de acordo: o entregador carrega com cuidado; o caixa aplica o desconto.

Uma anotação é a mesma coisa no código. @Override é uma etiqueta que diz ao compilador "este método substitui um da classe mãe; confira para mim". @Deprecated diz "este método está aposentado; avise quem usar". Quem lê as etiquetas pode ser:

  • o compilador (javac), na hora de compilar;
  • uma ferramenta, como o Javadoc ou o IDE;
  • o próprio programa ou um framework, enquanto roda, usando reflexão.

Como se escreve

FormaExemploQuando
Só o nome@OverrideA anotação não precisa de informação extra
Um valor@SuppressWarnings("deprecation")Tem um elemento principal (chamado value)
Pares nome = valor@Deprecated(since = "2.0", forRemoval = true)Vários elementos
Uma lista@SuppressWarnings({"deprecation", "unchecked"})O elemento aceita vários valores

A anotação fica logo antes do que ela etiqueta, normalmente na linha de cima. Pode etiquetar classes, métodos, campos, parâmetros e variáveis locais.


1. @Override: "confira que estou sobrescrevendo"

A classe Produto quer mostrar um texto bonito no println. Para isso, ela precisa sobrescrever o método toString() que herda de Object. A Ana digitou o nome com um s minúsculo:

class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public String tostring() {
        return nome + " (R$ " + preco + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        Produto p = new Produto("Café", 12.5);
        System.out.println(p);
    }
}
$ javac Main.java && java Main
Produto@1dbd16a6

Compilou sem reclamar e mostrou um texto estranho: o toString() padrão do Object (o nome da classe, @ e um número que muda de uma execução para outra). Para o Java, tostring é só mais um método, com outro nome, que ninguém chama. O erro passou despercebido.

Agora com a etiqueta:

    @Override
    public String tostring() {
        return nome + " (R$ " + preco + ")";
    }
$ javac Main.java
Main.java:10: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

"O método não sobrescreve nem implementa um método de um supertipo (classe mãe ou interface)." O compilador leu a etiqueta, foi conferir e achou o problema antes de o programa rodar. Corrigindo para toString:

$ javac Main.java && java Main
Café (R$ 12.5)
@
Marca o início de uma anotação.
Override
"Sobrescrever", em inglês. É uma anotação que vem com o Java (java.lang.Override), por isso não precisa de import.

Regra de ouro: sempre que sobrescrever um método, escreva @Override. Ela custa uma linha e pega erros de digitação, de parâmetros e de nomes que mudaram na classe mãe.

Missão: acrescente @Override acima do método tostring e rode: veja o javac acusar o problema. Depois corrija o nome do método para que o programa mostre Café (R$ 12.5).

class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public String tostring() {
        return nome + " (R$ " + preco + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        Produto p = new Produto("Café", 12.5);
        System.out.println(p);
    }
}
class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    @Override
    public String toString() {
        return nome + " (R$ " + preco + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        Produto p = new Produto("Café", 12.5);
        System.out.println(p);
    }
}

2. @FunctionalInterface: "esta interface é para lambdas"

Uma interface funcional tem exatamente um método abstrato, e por isso pode receber uma lambda. A etiqueta @FunctionalInterface pede ao compilador que garanta isso. Se alguém acrescentar um segundo método abstrato:

Este código tem um erro de propósito.
@FunctionalInterface
interface Desconto {
    double aplicar(double preco);
    double reverter(double preco);
}
$ javac Main.java
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Desconto is not a functional interface
    multiple non-overriding abstract methods found in interface Desconto
1 error

"Anotação @FunctionalInterface inesperada: Desconto não é uma interface funcional; foram encontrados vários métodos abstratos." Sem a etiqueta, a interface compilaria, e o erro só apareceria em outro arquivo, no dia em que alguém tentasse usar uma lambda com ela. Com um método só, tudo funciona:

@FunctionalInterface
interface Desconto {
    double aplicar(double preco);
}

public class Main {
    public static void main(String[] args) {
        Desconto metade = preco -> preco / 2;
        Desconto dezReais = preco -> preco - 10;
        System.out.println(metade.aplicar(80.0));
        System.out.println(dezReais.aplicar(80.0));
    }
}
$ java Main.java
40.0
70.0

3. @Deprecated: "aposentado, não use"

Deprecated quer dizer "desaprovado", "obsoleto". Quando um método antigo foi substituído por um melhor, mas ainda não pode ser apagado (porque tem gente usando), ele ganha essa etiqueta. O código continua funcionando, mas o compilador avisa quem o usa.

class Loja {
    @Deprecated
    static double precoAntigo(double valor) {
        return valor + 10;
    }

    static double preco(double valor) {
        return valor + 8;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Loja.precoAntigo(100));
    }
}
$ javac Main.java
Note: Main.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
$ javac -Xlint:deprecation Main.java
Main.java:14: warning: [deprecation] precoAntigo(double) in Loja has been deprecated
        System.out.println(Loja.precoAntigo(100));
                               ^
1 warning
$ java Main
110.0

Repare nos três momentos:

  1. O javac comum mostra só duas notas ("Main.java usa ou sobrescreve uma API obsoleta"; "recompile com -Xlint:deprecation para ver os detalhes"). O arquivo .class foi gerado: nota não é erro.
  2. Com a opção -Xlint:deprecation, aparece o aviso (warning) completo, com linha e setinha: "precoAntigo(double) em Loja has been deprecated" (foi marcado como obsoleto).
  3. O programa roda normalmente.

Marcado para remoção

A anotação aceita dois elementos: since ("desde qual versão") e forRemoval ("vai ser removido"). Com forRemoval = true, o aviso fica mais forte e aparece mesmo sem o -Xlint:

class Loja {
    @Deprecated(since = "2.0", forRemoval = true)
    static double precoAntigo(double valor) {
        return valor + 10;
    }
}
$ javac Main.java && java Main
Main.java:10: warning: [removal] precoAntigo(double) in Loja has been deprecated and marked for removal
        System.out.println(Loja.precoAntigo(100));
                               ^
1 warning
110.0

"…foi marcado como obsoleto e para remoção." Ou seja: numa versão futura, esse método vai sumir, e o seu código vai parar de compilar. Hora de trocar para o método novo.

A própria biblioteca do Java usa essa etiqueta. O construtor new Integer(5) é obsoleto desde o Java 9 (o certo é Integer.valueOf(5) ou só 5, veja wrappers):

$ javac -Xlint:deprecation Main.java
Main.java:3: warning: [deprecation] Integer(int) in Integer has been deprecated
        Integer quantidade = new Integer(5);
                             ^
1 warning
Documente o substituto

Junto com @Deprecated, escreva no comentário de documentação o que usar no lugar: /** @deprecated use {@link #preco(double)} */. A etiqueta avisa; o comentário explica. Veja Javadoc.


4. @SuppressWarnings: "eu sei, pode ficar quieto"

Suppress warnings = "suprimir avisos". Às vezes você usa algo obsoleto de propósito (por exemplo, enquanto migra um sistema antigo) e não quer que o aviso esconda outros mais importantes. A etiqueta diz qual categoria de aviso calar, o nome que aparece entre colchetes na mensagem ([deprecation], [removal], [unchecked]…):

public class Main {
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        Integer quantidade = new Integer(5);
        System.out.println(quantidade);
    }
}
$ javac Main.java && java Main
5

Sem notas e sem avisos. Use com moderação e no menor pedaço possível (um método, não a classe inteira): um aviso calado é um aviso que ninguém mais vai ver.

ValorCala o aviso de…
"deprecation"Uso de algo @Deprecated
"removal"Uso de algo @Deprecated(forRemoval = true)
"unchecked"Conversões de generics que o compilador não consegue conferir
"rawtypes"Uso de List sem <…>

5. Anotações de frameworks

Até aqui, quem lia as etiquetas era o compilador. Mas as anotações mais famosas do mundo Java são lidas por frameworks (bibliotecas grandes que "chamam o seu código") enquanto o programa roda. Você não precisa saber usá-las agora; só reconhecer o padrão quando encontrar:

AnotaçãoDe ondeO que diz
@TestJUnit"Este método é um teste: rode-o e confira." Você vai usar na aula de JUnit.
@BeforeEachJUnit"Rode isto antes de cada teste."
@Component, @ServiceSpring"Crie um objeto desta classe e entregue a quem precisar."
@GetMapping("/produtos")Spring"Quando alguém acessar o endereço /produtos no site, chame este método."
@Entity, @IdJakarta Persistence (JPA)"Esta classe vira uma tabela do banco de dados; este campo é a chave."

Como um framework "lê" uma etiqueta? Com reflexão: ele examina as suas classes enquanto o programa roda, procura os métodos com @Test e os chama. Na próxima seção você faz isso em miniatura.


6. Criando uma anotação

Você pode criar suas próprias etiquetas. A Ana quer marcar os métodos que são tarefas da loja, com o nome do responsável e uma prioridade. Esta seção é uma apresentação: criar anotações é mais comum em quem escreve bibliotecas do que no dia a dia.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Tarefa {
    String responsavel();
    int prioridade() default 1;
}

public class Main {
    @Tarefa(responsavel = "Ana", prioridade = 3)
    static void conferirEstoque() { }

    @Tarefa(responsavel = "Bruno")
    static void limparVitrine() { }

    static void almocar() { }

    public static void main(String[] args) {
        Method[] metodos = Main.class.getDeclaredMethods();
        Arrays.sort(metodos, Comparator.comparing(Method::getName));
        for (Method m : metodos) {
            Tarefa t = m.getAnnotation(Tarefa.class);
            if (t != null) {
                System.out.println(m.getName() + " -> " + t.responsavel() + ", prioridade " + t.prioridade());
            }
        }
    }
}
$ javac Main.java && java Main
conferirEstoque -> Ana, prioridade 3
limparVitrine -> Bruno, prioridade 1
LinhaO que faz
@interface TarefaDeclara uma anotação (repare no @ antes de interface). O nome é o da etiqueta: @Tarefa.
String responsavel();Um elemento: parece um método, mas é um "campo" da etiqueta. Sem valor padrão, é obrigatório.
int prioridade() default 1;Elemento com valor padrão: quem não informar recebe 1 (foi o caso do Bruno).
@Retention(RetentionPolicy.RUNTIME)"Retenção": a etiqueta deve continuar existindo enquanto o programa roda. Sem isso, ela some depois da compilação, e a reflexão não a enxerga.
@Target(ElementType.METHOD)"Alvo": esta etiqueta só pode ser colada em métodos.
getDeclaredMethods()Reflexão: a lista dos métodos da classe Main. Ela vem em ordem qualquer, por isso o Arrays.sort por nome.
m.getAnnotation(Tarefa.class)Lê a etiqueta @Tarefa do método, ou null se ele não tiver (o almocar e o próprio main).

Repare que as anotações @Retention e @Target são anotações sobre a anotação (os nomes técnicos são "meta-anotações"). É exatamente assim que o JUnit define o @Test.


Qual usar?

SituaçãoAnotação
Você sobrescreveu um método da classe mãe ou de uma interface@Override (sempre)
Criou uma interface para receber lambdas@FunctionalInterface
Um método seu foi substituído por outro melhor@Deprecated (+ since, e forRemoval = true se vai sumir)
Usa algo obsoleto de propósito e quer calar o aviso@SuppressWarnings("deprecation"), no menor trecho possível
Escreve testes@Test do JUnit
Quer marcar código para uma ferramenta sua lerUma anotação própria com @Retention(RUNTIME)

Quando dá errado

method does not override or implement a method from a supertype

Um método com @Override não corresponde a nenhum método da classe mãe ou das interfaces: nome digitado errado (tostring), parâmetros diferentes ou classe que não herda de quem você pensa. Conserto: confira nome, parâmetros e o extends/implements. Não apague o @Override para "fazer compilar": ele está mostrando um erro de verdade.

Unexpected @FunctionalInterface annotation

A interface tem mais de um método abstrato (seção 2). Conserto: deixe só um abstrato; os outros podem virar default (com corpo), ou tire a anotação se a interface não é mesmo para lambdas.

Faltou um elemento obrigatório

Este código tem um erro de propósito.
    @Tarefa(prioridade = 2)
    static void conferirEstoque() { }
$ javac Main.java
Main.java:14: error: annotation @Tarefa is missing a default value for the element 'responsavel'
    @Tarefa(prioridade = 2)
    ^
1 error

O que quer dizer: "falta um valor para o elemento responsavel", que não tem default. Conserto: @Tarefa(responsavel = "Ana", prioridade = 2).

Etiqueta no lugar errado

Este código tem um erro de propósito.
@Tarefa(responsavel = "Ana")
public class Main {
$ javac Main.java
Main.java:13: error: annotation interface not applicable to this kind of declaration
@Tarefa(responsavel = "Ana")
^
1 error

O que quer dizer: "a anotação não se aplica a este tipo de declaração". O @Target(ElementType.METHOD) só permite métodos, e ela foi colada numa classe. Conserto: mova-a para um método, ou acrescente ElementType.TYPE ao @Target: @Target({ElementType.METHOD, ElementType.TYPE}).

Avisos de deprecated não são erros

As linhas Note: … uses or overrides a deprecated API. e warning: [deprecation] … não impedem a compilação: o .class é gerado e o programa roda. Mas leve-as a sério, principalmente [removal]: numa próxima versão, aquele código deixa de compilar.


Referência

AnotaçãoOndeQuem lêEfeito
@OverrideMétodojavacErro se não sobrescrever nada
@FunctionalInterfaceInterfacejavacErro se não tiver exatamente 1 método abstrato
@DeprecatedQualquer declaraçãojavac, Javadoc, IDENota/aviso para quem usa
@Deprecated(since = "…", forRemoval = true)Qualquer declaraçãojavacAviso [removal] sempre
@SuppressWarnings("…")Classe, método, variáveljavacCala uma categoria de aviso
@SafeVarargsMétodo com …javacCala um aviso de generics com varargs
@Retention, @TargetDeclaração de anotaçãojavac, JVMPor quanto tempo existe; onde pode ser usada

Exercícios

1. Complete com a anotação que pede ao compilador para conferir a sobrescrita:


public String toString() { return "Café"; }

2. Complete para marcar o método como obsoleto e com remoção prevista:

@Deprecated(since = "2.0", )

3. Complete para calar os avisos de uso de API obsoleta:

@SuppressWarnings("")

4. Sem @Override, um método tostring() (com s minúsculo) numa classe…

5. Para que um framework encontre a sua anotação enquanto o programa roda, ela precisa de…

Missão final: etiquete a interface Desconto com @FunctionalInterface e crie as duas lambdas que faltam: metade (divide o preço por 2) e dezReais (tira 10). O programa deve mostrar 40.0 e 70.0. Depois, para ver a etiqueta trabalhando, tente acrescentar um segundo método abstrato à interface.

interface Desconto {
    double aplicar(double preco);
}

public class Main {
    public static void main(String[] args) {
        // crie as lambdas metade e dezReais aqui

        System.out.println(metade.aplicar(80.0));
        System.out.println(dezReais.aplicar(80.0));
    }
}
@FunctionalInterface
interface Desconto {
    double aplicar(double preco);
}

public class Main {
    public static void main(String[] args) {
        Desconto metade = preco -> preco / 2;
        Desconto dezReais = preco -> preco - 10;
        System.out.println(metade.aplicar(80.0));
        System.out.println(dezReais.aplicar(80.0));
    }
}

Resumo

  • Uma anotação (@Nome) é uma etiqueta: não muda o que o código faz, mas informa o compilador, ferramentas ou frameworks.
  • @Override faz o javac conferir a sobrescrita (method does not override or implement a method from a supertype). Use sempre.
  • @FunctionalInterface garante um único método abstrato.
  • @Deprecated gera nota/aviso has been deprecated; com forRemoval = true, o aviso [removal] aparece sempre.
  • @SuppressWarnings("categoria") cala avisos, no menor trecho possível.
  • Frameworks (JUnit, Spring, JPA) leem anotações com reflexão. Você cria as suas com @interface, @Retention e @Target.

Próxima aula: Reflexão, o jeito de um programa examinar a si mesmo.