Novidades do Java: records, sealed classes e virtual threads

O Java, uma das linguagens mais maduras do ecossistema de desenvolvimento, passou por uma transformação significativa nos últimos anos. Três features se destacam por mudar drasticamente a forma como escrevemos código: records, sealed classes e virtual threads. Vamos explorar cada uma delas em detalhes, com exemplos práticos que mostram como essas novidades podem simplificar seu dia a dia.

1. Records: Classes de Dados Imutáveis e Concisas

Records são uma forma concisa de declarar classes que são simples portadoras de dados. Antes dos records, criar uma classe imutável exigia escrever manualmente construtor, getters, equals(), hashCode() e toString().

1.1. Eliminando Boilerplate

Antes dos records:

public class Pessoa {
    private final String nome;
    private final int idade;

    public Pessoa(String nome, int idade) {
        this.nome = nome;
        this.idade = idade;
    }

    public String getNome() { return nome; }
    public int getIdade() { return idade; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Pessoa)) return false;
        Pessoa pessoa = (Pessoa) o;
        return idade == pessoa.idade && Objects.equals(nome, pessoa.nome);
    }

    @Override
    public int hashCode() {
        return Objects.hash(nome, idade);
    }

    @Override
    public String toString() {
        return "Pessoa{nome='" + nome + "', idade=" + idade + "}";
    }
}

Com records (Java 16+):

public record Pessoa(String nome, int idade) {}

Um record automaticamente gera:
- Construtor com todos os parâmetros
- Métodos de acesso (sem "get" prefixo, apenas nome() e idade())
- equals() e hashCode() baseados em todos os campos
- toString() legível

1.2. Limitações e Regras

Records não podem estender outras classes (já estendem implicitamente java.lang.Record), mas podem implementar interfaces. Campos são implicitamente final. Você pode adicionar construtores compactos para validação:

public record Email(String endereco) {
    public Email {
        if (!endereco.contains("@")) {
            throw new IllegalArgumentException("Email inválido");
        }
    }
}

1.3. Casos de Uso

Records são ideais para DTOs, value objects e resultados de operações:

// Usando com Optional e Streams
public record ResultadoBusca(String termo, int totalResultados) {}

List<ResultadoBusca> resultados = termos.stream()
    .map(termo -> new ResultadoBusca(termo, buscar(termo)))
    .collect(Collectors.toList());

2. Sealed Classes: Hierarquias Controladas e Exaustivas

Sealed classes permitem restringir quais classes podem estender uma classe ou interface, criando hierarquias fechadas e previsíveis.

2.1. Conceito

Com sealed classes, você define explicitamente todas as subclasses permitidas. Isso é poderoso para modelar domínios onde o conjunto de variações é conhecido e limitado.

2.2. Sintaxe e Exemplos Práticos

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

public final class Circulo extends Forma {
    private final double raio;

    public Circulo(double raio) { this.raio = raio; }

    @Override
    public double area() { return Math.PI * raio * raio; }
}

public final class Retangulo extends Forma {
    private final double largura;
    private final double altura;

    public Retangulo(double largura, double altura) {
        this.largura = largura;
        this.altura = altura;
    }

    @Override
    public double area() { return largura * altura; }
}

// Classe non-sealed permite extensão ilimitada
public non-sealed class Triangulo extends Forma {
    private final double base;
    private final double altura;

    public Triangulo(double base, double altura) {
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double area() { return (base * altura) / 2; }
}

2.3. Benefícios para Pattern Matching

Sealed classes preparam o terreno para pattern matching exaustivo. O compilador pode verificar se todos os casos foram tratados:

public String descreverForma(Forma forma) {
    return switch (forma) {
        case Circulo c -> "Círculo com raio " + c.raio();
        case Retangulo r -> "Retângulo " + r.largura() + "x" + r.altura();
        case Triangulo t -> "Triângulo base " + t.base() + " altura " + t.altura();
        // Compilador sabe que não precisa de default
    };
}

3. Virtual Threads: Concorrência Leve e Escalável

Virtual threads (Project Loom) revolucionam a concorrência no Java, permitindo criar milhões de threads com custo mínimo.

3.1. O Problema das Threads Tradicionais

Threads tradicionais (plataforma threads) são caras: cada uma consome cerca de 1MB de memória de pilha e o escalonamento pelo sistema operacional é custoso. Virtual threads são gerenciadas pela JVM e consomem apenas alguns kilobytes.

3.2. Criando e Gerenciando Virtual Threads

// Criando uma virtual thread diretamente
Thread vThread = Thread.ofVirtual()
    .name("minha-virtual-thread")
    .start(() -> {
        System.out.println("Executando em: " + Thread.currentThread());
    });

// Usando ExecutorService com virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
        final int taskId = i;
        futures.add(executor.submit(() -> {
            Thread.sleep(100); // Simula I/O
            return "Tarefa " + taskId + " concluída";
        }));
    }

    for (Future<String> future : futures) {
        System.out.println(future.get());
    }
}

