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.
- O que é uma hierarquia fechada e por que ela é útil.
- As palavras
sealedepermitsJava 17+. - As três escolhas de cada filha:
final,sealedounon-sealed. - Interfaces seladas com records, e quando dá para omitir o
permits. - O
switchexaustivo, semdefault, e os erros reais dojavac(class is not allowed to extend sealed classe 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
Circulonã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:
| Modificador | Tradução | Quem pode herdar da filha |
|---|---|---|
final | final, "última" | Ninguém. A família termina aqui. É o caso mais comum. |
sealed … permits … | selada | Só 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.
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… | Use | Exemplo |
|---|---|---|
| Que qualquer um possa herdar | Classe comum (nada) | class Animal |
| Que ninguém herde | final | final class Cpf |
| Uma lista fechada de subtipos com dados diferentes | sealed interface + records | sealed interface Forma permits Circulo, Retangulo |
| Lista fechada, mas com código comum na mãe | sealed abstract class | sealed abstract class Conta permits Corrente, Poupanca |
| Fechar o primeiro nível e abrir um galho | non-sealed numa filha | non-sealed class Retangulo |
| Uma lista fechada de valores simples | enum | enum Tamanho { P, M, G } |
| Rodar num Java anterior ao 17 | Classe 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
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
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
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
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
| Sintaxe | Significado |
|---|---|
sealed class X permits A, B | Só A e B podem estender X |
sealed interface X permits A, B | Só A e B podem implementar X |
sealed interface X { } (sem permits) | As filhas são as do mesmo arquivo |
final class A extends X | Filha sem filhas |
sealed class B extends X permits C | Filha com a sua própria lista |
non-sealed class B extends X | Filha aberta a qualquer herdeira |
record A(…) implements X | Records já são final |
switch (x) { case A a -> …; case B b -> …; } | Exaustivo sem default |
Mensagem do javac | Causa |
|---|---|
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 expected | Filha sem uma das três escolhas |
invalid permits clause | Classe da lista que não herda da selada |
the switch expression does not cover all possible input values | Falta um case para uma filha |
Exercícios
1. Complete para que só Pix e Boleto possam implementar a interface:
2. Complete com o modificador que deixa qualquer classe herdar de Boleto:
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+permitsJava 17+ criam uma hierarquia fechada: só as classes da lista podem herdar.- Cada filha escolhe:
final(fim da linha),sealed(outra lista) ounon-sealed(porta aberta). Records já sãofinal. - No mesmo arquivo, o
permitspode ser omitido; em arquivos separados, as filhas ficam no mesmo pacote. - O grande ganho: o
switchsobre um tipo selado pode dispensar odefault, e ojavacavisa 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.