Classes Seladas em Java
Restrinja quais classes podem estender ou implementar um tipo em Java usando classes seladas e a cláusula permits.
Uma classe ou interface sealed restringe quem pode estendê-la ou implementá-la a uma lista fixa e nomeada de subtipos. final diz "ninguém pode me estender." sealed diz "apenas essas classes específicas podem." Isso fornece uma hierarquia fechada — o compilador conhece toda a família antecipadamente, o que viabiliza switch exaustivo e modelagem disciplinada de "um entre N" formatos.
Sem o selamento, uma abstract class Shape fica aberta para o mundo: qualquer pessoa com acesso ao tipo pode escrever class Banana extends Shape. Com sealed, o autor de Shape declara exatamente quais subtipos existem, e adicionar um requer editar o pai.
A sintaxe básica
Uma classe selada lista seus subtipos permitidos com permits:
public sealed class Shape
permits Circle, Square, Triangle {
// common state and behavior
}Cada subtipo permitido deve declarar o que faz com o selamento — uma das opções: final, sealed (com sua própria lista de permits) ou non-sealed:
public final class Circle extends Shape { /* leaf */ }
public final class Square extends Shape { /* leaf */ }
public non-sealed class Triangle extends Shape { /* re-opens the door */ }final— sem mais subclasses; este é um nó folha na hierarquia.sealed— estende o mesmo modelo; possui sua própria listapermits.non-sealed— volta atrás; qualquer um pode agora estenderTriangle. Útil quando se deseja uma família de nível superior fechada com um ramo aberto.
Um tipo selado sem modificador em um subtipo é um erro de compilação — o compilador força você a escolher.
Interfaces seladas
As interfaces seguem as mesmas regras e geralmente são a escolha mais natural para modelar famílias de casos:
public sealed interface Result<T>
permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String message) implements Result<T> {}Combinado com records, você obtém algo próximo ao "sum type" ou "tagged union" de linguagens funcionais — uma lista fechada de alternativas nomeadas, cada uma com seus próprios dados.
Mesmo módulo, mesmo pacote (ou permits explícito)
Os subtipos permitidos precisam ser acessíveis à declaração selada em tempo de compilação. A configuração mais simples é colocar a classe selada e seus subtipos permitidos no mesmo arquivo-fonte — então você pode até omitir permits, pois o compilador o infere:
public sealed interface Tree {
record Leaf(int value) implements Tree {}
record Node(Tree left, Tree right) implements Tree {}
}Se estiverem em arquivos separados, devem estar no mesmo pacote (ou, em um projeto modular, no mesmo módulo), e a cláusula permits é obrigatória.
A recompensa: switch exaustivo
O compilador conhece todos os subtipos possíveis de um tipo selado. Isso permite que switch imponha exaustividade sem um default:
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square q -> q.side() * q.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}Se você adicionar posteriormente um Hexagon permitido, esse switch para de compilar em todo lugar onde aparece, até que você trate o novo caso. Essa é exatamente a rede de segurança que default destruiria silenciosamente.
Regras que o compilador impõe
Algumas restrições são fáceis de ignorar por descuido:
- Todo subtipo permitido deve estender ou implementar diretamente o tipo selado. Você não pode listar um neto em
permits— apenas subtipos imediatos. - Cada subtipo permitido deve escolher um modificador:
final,sealedounon-sealed. Esquecer um é erro de compilação. - Os tipos permitidos devem ser encontráveis em tempo de compilação — mesmo arquivo, mesmo pacote ou o mesmo módulo nomeado. Um tipo selado não pode permitir uma classe em um módulo não relacionado.
- Records são implicitamente
final, portanto um record pode ser um subtipo permitido sem escreverfinalvocê mesmo. É por isso que sealed-interface-mais-records é uma combinação tão limpa.
O modificador non-sealed é a saída deliberada. Use-o quando a maior parte de uma hierarquia deve permanecer fechada, mas um ramo é um ponto de extensão intencional:
public sealed interface Vehicle permits Car, Truck, CustomBuild {}
public record Car(int doors) implements Vehicle {}
public record Truck(double tons) implements Vehicle {}
// Re-opened: third parties may extend this branch.
public non-sealed interface CustomBuild extends Vehicle {}Como CustomBuild é non-sealed, um switch sobre Vehicle ainda precisa de um fallback para ele — o compilador não pode mais provar que esse ramo é exaustivo.
Quando selar
Use o selamento quando a abstração é genuinamente um conjunto fechado de casos:
- Nós de AST ou expressão (
Literal,Add,Multiply...). - Resultados de domínio que são "sucesso ou uma destas falhas."
- Hierarquias de comando/evento onde todo consumidor downstream precisa tratar cada caso.
Não sele tipos que são pontos de extensão — interfaces de plugin, hooks de framework, qualquer coisa que os chamadores devem subtipar. Selar esses tipos derrota seu propósito.
Um exemplo completo
O que vem a seguir
O selamento limita a lista de subtipos. O próximo capítulo trata de perguntar, em tempo de execução, qual subtipo você realmente tem — o operador instanceof e sua forma moderna de correspondência de padrões, que é o que torna o switch acima tão conciso. Continue em Operador instanceof em Java.