Pular para conteúdo

Java Avançado

Recursos de Java que vão além da sintaxe básica — concorrência, reflection, módulos, serialização, I/O e logging. A base da linguagem está em Java, os conceitos de OO em Orientação a Objetos e os padrões de projeto em Padrões Arquiteturais.

Generics avançados

Sem generics, coleções guardam Object e exigem casts manuais — erros só aparecem em tempo de execução (ClassCastException). Com eles, o compilador valida os tipos.

class Caixa<T> {                                     // classe genérica
    private T valor;
    void setValor(T valor) { this.valor = valor; }
    T getValor() { return valor; }
}
public static <T> T primeiro(List<T> lista) { return lista.get(0); }   // método genérico
Recurso Sintaxe Uso
Tipo limitado (bounded type) <T extends Number> Restringe os tipos aceitos (T tem de ser Number ou subtipo)
Wildcard ? List<?> Tipo desconhecido (leitura de qualquer lista)
Limite superior List<? extends Number> Aceita Number e subtipos; só leitura (producer)
Limite inferior List<? super Integer> Aceita Integer e supertipos; escrita (consumer)
Diamante new ArrayList<>() O compilador infere o tipo (Java 7+)

Definição: PECS

Producer Extends, Consumer Super: use ? extends T quando a estrutura fornece elementos (você só lê) e ? super T quando ela consome elementos (você escreve).

Definição: Type erasure (apagamento de tipos)

Os tipos genéricos existem só em tempo de compilação: em execução, List<String> vira List e a informação do tipo desaparece. Por isso não se pode usar primitivos (List<int> — use Integer) nem criar new T().

enum em detalhe

enum Nivel {
    BASICO(1, "Básico"), INTERMEDIARIO(2, "Intermediário"), AVANCADO(3, "Avançado");
    private final int codigo;
    private final String descricao;
    private Nivel(int codigo, String descricao) { this.codigo = codigo; this.descricao = descricao; }
    public int getCodigo() { return codigo; }
}

Nivel n = Nivel.valueOf("AVANCADO");       // IllegalArgumentException se o nome não existir
for (Nivel x : Nivel.values()) System.out.println(x);
  • Compare enums com == (seguro); funcionam em switch.
  • ordinal() devolve a posição, mas não use como identificador persistente: a ordem pode mudar quando o enum evoluir — grave um código explícito (como codigo acima).
  • Bons usos: estados (pedido PENDENTE/ENVIADO/ENTREGUE), níveis, tipos de usuário, dias da semana, configurações. Trate entradas externas, pois valueOf pode lançar exceção.

Datas e horas: cuidados

Complementa java.time.

Classe Representa
LocalDate / LocalTime / LocalDateTime Data / hora / ambas, sem fuso horário
Instant Instante na linha do tempo em UTC
ZonedDateTime Data e hora com fuso
Period x Duration Período (dias/meses/anos) x duração (horas/minutos/segundos)

Evite as antigas Date e Calendar; considere fuso horário e horário de verão em aplicações globais; os objetos são imutáveis (plusDays devolve um novo objeto). Armazene instantes em UTC e converta para exibir.

Arquivos e I/O

Path path = Paths.get("dados.txt");
String texto = Files.readString(path);                      // UTF-8 por padrão
List<String> linhas = Files.readAllLines(path);
Files.writeString(path, "Execução concluída!\n",
        StandardOpenOption.CREATE, StandardOpenOption.APPEND);   // CREATE, APPEND, TRUNCATE_EXISTING

try (Stream<String> s = Files.lines(path)) {                // lê em fluxo: arquivos grandes
    s.filter(l -> l.contains("Java")).forEach(System.out::println);
}
try (BufferedReader br = Files.newBufferedReader(path)) { /* linha a linha */ }
  • Operações de arquivo lançam IOException: trate com try-catch ou declare throws.
  • Files.lines devolve um Stream que deve ser fechado (use try-with-resources).
  • Segurança (path traversal): valide caminhos recebidos do usuário — base.resolve(entrada).normalize() e verifique que startsWith(base), impedindo ../../config.txt.

Lambdas e interfaces funcionais

Complementa Expressões lambda e Streams.

Interface Método Função
Predicate<T> boolean test(T) Condição (filtrar)
Function<T,R> R apply(T) Transformar
Consumer<T> void accept(T) Executar uma ação
Supplier<T> T get() Fornecer um valor
BiFunction<T,U,R> R apply(T,U) Transformar dois valores
Function<String, String> upper = String::toUpperCase;          // Classe::método
Consumer<String> printer = System.out::println;                // objeto::método
BiFunction<String, Integer, Pessoa> criador = Pessoa::new;     // Classe::new

Usos: Comparator, filter/map/forEach de coleções, callbacks e tratamento de eventos. Use com sabedoria: nomes e expressões continuam precisando ser legíveis.

Streams: operações

Um stream é um fluxo de elementos (não uma coleção) e só pode ser consumido uma vez. O pipeline tem uma fonte, operações intermediárias (preguiçosas — só executam quando há operação terminal) e uma operação terminal.

Operação Tipo Efeito
filter(Predicate) Intermediária Mantém só o que satisfaz a condição
map(Function) Intermediária Transforma cada elemento
sorted() / distinct() Intermediária Ordena / remove duplicados
reduce(0, Integer::sum) Terminal Combina tudo em um valor
collect(...) Terminal Coleta em List, Set, Map (Collectors.toMap)
List<Integer> r = List.of(5, 3, 1, 4, 3).stream().distinct().sorted().toList();   // [1, 3, 4, 5]

