1. Sintaxe de sealed classes: como é
Vamos começar com um problema clássico de OOP em Java: hierarquia de herança aberta. Em Java, qualquer pessoa pode herdar da sua classe, a menos que ela seja declarada como final. Isso é conveniente, mas às vezes leva a surpresas — por exemplo, você não tem como saber de antemão quais classes serão subclasses da sua classe e, portanto, não pode garantir que tratou todos os casos em switch ou if-else.
Como resultado, quando você escreve tratamento por tipo de objetos (por exemplo, usando pattern matching em switch), você é obrigado a adicionar um ramo default “por via das dúvidas”: e se alguém em algum lugar criou uma nova subclasse?
Sealed classes resolvem esse problema: elas permitem limitar explicitamente a lista de subclasses. Isso torna a hierarquia fechada e controlada, e seu código — mais previsível e seguro.
Sintaxe básica
Sealed classes apareceram no Java 17. Elas são declaradas com o modificador sealed, e a lista de subclasses permitidas é especificada com a palavra-chave permits:
public sealed class Shape permits Circle, Rectangle, Square {
// Comportamento comum para todas as figuras
}
Aqui declaramos a classe Shape, e somente as classes Circle, Rectangle e Square podem ser suas subclasses diretas. Mais ninguém poderá estender Shape — o compilador não permitirá.
Importante: Todas as classes listadas em permits devem ser declaradas no mesmo arquivo ou ser visíveis para o compilador (geralmente no mesmo pacote). Aliás, se todas as subclasses forem declaradas no mesmo arquivo que a sealed class, permits pode ser omitido — o compilador entenderá sozinho.
Exemplo:
// Tudo em um único arquivo - permits é opcional
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
Requisitos para as subclasses
Cada subclasse é obrigada a declarar explicitamente seu status:
- ser final (proíbe herança posterior),
- ou ser sealed (e continuar restringindo a herança),
- ou ser non-sealed (permite herança, removendo as restrições).
Exemplo:
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle — é final; não pode ser herdada.
- Rectangle — é sealed, permitindo apenas duas subclasses.
- Square — é non-sealed; qualquer um pode estender.
Exemplo mínimo
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
Tente declarar a nova classe public class Wolf extends Animal {} — você receberá um erro de compilação:
class Wolf is not allowed to extend sealed class Animal
2. Uso de sealed classes: onde e por que usar
Pattern matching e switch
Sealed classes combinam especialmente bem com pattern matching em switch. Se o compilador conhece todas as possíveis subclasses, ele pode verificar que você tratou cada variante e nem exigirá o ramo default.
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Viva!");
switch (result) {
case Success s -> System.out.println("Sucesso: " + s.data);
case Error e -> System.out.println("Erro: " + e.message);
}
}
}
O compilador sabe que não pode haver outras variantes de Result e não exige default. Se você esquecer de tratar uma das variantes, o compilador avisará imediatamente.
Observe: a partir do Java 21, se nem todas as variantes forem tratadas no switch, você receberá um erro de compilação. Em versões mais antigas (17–20), pode ser necessário default, mas a IDE ainda avisará sobre cobertura incompleta.
Segurança e controle
Sealed classes permitem ao desenvolvedor controlar completamente a hierarquia. Isso é especialmente importante para modelos de domínio, onde o conjunto de variantes deve ser fixo (por exemplo, estado de um pedido: New, Paid, Cancelled).
Facilita a manutenção e a evolução do código
Quando você conhece todas as variantes de subclasses, é mais fácil adicionar novas funcionalidades, manter o código e realizar refatorações. A IDE também “saberá” todas as variantes e poderá ajudar com autocompletar e análise.
3. Exemplos práticos de uso de sealed classes
Exemplo 1: Figuras geométricas
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
Agora é possível usar com segurança o switch com pattern matching:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("Círculo com raio " + c.radius);
case Rectangle r -> System.out.println("Retângulo " + r.width + "x" + r.height);
case Square s -> System.out.println("Quadrado com lado " + s.side);
}
Exemplo 2: Transações financeiras
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
Agora, no handler, você pode ter certeza de que não esqueceu nenhum tipo de transação:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("Depósito: " + d.amount);
case Withdraw w -> System.out.println("Saque: " + w.amount);
case Transfer t -> System.out.println("Transferência: " + t.amount + " para " + t.toAccount);
}
Exemplo 3: Hierarquia restrita com non-sealed
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// Agora qualquer um pode estender SmsNotification
public class ViberNotification extends SmsNotification {}
4. Particularidades, limitações e nuances das sealed classes
Requisitos para modificadores
- Uma sealed class deve listar explicitamente todas as subclasses em permits.
- Todas as subclasses devem ser final, sealed ou non-sealed.
- As subclasses devem ser declaradas no mesmo arquivo ou ser visíveis para o compilador.
Sealed classes abstratas
Uma sealed class pode ser abstrata, um interface ou uma classe comum. Por exemplo:
public sealed abstract class Expr permits Const, Add, Mul {}
Compatibilidade com outros modificadores
- Não é possível declarar uma sealed class como final ou non-sealed.
- interface também pode ser sealed (e isso é muito conveniente!).
Uso com classes record
Classes record podem ser subclasses de uma sealed class, desde que sejam declaradas como final (por padrão, record é sempre final):
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. Nuances úteis
Sealed classes e pattern matching: como isso se relaciona
O principal atrativo das sealed classes é o pattern matching exaustivo. Parece complicado, mas funciona de forma simples. O compilador conhece todas as variantes possíveis, e você pode escrever switch sem o ramo default:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
Se você deixar de tratar alguma variante, o compilador não deixará o projeto compilar — isso é muito prático e seguro.
Aplicações em cenários reais
Onde as sealed classes são realmente úteis? Em qualquer lugar em que você tenha um conjunto fixo de variantes:
- Resultado de operação: Success, Error (como no exemplo acima).
- Estado de pedido: New, Paid, Cancelled.
- Árvores sintáticas abstratas (AST) para parsers.
- Respostas de API: Ok, NotFound, Error.
- Eventos no sistema: UserLoggedIn, UserLoggedOut, UserRegistered.
6. Erros comuns ao trabalhar com sealed classes
Erro nº 1: esqueceu de listar todas as subclasses em permits.
Se você não listar todas as subclasses necessárias em permits, o compilador reclamará imediatamente. Por exemplo, se você escreveu permits Circle, Rectangle, mas esqueceu Square e essa classe existe — você receberá um erro.
Erro nº 2: a subclasse não é final, sealed ou non-sealed.
Se a subclasse não for declarada com o modificador adequado, o compilador emitirá o erro: “Class must be either final, sealed or non-sealed”.
Erro nº 3: a subclasse é declarada em outro arquivo e não é visível para o compilador.
Todas as classes listadas em permits devem estar acessíveis ao compilador — no mesmo arquivo ou no mesmo pacote.
Erro nº 4: nem todas as variantes foram tratadas no switch.
Se você usar uma sealed class em um switch com pattern matching e esquecer de tratar alguma variante, o compilador não permitirá compilar o projeto. Isso é bom — você não deixará nenhum caso passar.
Erro nº 5: tentativa de herdar de uma sealed class que não está em permits.
Se você tentar criar uma subclasse que não está listada em permits, receberá o erro: “is not allowed to extend sealed class”.
Erro nº 6: tentativa de usar sealed classes em versões antigas do JDK.
Sealed classes apareceram apenas no Java 17. Se você tentar usá-las em uma versão mais antiga, receberá um erro de compilação ou nem conseguirá montar o projeto.
GO TO FULL VERSION