Skip to content

Latest commit

 

History

History
611 lines (473 loc) · 19.6 KB

File metadata and controls

611 lines (473 loc) · 19.6 KB

3.1 — Recursos Modernos do Java 21 e 25

Java evoluiu radicalmente nos últimos anos. O Java 21 LTS (setembro de 2023) e o Java 25 LTS (setembro de 2025) trouxeram recursos que tornam o código mais expressivo, seguro e eficiente. Este guia explora cada feature com o problema que resolve, o código antes e depois, e os casos onde você deve evitá-las em produção.


Contexto: Java 21 LTS e Java 25 LTS

Java 21 (LTS):

  • Virtual Threads (Project Loom) — finalizado
  • Record Patterns, Pattern Matching para switch — finalizado
  • Sequenced Collections
  • String Templates (preview)

Java 22:

  • Unnamed Variables e Unnamed Patterns (JEP 456) — finalizado

Java 25 (LTS):

  • Scoped Values — finalizado
  • Flexible Constructor Bodies
  • Module Import Declarations
  • Primitive Types in Patterns

LTS (Long-Term Support): versões com suporte por 8+ anos — use em produção. Versões intermediárias recebem suporte por apenas 6 meses.


var — Inferência de tipo local

Disponível desde Java 10. O compilador infere o tipo da variável com base na expressão do lado direito.

Problema que resolve: verbosidade excessiva em tipos genéricos longos.

// ANTES — verboso
HashMap<String, List<PedidoDTO>> pedidosPorCliente = new HashMap<String, List<PedidoDTO>>();
Iterator<Map.Entry<String, List<PedidoDTO>>> iterator = pedidosPorCliente.entrySet().iterator();

// DEPOIS — com var
var pedidosPorCliente = new HashMap<String, List<PedidoDTO>>();
var iterator = pedidosPorCliente.entrySet().iterator();

Quando usar var

// BOM — o tipo é óbvio pela inicialização
var nome = "João";
var lista = new ArrayList<Pedido>();
var servico = new PagamentoService();
var resultado = calcularTotal(itens);

// BOM — evita repetição óbvia
var pedido = pedidoRepository.findById(id).orElseThrow();

// BOM — em loops onde o tipo é claro
for (var pedido : pedidos) {
    processar(pedido);
}

// BOM — resultado de streams onde o tipo seria gigantesco
var agrupado = pedidos.stream()
    .collect(Collectors.groupingBy(Pedido::getCliente,
             Collectors.summingDouble(Pedido::getValor)));

Quando NÃO usar var

// RUIM — tipo não é óbvio, precisa do contexto do método
var resultado = processar(dados);    // o que é 'resultado'?
var x = obterConfig();               // que tipo retorna?

// RUIM — com literais numéricos (pode mudar o tipo implicitamente)
var numero = 1;        // int, não long — pode surpreender
var pi = 3.14;         // double, não float

// RUIM — campos de classe (var só funciona em variáveis locais)
// Não compila:
// private var nome = "João";

// RUIM — parâmetros de método (não suportado)
// Não compila:
// public void processar(var dado) {}

// RUIM — quando o tipo é importante para o leitor entender o contrato
var pagamento = criarPagamento();   // é Pagamento? PagamentoDTO? PagamentoCommand?
// Prefira:
Pagamento pagamento = criarPagamento();

Regra prática: use var quando o tipo for óbvio da inicialização. Se alguém precisar inspecionar o retorno do método para entender a variável, escreva o tipo explicitamente.


Switch Expressions (Java 14+)

O switch tradicional era propenso a erros (esquecer break), não retornava valor e era verboso.

Problema que resolve: switch como expressão que retorna valor, sem fall-through acidental.

// ANTES — switch tradicional (verboso e perigoso)
String descricao;
switch (status) {
    case PENDENTE:
        descricao = "Aguardando pagamento";
        break;
    case PAGO:
        descricao = "Pagamento confirmado";
        break;
    case CANCELADO:
        descricao = "Pedido cancelado";
        break;
    default:
        descricao = "Status desconhecido";
        break;
}