Evite efeitos colaterais (não altere estado externo dentro do pipeline). O parallelStream() usa várias threads, mas exige cuidado com a ordem e com operações não associativas.

Anotações e Reflection

Anotações customizadas

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MinhaAnotacao {
    String valor() default "";
    int prioridade() default 0;
}
Meta-anotação Valores
@Retention (até quando existe) SOURCE (só no código-fonte), CLASS (no .class, padrão), RUNTIME (disponível em execução, por reflection)
@Target (onde pode ser usada) TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, PACKAGE

Anotações não alteram diretamente o comportamento do programa — fornecem metadados que o compilador, ferramentas e frameworks (Spring, JPA, JUnit, Jackson) leem, em compilação (annotation processing) ou em execução (reflection).

Reflection

Definição: Reflection

API que permite inspecionar e manipular classes, métodos, campos e construtores em tempo de execução, mesmo sem conhecê-los em tempo de compilação.

Class<?> c = Pessoa.class;                       // ou obj.getClass(), Class.forName("pacote.Pessoa")
Method m = c.getMethod("somar", int.class, int.class);
Object r = m.invoke(new Calculadora(), 3, 5);     // 8

Field campo = Pessoa.class.getDeclaredField("idade");
campo.setAccessible(true);                        // acessa membro privado
campo.set(pessoa, 30);

Pessoa p = (Pessoa) c.getConstructor(String.class, int.class).newInstance("Ana", 25);
  • Aplicações: Spring (injeção de dependência), Hibernate/JPA (mapeamento), Jackson (serialização JSON), JUnit, plugins e descoberta de anotações.
  • Cautelas: é mais lenta que chamadas diretas, quebra o encapsulamento (setAccessible) e pode falhar com o sistema de módulos (Java 9+); trate NoSuchMethodException, IllegalAccessException e InvocationTargetException; use só quando realmente necessário.

Optional em detalhe

Complementa Optional.

Optional<String> a = Optional.of("Java");          // exige valor não nulo
Optional<String> b = Optional.ofNullable(valor);   // aceita null
Optional<String> c = Optional.empty();

b.isPresent();   b.isEmpty();                      // isEmpty: Java 11+
b.orElse("padrão");                                // sempre avaliado
b.orElseGet(() -> buscarPadrao());                 // só se vazio (preferir se for custoso)
b.orElseThrow(() -> new IllegalStateException("Valor ausente"));

Optional<Integer> tam = b.map(String::length);     // transforma
Optional<Integer> r = b.flatMap(s -> Optional.of(s.length()));  // evita Optional<Optional<T>>
b.filter(s -> s.length() > 3);                     // mantém só se a condição for verdadeira

Boas práticas: não usar como campo de classe nem parâmetro de método; usar como tipo de retorno; evitar get() direto; preferir orElseGet a orElse quando o valor padrão for custoso; lembrar que Optional não é serializável (evite em entidades).

Threads e concorrência

Definição: Processo x Thread

Processo: programa em execução, com recursos próprios (memória, arquivos). Thread: linha de execução dentro de um processo; várias threads compartilham os recursos do mesmo processo e executam tarefas ao mesmo tempo.

class MinhaTarefa implements Runnable {            // preferível a estender Thread
    @Override public void run() { System.out.println("Executando..."); }
}
Thread t = new Thread(new MinhaTarefa());
t.start();      // cria uma nova thread
// t.run();     // ERRADO: executa no thread atual, sem criar thread nova
  • Runnable permite herdar de outra classe e se integra a executors.
  • Estados: NEW → RUNNABLE → RUNNING → BLOCKED/WAITING → TERMINATED (uma thread finalizada não pode ser reiniciada).
  • Thread.sleep(ms) pausa (lança InterruptedException); join() espera outra thread terminar; interrupt() solicita a interrupção de forma cooperativa — a thread deve checar isInterrupted() e tratar a InterruptedException.

Sincronização

Definição: Race condition

Condição de corrida: várias threads acessam e alteram o mesmo dado e o resultado depende da ordem de execução (ex.: dois contador++ simultâneos podem gravar 1 em vez de 2).

Recurso Função
synchronized Exclusão mútua: uma thread por vez no método ou bloco (synchronized (objeto) { ... }); o lock é do objeto
Lock (ReentrantLock) Mais flexível: tryLock com timeout, interrompível, fairness; sempre unlock() em finally
volatile Garante visibilidade da variável entre threads; não torna operações compostas atômicas (útil para flags)
Classes atômicas (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference) Operações atômicas sem bloqueio: contador.incrementAndGet()
private final AtomicInteger contador = new AtomicInteger(0);
public void inc() { contador.incrementAndGet(); }

Lock lock = new ReentrantLock();
lock.lock();
try { /* seção crítica */ } finally { lock.unlock(); }

Definição: Deadlock

Threads bloqueadas para sempre, cada uma esperando um recurso que a outra segura. Prevenção: adquira os locks sempre na mesma ordem, evite segurá-los por muito tempo e minimize o uso de locks.

ExecutorService e Future

Em aplicações reais, prefira pools de threads a criar Thread manualmente.

ExecutorService executor = Executors.newFixedThreadPool(4);

