Java Classes Seladas

Normalmente, qualquer classe pode herdar de qualquer outra que não seja final. Uma classe selada (sealed, em inglês "lacrada") muda isso: ela escreve uma lista fechada de quem pode herdar dela, e o compilador recusa qualquer outra. Isso parece uma restrição boba, mas é o que permite ao Java ter certeza de que um switch tratou todos os casos.

Você vai aprender
  • O que é uma hierarquia fechada e por que ela é útil.
  • As palavras sealed e permits Java 17+.
  • As três escolhas de cada filha: final, sealed ou non-sealed.
  • Interfaces seladas com records, e quando dá para omitir o permits.
  • O switch exaustivo, sem default, e os erros reais do javac (class is not allowed to extend sealed class e outros).

Para acompanhar, você precisa conhecer herança, classes abstratas, interfaces, final e records. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.


A ideia antes do código

Pense numa festa com lista de convidados. Na porta, o segurança só deixa entrar quem está na lista. Não importa se a pessoa é simpática: se o nome não está lá, ela fica de fora. E o dono da festa sabe exatamente quem pode estar lá dentro.

Uma classe comum é uma festa de portas abertas: qualquer um, em qualquer arquivo, pode escrever extends e entrar na família. Uma classe final é a festa cancelada: ninguém entra. A classe sealed é o meio-termo: a festa com lista. A lista se chama permits ("permite").

O exemplo clássico é uma Forma geométrica que, no seu programa, só pode ser um círculo ou um retângulo. Em linguagem de matemático: Forma = Circulo | Retangulo (a barra quer dizer "ou"). Nada mais.


1. Selando uma classe

sealed abstract class Forma permits Circulo, Retangulo {
    abstract double area();
}

final class Circulo extends Forma {
    double raio;
    Circulo(double raio) { this.raio = raio; }
    double area() { return Math.PI * raio * raio; }
}

final class Retangulo extends Forma {
    double largura, altura;
    Retangulo(double largura, double altura) { this.largura = largura; this.altura = altura; }
    double area() { return largura * altura; }
}

public class Main {
    public static void main(String[] args) {
        Forma[] formas = { new Circulo(2), new Retangulo(3, 5) };
        for (Forma f : formas) {
            System.out.println(f.getClass().getSimpleName() + ": " + f.area());
        }
    }
}
$ javac Main.java && java Main
Circulo: 12.566370614359172
Retangulo: 15.0
sealed
"Selada": só as classes da lista podem herdar desta.
abstract class Forma
Uma classe abstrata comum: ninguém cria uma "forma genérica" com new Forma().
permits
"Permite": começa a lista de convidados.
Circulo, Retangulo
As únicas subclasses permitidas, separadas por vírgula.

E nas filhas:

final
Obrigatório (ou uma das alternativas da seção 2): diz que Circulo não terá filhas.
class Circulo
A classe que está na lista.
extends Forma
Herda de Forma, como sempre.

O programa roda igual a qualquer herança: cada forma calcula a própria área (é polimorfismo). O método getClass().getSimpleName() devolve o nome da classe do objeto; você vai vê-lo de novo na aula de reflexão. A diferença está no que não dá para fazer, como você vai ver em "Quando dá errado".


2. As três escolhas de cada filha

O Java exige que cada classe da lista diga, de forma explícita, o que acontece com a família dela. Há três opções:

ModificadorTraduçãoQuem pode herdar da filha
finalfinal, "última"Ninguém. A família termina aqui. É o caso mais comum.
sealed … permits …seladaSó quem estiver na lista dela. A hierarquia continua fechada, um nível abaixo.
non-sealed"não selada"Qualquer um. A filha reabre a porta para a família dela.

Se você esquecer de escolher, o javac recusa: sealed, non-sealed or final modifiers expected (veja "Quando dá errado"). non-sealed é a única palavra-chave do Java com hífen.

non-sealed: reabrindo um galho

Suponha que você queira deixar outros programadores criarem tipos especiais de retângulo, como um Quadrado. Marque Retangulo como non-sealed:

sealed abstract class Forma permits Circulo, Retangulo {
    abstract double area();
}

final class Circulo extends Forma {
    double raio;
    Circulo(double raio) { this.raio = raio; }
    double area() { return Math.PI * raio * raio; }
}

non-sealed class Retangulo extends Forma {
    double largura, altura;
    Retangulo(double largura, double altura) { this.largura = largura; this.altura = altura; }
    double area() { return largura * altura; }
}

class Quadrado extends Retangulo {
    Quadrado(double lado) { super(lado, lado); }
}

public class Main {
    static String nome(Forma f) {
        return switch (f) {
            case Circulo c -> "círculo de raio " + c.raio;
            case Retangulo r -> "retângulo " + r.largura + " x " + r.altura;
        };
    }

