W3docs

Tipos Sealed do Java em Profundidade

Modele hierarquias de tipos fechadas em Java com classes e interfaces sealed, especialmente com correspondência de padrões.

Classes e interfaces sealed (finalizadas no Java 17) permitem que um tipo declare exatamente quais outros tipos têm permissão para estendê-lo ou implementá-lo. Em vez de uma hierarquia aberta que qualquer pessoa pode subclassificar, você escreve um conjunto fechado sobre o qual o compilador pode raciocinar. Essa única garantia — estes e somente estes — é o que alimenta a correspondência de padrões exaustiva e torna seguro modelar hierarquias de classes baseadas em dados.

Um tipo sealed é o parceiro natural dos records. Os records fornecem os dados; o sealing fornece o conjunto fechado de casos. Juntos, eles trazem tipos de dados algébricos (o "tipo soma" que você pode conhecer de Kotlin, Rust ou Scala) para o Java puro, e mudam como um switch sobre uma hierarquia se comporta.

Este capítulo aborda como selar um tipo com permits, a escolha obrigatória entre final / sealed / non-sealed que cada subtipo deve fazer, por que uma hierarquia fechada desbloqueia um switch exaustivo sem default, e como o sealing se combina com a desconstrução de records e padrões guardados. Ele se baseia em interfaces e herança.

Selando um Tipo com permits

Um tipo se torna sealed com o modificador sealed e uma cláusula permits que lista cada subtipo direto. Nenhuma outra classe pode entrar na hierarquia, mesmo no mesmo pacote. Os subtipos permitidos devem ser acessíveis ao tipo sealed e, no módulo sem nome, residir no mesmo pacote (ou no mesmo módulo).

public sealed interface Payment
        permits Cash, Card, BankTransfer {}

public record Cash(int amount) implements Payment {}
public record Card(String number, int amount) implements Payment {}
public record BankTransfer(String iban, int amount) implements Payment {}

Se um subtipo estiver no mesmo arquivo-fonte, a cláusula permits é opcional — o compilador a infere a partir do arquivo. Você só precisa especificar permits explicitamente quando os subtipos residem em arquivos separados.

// Same file: permits is inferred, so it can be omitted.
sealed interface Expr {
    record Num(int value) implements Expr {}
    record Add(Expr left, Expr right) implements Expr {}
}

A Regra de final, sealed e non-sealed

Cada subtipo permitido deve, por sua vez, indicar como sua parte da hierarquia é fechada. O compilador exige uma escolha: cada subtipo direto deve ser declarado final, sealed ou non-sealed. Não existe opção "não fazer nada" — omitir o modificador é um erro de compilação.

ModificadorSignificado para o subtipo
finalO subtipo não pode ser estendido mais. Records são implicitamente final.
sealedO subtipo é ele próprio fechado e fornece sua própria lista permits.
non-sealedO subtipo reabre a hierarquia — qualquer um pode estendê-lo novamente.
public sealed class Shape permits Circle, Polygon, Freeform {}

public final class Circle extends Shape {}          // closed here
public sealed class Polygon extends Shape            // closed, but to a set
        permits Triangle, Rectangle {}
public non-sealed class Freeform extends Shape {}    // reopened: any subclass allowed

public final class Triangle extends Polygon {}
public final class Rectangle extends Polygon {}

non-sealed é a saída de emergência: permite que um ramo de uma hierarquia, de outro modo fechada, permaneça aberto para extensão. Use com parcimônia, pois isso anula a garantia de exaustividade para aquele ramo.

Por que o Sealing Permite um Switch Exaustivo

A recompensa por fechar uma hierarquia é que o compilador conhece a lista completa de casos. Um switch sobre um tipo sealed que cobre cada subtipo permitido é exaustivo, portanto você não precisa escrever um ramo default. Melhor ainda, se alguém posteriormente adicionar um novo subtipo permitido, todo switch não exaustivo para de compilar — o compilador aponta para o código que esqueceu o novo caso.

sealed interface Payment permits Cash, Card, BankTransfer {}
record Cash(int amount) implements Payment {}
record Card(String number, int amount) implements Payment {}
record BankTransfer(String iban, int amount) implements Payment {}

static String fee(Payment p) {
    return switch (p) {                 // no default needed
        case Cash c         -> "no fee";
        case Card c         -> "2% card fee";
        case BankTransfer b -> "flat fee";
    };
}

Remova o caso BankTransfer e o código não compilará: "the switch expression does not cover all possible input values." Essa verificação em tempo de compilação é a razão central para selar uma hierarquia.

Records, Desconstrução e Guards

Como os subtipos permitidos geralmente são records, você pode combinar sealing com padrões de desconstrução de records e padrões guardados (when) — consulte correspondência de padrões para o recurso completo. A desconstrução vincula os componentes do record diretamente no rótulo case; um guard adiciona uma condição boolean. A ordem importa: casos guardados mais específicos devem vir antes do fallback sem guard para o mesmo tipo.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

static String describe(Shape s) {
    return switch (s) {
        case Circle(double r) when r > 10        -> "big circle";
        case Circle(double r)                    -> "circle r=" + r;
        case Rectangle(double w, double h) when w == h -> "square";
        case Rectangle(double w, double h)       -> "rectangle";
    };
}

O compilador ainda trata isso como exaustivo: cada subtipo permitido é correspondido por pelo menos um rótulo sem guard, portanto o switch completo é total mesmo que alguns rótulos sejam guardados.

Um Exemplo Prático

O exemplo executável abaixo une tudo: uma interface Shape sealed com três subtipos record, um switch exaustivo para área, um switch de desconstrução guardada para descrição, e uma demonstração dos metadados de sealing por meio de reflexão. Usa apenas o JDK, portanto é executado sem modificações.

java— editable, runs on the server

O que observar na execução:

  • O switch de área não tem ramo default — como Shape é sealed, cobrir os três records já é exaustivo.
  • describe imprime big circle r=12.0 apenas para o círculo de raio 12, provando que o guard when r > 10 é avaliado antes do rótulo Circle sem guard.
  • O retângulo com lados de 5 cada imprime square side=5.0, mostrando que o guard w == h prevalece sobre o caso Rectangle simples que vem a seguir.
  • A área total (525.96) é acumulada em todos os subtipos record, confirmando que um único loop polimórfico lida com toda a hierarquia fechada.
  • Shape.class.isSealed() retorna true e getPermittedSubclasses() lista Circle, Rectangle e Triangle — o conjunto permits persiste nos metadados em tempo de execução.

Prática

Prática
Por que um switch exaustivo sobre uma interface sealed pode omitir o ramo default?
Por que um switch exaustivo sobre uma interface sealed pode omitir o ramo default?
Was this page helpful?