Enums são uma forma de restringir uma variável para esta poder assumir apenas alguns pouco valores pré-definidos, resumindo um valor de uma lista enumerada.

O uso de enums pode reduzir os bugs do código como também deixar ele mais legível. Pensando um pouco, se estivermos em desenvolvendo um sistema de lanchonete e precisarmos especificar o tamanho do café. Poderíamos guardar este valor em um int, ou em uma String mas teríamos que escrever várias regras para validar e somente aceitar por exemplo os três tamanhos que trabalhamos( MEDIO, GRANDE, MEGABOGA). Criando um enum ficaria mais fácil, pois estamos criando um “tipo” que só aceitaria estes três valores.

public enum TamanhoCafe{
    MEDIO,
    GRANDE,
    MEGABOGA
}
 
public Cafe{
    private String nome;
    private Double preco;
    private TamanhoCafe tamanhoCafe;
 
    ...
    //gets, sets
    ...
}

Agora para atribuir valores para tamanhoCafe só será possível desta maneira:

tamanhoCafe = TamanhoCafe.MEDIO;
tamanhoCafe = TamanhoCafe.GRANDE;
tamanhoCafe = TamanhoCafe.MEGABOA;

E qualquer tentativa de usar um valor que não tenha sido especificado na criação do enum, que são suas constantes, não irá compilar.

Cada uma das constantes do enum não são um int ou uma String e sim seu próprio tipo, no nosso caso TamanhoCafe. Nele podemos criar atributos, métodos e construtores se forem necessários, como no exemplo abaixo.

public enum TamanhoCafe{
    MEDIO("Médio", 0),
    GRANDE("Grande" , 1),
    MEGABOGA("Megaboa" , 2);
     
    private int tamanho;
    private String nome;
 
    TamanhoCafe(String nome, int tamanho){
        this.nome = nome;
        this.tamanho = tamanho;
    }
     
    public String getNome(){
        return this.nome;
    }
    public int getTamanho(){
        return tamanho;
    }
}

Note que agora quando criamos as constantes de nosso enum estamos passando parâmetros para cada uma das instâncias dele, este parâmetros são passados para o construtor do enum, que irá inicializar as variáveis de instâncias. Neste momento vale lembra que o construtor não pode ser chamado diretamente, ele só é chamado na definição das constantes.

Cafe cafe = new Cafe();
cafe.setNome("Simples");
cafe.setPreco(3.50);
cafe.setTamanhoCage( TamanhoCafe.MEGABOGA );
 
System.out.println("Nome: "+cafe.getNome() );
System.out.println("Preço: "+cafe.getPreco() );
System.out.println("Tamanho do Café: "+cafe.getTamanhoCafe() );
System.out.println("Tamanho do Café - Nome: "+cafe.getTamanhoCafe().getNome() );
System.out.println("Tamanho do Café - Tamanho: "+cafe.getTamanhoCafe().getTamanho() );

Se for necessário, como por exemplo para popular uma lista de seleção como <select> ou JComboBox, podemos pegar todos os valores constantes de um enum através do método TamanhoCafe.values()

TamanhoCafe[] tcs = TamanhoCafe.values();
         
for( TamanhoCafe tc : tcs ){
    System.out.println( tc );
}

Em muitos casos, como salvar o valor de um enum em banco de dados por exemplo, se faz necessário transformá-lo em um String e após voltar para o próprio tipo enum. Isso pode ser feito através do método name() para transformar em String e valueOf( String constante) para transformar novamente em enum. Vale lembrar que se utilizarmos um String de parâmetro que não seja uma das constantes definidas irá ser lançada uma exceção como: java.lang.IllegalArgumentException: No enum constant TamanhoCafe.PEQUENO

TamanhoCafe tc = TamanhoCafe.MEDIO;
         
String sTc = tc.name();
         
TamanhoCafe tc2 = TamanhoCafe.valueOf( sTc );
         
System.out.println( tc2.getNome()+" - "+tc2.getTamanho());

Bom era isso, uma pequena introdução aos enums. T++ Pessoal!