    public static void main(String[] args) {
        System.out.println(nome(new Circulo(1)));
        System.out.println(nome(new Retangulo(2, 3)));
        System.out.println(nome(new Quadrado(4)));
    }
}
$ javac Main.java && java Main
círculo de raio 1.0
retângulo 2.0 x 3.0
retângulo 4.0 x 4.0

Quadrado não está na lista de Forma, e mesmo assim compilou: ele não herda diretamente de Forma, e sim de Retangulo, que abriu a porta. Repare que o switch continua sem default: todo Quadrado é um Retangulo, então o caso case Retangulo r também o cobre (por isso apareceu "retângulo 4.0 x 4.0"). A família de primeiro nível continua fechada: uma Forma é sempre um Circulo ou um Retangulo (de qualquer tipo).


3. Interfaces seladas e records

Uma interface também pode ser selada. Combinada com records, é a forma mais curta e mais usada no Java moderno: o record descreve os dados de cada variante, e a interface selada diz quais variantes existem.

sealed interface Forma permits Circulo, Retangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}

public class Main {
    static double area(Forma f) {
        return switch (f) {
            case Circulo c -> Math.PI * c.raio() * c.raio();
            case Retangulo r -> r.largura() * r.altura();
        };
    }

    public static void main(String[] args) {
        System.out.println(area(new Circulo(1)));
        System.out.println(area(new Retangulo(3, 4)));
    }
}
$ javac Main.java && java Main
3.141592653589793
12.0

Os records não precisaram de final: todo record já é final automaticamente (ninguém pode herdar de um record). Para classes comuns que implementam a interface, a regra das três escolhas continua valendo.

Missão: este código não compila. Clique em Rodar, leia o erro do javac e acrescente o modificador que falta na classe Circulo para que ela não tenha filhas.

sealed abstract class Forma permits Circulo, Retangulo {
    abstract String nome();
}

class Circulo extends Forma {
    String nome() { return "Círculo"; }
}

final class Retangulo extends Forma {
    String nome() { return "Retângulo"; }
}

public class Main {
    public static void main(String[] args) {
        Forma[] formas = { new Circulo(), new Retangulo() };
        for (Forma f : formas) {
            System.out.println(f.nome() + " pronto");
        }
    }
}
sealed abstract class Forma permits Circulo, Retangulo {
    abstract String nome();
}

final class Circulo extends Forma {
    String nome() { return "Círculo"; }
}

final class Retangulo extends Forma {
    String nome() { return "Retângulo"; }
}

public class Main {
    public static void main(String[] args) {
        Forma[] formas = { new Circulo(), new Retangulo() };
        for (Forma f : formas) {
            System.out.println(f.nome() + " pronto");
        }
    }
}

4. Onde as filhas podem morar

No mesmo arquivo: o permits é opcional

Quando a classe selada e todas as filhas estão no mesmo arquivo, você pode omitir o permits: o compilador monta a lista sozinho com as filhas que encontrar ali.

sealed interface Forma {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}

public class Main {
    public static void main(String[] args) {
        Forma f = new Circulo(2);
        String r = switch (f) {
            case Circulo c -> "círculo";
            case Retangulo q -> "retângulo";
        };
        System.out.println(r);
    }
}
$ javac Main.java && java Main
círculo

Mesmo assim, muita gente escreve o permits: ele serve de documentação, deixando a lista à vista logo na primeira linha.

Em arquivos separados

Num projeto de verdade, cada tipo público fica no seu arquivo. Aí o permits é obrigatório, e as filhas precisam estar no mesmo pacote da classe selada (ou no mesmo módulo, se o projeto usar módulos). Aqui, todos estão na mesma pasta, sem package, então estão no mesmo pacote "sem nome":

public sealed interface Forma permits Circulo, Retangulo {
    double area();
}
public record Circulo(double raio) implements Forma {
    public double area() { return Math.PI * raio * raio; }
}
public record Retangulo(double largura, double altura) implements Forma {
    public double area() { return largura * altura; }
}
public class Main {
    public static void main(String[] args) {
        Forma f = new Retangulo(2, 3);
        System.out.println(f.area());
    }
}
$ ls
Circulo.java
Forma.java
Main.java
Retangulo.java
$ javac *.java
$ java Main
6.0

O javac *.java compila os quatro arquivos de uma vez (o * é o curinga do terminal: "todos os arquivos terminados em .java"). Veja mais em javac e java.


5. Por que selar: o switch sabe que acabou

Você já viu nos exemplos acima: um switch sobre Forma com um case para Circulo e outro para Retangulo compila sem default. O compilador leu a lista do permits e conferiu que todas as possibilidades foram tratadas. Isso se chama exaustividade, e foi apresentado na aula de Pattern Matching.

O ganho aparece no dia em que a família cresce. A Ana acrescenta Triangulo à lista, mas esquece de mudar o método area:

sealed interface Forma permits Circulo, Retangulo, Triangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}
record Triangulo(double base, double altura) implements Forma {}

public class Main {
    static double area(Forma f) {
        return switch (f) {
            case Circulo(double r) -> Math.PI * r * r;
            case Retangulo(double l, double a) -> l * a;
        };
    }

    public static void main(String[] args) {
        System.out.println(area(new Circulo(1)));
    }
}
$ javac Main.java
Main.java:8: error: the switch expression does not cover all possible input values
        return switch (f) {
               ^
1 error

O compilador encontrou o esquecimento antes de o programa rodar. Num sistema com dezenas de switch espalhados, essa lista de erros vira a lista de tarefas: cada linha apontada é um lugar que precisa aprender a lidar com triângulos.

O default desliga esse aviso

Um switch sobre uma interface selada pode ter default (o javac aceita). Mas aí o Triangulo novo cairia nele em silêncio, e o erro só apareceria com o programa rodando, talvez como uma área zero. Com hierarquias seladas, prefira listar todos os casos e deixar o default de fora.

Selada × enum

Um enum também é uma lista fechada, mas de valores: PEQUENO, MEDIO, GRANDE, todos com os mesmos campos. Uma hierarquia selada é uma lista fechada de tipos, e cada um tem os seus próprios dados: um círculo tem raio; um retângulo tem largura e altura. Se as variantes guardam informações diferentes, use sealed; se são só nomes (ou têm todas os mesmos campos), use enum.


Formas diferentes de controlar a herança

Você quer…UseExemplo
Que qualquer um possa herdarClasse comum (nada)class Animal
Que ninguém herdefinalfinal class Cpf
Uma lista fechada de subtipos com dados diferentessealed interface + recordssealed interface Forma permits Circulo, Retangulo
Lista fechada, mas com código comum na mãesealed abstract classsealed abstract class Conta permits Corrente, Poupanca
Fechar o primeiro nível e abrir um galhonon-sealed numa filhanon-sealed class Retangulo
Uma lista fechada de valores simplesenumenum Tamanho { P, M, G }
Rodar num Java anterior ao 17Classe abstrata com construtor sem public e default no switch(o compilador não confere a lista)

Quando dá errado

Uma classe fora da lista tenta herdar

Este código tem um erro de propósito.
sealed abstract class Forma permits Circulo, Retangulo {
    abstract double area();
}

final class Circulo extends Forma {
    double area() { return 3.14; }
}

final class Retangulo extends Forma {
    double area() { return 15; }
}

class Triangulo extends Forma {
    double area() { return 6; }
}
$ javac Main.java
Main.java:13: error: class is not allowed to extend sealed class: Forma (as it is not listed in its 'permits' clause)
class Triangulo extends Forma {
^
1 error

O que quer dizer: "a classe não tem permissão para estender a classe selada Forma (porque não está listada na cláusula permits)". É o segurança barrando quem não está na lista. Com interfaces a mensagem é a mesma (um record Triangulo(…) implements Forma fora da lista recebe exatamente este texto). Como consertar: se o triângulo deve mesmo fazer parte da família, acrescente-o ao permits (e marque-o como final, sealed ou non-sealed). Se não deve, ele não pode herdar de Forma.

A filha não disse final, sealed nem non-sealed

Este código tem um erro de propósito.
sealed abstract class Forma permits Circulo, Retangulo {
    abstract double area();
}

class Circulo extends Forma {
    double area() { return 3.14; }
}
$ javac Main.java
Main.java:5: error: sealed, non-sealed or final modifiers expected
class Circulo extends Forma {
^
1 error

O que quer dizer: "esperava os modificadores sealed, non-sealed ou final". Como consertar: escolha um dos três (na dúvida, final).

Listar no permits quem não herda

Este código tem um erro de propósito.
sealed interface Forma permits Circulo, Retangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) {}
$ javac Main.java
Main.java:1: error: invalid permits clause
sealed interface Forma permits Circulo, Retangulo {}
                                        ^
  (record Retangulo must extend sealed interface)
1 error

O que quer dizer: "cláusula permits inválida: o record Retangulo precisa estender a interface selada". A lista convidou o retângulo, mas ele esqueceu o implements Forma. Como consertar: acrescente implements Forma ao Retangulo (ou tire-o da lista).

Herdar de uma filha final

Este código tem um erro de propósito.
sealed abstract class Forma permits Circulo, Retangulo {}
final class Circulo extends Forma {}
final class Retangulo extends Forma {}
class CirculoColorido extends Circulo {}
$ javac Main.java
Main.java:4: error: cannot inherit from final Circulo
class CirculoColorido extends Circulo {}
                              ^
1 error

O que quer dizer: "não é possível herdar da classe final Circulo". É o mesmo erro da aula de final. Como consertar: se Circulo deve ter filhas, troque final por non-sealed (porta aberta) ou por sealed permits CirculoColorido (outra lista).

Missão: provoque o erro de propósito. Rode o código uma vez (ele funciona). Depois acrescente a linha record Triangulo(double base, double altura) implements Forma {} logo abaixo do Retangulo, sem mexer no permits, e rode de novo para ver o javac barrar o triângulo.

sealed interface Forma permits Circulo, Retangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}

public class Main {
    public static void main(String[] args) {
        Forma f = new Circulo(2);
        System.out.println(f);
    }
}
sealed interface Forma permits Circulo, Retangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}
record Triangulo(double base, double altura) implements Forma {}

