Pular para o conteúdo principal

Aula 2: Java Moderno para Desenvolvimento Enterprise

Objetivos

  • Dominar as estruturas de dados do Java Collections Framework e seus trade-offs de performance
  • Aplicar Expressões Lambda e Streams API para processamento declarativo de dados
  • Utilizar Records, Text Blocks e Pattern Matching para código mais conciso e seguro
  • Compreender Virtual Threads e suas implicações para escalabilidade cloud-native
  • Preparar a base de linguagem necessária para as especificações Jakarta EE abordadas no curso

Conteúdo

Nesta aula, vamos mergulhar nas ferramentas que a própria linguagem Java nos oferece para sermos mais produtivos e para construirmos aplicações mais rápidas, seguras e eficientes. O que veremos aqui não é apenas sintaxe; é a base para escrever código de alta qualidade, pronto para os desafios da nuvem.

Por que Dominar o Java Moderno é Essencial?

O Java moderno não é apenas sobre sintaxe mais concisa — é sobre uma mudança fundamental de paradigma. As empresas que adotam práticas de desenvolvimento moderno relatam reduções significativas no tempo de desenvolvimento, menor incidência de bugs em produção e maior satisfação das equipes de desenvolvimento. Isso acontece porque o Java moderno elimina muito do código boilerplate, permitindo foco na lógica de negócio real.

Um exemplo concreto: uma operação de filtragem e agrupamento de dados que tradicionalmente exigiria 30-40 linhas de código imperativo com múltiplos loops aninhados pode ser expressa em 3-5 linhas usando Streams API.

Parte 1: A Fundação — Organizando e Processando Dados

O Básico: Linguagem de Programação Java

Java é uma linguagem de programação orientada a objetos, fortemente tipada e projetada para ser "write once, run anywhere" (WORA). Suas características fundamentais incluem:

  • Orientação a Objetos: Todo código é organizado em classes e objetos
  • Tipagem Estática: Variáveis têm tipos declarados explicitamente, funcionando como contratos verificados pelo compilador
  • Gerenciamento Automático de Memória: O Garbage Collector monitora e libera objetos não utilizados
  • Independência de Plataforma: O bytecode Java executa em qualquer JVM

Tipos de Dados Básicos

TipoDescriçãoCasos de Uso TípicosTamanho
byteInteiro de 8 bits (-128 a 127)Flags, estados binários, dados de rede1 byte
shortInteiro de 16 bitsContadores pequenos, índices limitados2 bytes
intInteiro de 32 bitsIdentificadores, contadores gerais, índices4 bytes
longInteiro de 64 bitsTimestamps, IDs de banco de dados8 bytes
floatPonto flutuante 32 bitsGráficos, jogos, cálculos com precisão moderada4 bytes
doublePonto flutuante 64 bitsCálculos científicos, engenharia8 bytes
booleanValor lógico (true/false)Flags de controle, condições, validações1 bit
charCaractere Unicode 16 bitsManipulação de texto baixo nível, parsers2 bytes
aviso

Para valores monetários, nunca use float ou double devido a problemas de precisão. Use BigDecimal.

// Nunca faça isso para valores monetários
double preco1 = 0.1;
double preco2 = 0.2;
double total = preco1 + preco2; // Resultado: 0.30000000000000004

// Use BigDecimal para precisão financeira
BigDecimal preco1 = new BigDecimal("0.1");
BigDecimal preco2 = new BigDecimal("0.2");
BigDecimal total = preco1.add(preco2); // Resultado: 0.3 (exato)

Revisão Essencial: Estruturas de Dados

O Java Collections Framework fornece estruturas de dados otimizadas para diferentes cenários:

InterfaceDescriçãoComplexidade Típica
java.util.ListColeção ordenada com duplicatas permitidas. Implementações: ArrayList (acesso O(1) por índice), LinkedList (inserção/remoção O(1))ArrayList: O(1) acesso, O(n) inserção
java.util.SetColeção sem duplicatas. Implementações: HashSet (O(1)), TreeSet (O(log n)), LinkedHashSet (O(1) + ordem)HashSet: O(1) para add/contains
java.util.MapEstrutura chave-valor. Implementações: HashMap (O(1)), TreeMap (O(log n)), ConcurrentHashMap (thread-safe)HashMap: O(1) para get/put
dica

A escolha correta da estrutura de dados é uma das decisões de micro-design mais importantes. Em sistemas de alta escala, a diferença entre O(1) e O(n) pode ser a diferença entre uma aplicação responsiva e uma inutilizável.

A Revolução Funcional: Expressões Lambda (Java 8)

Expressões Lambda permitem tratar comportamento como dados, possibilitando um estilo de programação mais declarativo e conciso.

// Abordagem Tradicional (Java 7 e anterior)
List<Product> activeProducts = new ArrayList<>();
for (Product product : products) {
if (product.isActive()) {
activeProducts.add(product);
}
}
Collections.sort(activeProducts, new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return p1.getName().compareTo(p2.getName());
}
});

// Abordagem Moderna com Lambdas (Java 8+)
List<Product> activeProductsSorted = products.stream()
.filter(Product::isActive)
.sorted(Comparator.comparing(Product::getName))
.collect(Collectors.toList());

Interfaces Funcionais principais:

  • Predicate<T>: Recebe um objeto e retorna boolean (usado em filter())
  • Function<T,R>: Transforma um objeto T em R (usado em map())
  • Consumer<T>: Consome um objeto T sem retornar nada (usado em forEach())
  • Supplier<T>: Fornece um objeto T sem receber parâmetros (lazy evaluation)

Streams API: Processamento Declarativo de Dados (Java 8)

A Streams API transforma operações complexas de manipulação de dados em pipelines legíveis e expressivos. Um pipeline consiste em:

  1. Fonte: A coleção original (.stream() ou .parallelStream())
  2. Operações Intermediárias (0 ou mais): filter(), map(), sorted(), distinct(), limit()
  3. Operação Terminal (1): collect(), forEach(), count(), reduce(), findFirst()
List<String> names = products.stream()            // 1. Fonte
.filter(product -> product.isActive()) // 2. Intermediária
.filter(product -> product.getPrice() > 100) // 2. Intermediária
.map(Product::getName) // 2. Transformação
.sorted() // 2. Ordenação
.collect(Collectors.toList()); // 3. Terminal

Operações de Agrupamento e Estatísticas:

// Agrupamento por categoria
Map<String, List<Product>> productsByCategory = products.stream()
.collect(Collectors.groupingBy(
product -> product.getCategory().getName()
));

// Estatísticas numéricas
IntSummaryStatistics priceStats = products.stream()
.collect(Collectors.summarizingInt(
product -> product.getPrice().intValue()
));

Parte 2: A Produtividade — Código Mais Limpo e Conciso

Records: A Forma Ideal para DTOs (Java 16)

Records eliminam o código boilerplate necessário para criar classes de dados imutáveis. O compilador gera automaticamente construtor, getters, equals(), hashCode() e toString().

// Abordagem moderna com Records — conciso e limpo
public record ProductDTO(Long id, String name, BigDecimal price,
boolean active, CategoryDTO category) {

// Métodos customizados quando necessário
public boolean isPremium() {
return price.compareTo(BigDecimal.valueOf(1000)) > 0;
}

// Validação no compact constructor
public ProductDTO {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Nome do produto não pode ser vazio");
}
if (price == null || price.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Preço deve ser positivo");
}
}
}
dica

Records são a ferramenta perfeita para representar os dados que trafegam nas suas APIs REST (JAX-RS). Eles garantem imutabilidade, reduzem drasticamente a verbosidade e são automaticamente serializáveis para JSON sem configuração adicional.

Text Blocks: Queries e JSON Legíveis (Java 15)

// Abordagem moderna com Text Blocks — limpo e legível
String sqlQuery = """
SELECT p.id, p.name, p.price, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.active = true
AND p.price BETWEEN ? AND ?
ORDER BY p.price DESC, p.name ASC
""";

Parte 3: Segurança e Performance Avançada

Pattern Matching (Java 16 a 21)

// Pattern matching elimina casting redundante
public String processPayment(Object payment) {
if (payment instanceof CreditCard cc) {
return "Processing credit card: " + cc.getNumber();
} else if (payment instanceof Pix pix) {
return "Processing PIX: " + pix.getKey();
}
return "Unknown payment method";
}

// Switch Expressions com Pattern Matching (Java 17+)
public BigDecimal calculateProcessingFee(PaymentMethod payment) {
return switch (payment) {
case CreditCard cc -> {
var fee = cc.getAmount().multiply(BigDecimal.valueOf(0.035));
yield cc.isInternational() ? fee.multiply(BigDecimal.valueOf(1.5)) : fee;
}
case Pix pix -> BigDecimal.ZERO;
case BankTransfer bt -> BigDecimal.valueOf(5.0);
case null -> throw new IllegalArgumentException("Payment cannot be null");
default -> throw new IllegalArgumentException("Unsupported: " + payment.getClass());
};
}

Virtual Threads: A Resposta para a Nuvem (Java 21)

Virtual Threads são threads extremamente leves gerenciadas pela JVM, resolvendo o maior gargalo de escalabilidade em aplicações web: o modelo de "uma requisição por thread do sistema operacional".

  • Threads de Plataforma (Tradicionais): Caras e limitadas em número (alguns milhares). Cada thread bloqueada por I/O consome recurso precioso.
  • Virtual Threads: Baratas e abundantes (milhões). Quando bloqueiam em I/O, a JVM as "desmonta" automaticamente e usa a thread do sistema para processar outra virtual thread.
important

Virtual Threads são a resposta direta da linguagem Java ao desafio da escalabilidade na nuvem. Jakarta EE 10 e runtimes como o Quarkus 3.31 estão adotando Virtual Threads para permitir escalabilidade massiva com hardware modesto.

// No Quarkus, você pode usar @RunOnVirtualThread
@GET
@Path("/process-heavy-task")
@RunOnVirtualThread
public Response processHeavyTask() {
// Esta requisição será processada em uma Virtual Thread
var result = heavyIOBoundOperation();
return Response.ok(result).build();
}

Exercícios (Checkpoints)

  1. Explique a diferença entre ArrayList e LinkedList e quando usar cada uma.
  2. Dada uma lista de produtos com nome, preço e categoria, escreva um pipeline Streams que retorne os 3 produtos mais caros de cada categoria, ordenados por preço decrescente.
  3. Crie um record ClienteDTO(Long id, String nome, String email, LocalDate dataNascimento) com validação no compact constructor que rejeite emails sem @ e datas de nascimento futuras.
  4. Reescreva o seguinte código imperativo usando Pattern Matching com switch:
    if (obj instanceof String s) { return s.length(); }
    else if (obj instanceof Integer i) { return i; }
    else if (obj instanceof List<?> l) { return l.size(); }
    else { return -1; }
  5. Pesquise sobre Virtual Threads e explique por que elas são mais eficientes que threads tradicionais para operações I/O-bound.

Referências