Callable<Integer> tarefa = () -> { Thread.sleep(1000); return 42; };   // devolve resultado
Future<Integer> future = executor.submit(tarefa);

try {
    Integer r = future.get(2, TimeUnit.SECONDS);       // espera, com timeout
} catch (TimeoutException e) { System.out.println("Tempo esgotado!"); }

executor.shutdown();                                    // encerra graciosamente
executor.awaitTermination(10, TimeUnit.SECONDS);
// shutdownNow() tenta cancelar as tarefas em andamento
Fábrica (Executors) Pool
newFixedThreadPool(n) Tamanho fixo; fila ilimitada
newCachedThreadPool() Cria threads sob demanda
newSingleThreadExecutor() Uma thread; execução em ordem FIFO
newScheduledThreadPool(n) Tarefas com atraso ou periódicas

Callable é como Runnable, mas retorna resultado e pode lançar exceção verificada. Future representa o resultado futuro (isDone, get, cancelamento). Sempre finalize o executor para liberar recursos.

CompletableFuture: fluxos assíncronos não bloqueantes

CompletableFuture<String> f = CompletableFuture
        .supplyAsync(() -> "id")                        // runAsync se não devolve valor
        .thenCompose(id -> buscarUsuario(id))           // encadeia outra tarefa assíncrona
        .thenApply(String::toUpperCase)                 // transforma o resultado
        .exceptionally(ex -> "valor alternativo");      // erro: valor padrão

CompletableFuture<String> c = f1.thenCombine(f2, (a, b) -> a + b);   // combina duas independentes
Método Quando
thenApply Transforma o resultado (função simples)
thenCompose A próxima etapa também devolve um CompletableFuture (evita aninhamento)
thenCombine Combina o resultado de duas tarefas independentes
exceptionally Trata o erro devolvendo um valor alternativo
handle Trata resultado e exceção (sempre executado)

Passe um Executor próprio para operações de I/O longas (evita sobrecarregar o ForkJoinPool comum). Boas práticas gerais de threads: não compartilhe estado sem proteção, use sincronização só quando necessário, cheque o estado de interrupção, finalize recursos e prefira executors a threads manuais.

Serialização

Definição: Serialização

Converter um objeto em uma sequência de bytes (para gravar em arquivo, enviar pela rede, guardar em cache) e reconstruí-lo depois (desserialização). Em Java nativo, a classe implementa a interface marcadora Serializable.

public class Usuario implements Serializable {
    private static final long serialVersionUID = 1L;
    private String nome;
    private transient String senha;        // transient: NÃO é serializado
}

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("usuario.bin"))) {
    oos.writeObject(usuario);
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("usuario.bin"))) {
    Usuario u = (Usuario) ois.readObject();
}
  • serialVersionUID: identifica a versão da classe; defina-o explicitamente (evita InvalidClassException quando a classe evolui) e mantenha compatibilidade (adicione campos, evite remover).
  • transient: exclui campos sensíveis ou calculados; após desserializar, ficam com o valor padrão (null, 0, false).
  • Riscos: nunca desserialize dados não confiáveis — objetos maliciosos podem executar código (gadget chains); valide e filtre o que entra.
  • Alternativa moderna: JSON com Jackson/Gson — legível, interoperável, mais seguro, formato padrão de APIs. Use DTOs (ou records) para transferir só os dados necessários.
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(usuario);            // serializa
Usuario u = mapper.readValue(json, Usuario.class);           // desserializa

JUnit 5 e testes unitários

Complementa Qualidade.

  • Um teste unitário verifica uma pequena unidade (método ou classe) de forma automática e isolada: detecta erros cedo, facilita a manutenção e aumenta a confiança.
  • Padrão AAA: Arrange (prepara o cenário), Act (executa) e Assert (verifica).
@Test
void deveCalcularTotal() {
    Pedido pedido = new Pedido(100.0);                 // Arrange
    double total = pedido.calcularTotal();             // Act
    assertEquals(100.0, total);                        // Assert
}

@ParameterizedTest
@ValueSource(ints = {2, 4, 6})
void deveSerPar(int numero) { assertTrue(numero % 2 == 0); }
  • Assertions: assertEquals, assertTrue, assertFalse, assertNull, assertNotNull, assertThrows. Testes parametrizados rodam o mesmo teste com várias entradas (@ValueSource, @CsvSource, @MethodSource, @EnumSource).
  • Mocks e fakes simulam dependências (Mockito: @Mock, when(...).thenReturn(...)).
  • Cobertura (JaCoCo, IntelliJ, SonarQube) mostra quanto do código os testes executam — não substitui a qualidade dos testes; cubra regras de negócio e casos relevantes.
  • Cada teste deve ser independente (sem depender da ordem, de banco real ou de recursos externos), rápido e determinístico. Ciclo TDD: vermelho → verde → refatorar.

Qualidade e build

Ferramenta Função
Maven (pom.xml) Build e dependências; ciclo de vida: validate, compile, test, package, verify, install, deploy
Gradle (build.gradle) Build flexível e incremental; suporta multiprojetos; scripts em Groovy ou Kotlin
Dependências Resolvidas e baixadas automaticamente; escopos (compile, test, runtime); verifique conflitos
JAR Resultado do build; java -jar app.jar; o JAR "fat" inclui as dependências (ex.: Spring Boot)
Git Versionamento; mensagens claras; fluxo de branches
CI (GitHub Actions, GitLab CI, Jenkins) Executa build e testes a cada alteração e gera o artefato
Análise estática (SonarQube, Checkstyle, PMD, SpotBugs) Detecta má práticas e vulnerabilidades

