O que é?

Saiu recentemente a nova versão LTS (Long Term Support) do Java. Essas versões são as mais importantes no calendário de lançamentos, uma vez que possuem oito anos de suporte.

Desta vez, além de melhorias em geral e de performance, um novo recurso chamado sealed classes foi adicionado à linguagem. Ele impactará nossa forma de usar a herança.

Por que é útil?

Herança é uma solução eficaz para resolver problemas de modelagem. Porém, como Joshua Bloch apontou em Effective Java, se uma classe deve ser herdada ela precisa ser escrita e bem documentada para tal. Do contrário, o código pode não funcionar como esperado.

Atualmente em Java a herança é controlada de forma binária: uma classe pode ou não ser estendida por outras classes. Sealed classes torna esse controle mais granular, permitindo ao autor de uma superclasse descrever quais subclasses devem estendê-la.

Características

  • Introduz as palavras-chave sealed e permits para serem usadas com classes ou interfaces

Como utilizar: sintaxe

Para criar uma sealed class ou interface introduzimos a palavra sealed.

Por exemplo, vamos criar a relação de herança apresentada no diagrama da Figura 1.

Diagrama que representa a criação de herança
Figura 1. Diagrama que representa a criação de herança

O Código 1 apresenta a sealed interface Pagavel, descrevendo que apenas PagavelPorDinheiro e PagavelPorCartaoDeCredito devem estendê-la. Tanto classes quanto interfaces podem ser sealed.


  public sealed interface Pagavel 
  permits PagavelPorDinheiro, PagavelPorCartaoDeCredito {}
  
Código 1. Interface Pagavel

Agora, ambas as subclasses devem ser declaradas como final, como mostra o Código 2.


  public final class PagavelPorCartaoDeCredito implements Pagavel {}
   
  public final class PagavelPorDinheiro implements Pagavel {}
Código 2. Classe PagavelPorCartaoDeCredito/PagavelPorDinheiro implementando a interface Pagavel

Dessa forma, temos uma relação de herança entre as classes documentada pelo código. Somente as classes PagavelPorDinheiro e PagavelPorCartaoDeCredito podem estender a interface Pagavel.

Exemplo prático

Veja agora alguns exemplos práticos de como criar sealed classes.

Exemplo 1 - Nested Classes

No Código 3 vemos como declarar uma interface como sealed. Esse código não apresenta novidades em relação ao anterior, exceto que dessa vez declaramos as classes que implementam a interface, PagavelPorDinheiro e PagavelPorCartaoDeCredito, dentro da própria interface.


  public sealed interface Pagavel {
    final class PagavelPorDinheiro implements Pagavel {}
    
    final class PagavelPorCartaoDeCredito implements Pagavel {}
  } 
 
Código 3. Declarando uma interface como sealedl

Essa sintaxe, chamada nested classes, tem sido cada vez mais comum, dada a chegada das sealed classes. Com ela, não precisamos colocar as subclasses de Pagavel em arquivos separados.

Exemplo 2 - Classes Abstratas

Classes abstratas também podem ser sealed. O Código 4 mostra como isso pode ser feito.


  public abstract sealed class Pagavel 
      permits PagavelPorCartaoDeCredito, PagavelPorDinheiro {}
 
Código 4. Classe abstrata

Exemplo 3 - Usando permits

No Exemplo 1 mostramos como usar nested classes para declarar, dentro da superclasse, as subclasses que a implementam ou estendem. Às vezes, isso não é possível, por exemplo, quando a superclasse e a subclasse estão em pacotes diferentes.

Em casos assim, declarar todas as classes no mesmo arquivo não é possível, uma vez que em Java um pacote corresponde a uma estrutura de pastas em disco. Como alternativa, podemos usar a palavra-chave permits para descrever quais podem ser as subclasses de uma superclasse. O Código 5 apresenta como usar permits.


  public sealed interface Pagavel 
  permits PagavelPorDinheiro, PagavelPorCartaoDeCredito {}
  