3.3. Limitações e Boas Práticas

Virtual threads têm limitações importantes:
- Pinned threads: operações synchronized e código nativo podem "prender" a thread virtual a uma thread de plataforma
- Pooling desnecessário: não crie pools de virtual threads; crie-as sob demanda
- Não use para CPU-bound: virtual threads brilham em operações I/O, não em processamento intensivo

4. Impacto no Dia a Dia do Desenvolvedor Java

4.1. Redução de Código Boilerplate

Records eliminam dezenas de linhas de código repetitivo. Sealed classes tornam hierarquias mais seguras e explícitas.

4.2. Simplificação de Servidores Web

Com virtual threads, servidores web podem tratar cada requisição em uma thread dedicada sem se preocupar com escalabilidade:

// Servidor HTTP simples com virtual threads
try (var server = Executors.newVirtualThreadPerTaskExecutor()) {
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
        Socket clientSocket = serverSocket.accept();
        server.submit(() -> handleClient(clientSocket));
    }
}

4.3. Exemplo Combinado: Sistema de Pedidos

public sealed interface StatusPedido permits Pendente, Processando, Enviado, Entregue {}
public record Pendente() implements StatusPedido {}
public record Processando() implements StatusPedido {}
public record Enviado(String codigoRastreio) implements StatusPedido {}
public record Entregue() implements StatusPedido {}

public record Pedido(int id, String cliente, List<Item> itens, StatusPedido status) {}

// Processamento com virtual threads
public class ProcessadorPedidos {
    public void processar(List<Pedido> pedidos) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            pedidos.forEach(pedido -> executor.submit(() -> {
                var novoStatus = switch (pedido.status()) {
                    case Pendente p -> new Processando();
                    case Processando p -> new Enviado(gerarCodigoRastreio());
                    case Enviado e -> verificarEntrega(e) ? new Entregue() : e;
                    case Entregue e -> e;
                };
                atualizarPedido(new Pedido(
                    pedido.id(), pedido.cliente(), pedido.itens(), novoStatus
                ));
            }));
        }
    }
}

5. Compatibilidade e Migração para Versões Recentes

5.1. Versões de Introdução

Feature Preview Stable
Records Java 14 Java 16
Sealed Classes Java 15 Java 17
Virtual Threads Java 19 Java 21

5.2. Habilitando Preview Features

Para usar features em preview no compilador e runtime:

# Compilação
javac --enable-preview --release 21 MeuPrograma.java

# Execução
java --enable-preview MeuPrograma

5.3. Estratégias de Adoção

  • Records: substitua gradualmente DTOs e value objects
  • Sealed Classes: use em domínios fechados como estados de máquina, tipos de eventos
  • Virtual Threads: comece em novos serviços I/O-bound, refatore código legado com cuidado

6. Comparação com Outras Linguagens

6.1. Records vs. Data Classes

Kotlin tem data class, C# tem record struct, TypeScript tem tuplas tipadas. Records do Java são mais restritos (imutáveis, sem herança), mas mais integrados ao ecossistema.

6.2. Sealed Classes

Kotlin tem sealed class similar. TypeScript tem union types que são mais flexíveis mas sem verificação exaustiva do compilador.

6.3. Virtual Threads

Go usa goroutines (mais leves, canais nativos). Rust/Kotlin usam async/await (baseado em Futures). Virtual threads são mais próximos de goroutines, mas integrados ao ecossistema Java existente.

Referências