(CI/CD e pipelines em Spring — CI/CD.)

Memória e GC: não dependa do coletor

O garbage collector libera objetos sem referência, mas você não controla quando ele roda. Algumas consequências práticas:

  • Não chame System.gc() esperando liberar memória: é só uma sugestão, a JVM pode ignorá-la (ou ela pode ser desabilitada por parâmetro), e forçá-la em laços causa pausas desnecessárias.
  • Evite finalize() (obsoleto desde o Java 9 e marcado para remoção): não há garantia de quando ou se será executado, pode "ressuscitar" o objeto, atrasa a coleta e dificulta o raciocínio. Para liberar recursos (arquivos, conexões, sockets), exponha um método close() e use try-with-resources com AutoCloseable. Em casos raros, use java.lang.ref.Cleaner.
  • Vazamentos de memória existem em Java: são referências que o programa mantém sem precisar — coleções estáticas que só crescem, caches sem limite, listeners nunca removidos, ThreadLocal não limpo em thread pools. O GC não coleta o que ainda é "alcançável". Diagnóstico: heap dump e análise de retenção (Ferramentas da JVM).
  • Referências fracas (WeakReference, WeakHashMap, SoftReference) não impedem a coleta do objeto; úteis para caches que podem ser descartados sob pressão de memória.
  • JIT e otimização adaptativa: boa parte do desempenho do Java vem de otimizar o código conforme o uso real (inlining de métodos pequenos, remoção de código morto). Por isso métodos pequenos e simples costumam ser bons para o JIT, e microbenchmarks sem warm-up enganam (use JMH).

ClassLoader e o classloader hell

Definição: ClassLoader

ClassLoader é o objeto da JVM que carrega classes para a memória a partir dos bytes do bytecode (java.lang.ClassLoader). Uma classe é identificada na JVM pelo nome completo + o ClassLoader que a carregou: a mesma classe carregada por dois ClassLoaders diferentes são, para a JVM, tipos distintos.

Hierarquia padrão (cada um tem um pai, exceto o primeiro):

ClassLoader Carrega Observação
Bootstrap Classes centrais (java.lang.*, etc.) Sem pai; impede que alguém substitua String
Platform/Extension Módulos e extensões da plataforma No Java 8: Extension; no 9+: Platform
Application/System O classpath da aplicação (-cp) Pai: o anterior

Delegação ao pai: antes de tentar carregar uma classe, o ClassLoader pergunta ao pai (modelo parent-first). Isso protege as classes centrais, mas explica problemas clássicos.

O problema (classloader hell): servidores de aplicação rodam várias aplicações na mesma JVM com um ClassLoader por aplicação. Se alguém copia um JAR para um diretório compartilhado (ou para o classpath global), ele passa a ser carregado pelo ClassLoader pai e vence a versão que cada aplicação trouxe. Sintomas:

  • NoSuchMethodError / NoSuchFieldError / AbstractMethodError: a aplicação nova chama um método que só existe na versão mais nova, mas a JVM carregou a antiga.
  • ClassCastException "X cannot be cast to X": duas cópias da mesma classe, de ClassLoaders diferentes.
  • LinkageError, NoClassDefFoundError e comportamento errático conforme a ordem em que classes foram carregadas.
  • Vazamento de Metaspace em redeploys a quente: classes e ClassLoaders antigos não são liberados porque algo ainda os referencia (ThreadLocal, threads, drivers JDBC).

Como mitigar:

  • Cada aplicação traz suas dependências (WEB-INF/lib), e nada vai para diretórios compartilhados do servidor. Muitos contêineres Web usam o modelo invertido (parent-last) para a aplicação, justamente para que a versão que ela empacotou prevaleça (conforme a especificação de servlets).
  • Entender a árvore de dependências (mvn dependency:tree, gradle dependencies) e excluir versões conflitantes; usar dependency management (BOM) para alinhar versões (Governança de dependências).
  • Shading/relocation (renomear pacotes de uma biblioteca dentro do JAR final) quando duas versões precisam coexistir.
  • Módulos (JPMS) e imagens fat jar ou contêineres (uma aplicação por JVM) eliminam grande parte do problema (JPMS).
  • Diagnóstico: -verbose:class (de onde cada classe foi carregada), jcmd VM.classloader_stats.

Pode-se também criar ClassLoaders próprios (URLClassLoader) para plugins e hot swap: o segundo argumento do construtor decide o pai (null = Bootstrap), o que muda se classes comuns são vistas ou não.

Módulos Java (JPMS)

Definição: Módulo Java

Unidade de organização (Java 9+) que agrupa pacotes e declara, no arquivo module-info.java (na raiz de src/main/java), do que depende e o que expõe. Fortalece o encapsulamento entre grandes partes da aplicação.

module com.exemplo.app {
    requires java.logging;                 // depende de outro módulo
    requires transitive java.sql;          // quem usa este módulo também lê java.sql
    exports com.exemplo.api;               // só estes pacotes ficam visíveis a outros módulos
    opens com.exemplo.modelo to spring.core;   // abre acesso por reflection (Spring, JPA)
}
Diretiva Efeito
requires Declara a dependência de outro módulo (JDK ou de terceiros)
requires transitive A dependência também é "herdada" por quem usa este módulo
exports Torna públicos os pacotes para outros módulos (pacote não exportado fica encapsulado)
opens Permite acesso profundo por reflection (diferente de exports, que expõe a API)

Executa-se com java --module-path mods -m com.exemplo.app/.... A ferramenta jlink cria uma imagem de runtime personalizada só com os módulos necessários (menor e mais segura). Módulos nomeados têm module-info.java; o código no classpath forma o módulo "sem nome".

Configuração e ambientes

Mecanismo Uso
.properties chave=valor, comentários com #; carregado do classpath
YAML Hierárquico e legível (muito usado no Spring Boot)
Variáveis de ambiente System.getenv("APP_NOME") — independentes do código; úteis em contêineres
Propriedades da JVM -Dapp.nome=MeuProjeto lidas com System.getProperty("app.nome", "padrão")
Perfis dev, test, prod: configuração separada por ambiente
Segredos Senhas e chaves em variáveis de ambiente ou cofre — nunca no Git

Valide as configurações na inicialização (falhar cedo, com mensagem clara, se uma variável obrigatória estiver ausente). Detalhes em Spring — Configuração.

Logging

O SLF4J é a fachada de logging (API simples, independente da implementação); Logback é a implementação padrão, usada pelo Spring Boot.

private static final Logger log = LoggerFactory.getLogger(PedidoService.class);

log.info("Processando pedido {} para o usuário {}", pedidoId, usuario);   // placeholders {}
log.error("Falha ao processar o pedido {}", pedidoId, ex);
  • Níveis: TRACE, DEBUG, INFO, WARN, ERROR; em produção, evite TRACE/DEBUG.
  • Placeholders {} evitam concatenação desnecessária de strings.
  • MDC (Mapped Diagnostic Context): adiciona contexto (ex.: correlationId) a todos os logs da requisição/thread — rastreia fluxos em sistemas distribuídos; remova o contexto ao final.
  • Rotação de logs: por tamanho/data, com compressão e retenção máxima, para não encher o disco.
  • Boas práticas: registre eventos relevantes, use o nível correto, nunca registre dados sensíveis (senhas, tokens), inclua contexto e mantenha o formato consistente. Observabilidade completa em SRE.

HTTP e APIs REST em Java

Teoria de HTTP e REST em Backend. O java.net.http.HttpClient (Java 11+) é o cliente moderno da biblioteca padrão.

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .connectTimeout(Duration.ofSeconds(10))
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.exemplo.com/dados"))
        .header("Accept", "application/json")
        .timeout(Duration.ofSeconds(30))
        .GET()                                          // .POST(BodyPublishers.ofString(json))
        .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();                     // 200, 201, 404, 500...
  • Assíncrono: client.sendAsync(...) devolve um CompletableFuture (não bloqueia).
  • Tratamento de erros: IOException, InterruptedException e códigos de status inesperados; defina timeouts de conexão e de leitura.
  • O HttpClient é imutável e reutilizável.
  • JSON: ObjectMapper (Jackson) converte objetos ↔ JSON; records são ótimos DTOs; use Bean Validation para validar os dados recebidos e @JsonInclude para campos opcionais.

Arquitetura de aplicações Java

Princípios de organização (detalhes em Padrões Arquiteturais e Boas Práticas):

Tema Resumo
Camadas controller (apresentação) → service (regras de negócio) → repository (acesso a dados) → model (entidades/DTOs)
MVC Model (dados e regras), View (apresentação), Controller (recebe requisições e coordena)
Hexagonal Domínio isolado, com portas (interfaces) e adaptadores externos
SOLID Cinco princípios para código manutenível
DTO Objeto de transferência entre camadas, evitando expor entidades
Injeção de dependência O contêiner (Spring) fornece as dependências, reduzindo acoplamento; prefira injeção por construtor
Coesão e acoplamento Alta coesão (uma responsabilidade por módulo) e baixo acoplamento (interfaces em vez de implementações)

Padrões de projeto mais comuns

Padrão Propósito
Singleton Uma única instância com ponto global de acesso (cuidado com estado global)
Factory Centraliza a criação de objetos, desacoplando o cliente do tipo concreto
Builder Constrói objetos complexos passo a passo, evitando construtores telescópicos
Strategy Família de algoritmos intercambiáveis em tempo de execução
Observer Notifica automaticamente os observadores quando o estado muda
Adapter Converte uma interface em outra esperada pelo cliente

Aplique um padrão somente quando houver o problema que ele resolve: evite engenharia excessiva e prefira a simplicidade. Cada padrão é detalhado, com diagramas e exemplos, em Padrões Arquiteturais.

JVM e desempenho

Definição: Heap, stack e Garbage Collector

Heap: memória principal onde ficam os objetos e arrays, dividida em gerações (young e old); tamanho configurável com -Xms (inicial) e -Xmx (máximo). Stack: pilha de cada thread, com um quadro por chamada de método (variáveis locais e parâmetros); tamanho com -Xss; recursão profunda causa StackOverflowError. Garbage Collector (GC): libera automaticamente objetos que não têm mais referências.

java -Xms256m -Xmx2g -Xss1m -jar minhaapp.jar
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar minhaapp.jar
java -Xlog:gc*:file=gc.log:time,level,tags -jar minhaapp.jar    # log detalhado de GC
Tema Resumo
G1 (Garbage First) Coletor padrão desde o Java 9: coleta incremental e paralela, divide o heap em regiões e prioriza as com mais lixo; pausas previsíveis (ideal para baixa latência)
JIT (Just-In-Time) Compila o bytecode em código nativo durante a execução, otimizando os métodos mais usados (hot spots); melhora com o tempo ("aquecimento")
Latência x throughput Latência: tempo de uma operação; throughput: operações por unidade de tempo. Há trade-off entre eles — escolha pelo objetivo
Pools Reutilizam recursos caros (ExecutorService, HikariCP, buffers); ajuste o tamanho conforme a carga
Boas práticas Usar versão LTS recente, ajustar -Xms/-Xmx, evitar alocação excessiva de objetos, monitorar GC e medir antes de otimizar (teste com carga realista)

Profiling: use o Java Flight Recorder (JFR) (-XX:StartFlightRecording=duration=60s, filename=perfil.jfr), VisualVM ou JProfiler para achar gargalos de CPU, memória e threads.

Ferramentas da JVM

Ferramenta Função
jps Lista JVMs em execução (PID e classe principal)
jstack <pid> Thread dump: mostra o estado das threads e detecta deadlocks
jmap -dump:format=b,file=heap.hprof <pid> Heap dump para investigar vazamentos (analisar no VisualVM ou Eclipse MAT)
jcmd <pid> ... Ferramenta versátil: VM.version, Thread.print, GC.heap_dump
jconsole / VisualVM Monitoramento gráfico (CPU, memória, threads, GC; JMX)
JFR Coleta contínua com baixo overhead; análise no JDK Mission Control

Roteiro de diagnóstico: identificar o PID (jps) → verificar CPU/memória → thread dump (jstack) para travamentos → heap dump (jmap) para vazamentos → analisar.

GraalVM e Native Image

Definição: GraalVM e Native Image

GraalVM é uma JVM de alto desempenho com a ferramenta native-image, que compila a aplicação antecipadamente (AOT) em um executável nativo que dispensa a JVM em produção. Resultado: inicialização em milissegundos e menor uso de memória (ideal para serverless e contêineres).

native-image -jar minha-app.jar minha-app      # gera o executável
./mvnw -Pnative native:compile                  # Spring Boot
  • Limitações: análise estática do código — reflection, proxies e carregamento dinâmico exigem configuração (arquivos JSON ou hints); build mais lento; nem toda biblioteca é compatível. Frameworks (Spring, Quarkus) geram parte da configuração.
  • Compare: JVM tradicional ~2–5 s de startup; nativo ~dezenas de ms.

Java moderno (17 a 21)

Linha do tempo (versões LTS): Java 8 (2014: lambdas, streams), 11 (2018), 17 (2021), 21 (2023). Principais recursos recentes:

record avançado

public record Cliente(Long id, String nome, String email) {
    public Cliente {                                   // construtor compacto: validação
        Objects.requireNonNull(nome, "nome obrigatório");
        email = email.trim().toLowerCase();            // normaliza o parâmetro
    }
    public String dominio() { return email.substring(email.indexOf('@') + 1); }
    public static Cliente vazio() { return new Cliente(0L, "", "x@x"); }
}

Pode implementar interfaces, ter métodos e static; é imutável. Limites: não estende outras classes, campos são final, não serve como entidade JPA (precisa de construtor sem argumentos e campos mutáveis). Ótimo para DTOs.

sealed (classes seladas)

public sealed interface Forma permits Circulo, Retangulo, Quadrado {}
public record Circulo(double raio) implements Forma {}
public record Retangulo(double l, double a) implements Forma {}
public final class Quadrado implements Forma { /* ... */ }

sealed restringe quais tipos podem estender/implementar; permits os lista; as subclasses devem ser final, sealed ou non-sealed (abertas). Modela domínios fechados (tipos de pagamento, estados de pedido) e permite ao compilador verificar a exaustividade de um switch.

Pattern matching

if (obj instanceof String s && s.length() > 3) { System.out.println(s.toUpperCase()); }

String descricao = switch (obj) {
    case Integer i when i > 10 -> "Inteiro grande: " + i;     // guarda (when)
    case Integer i -> "Inteiro: " + i;
    case String s -> "Texto: " + s;
    case null -> "Valor nulo";                                 // trata null
    default -> "Outro";
};

double area(Forma f) {                                         // sem default: sealed é exaustivo
    return switch (f) {
        case Circulo c -> Math.PI * c.raio() * c.raio();
        case Retangulo r -> r.l() * r.a();
        case Quadrado q -> q.lado() * q.lado();
    };
}

instanceof com padrão (Java 16) elimina o cast; switch com padrões (Java 21) substitui cadeias de if/instanceof. Cuidado com a ordem dos case (do mais específico ao mais geral) e mantenha as guardas simples.

Threads virtuais (Project Loom, Java 21)

Definição: Thread virtual

Thread leve, gerenciada pela JVM (não pelo SO), com custo de memória muito baixo: é possível ter milhares ou milhões. Quando uma thread virtual bloqueia (I/O, banco), a JVM a desmonta da thread de plataforma, liberando-a para outras tarefas.

Thread.startVirtualThread(() -> System.out.println("virtual"));
Thread t = Thread.ofVirtual().name("v-1").start(() -> processar());

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) executor.submit(() -> tratarRequisicao());
}

Mantém o modelo imperativo (sem programação reativa) e é compatível com o código existente. Ideal para servidores com muita concorrência e I/O bloqueante. Não use pools de threads virtuais (crie uma por tarefa). Não acelera tarefas de CPU intensivo.

Concorrência estruturada (preview no Java 21)

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<Usuario> u = scope.fork(() -> buscarUsuario(id));
    Subtask<Pedidos> p = scope.fork(() -> buscarPedidos(id));
    scope.join().throwIfFailed();                        // espera e propaga a primeira falha
    return new Perfil(u.get(), p.get());
}

Trata um grupo de subtarefas como uma unidade com ciclo de vida definido: se uma falha (ShutdownOnFailure) ou termina (ShutdownOnSuccess), as demais são canceladas; evita vazamento de threads. Recurso em evolução — verifique a versão.

Programação reativa

Definição: Programação reativa

Paradigma baseado em fluxos de dados assíncronos e não bloqueantes, com backpressure (o consumidor controla o ritmo do produtor). Baseia-se em Publisher, Subscriber, Subscription e Processor (Reactive Streams).

Tipo (Reactor) Emite
Mono<T> 0 ou 1 elemento (resposta única)
Flux<T> 0 a N elementos (sequências, eventos)
Flux.range(1, 5).filter(n -> n % 2 == 0).map(n -> n * 10)
    .subscribe(System.out::println);                    // 20, 40
Mono.just("Ana").map(String::toUpperCase).onErrorReturn("padrão");
  • Operadores: map, filter, flatMap, take, zip, merge, retry, timeout, onErrorResume... Nada acontece até alguém fazer subscribe.
  • Spring WebFlux: stack reativa do Spring sobre Netty (event loop, poucas threads): @RestController com Mono/Flux ou RouterFunction + Handler; cliente WebClient (substitui o RestTemplate).
  • Bloqueante x reativo: Servlet/Spring MVC usa uma thread por requisição (simples); reativo usa poucas threads para muitas conexões (mais escalável, mais complexo). Hoje, threads virtuais são uma alternativa que mantém o código imperativo.

Collections avançadas

Estrutura Complexidade típica Observação
ArrayList acesso O(1); inserção/remoção no meio O(n) Melhor padrão para listas
LinkedList acesso O(n); inserção nas pontas O(1) Raramente melhor que ArrayDeque
HashMap / HashSet busca/inserção O(1) médio Sem ordem
TreeMap / TreeSet O(log n) Mantêm ordem das chaves
ArrayDeque pontas O(1) Fila e pilha
  • ConcurrentHashMap: Map thread-safe de alto desempenho (bloqueio granular): putIfAbsent, compute, merge atômicos.
  • Coleções imutáveis: List.of(...), Set.of(...), Map.of(...) (lançam UnsupportedOperationException ao alterar); Collections.unmodifiableList é só uma visão.
  • Comparator: Comparator.comparing(Produto::getPreco).thenComparing(Produto::getNome).

Streams avançados

Operação Uso
flatMap "Achata" coleções internas em um só fluxo (pessoas.stream().flatMap(p -> p.getEmails().stream()))
Collectors.groupingBy(chave) Agrupa em Map<K, List<V>>; combine com counting(), summingDouble(), mapping()
Collectors.partitioningBy(pred) Divide em duas listas (true/false)
Collectors.reducing, toMap, joining Reduções e agrupamentos prontos

parallelStream()

Processa em várias threads via ForkJoinPool comum (número de núcleos − 1). Use só para coleções grandes e operações independentes, sem estado compartilhado e custosas (CPU); há overhead de divisão — meça (JMH). Não use para I/O bloqueante nem quando a ordem importa (forEachOrdered). Use coletores concorrentes (groupingByConcurrent) com cuidado.

NIO.2: arquivos e diretórios

Complementa Arquivos e I/O.

Path dir = Path.of("docs");
Files.createDirectories(dir);
try (Stream<Path> paths = Files.list(dir)) { paths.forEach(System.out::println); }
Files.walk(dir)                                          // percorre recursivamente
Files.copy(origem, destino, StandardCopyOption.REPLACE_EXISTING);
Files.move(origem, destino, StandardCopyOption.ATOMIC_MOVE);
  • Path é imutável e portável (resolve, relativize, normalize, getFileName).
  • WatchService monitora criação, alteração e remoção de arquivos em um diretório.
  • Permissões POSIX: Files.setPosixFilePermissions (nem todo sistema suporta).

Redes e sockets

try (Socket socket = new Socket("exemplo.com", 8080);
     PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
    out.println("Olá servidor!");
}
try (ServerSocket server = new ServerSocket(8080)) {
    while (true) { Socket cliente = server.accept(); /* uma thread por cliente */ }
}
  • TCP (Socket/ServerSocket): confiável, orientado a conexão, ordem garantida. UDP (DatagramSocket): mais rápido, sem garantia de entrega.
  • Defina timeouts (setSoTimeout, connectTimeout) e feche os recursos (try-with-resources). Portas: bem conhecidas (0–1023), registradas (1024–49151), dinâmicas (49152–65535). Para HTTP use o HttpClient (Java Avançado). Modelo OSI em Redes.

Criptografia em Java

MessageDigest md = MessageDigest.getInstance("SHA-256");          // hash (não reversível)
byte[] hash = md.digest("Java".getBytes(StandardCharsets.UTF_8));

Mac mac = Mac.getInstance("HmacSHA256");                           // autenticidade com chave
mac.init(new SecretKeySpec(chave, "HmacSHA256"));

KeyGenerator kg = KeyGenerator.getInstance("AES"); kg.init(256);   // simétrica
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048);   // assimétrica
Conceito Resumo
Hash (MessageDigest, SHA-256) Resumo de tamanho fixo; verifica integridade; não serve sozinho para senhas (use BCrypt/Argon2)
HMAC Hash com chave secreta: integridade e autenticidade (usado em JWT)
AES (simétrica) Mesma chave cifra e decifra; rápida; use modos GCM (com autenticação) e IV aleatório
RSA (assimétrica) Par de chaves (pública/privada); cifra, assinatura digital e troca de chaves; mais lenta
KeyStore Armazena chaves e certificados com segurança (JKS/PKCS12)
TLS Protocolo de comunicação segura (HTTPS); SSLContext e certificados X.509

Boas práticas: algoritmos modernos (AES-256-GCM, SHA-256); nunca invente criptografia; chaves fora do código; evite MD5 e SHA-1; rotacione chaves. Teoria em Segurança.

Internacionalização (i18n)

Locale pt = new Locale("pt", "BR");
ResourceBundle msgs = ResourceBundle.getBundle("messages", pt);   // messages_pt_BR.properties
String titulo = msgs.getString("app.titulo");
String texto = MessageFormat.format(msgs.getString("pedido.confirmado"), 123);

NumberFormat moeda = NumberFormat.getCurrencyInstance(pt);        // R$ 1.234,56
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(pt);

Separe texto do código em arquivos de recurso, formate números, datas e moedas por locale, trate fusos horários (ZoneId) e plural (ChoiceFormat/MessageFormat). Teste vários idiomas. No Spring: MessageSource.

Jakarta EE (visão geral)

Definição: Jakarta EE

Conjunto de especificações corporativas do Java (sucessor do Java EE, pacotes jakarta.*), implementadas por servidores como WildFly, Payara e GlassFish.

Especificação Para quê
Servlet Base web: requisições HTTP, sessões, filtros
CDI Injeção de dependências e contextos (@Inject, @ApplicationScoped)
JAX-RS APIs REST (@Path, @GET, @Produces; filtros, interceptors, ExceptionMapper, negociação de conteúdo)
JPA Persistência ORM (Acesso a Dados)
Bean Validation Validação por anotações
EJB Componentes de negócio com transações e segurança (@Stateless)
JMS / Mail / Batch / WebSocket / Concurrency Mensageria, e-mail, processamento em lote, tempo real, concorrência gerenciada

Empacota-se em WAR/EAR e implanta-se no servidor. O Spring cobre os mesmos problemas (Spring); a migração javax.* → jakarta.* ocorreu no Jakarta EE 9 / Spring Boot 3.

Migração de versão do JDK

Evolução: Java 8 → 11 → 17 → 21. Passos:

  1. Inventário de dependências e análise com jdeps (jdeps --jdk-internals) e jdeprscan (APIs removidas ou depreciadas).
  2. Corrigir o que foi removido: Java EE (javax.* → jakarta.*), javax.xml.bind, SecurityManager, finalizadores; encapsulamento de módulos (--add-opens como paliativo).
  3. Atualizar frameworks e bibliotecas (ex.: Spring Boot 2.x → 3.x exige Java 17).
  4. Testar (unidade, regressão, desempenho) e comparar o GC/JVM flags.
  5. Rollout gradual (canary/blue-green) com plano de rollback e monitoramento.

Evoluindo sistemas legados

  • Diagnóstico: mapear módulos, dependências e riscos; métricas de complexidade (SonarQube) e cobertura.
  • Testes de caracterização: capturam o comportamento atual antes de refatorar (protegem contra regressões).
  • Strangler Fig: criar o novo sistema ao redor do antigo, roteando aos poucos as funcionalidades (feature toggles) até poder desligar o legado.
  • Camada anticorrupção (ACL) e APIs para isolar o legado; modularização (Maven multi-módulo) para reduzir acoplamento; migração incremental por funcionalidade com métricas e feedback (Padrões Arquiteturais).

Trilha de domínio: do básico ao profissional

Um roteiro para checar se você domina o ecossistema Java, indo do mais fundamental ao mais avançado (cada item aponta para onde o assunto está nesta base):

  1. Linguagem: sintaxe, tipos, controle de fluxo, strings, exceções, coleções, lambdas e Streams, módulos — Java e este guia.
  2. Orientação a objetos: classes, herança, polimorfismo, interfaces, composição, SOLID e padrões de projeto — OO, Boas práticas e Padrões.
  3. APIs do JDK: data/hora, arquivos (NIO.2), concorrência, HttpClient, serialização, criptografia, i18n.
  4. Frameworks: Spring Boot (injeção de dependências, REST, JPA, segurança, testes) — Spring.
  5. Dados: JDBC, JPA/Hibernate, SQL, NoSQL, migrações — Acesso a dados com Java.
  6. Nuvem e operação: contêineres, CI/CD, Kubernetes, observabilidade, SRE — Docker e SRE.
  7. Segurança e qualidade: OWASP, autenticação/autorização, testes em todos os níveis, análise estática, LGPD.
  8. Projetos e carreira: construir um projeto de portfólio completo, ler código de outros, contribuir com código aberto e escolher uma especialização (nuvem, mensageria, desempenho, arquitetura).