// DEPOIS — switch expression com ->
String descricao = switch (status) {
    case PENDENTE  -> "Aguardando pagamento";
    case PAGO      -> "Pagamento confirmado";
    case CANCELADO -> "Pedido cancelado";
};
// Se for enum exaustivo, o default é desnecessário

Com yield para blocos complexos

double desconto = switch (categoria) {
    case VIP -> 0.20;
    case PREMIUM -> 0.15;
    case COMUM -> {
        // lógica mais complexa exige yield em vez de ->
        double base = 0.05;
        double extra = calcularExtraComum(cliente);
        yield base + extra;   // yield retorna o valor do bloco
    }
    case NOVO -> 0.0;
};

Retornando objetos

// Switch expression pode retornar qualquer coisa
Notificacao notificacao = switch (evento) {
    case PAGAMENTO_APROVADO -> new NotificacaoEmail("Pagamento aprovado!");
    case PAGAMENTO_RECUSADO -> new NotificacaoSMS("Pagamento recusado.");
    case ENTREGA_REALIZADA  -> new NotificacaoPush("Seu pedido chegou!");
};

// Pattern Matching no switch (Java 21)
String formato = switch (objeto) {
    case Integer i -> "Inteiro: " + i;
    case String s  -> "Texto com " + s.length() + " caracteres";
    case null      -> "Nulo";
    default        -> "Tipo desconhecido: " + objeto.getClass().getSimpleName();
};

Quando NÃO usar em produção

Switch expression não apresenta riscos em produção — é uma melhoria direta. Use sem receio. O cuidado é garantir exaustividade: para enum, o compilador garante; para outros tipos, adicione default.


Text Blocks (Java 15+)

Strings multi-linha sem escape manual de aspas e quebras de linha.

Problema que resolve: SQL, JSON, HTML e XML embutidos em código Java são ilegíveis com concatenação tradicional.

// ANTES — ilegível
String sql = "SELECT u.id, u.nome, u.email\n" +
             "FROM usuarios u\n" +
             "JOIN pedidos p ON p.usuario_id = u.id\n" +
             "WHERE u.ativo = true\n" +
             "  AND p.criado_em >= :dataInicio\n" +
             "ORDER BY u.nome ASC";

String json = "{\n" +
              "  \"nome\": \"João\",\n" +
              "  \"email\": \"joao@email.com\",\n" +
              "  \"ativo\": true\n" +
              "}";

// DEPOIS — text blocks
String sql = """
        SELECT u.id, u.nome, u.email
        FROM usuarios u
        JOIN pedidos p ON p.usuario_id = u.id
        WHERE u.ativo = true
          AND p.criado_em >= :dataInicio
        ORDER BY u.nome ASC
        """;

String json = """
        {
          "nome": "João",
          "email": "joao@email.com",
          "ativo": true
        }
        """;

Indentação inteligente

O Java remove automaticamente a indentação comum a todas as linhas:

// A indentação "real" é determinada pela posição do """ de fechamento
// Estas três formas geram strings diferentes:

// Forma 1: """ no final — remove toda a indentação relativa
String texto = """
        Linha 1
        Linha 2
        """;
// Resultado: "Linha 1\nLinha 2\n"

// Forma 2: """ mais à esquerda — preserva indentação adicional
String texto = """
        Linha 1
        Linha 2
""";
// Resultado: "        Linha 1\n        Linha 2\n"

formatted() para interpolação

String template = """
        Olá, %s!
        Seu pedido #%d foi confirmado.
        Valor total: R$ %.2f
        Previsão de entrega: %s
        """.formatted(cliente.getNome(), pedido.getId(),
                      pedido.getValor(), pedido.getPrevisaoEntrega());

Quando NÃO usar

  • Strings de uma única linha — use aspas normais
  • Strings que precisam de trim no final — text blocks incluem a newline final por padrão

Record Classes (Java 16+)

Records são classes imutáveis para transportar dados. Eliminam boilerplate de equals(), hashCode(), toString() e construtores.

Problema que resolve: DTOs, Value Objects e POJOs com toneladas de código repetitivo.

// ANTES — classe DTO manual (55+ linhas de boilerplate)
public final class EnderecoDTO {
    private final String logradouro;
    private final String numero;
    private final String cep;
    private final String cidade;
    private final String estado;

    public EnderecoDTO(String logradouro, String numero, String cep,
                       String cidade, String estado) {
        this.logradouro = logradouro;
        this.numero = numero;
        this.cep = cep;
        this.cidade = cidade;
        this.estado = estado;
    }

    public String getLogradouro() { return logradouro; }
    public String getNumero()     { return numero; }
    public String getCep()        { return cep; }
    public String getCidade()     { return cidade; }
    public String getEstado()     { return estado; }

    @Override
    public boolean equals(Object o) { /* ... 10 linhas ... */ }

    @Override
    public int hashCode() { /* ... 5 linhas ... */ }

    @Override
    public String toString() {
        return "EnderecoDTO{logradouro='" + logradouro + "', ...}";
    }
}

// DEPOIS — record (1 linha)
public record EnderecoDTO(
    String logradouro,
    String numero,
    String cep,
    String cidade,
    String estado
) {}

Compact Constructor — validação

public record Dinheiro(BigDecimal valor, String moeda) {

    // Compact constructor — sem parâmetros, executa antes da atribuição automática
    public Dinheiro {
        Objects.requireNonNull(valor, "valor não pode ser nulo");
        Objects.requireNonNull(moeda, "moeda não pode ser nula");

        if (valor.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Valor não pode ser negativo: " + valor);
        }

        // Normalização — os campos ainda não foram atribuídos aqui
        moeda = moeda.toUpperCase().trim();
    }
}

Métodos adicionais em records

public record Periodo(LocalDate inicio, LocalDate fim) {

    // Validação no compact constructor
    public Periodo {
        if (fim.isBefore(inicio)) {
            throw new IllegalArgumentException("Fim não pode ser antes do início");
        }
    }

    // Métodos de negócio são permitidos
    public long diasDeDuracao() {
        return ChronoUnit.DAYS.between(inicio, fim);
    }

    public boolean contem(LocalDate data) {
        return !data.isBefore(inicio) && !data.isAfter(fim);
    }

    public boolean sobrepoeCom(Periodo outro) {
        return !this.fim.isBefore(outro.inicio) && !outro.fim.isBefore(this.inicio);
    }

    // "with" pattern — retorna nova instância com campo modificado (imutável)
    public Periodo comInicio(LocalDate novoInicio) {
        return new Periodo(novoInicio, this.fim);
    }

    public Periodo comFim(LocalDate novoFim) {
        return new Periodo(this.inicio, novoFim);
    }
}

Records com interfaces

// Records podem implementar interfaces
public sealed interface Resultado<T> permits Resultado.Sucesso, Resultado.Falha {

    record Sucesso<T>(T valor) implements Resultado<T> {}
    record Falha<T>(String mensagem, Exception causa) implements Resultado<T> {}
}

// Uso
Resultado<Pedido> resultado = criarPedido(command);
String resposta = switch (resultado) {
    case Resultado.Sucesso<Pedido> s -> "Pedido criado: " + s.valor().getId();
    case Resultado.Falha<Pedido> f  -> "Erro: " + f.mensagem();
};

Quando NÃO usar records

  • Entidades JPA/Hibernate — precisam ser mutáveis e ter construtor padrão
  • Classes com lógica complexa de negócio — prefira uma classe normal
  • Quando você precisa de herança de classe (records não podem estender outras classes)

Pattern Matching para instanceof (Java 16+)

Elimina o cast manual após verificação de tipo.

Problema que resolve: código verbose e propenso a erros com instanceof + cast.

// ANTES — verboso e repetitivo
Object objeto = obterObjeto();
if (objeto instanceof String) {
    String texto = (String) objeto;   // cast redundante
    System.out.println(texto.toUpperCase());
}

// DEPOIS — pattern matching
if (objeto instanceof String texto) {
    System.out.println(texto.toUpperCase());
}

// Com condição adicional (guard)
if (objeto instanceof String texto && texto.length() > 10) {
    System.out.println("Texto longo: " + texto);
}

Uso real em método equals

public record Ponto(int x, int y) {

    @Override
    public boolean equals(Object obj) {
        // ANTES
        // if (!(obj instanceof Ponto)) return false;
        // Ponto outro = (Ponto) obj;
        // return this.x == outro.x && this.y == outro.y;

        // DEPOIS — pattern matching
        return obj instanceof Ponto outro
            && this.x == outro.x
            && this.y == outro.y;
    }
}

Sealed Classes and Interfaces (Java 17+)

Sealed classes permitem definir exatamente quais classes podem estender ou implementar um tipo, criando hierarquias fechadas.

Problema que resolve: hierarquias de tipos onde você quer exaustividade garantida pelo compilador (sem default esquecido).

// Definindo a hierarquia selada
public sealed interface Forma
    permits Circulo, Retangulo, Triangulo {}

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

// Se tentar criar:
// public record Hexagono(double lado) implements Forma {}
// Erro: Hexagono não está na lista de 'permits'

Exhaustive switch — o compilador garante cobertura

// O compilador sabe todos os subtipos de Forma
// Se você esquecer um case, ERRO DE COMPILAÇÃO
double calcularArea(Forma forma) {
    return switch (forma) {
        case Circulo c     -> Math.PI * c.raio() * c.raio();
        case Retangulo r   -> r.largura() * r.altura();
        case Triangulo t   -> (t.base() * t.altura()) / 2;
        // Sem default — se adicionar Hexagono sem tratar aqui, não compila
    };
}

Hierarquia de eventos de domínio

// Modelo de domínio rico com sealed interfaces
public sealed interface EventoPedido
    permits EventoPedido.Criado, EventoPedido.Pago,
            EventoPedido.Enviado, EventoPedido.Entregue, EventoPedido.Cancelado {

    record Criado(UUID pedidoId, LocalDateTime em, String cliente) implements EventoPedido {}
    record Pago(UUID pedidoId, LocalDateTime em, BigDecimal valor) implements EventoPedido {}
    record Enviado(UUID pedidoId, LocalDateTime em, String codigoRastreio) implements EventoPedido {}
    record Entregue(UUID pedidoId, LocalDateTime em) implements EventoPedido {}
    record Cancelado(UUID pedidoId, LocalDateTime em, String motivo) implements EventoPedido {}
}

// Processador de eventos — compilador garante que todos os casos são tratados
public void processarEvento(EventoPedido evento) {
    switch (evento) {
        case EventoPedido.Criado e -> {
            log.info("Pedido {} criado por {}", e.pedidoId(), e.cliente());
            notificarCliente(e.cliente(), "Pedido recebido!");
        }
        case EventoPedido.Pago e -> {
            log.info("Pedido {} pago: R$ {}", e.pedidoId(), e.valor());
            iniciarSeparacao(e.pedidoId());
        }
        case EventoPedido.Enviado e -> {
            log.info("Pedido {} enviado. Rastreio: {}", e.pedidoId(), e.codigoRastreio());
            notificarRastreio(e.pedidoId(), e.codigoRastreio());
        }
        case EventoPedido.Entregue e -> finalizarPedido(e.pedidoId());
        case EventoPedido.Cancelado e -> processarCancelamento(e.pedidoId(), e.motivo());
    }
}

Quando NÃO usar em produção

  • Hierarquias que precisam ser extensíveis por clientes/usuários do framework — use interfaces abertas
  • Quando você quer permitir que outras equipes/libs estendam o tipo

Scoped Values (Java 21+)

Substituto mais seguro e eficiente que ThreadLocal para compartilhar dados imutáveis em Virtual Threads.

Problema que resolve: ThreadLocal vaza memória com Virtual Threads (são milhares delas) e permite modificação acidental.

// ANTES — ThreadLocal (problemático com Virtual Threads)
public class ContextoUsuario {
    private static final ThreadLocal<Usuario> usuarioAtual = new ThreadLocal<>();

    public static void set(Usuario usuario) { usuarioAtual.set(usuario); }
    public static Usuario get() { return usuarioAtual.get(); }
    public static void clear() { usuarioAtual.remove(); }  // Não pode esquecer isso!
}

// DEPOIS — ScopedValue (Java 21)
public class ContextoUsuario {
    public static final ScopedValue<Usuario> USUARIO_ATUAL = ScopedValue.newInstance();
}

// Uso: o valor só existe dentro do escopo definido
ScopedValue.where(ContextoUsuario.USUARIO_ATUAL, usuarioLogado)
    .run(() -> {
        // Dentro deste bloco, USUARIO_ATUAL está disponível
        processarRequisicao();
        // Ao sair do bloco, o valor some automaticamente — sem vazamento
    });

// Leitura segura
public void processarRequisicao() {
    var usuario = ContextoUsuario.USUARIO_ATUAL.get();
    log.info("Processando para: {}", usuario.getNome());
}

Unnamed Variables (Java 22+)

Permite usar _ para ignorar variáveis que você precisa declarar mas não vai usar.

Problema que resolve: warnings de "variável não utilizada" e código que deixa claro a intenção de ignorar.

// ANTES — variável obrigatória mas não usada
try {
    int resultado = Integer.parseInt(texto);
    return true;
} catch (NumberFormatException e) {  // 'e' não é usado
    return false;
}

// DEPOIS — unnamed variable
try {
    Integer.parseInt(texto);
    return true;
} catch (NumberFormatException _) {  // explicitamente ignorado
    return false;
}

// Em loops onde o índice não importa
for (int _ : lista) {
    contador++;
}

// Em pattern matching quando o valor não interessa
if (objeto instanceof String _) {
    // só me importa que é uma String, não o valor
    tratarString();
}

// Em lambdas
mapa.forEach((_, valor) -> processar(valor));

Virtual Threads (Java 21)

Virtual Threads são o mecanismo que permite escrever código I/O bloqueante com a escalabilidade de código reativo, sem a complexidade de Mono/Flux. Criadas em milhões com custo de memória de poucos kilobytes cada, elas mudam a relação entre concorrência e simplicidade de código.

O funcionamento interno — carrier threads, mounting/unmounting, o problema de pinning com synchronized — está coberto no módulo 3.4, onde o tema é tratado junto com ExecutorService, race conditions e as demais ferramentas de concorrência. A configuração do Spring Boot para habilitar virtual threads por request também está lá.

Para referência rápida: spring.threads.virtual.enabled=true em application.properties é suficiente no Spring Boot 3.2+.

Resumo visual das features por versão

Feature Versão final Use em produção? Principal benefício
var Java 10 Sim Menos verbosidade em tipos longos
Switch Expressions Java 14 Sim Switch que retorna valor, sem fall-through
Text Blocks Java 15 Sim Strings multi-linha legíveis
Records Java 16 Sim DTOs sem boilerplate
Pattern Matching instanceof Java 16 Sim Sem cast manual
Sealed Classes Java 17 Sim Hierarquias fechadas e exaustivas
Virtual Threads Java 21 Sim (I/O) Escalabilidade sem reativo
Scoped Values Java 21 Sim ThreadLocal seguro para Virtual Threads
Unnamed Variables (_) Java 22 Sim Intenção explícita de ignorar