Código 5. Usando permits para descrever quais podem ser as subclasses de uma superclasse

Ao usar permits devemos ter cuidado, pois se alguma subclasse for omitida na lista de classes que podem estender ou implementar a superclasse um erro será emitido pelo compilador.

Por exemplo, se declararmos uma classe chamada PagavelPorBoleto (Código 6), que não está listada na cláusula permits de Pagavel (Código 5), o erro "PagavelPorBoleto is not allowed in the sealed hierarchy" da Figura 2 será emitido pelo compilador.


public final class PagavelPorBoleto implements Pagavel {}
Código 6. Classe PagavelPorBoleto não listada em permits
Erro gerado porque PagavelPorBoleto não está
listada na cláusula permits
Figura 2. Erro gerado porque PagavelPorBoleto não está listada na cláusula permits

Exemplo 4 - instanceof

Continuando o exemplo anterior, considere que agora temos uma classe chamada PagavelPorCartaoDeCredito (Código 7), que também não é uma subclasse de Pagavel.

 
 public final class PagavelPorCartaoDeCredito {}
Código 7. Classe PagavelPorCartaoDeCredito não listada em permits

Com isso, em tempo de compilação se pode saber que PagavelPorCartaoDeCredito e Pagavel não são compatíveis entre si. Conseguir identificar isso em tempo de compilação modifica o comportamento do operador instanceof, que checa a compatibilidade entre um tipo e um valor.

Por exemplo, no Código 8 tentamos checar se PagavelPorCartaoDeCredito é uma instância de Pagavel. Antes, esse código compilaria e instanceof retornaria false em tempo de execução. Agora, ele falhará em tempo de compilação, pois o compilador sabe que PagavelPorCartaoDeCredito e Pagavel são incompatíveis.

 
PagavelPorCartaoDeCredito pagavelPorCartaoDeCredito = new PagavelPorCartaoDeCredito();

if(pagavelPorCartaoDeCredito instanceof Pagavel) {

 System.out.println("Pagável por cartão de crédito");

} 

PagavelPorBoleto pagavelPorBoleto = new PagavelPorBoleto();


if(pagavelPorBoleto instanceof Pagavel) {

  System.out.println("Pagável por boleto");

}
Código 8. instanceof

Com isso será emitido o erro Inconvertible types; cannot cast 'PagavelPorCartaoDeCredito' to 'Pagavel', uma vez que é impossível que PagavelPorCartaoDeCredito seja convertida automaticamente para Pagavel, já que todas as subclasses de Pagavel são conhecidas e PagavelPorCartaoDeCredito não é uma delas.

Exemplo 5 - Switch

O switch como expressão, que vemos no Código 9, ainda está em preview, ou seja, embora possamos utilizá-la não há garantias de que ela sequer existirá em uma versão futura.

Com as sealed classes, ao usar switch como expressão, não precisamos de uma cláusula default quando todos os subtipos são usados. Isso porque o compilador sabe quais são todas as possibilidades que podem ser usadas para uma determinada classe e, caso não se use todas elas, o código falhará em tempo de compilação.



Pagavel pagavel = new PagavelPorCartaoDeCredito();

switch (pagavel) {

 case PagavelPorCartaoDeCredito c -> System.out.println("Pagável por cartão de crédito");

 case PagavelPorDinheiro d -> System.out.println("Pagável por dinheiro");

}
Código 9. Usando switch como expressão

Nesse caso, switch funciona como uma alternativa mais concisa a instanceof. Se pagavel for do subtipo PagavelPorCartaoDeCredito o texto "Pagável por cartão de crédito" será impresso. Caso contrário, se Pagavel for do subtipo PagavelPorDinheiro "Pagável por dinheiro" será impresso.

Conclusão

Sealed classes, assim como diversas outras funcionalidades do Java, passaram por diversas JEPs (Java Enhacement Proposal). A que corresponde a versão final é a JEP 409: Sealed Classes, que pode ser estudada mais a fundo no site do OpenJDK.