public class Main {
    public static void main(String[] args) {
        Forma f = new Circulo(2);
        System.out.println(f);
    }
}

Referência

SintaxeSignificado
sealed class X permits A, BSó A e B podem estender X
sealed interface X permits A, BSó A e B podem implementar X
sealed interface X { } (sem permits)As filhas são as do mesmo arquivo
final class A extends XFilha sem filhas
sealed class B extends X permits CFilha com a sua própria lista
non-sealed class B extends XFilha aberta a qualquer herdeira
record A(…) implements XRecords já são final
switch (x) { case A a -> …; case B b -> …; }Exaustivo sem default
Mensagem do javacCausa
class is not allowed to extend sealed class: X (as it is not listed in its 'permits' clause)Classe fora da lista
sealed, non-sealed or final modifiers expectedFilha sem uma das três escolhas
invalid permits clauseClasse da lista que não herda da selada
the switch expression does not cover all possible input valuesFalta um case para uma filha

Exercícios

1. Complete para que só Pix e Boleto possam implementar a interface:

interface Pagamento Pix, Boleto {}

2. Complete com o modificador que deixa qualquer classe herdar de Boleto:

class Boleto implements Pagamento {}

3. Por que record Pix(double valor) implements Pagamento {} não precisa de final?

4. Qual a principal vantagem de um switch sem default sobre uma interface selada?

5. A partir de qual versão as classes seladas são um recurso final do Java?

Missão final: o pedágio da estrada cobra 10.0 de carro, 5.0 de moto e 8.0 por eixo de caminhão. Escreva o switch do método pedagio sem default (a interface é selada) e faça o programa mostrar Total: 39.0.

sealed interface Veiculo permits Carro, Moto, Caminhao {}
record Carro(String placa) implements Veiculo {}
record Moto(String placa) implements Veiculo {}
record Caminhao(String placa, int eixos) implements Veiculo {}

public class Main {
    static double pedagio(Veiculo v) {
        return switch (v) {
            case Carro c -> 10.0;
            // falta a moto e o caminhão
        };
    }

    public static void main(String[] args) {
        Veiculo[] fila = { new Carro("ABC1D23"), new Moto("XYZ9K88"), new Caminhao("CAM0A01", 3) };
        double total = 0;
        for (Veiculo v : fila) {
            System.out.println(v + " paga " + pedagio(v));
            total = total + pedagio(v);
        }
        System.out.println("Total: " + total);
    }
}
sealed interface Veiculo permits Carro, Moto, Caminhao {}
record Carro(String placa) implements Veiculo {}
record Moto(String placa) implements Veiculo {}
record Caminhao(String placa, int eixos) implements Veiculo {}

public class Main {
    static double pedagio(Veiculo v) {
        return switch (v) {
            case Carro c -> 10.0;
            case Moto m -> 5.0;
            case Caminhao c -> c.eixos() * 8.0;
        };
    }

    public static void main(String[] args) {
        Veiculo[] fila = { new Carro("ABC1D23"), new Moto("XYZ9K88"), new Caminhao("CAM0A01", 3) };
        double total = 0;
        for (Veiculo v : fila) {
            System.out.println(v + " paga " + pedagio(v));
            total = total + pedagio(v);
        }
        System.out.println("Total: " + total);
    }
}

Saída real da solução:

$ java Main.java
Carro[placa=ABC1D23] paga 10.0
Moto[placa=XYZ9K88] paga 5.0
Caminhao[placa=CAM0A01, eixos=3] paga 24.0
Total: 39.0

Resumo

  • sealed + permits Java 17+ criam uma hierarquia fechada: só as classes da lista podem herdar.
  • Cada filha escolhe: final (fim da linha), sealed (outra lista) ou non-sealed (porta aberta). Records já são final.
  • No mesmo arquivo, o permits pode ser omitido; em arquivos separados, as filhas ficam no mesmo pacote.
  • O grande ganho: o switch sobre um tipo selado pode dispensar o default, e o javac avisa quando um caso novo não foi tratado.
  • Erros típicos: class is not allowed to extend sealed class, sealed, non-sealed or final modifiers expected, invalid permits clause.

Próxima aula: Expressões Regulares.