CodeGym /Cursos /JAVA 25 SELF /Sealed classes: sintaxe e aplicação

Sealed classes: sintaxe e aplicação

JAVA 25 SELF
Nível 65 , Lição 0
Disponível

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION