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
| Tipo | Descrição | Casos de Uso Típicos | Tamanho |
|---|---|---|---|
byte | Inteiro de 8 bits (-128 a 127) | Flags, estados binários, dados de rede | 1 byte |
short | Inteiro de 16 bits | Contadores pequenos, índices limitados | 2 bytes |
int | Inteiro de 32 bits | Identificadores, contadores gerais, índices | 4 bytes |
long | Inteiro de 64 bits | Timestamps, IDs de banco de dados | 8 bytes |
float | Ponto flutuante 32 bits | Gráficos, jogos, cálculos com precisão moderada | 4 bytes |
double | Ponto flutuante 64 bits | Cálculos científicos, engenharia | 8 bytes |
boolean | Valor lógico (true/false) | Flags de controle, condições, validações | 1 bit |
char | Caractere Unicode 16 bits | Manipulação de texto baixo nível, parsers | 2 bytes |
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:
| Interface | Descrição | Complexidade Típica |
|---|---|---|
java.util.List | Coleçã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.Set | Coleção sem duplicatas. Implementações: HashSet (O(1)), TreeSet (O(log n)), LinkedHashSet (O(1) + ordem) | HashSet: O(1) para add/contains |
java.util.Map | Estrutura chave-valor. Implementações: HashMap (O(1)), TreeMap (O(log n)), ConcurrentHashMap (thread-safe) | HashMap: O(1) para get/put |
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 emfilter())Function<T,R>: Transforma um objeto T em R (usado emmap())Consumer<T>: Consome um objeto T sem retornar nada (usado emforEach())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:
- Fonte: A coleção original (
.stream()ou.parallelStream()) - Operações Intermediárias (0 ou mais):
filter(),map(),sorted(),distinct(),limit() - 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");
}
}
}
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.
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)
- Explique a diferença entre
ArrayListeLinkedListe quando usar cada uma. - 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.
- 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. - 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; } - Pesquise sobre Virtual Threads e explique por que elas são mais eficientes que threads tradicionais para operações I/O-bound.