Skip to content

Java Folha de referência

Linguagem orientada a objetos de nível empresarial, escreva uma vez execute em qualquer lugar.

01

Primeiros Passos

Hello World

Todo programa Java começa a partir de main(). O nome do arquivo deve corresponder ao nome da classe pública (Main.java → Main.class). javac compila para bytecode (.class), java executa na JVM. System.out.println imprime para stdout; printf suporta especificadores de formato (%s, %d, %f, %n para nova linha).

java
// Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
    }
}

// Compile: javac Main.java  -> Main.class
// Run:     java Main
// Package: java -cp . com.example.Main

// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)

Variáveis & Tipos Primitivos

Java tem 8 tipos primitivos (int, long, double, float, boolean, char, byte, short) e tipos de referência (String, arrays, objetos). Use 'final' para constantes. 'var' (Java 10+) infere o tipo em tempo de compilação — use para variáveis locais com tipos óbvios. Underscores em números (100_000) melhoram a legibilidade.

java
// Primitive types (8 total)
int age = 30;              // 32-bit integer
long bigNum = 100_000L;   // 64-bit integer
double price = 19.99;     // 64-bit float (default for decimals)
float pi = 3.14f;         // 32-bit float
boolean active = true;    // true/false
char grade = 'A';         // 16-bit Unicode character
byte b = 127;             // 8-bit signed
short s = 32767;          // 16-bit signed

// Reference types
String name = "Alice";    // Object (not primitive)
int[] nums = {1, 2, 3};  // Array object

// Constants
final double PI = 3.14159; // can't be reassigned

// var (Java 10+, local type inference)
var count = 42;    // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>

Classes Wrapper & Boxing

Classes wrapper (Integer, Double, Boolean, etc.) são versões de objeto de primitivos. Autoboxing/unboxing converte automaticamente. Integer armazena em cache valores de -128 a 127, então == funciona para números pequenos mas falha para maiores — sempre use .equals(). Wrappers são necessários para Collections (que não podem conter primitivos).

java
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42);  // explicit
Integer auto = 42;                       // autoboxing
int unboxed = auto;                      // unboxing

// Useful methods
int max = Integer.MAX_VALUE;     // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);

// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';

// Be careful with == on wrappers
Integer a = 127, b2 = 127;  // a == b2: true (cached)
Integer x = 128, y = 128;   // x == y: false (not cached)
// Always use .equals() for Integer comparison

Pacotes & Imports

Pacotes organizam classes e previnem conflitos de nomenclatura. Convenção: nome de domínio reverso (com.example.app). Importe classes específicas ou use wildcards (*). Imports estáticos trazem constantes e métodos (Math.PI, Math.sqrt). Nomes totalmente qualificados funcionam sem imports mas são verbosos. O pacote java.lang é auto-importado.

java
// Package declaration (must be first line)
package com.example.app;

// Import specific class
import java.util.List;
import java.util.ArrayList;

// Import all classes in a package
import java.util.*;

// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

// Usage
double area = PI * 5 * 5;
double root = sqrt(16);

// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();

// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channel

Entrada & Saída

System.out (stdout), System.err (stderr), System.in (stdin). Scanner é a maneira mais fácil de ler entrada do console — ele faz parse de tokens (nextInt, nextDouble, nextLine). Sempre feche o Scanner para liberar recursos. Argumentos de linha de comando estão em args[] (args[0] é o primeiro argumento, não o nome do programa como em C).

java
import java.util.Scanner;

// Console output
System.out.println("Hello");      // with newline
System.out.print("No newline");   // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted

// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();

System.out.print("Enter age: ");
int age = scanner.nextInt();

System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close

// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"
02

Strings & Formatação

Métodos de String

Strings são imutáveis — métodos retornam novas strings. Sempre use .equals() para comparação de conteúdo (== compara referências). compareTo() retorna negativo/zero/positivo para ordenação (útil para sorting). split() retorna um String[]. Para strings mutáveis, use StringBuilder.

java
String s = "Hello, World";

// Length and access
int len = s.length();        // 12
char c = s.charAt(0);        // 'H'

// Comparison
s.equals("Hello, World");    // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple");        // positive (s > "Apple")
"abc".compareTo("abd");      // negative

// Search
s.indexOf("World");          // 7 (-1 if not found)
s.lastIndexOf("l");          // 10
s.contains("World");         // true
s.startsWith("Hello");       // true
s.endsWith("World");         // true

// Extract
s.substring(7);              // "World"
s.substring(0, 5);           // "Hello"

// Transform
s.toUpperCase();             // "HELLO, WORLD"
s.toLowerCase();             // "hello, world"
s.replace("o", "0");         // "Hell0, W0rld"
s.trim();                    // remove whitespace
s.split(", ");               // ["Hello", "World"]

StringBuilder & Concatenação

Concatenação de strings com + cria uma nova String a cada vez (ineficiente em loops). StringBuilder é mutável e eficiente para construir strings incrementalmente. StringBuffer é a versão thread-safe (raramente necessária). String.join() combina com um delimitador. Java 11+ adiciona repeat() para multiplicação de strings.

java
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);

// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();

// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");

// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"

// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"

Formatação de Strings

printf/format usa especificadores de formato estilo C: %d (int), %f (float), %s (string), %c (char), %b (boolean), %x (hex). Largura (%5d), alinhar à esquerda (%-5d), preencher com zero (%05d), precisão (%.2f). %n é a nova linha da plataforma. Blocos de texto (Java 15+) com aspas triplas habilitam strings multilinha sem escape.

java
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159);  // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255);           // ff
System.out.printf("Octal: %o%n", 8);           // 10

// Width and padding
System.out.printf("[%5d]%n", 42);      // [   42]
System.out.printf("[%-5d]%n", 42);     // [42   ]
System.out.printf("[%05d]%n", 42);     // [00042]
System.out.printf("[%8.2f]%n", 3.14);  // [    3.14]

// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);

// Text blocks (Java 15+)
String json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Expressões Regulares

Regex em Java usa Pattern (compilado) e Matcher (aplicado à entrada). Métodos de String (matches, split, replaceAll) são atalhos convenientes. Barras invertidas devem ser duplicadas em literais de string Java (\\d para \d). Grupos são capturados com parênteses e referenciados como $1, $2 em substituições. Sempre compile padrões uma vez se usados repetidamente.

java
import java.util.regex.*;

// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(",");               // ["a", "b", "c"]
"hello".replaceAll("l", "L");     // "heLLo"

// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");

if (m.find()) {
    System.out.println(m.group()); // "2024-01-15"
}

// Find all matches
while (m.find()) {
    System.out.println(m.group());
}

// Replace with regex
String result = "2024-01-15".replaceAll(
    "(\\d{4})-(\\d{2})-(\\d{2})",
    "$3/$2/$1"); // "15/01/2024"

// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";

Números & Matemática

A classe Math fornece funções matemáticas estáticas. Math.random() retorna 0.0-1.0. Para mais controle, use java.util.Random (com seed) ou java.security.SecureRandom (criptográfico). Integer/Double têm métodos utilitários estáticos. Tenha cuidado com precisão de ponto flutuante — use BigDecimal para cálculos financeiros.

java
// Math class
double sqrt = Math.sqrt(16);     // 4.0
double pow = Math.pow(2, 10);    // 1024.0
int abs = Math.abs(-5);          // 5
int max = Math.max(3, 7);        // 7
int min = Math.min(3, 7);        // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1);    // 4.0
double floor = Math.floor(3.9);  // 3.0
double random = Math.random();   // 0.0 to 1.0

// Constants
double pi = Math.PI;             // 3.14159...
double e = Math.E;               // 2.71828...

// Integer/Long methods
int sum = Integer.sum(3, 4);     // 7
int max2 = Integer.max(3, 7);    // 7

// Rounding modes
double r = Math.round(3.5);      // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative

// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100);  // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();
03

Fluxo de Controle

If / Else

Java if/else funciona como C/C++. Condições devem ser booleanas — sem truthy/falsy como JavaScript (0 e strings não vazias NÃO são truthy). O operador ternário (cond ? a : b) é uma expressão, não uma instrução. Use chaves mesmo para corpos de uma linha (melhor prática de estilo de código).

java
int score = 85;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";

// Nested if
if (score >= 60) {
    if (score >= 90) {
        System.out.println("Excellent");
    }
}

// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not boolean

Switch & Expressões

Switch tradicional tem fall-through (use break). Expressões switch do Java 14+ (->) não fazem fall-through e podem retornar valores. Use vírgulas para múltiplos rótulos case (case 1, 2, 3). 'yield' retorna um valor de um bloco complexo. Expressões switch são exaustivas — você precisa de um default ou todos os cases para enums.

java
// Traditional switch (fall-through)
int day = 3;
switch (day) {
    case 1:
        System.out.println("Mon");
        break;
    case 2:
    case 3:
    case 4:
        System.out.println("Midweek");
        break;
    case 6:
    case 7:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Invalid");
}

// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

// Switch with yield (for complex blocks)
int result = switch (day) {
    case 1, 2, 3, 4, 5 -> {
        int hours = 8;
        yield hours * 5;
    }
    case 6, 7 -> 0;
    default -> -1;
};

Loops

Java tem loops for, while e do-while. O for aprimorado (for-each) funciona com arrays e qualquer Iterable. break sai do loop; continue pula para a próxima iteração. Para coleções, prefira for-each ou streams a loops indexados. do-while executa pelo menos uma vez (raramente usado).

java
// For loop
for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
    System.out.println(n);
}

List<String> names = List.of("Alice", "Bob");
for (String name : names) {
    System.out.println(name);
}

// While loop
int count = 0;
while (count < 3) {
    System.out.println(count);
    count++;
}

// Do-while (runs at least once)
int i = 0;
do {
    System.out.println(i);
    i++;
} while (i < 3);

// Break and continue
for (int j = 0; j < 10; j++) {
    if (j == 5) break;      // exit loop
    if (j % 2 == 0) continue; // skip iteration
    System.out.println(j);
}

Break & Continue com Rótulos

Rótulos (outer:) permitem break/continue de loops externos a partir de loops aninhados. Isso raramente é necessário — extrair para um método com return é geralmente mais limpo. Rótulos são colocados antes do loop, seguidos por dois pontos. break label sai do loop rotulado; continue label pula para sua próxima iteração.

java
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            break outer; // exits both loops
        }
        System.out.println(i + "," + j);
    }
}

// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) {
            continue outer; // skip to next i
        }
        System.out.println(i + "," + j);
    }
}

// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            if (matrix[i][j] == target) return; // exit method
        }
    }
}

Arrays

Arrays têm comprimento fixo (use ArrayList para dinâmicos). Arrays.sort() ordena in-place. Arrays.toString() fornece uma representação legível. Arrays.copyOf() cria uma cópia com um novo comprimento. Para arrays multidimensionais, cada linha pode ter comprimento diferente (jagged arrays). Use Arrays para métodos utilitários em arrays.

java
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]

// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5

// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2

// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);

// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]

// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2
04

Métodos & Funções

Definição de Método

Métodos Java estão sempre dentro de uma classe. 'static' significa que o método pertence à classe (chame sem uma instância). O tipo de retorno (int, String, void) é declarado antes do nome. Parâmetros são tipados. Java não tem valores de parâmetro padrão — use sobrecarga de método em vez disso.

java
public class Calculator {
    // Method with return type
    public static int add(int a, int b) {
        return a + b;
    }

    // Void method (no return)
    public static void printResult(int result) {
        System.out.println("Result: " + result);
    }

    // Method with default (no overloading needed)
    public static String greet(String name, String greeting) {
        return greeting + ", " + name + "!";
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        printResult(sum);

        String msg = greet("Alice", "Hello");
        System.out.println(msg);
    }
}

Sobrecarga de Método

Sobrecarga de método permite múltiplos métodos com o mesmo nome mas diferentes listas de parâmetros (tipo, contagem ou ordem). Java resolve sobrecargas em tempo de compilação com base nos tipos de argumento. Sobrecarga é comum para construtores e métodos utilitários. É diferente de overriding (que envolve herança e dispatch em runtime).

java
public class MathUtils {
    // Overloaded methods (same name, different params)
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static String add(String a, String b) {
        return a + b;
    }
}

// Java picks the most specific match
MathUtils.add(1, 2);        // int version -> 3
MathUtils.add(1.5, 2.5);    // double version -> 4.0
MathUtils.add(1, 2, 3);     // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"

Varargs & Passagem por Valor

Varargs (Type... name) permitem argumentos variáveis, recebidos como um array. Java é sempre pass-by-value: primitivos são copiados, referências de objeto são copiadas (mas apontam para o mesmo objeto). Então modificar um parâmetro dentro de um método não afeta a variável do chamador, mas modificar o objeto para o qual aponta sim.

java
// Varargs: variable number of arguments
public static int sum(int... nums) {
    int total = 0;
    for (int n : nums) {
        total += n;
    }
    return total;
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15
sum();                  // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr);               // 6 (pass array to varargs)

// Java is ALWAYS pass-by-value
public static void modify(int x) {
    x = 100; // doesn't affect the caller's variable
}

int n = 5;
modify(n);
System.out.println(n); // still 5

// For objects, the reference is passed by value
public static void addItem(List<String> list) {
    list.add("new"); // modifies the same list object
}

Recursão

Recursão é quando um método chama a si mesmo. Sempre tenha um caso base para parar. Java não otimiza recursão em cauda (ao contrário de algumas linguagens), então recursão profunda pode causar StackOverflowError. Para recursão crítica de desempenho ou profunda, converta para iteração. Memoization (cache de resultados) pode acelerar soluções recursivas como Fibonacci.

java
// Factorial
public static int factorial(int n) {
    if (n <= 1) return 1;        // base case
    return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120

// Fibonacci
public static int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)

// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop instead

Expressões Lambda

Lambdas (Java 8+) são funções anônimas. O tipo é uma interface funcional (um método abstrato). Comuns: Function<T,R> (entrada→saída), Predicate<T> (teste booleano), Consumer<T> (consome, sem retorno), Supplier<T> (produz, sem entrada). Method references (String::length) são atalhos para lambdas que chamam um único método.

java
import java.util.function.*;

// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;

// Multi-line lambda
Function<String, String> process = s -> {
    String upper = s.toUpperCase();
    return upper.substring(0, 3);
};

// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference

// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference

// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();

// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"
05

Classes & POO

Classe & Construtor

Classes são templates para objetos. Campos mantêm estado, métodos definem comportamento. Construtores inicializam novos objetos (use 'this' para distinguir campos de parâmetros). @Override indica que um método sobrescreve um método de superclasse (toString é de Object). Encapsulamento: campos private, getters/setters public.

java
public class Person {
    // Fields (instance variables)
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;  // 'this' refers to the current instance
        this.age = age;
    }

    // Methods
    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age >= 0) this.age = age;
    }

    public String greet() {
        return "Hi, I'm " + name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName());  // "Alice"
System.out.println(p);            // uses toString()

Modificadores de Acesso & Static

Modificadores de acesso: public (em todo lugar), private (apenas classe), protected (classe + subclasses + pacote), default/package-private (mesmo pacote). Membros estáticos pertencem à classe, não a instâncias — compartilhados entre todos os objetos. Inicializadores estáticos executam uma vez quando a classe carrega. Use static para constantes (static final), métodos utilitários e contadores.

java
public class BankAccount {
    // Access modifiers:
    public String owner;      // accessible everywhere
    private double balance;   // class only
    protected String type;    // class + subclasses + same package
    String id;                // package-private (default)

    // Static field (shared by all instances)
    private static int accountCount = 0;

    // Static constant
    public static final double MIN_BALANCE = 100.0;

    // Static method (call without instance)
    public static int getAccountCount() {
        return accountCount;
    }

    // Static initializer (runs once when class loads)
    static {
        System.out.println("BankAccount class loaded");
    }

    public BankAccount(String owner) {
        this.owner = owner;
        this.balance = MIN_BALANCE;
        accountCount++; // increment shared counter
    }
}

int count = BankAccount.getAccountCount(); // static method call

Herança & super

Java usa 'extends' para herança de classe (apenas herança única). super() chama o construtor pai (deve ser a primeira linha). @Override indica overriding de método (polimorfismo de runtime). Um Dog É-UM Animal. Use herança para relacionamentos 'é-um'; use composição (tem-um) para reuso de código. Java 17+ suporta sealed classes para restringir herança.

java
// Parent class
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor");
    }

    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Child class (extends)
class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // must be first line — call parent constructor
        this.breed = breed;
    }

    // Override parent method
    @Override
    public void eat() {
        super.eat(); // call parent's eat()
        System.out.println(name + " the " + breed + " eats dog food");
    }

    public void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog("Rex", "Labrador");
dog.eat();   // calls Dog's eat()
dog.bark();  // Dog-specific method

Classes Abstratas & Interfaces

Classes abstratas não podem ser instanciadas e podem ter tanto métodos abstratos (sem corpo) quanto concretos. Interfaces definem contratos — todos os métodos são public abstract por padrão. Java 8+ permite default methods (com corpo) e métodos estáticos em interfaces. Uma classe extends uma classe abstrata mas pode implementar múltiplas interfaces. Use classes abstratas para código compartilhado, interfaces para contratos.

java
// Abstract class (can't be instantiated)
abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method (must be implemented by subclasses)
    public abstract double area();

    // Concrete method (inherited)
    public String describe() {
        return color + " " + this.getClass().getSimpleName();
    }
}

// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
    void draw(); // abstract by default

    // Default method (Java 8+)
    default void drawTwice() {
        draw();
        draw();
    }

    // Static method in interface
    static Drawable empty() {
        return () -> System.out.println("nothing");
    }
}

// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

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

    @Override
    public void draw() {
        System.out.println("Drawing " + describe());
    }
}

Polimorfismo & Casting

Polimorfismo: uma referência pai pode conter um objeto filho. Chamadas de método fazem dispatch para a implementação do objeto real (polimorfismo de runtime). Use instanceof antes de downcast para evitar ClassCastException. Java 16+ pattern matching (instanceof Circle c) combina verificação e cast. Sobrescreva equals() e hashCode() juntos para comportamento correto em coleções.

java
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);

// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area

// instanceof check
if (s1 instanceof Circle) {
    Circle c = (Circle) s1; // downcast
    System.out.println("Radius: " + c.radius);
}

// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
    System.out.println("Radius: " + c.radius); // c is already cast
}

// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)

// Object class methods (all classes inherit from Object)
circle.equals(circle);   // reference equality by default
circle.hashCode();       // hash code
circle.getClass();       // Class<Circle>
circle.toString();       // string representation

Records & Enums

Records (Java 16+) são classes de dados imutáveis — o compilador gera construtor, getters, equals, hashCode e toString. Use para DTOs e objetos de valor. Enums são constantes type-safe que podem ter campos, métodos e construtores. Enums implementam Comparable e têm métodos values() e valueOf(). Ambos são essenciais para Java moderno.

java
// Record (Java 16+): concise data class
public record Point(int x, int y) {}

// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString

Point p = new Point(3, 4);
System.out.println(p.x());      // 3
System.out.println(p.y());      // 4
System.out.println(p);          // Point[x=3, y=4]

// Compact constructor (validation)
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// Enum (named constants)
public enum Direction {
    UP, DOWN, LEFT, RIGHT;

    public Direction opposite() {
        return switch (this) {
            case UP -> DOWN;
            case DOWN -> UP;
            case LEFT -> RIGHT;
            case RIGHT -> LEFT;
        };
    }
}

Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWN
06

Coleções & Generics

List (ArrayList & LinkedList)

ArrayList é backed por um array (get/set rápido, insert/delete lento no meio). LinkedList é backed por uma lista duplamente encadeada (insert/delete rápido nas extremidades, acesso aleatório lento). List.of() cria listas imutáveis. Use ArrayList para a maioria dos casos; LinkedList apenas para operações frequentes nas extremidades. Ambas implementam a interface List.

java
import java.util.*;

// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol");     // insert at index
list.set(1, "Dave");      // replace at index
String name = list.get(0); // "Carol"
list.remove(0);            // remove by index
list.remove("Dave");       // remove by value
int size = list.size();    // 1
boolean has = list.contains("Bob");

// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element

// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException

// Iterate
for (String s : list) {
    System.out.println(s);
}
list.forEach(System.out::println); // method reference

Set (HashSet & TreeSet)

Set armazena elementos únicos. HashSet é o mais rápido mas não ordenado. TreeSet mantém elementos ordenados (ordem natural ou Comparator). LinkedHashSet mantém ordem de inserção. Operações de set: addAll (união), retainAll (interseção), removeAll (diferença). Para objetos personalizados em um HashSet, sobrescreva equals() e hashCode().

java
import java.util.*;

// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");

// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]

// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b);    // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]

// Immutable set
Set<String> immutable = Set.of("x", "y", "z");

Map (HashMap & TreeMap)

Map armazena pares chave-valor. HashMap é o mais rápido (não ordenado). TreeMap ordena por chaves. LinkedHashMap mantém ordem de inserção. getOrDefault evita verificações de null. compute/merge são poderosos para atualizar valores. Para chaves personalizadas, sobrescreva equals() e hashCode(). Map.of() cria maps imutáveis (Java 9+).

java
import java.util.*;

// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite

// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0

// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);

// Remove
ages.remove("Bob");

// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

ages.forEach((key, val) -> System.out.println(key + "=" + val));

// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1

// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();

Queue & Deque

Queue é FIFO (first-in-first-out). Deque é double-ended (pode adicionar/remover de ambas as extremidades). PriorityQueue ordena elementos por ordem natural ou um Comparator (min-heap por padrão). Para uma stack, use ArrayDeque (push/pop) em vez da classe legada Stack. ArrayDeque é mais rápido que LinkedList para operações de queue/deque.

java
import java.util.*;

// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first");   // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head

// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast();  // 2
deque.pollFirst(); // 1
deque.pollLast();  // 2

// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)

// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)

// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop();   // 2 (remove from front)

Generics

Generics habilitam coleções e classes type-safe. <T> é um parâmetro de tipo. Tipos bounded (<T extends Comparable<T>>) restringem a tipos com certo comportamento. Wildcards: ? (qualquer), ? extends T (covariante, somente leitura), ? super T (contravariante, somente escrita). Generics usam type erasure — tipos são verificados em tempo de compilação, apagados em runtime.

java
// Generic class
public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);

// Generic method
public static <T> T firstOf(List<T> list) {
    return list.get(0);
}

String first = firstOf(List.of("a", "b"));

// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) {
            result = item;
        }
    }
    return result;
}

// Wildcards
void process(List<?> list) { }          // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { }      // Integer or superclass

Iterators & Comparable

Iterator permite remoção segura durante iteração (it.remove()). ListIterator adiciona travessia bidirecional e set/add. Comparable define ordenação natural (compareTo). Comparator define ordenação personalizada (comparing, comparingInt, reversed, thenComparing). Use Comparator.comparing() para sorting fluente. Collections.sort() usa a ordem natural.

java
import java.util.*;

// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
    // it.remove(); // safe removal during iteration
}

// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    lit.set(lit.next().toUpperCase()); // replace
}

// Comparable (natural ordering)
public class Person implements Comparable<Person> {
    String name;
    int age;

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();

List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses Comparable
07

Streams & Funcional

Básico de Stream

Streams (Java 8+) fornecem processamento de dados declarativo. Crie com .stream() (coleções) ou Stream.of(). Operações intermediárias (filter, map, sorted) são lazy — elas executam apenas quando uma operação terminal (collect, reduce, count, forEach) é chamada. toList() (Java 16+) é uma alternativa concisa a collect(Collectors.toList()).

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

// Filter and collect
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]

// Map (transform)
List<String> doubled = nums.stream()
    .map(n -> "num" + n)
    .collect(Collectors.toList());

// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);

// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3

// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);

// ForEach
nums.stream().forEach(System.out::println);

// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();

Operações de Stream

sorted() ordena elementos (natural ou com Comparator). distinct() remove duplicatas. limit(n)/skip(n) paginam. flatMap achata streams aninhados — essencial para transformações one-to-many. peek() é para depuração (side effects). groupingBy cria maps agrupando elementos por um classificador. Streams são lazy — operações encadeiam eficientemente.

java
List<String> names = List.of("Alice", "Bob", "Charlie", "David");

// Sorted
List<String> sorted = names.stream()
    .sorted()
    .toList();

// Sorted by length
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();

// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .toList(); // [1, 2, 3]

// Limit and Skip
List<Integer> limited = nums.stream()
    .skip(2)   // skip first 2
    .limit(3)  // take next 3
    .toList();

// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]

// Peek (debug, side-effect)
nums.stream()
    .peek(n -> System.out.println("before: " + n))
    .filter(n -> n > 2)
    .peek(n -> System.out.println("after: " + n))
    .toList();

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

Collectors & Redução

Collectors fornecem operações de redução ricas: joining (concatenar strings), groupingBy (agrupar por chave), partitioningBy (dividir por booleano), toMap (criar um map), summarizingInt (estatísticas: count, sum, min, max, average). Collectors podem ser compostos (groupingBy com downstream collector). Esses substituem loops verbosos por one-liners declarativos.

java
import java.util.stream.*;

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Join strings
String joined = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"

// Group by
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() > 28));

// Count by group
Map<Integer, Long> countByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

// Summarizing
IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax());     // 35

// To map
Map<String, Integer> nameToAge = people.stream()
    .collect(Collectors.toMap(Person::getName, Person::getAge));

// Reducing
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));

Optional

Optional<T> é um contêiner que pode ou não conter um valor. Ele força tratamento explícito de ausência — chega de NullPointerException. Use of() para valores não-null, ofNullable() para possivelmente-null. Encadeie com map/flatMap/filter. Nunca use get() sem isPresent() — prefira orElse/orElseThrow. Optional é projetado para tipos de retorno, não campos.

java
import java.util.Optional;

// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null

// Checking
present.isPresent(); // true
empty.isEmpty();     // true (Java 11+)

// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));

// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());

// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);

// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
    s -> System.out.println("Got: " + s),
    () -> System.out.println("Empty")
);

// Chaining (avoid null checks)
String result = getUser(1)
    .map(User::getProfile)
    .map(Profile::getEmail)
    .orElse("no email");

Interfaces Funcionais

Interfaces funcionais têm exatamente um método abstrato (podem ter múltiplos default methods). @FunctionalInterface é opcional mas documenta intenção. Java fornece muitas em java.util.function: Function, Predicate, Consumer, Supplier, além de variantes Bi- e primitivas. Use essas em vez de criar interfaces personalizadas quando possível. Elas habilitam expressões lambda e method references.

java
import java.util.function.*;

// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;

Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;

Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);

Supplier<List<String>> listFactory = ArrayList::new;

// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);

// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;

// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;

// Custom functional interface
@FunctionalInterface
interface StringProcessor {
    String process(String input);

    // Can have default methods
    default StringProcessor andThen(StringProcessor after) {
        return input -> after.process(process(input));
    }
}
08

Exceções & I/O

Try / Catch / Finally

try/catch/finally trata exceções. finally sempre executa (use para cleanup). Multi-catch (catch A | B) trata múltiplas exceções juntas. Try-with-resources fecha automaticamente qualquer AutoCloseable (arquivos, conexões, streams) — preferido a cleanup manual em finally. Recursos são fechados na ordem reversa de declaração.

java
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("General error: " + e);
} finally {
    // Always runs (even if return/throw in try/catch)
    System.out.println("Cleanup");
}

// Multi-catch (Java 7+)
try {
    riskyOperation();
} catch (IOException | SQLException e) {
    // Handle both exceptions the same way
    log.error(e);
}

// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
     PrintWriter pw = new PrintWriter("output.txt")) {
    String line = br.readLine();
    pw.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)

Checked vs Unchecked

Exceções checked (extends Exception) devem ser declaradas com 'throws' ou capturadas — o compilador impõe isso. Use para condições recuperáveis (arquivo não encontrado, erro de rede). Exceções unchecked (extends RuntimeException) não precisam de declaração — use para erros de programação (null pointer, argumento inválido). O debate: exceções checked forçam tratamento mas podem clutterar código; muitos frameworks preferem unchecked.

java
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    // IOException is checked — compiler enforces handling
}

// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Divisor cannot be zero");
        // RuntimeException — no 'throws' needed
    }
    return a / b;
}

// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException

// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException

// Custom checked exception
class DataException extends Exception {
    public DataException(String msg) { super(msg); }
}

// Custom unchecked exception
class ValidationException extends RuntimeException {
    public ValidationException(String msg) { super(msg); }
}

I/O de Arquivo (NIO.2)

NIO.2 (java.nio.file) é a API de arquivo moderna. Files.readString/writeString (Java 11+) são convenientes para texto. Files.lines() retorna um Stream lazy — eficiente para arquivos grandes (deve fechar com try-with-resources). Path.of() substitui a antiga classe File. Files.createDirectories() cria o caminho completo. Sempre trate IOException.

java
import java.nio.file.*;
import java.io.*;

// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));

// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);

// Append
Files.writeString(Path.of("log.txt"), "entry\n",
    StandardOpenOption.APPEND, StandardOpenOption.CREATE);

// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
    lineStream.filter(l -> l.contains("ERROR"))
              .forEach(System.out::println);
}

// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));

// Check existence
boolean exists = Files.exists(Path.of("file.txt"));

// Create directories
Files.createDirectories(Path.of("a/b/c"));

Reader & Writer (Texto)

BufferedReader/Writer são eficientes para I/O de texto (buffering reduz chamadas de sistema). PrintWriter oferece formatação estilo printf. Scanner faz parse de entrada (nextInt, nextDouble, nextLine). InputStreamReader faz bridge de byte streams para character streams (especifique charset para não-UTF-8). Sempre use try-with-resources para garantir que streams sejam fechados.

java
import java.io.*;
import java.nio.file.*;

// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
    bw.write("First line");
    bw.newLine();
    bw.write("Second line");
}

// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
    pw.println("Hello");
    pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}

// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // Parse tokens
        Scanner lineSc = new Scanner(line);
        if (lineSc.hasNextInt()) {
            int n = lineSc.nextInt();
        }
    }
}

// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);

Data & Hora (java.time)

java.time (Java 8+) é a API moderna de data/hora, substituindo o antigo Date/Calendar. LocalDate (apenas data), LocalTime (apenas hora), LocalDateTime (ambos), ZonedDateTime (com fuso horário). Todos são imutáveis e thread-safe. Use Period para diferenças de data, Duration para diferenças de tempo. DateTimeFormatter para parsing/formatação. Instant para timestamps de máquina (UTC).

java
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

// Current date/time
LocalDate today = LocalDate.now();      // 2024-01-15
LocalTime now = LocalTime.now();        // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone

// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);

// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"

// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);

// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34

// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());

// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);

Básico de Concorrência

Concorrência Java: Thread (baixo nível), ExecutorService (thread pools — preferido), CompletableFuture (composição async, como Promises). parallelStream() usa o ForkJoinPool para processamento paralelo. Blocos synchronized protegem estado compartilhado. Variáveis atomic (AtomicInteger, etc.) fornecem operações thread-safe sem lock. Para concorrência complexa, use coleções java.util.concurrent (ConcurrentHashMap, BlockingQueue).

java
import java.util.concurrent.*;

// Create a thread
Thread thread = new Thread(() -> {
    System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion

// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();

// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"

// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();

// Synchronized
synchronized (this) {
    // only one thread at a time
}

// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
09

Expressões Lambda

Sintaxe Básica de Lambda

Lambdas (Java 8+) são implementações concisas de interfaces funcionais. Sintaxe: (params) -> expression ou (params) -> { statements; }. O compilador infere tipos de parâmetro do tipo alvo. Lambdas de parâmetro único podem omitir parênteses; zero-parâmetro precisam de parênteses vazios. Lambdas habilitam programação funcional e são a espinha dorsal da Streams API.

java
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Old way");
    }
};

// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");

// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
    int diff = a - b;
    return diff;
};

// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;

// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);

// Zero params need empty parens
Runnable noop = () -> {};

Interfaces Funcionais

Uma interface funcional tem exatamente um método abstrato (tipo SAM). A annotation @FunctionalInterface faz o compilador impor isso. Lambdas só podem ter como alvo interfaces funcionais. Métodos default e estáticos são permitidos e não quebram a regra de método único. Esta é a fundação que faz lambdas funcionarem no sistema de tipos do Java.

java
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);

int result = add.operate(3, 4);  // 7
int m = mul.operate(3, 4);       // 12

// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
    String process(String s);
    default StringProcessor andThen(StringProcessor next) {
        return s -> next.process(this.process(s));
    }
}

Interfaces Funcionais Integradas

java.util.function fornece ~40 interfaces funcionais prontas então você raramente escreve a sua. As quatro principais: Function (transformar), Predicate (testar), Consumer (consumir), Supplier (produzir). Variantes Bi- recebem dois argumentos. Variantes primitivas (IntFunction, ToIntFunction, etc.) evitam overhead de autoboxing. Use essas em vez de criar interfaces personalizadas.

java
import java.util.function.*;

// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello");  // 5

// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test("");  // true

// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");

// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();

// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
    (s, i) -> s.repeat(i);

// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;

Method References

Method references (::) são atalhos para lambdas que apenas chamam um único método. Quatro tipos: static (Class::static), bound instance (obj::method), unbound instance (Class::method — primeiro parâmetro vira receiver) e constructor (Class::new). Use-os quando um lambda apenas encaminha para um método — eles são mais legíveis. Caso contrário, mantenha lambdas explícitos.

java
import java.util.*;

List<String> names = List.of("alice", "bob", "charlie");

// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);

// Four kinds of method references:

// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);

// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);

// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();

Captura de Variáveis (Effectively Final)

Lambdas podem capturar variáveis locais, mas elas devem ser final ou 'effectively final' (nunca reatribuídas). Isso é porque lambdas podem sobreviver ao stack frame. Para contornar isso, use um array de elemento único ou um objeto AtomicInteger/holder. Campos de instância e estáticos não têm tal restrição. 'this' dentro de um lambda refere-se à instância da classe envolvente, não ao lambda em si.

java
import java.util.function.*;

int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get());  // 20

// x = 20;  // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final

// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]);  // 2

// Instance/static fields CAN be modified (no restriction)
class Holder {
    int value = 0;
    Runnable bump = () -> value++;  // OK, field access
}

// 'this' inside a lambda refers to the enclosing instance
class Outer {
    String name = "Outer";
    Runnable r = () -> System.out.println(this.name);  // "Outer"
}

Constructor References

Constructor references (ClassName::new) criam novas instâncias concisamente. Útil com Collectors.toCollection() para escolher o tipo de resultado, com criação de array (Type[]::new) e em padrões de factory. Para records e objetos imutáveis, constructor references são a maneira idiomática de construir cópias. Eles pareiam naturalmente com alvos Function/Supplier.

java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();

// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");

// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));

// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5);  // new String[5]

// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));
10

Optional & Null Safety

Criando Optional

Optional é um contêiner que pode ou não conter um valor. Use empty() para nenhum valor, of() quando você tem certeza de que o valor é não-null (lança NPE caso contrário), e ofNullable() quando null é possível. Optional força chamadores a tratar o caso ausente explicitamente. Nunca retorne null onde Optional é esperado — isso derrota o propósito.

java
import java.util.Optional;

// empty() — no value
Optional<String> empty = Optional.empty();

// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");

// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());

// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();

// Common helper pattern
public Optional<User> findUser(long id) {
    User u = db.lookup(id);
    return Optional.ofNullable(u);
}

String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();

Consumindo Valores com Segurança

Prefira ifPresent/ifPresentOrElse a isPresent+get. orElse retorna um default constante; orElseGet recebe um Supplier então o default é computado lazy (importante quando o default é caro). orElseThrow converte ausência em uma exceção. O objetivo é nunca chamar .get() cegamente — isso reintroduz o risco de NPE que Optional deveria eliminar.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
    System.out.println(name.get());  // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException

// ifPresent: run action only if value exists
name.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("No name")
);

// orElse: provide default
String s1 = name.orElse("Anonymous");

// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());

// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));

String expensiveDefault() { return "computed"; }

Transformando com map & flatMap

map transforma o valor contido (Optional<T> -> Optional<R>). flatMap é usado quando a função de mapeamento ela mesma retorna um Optional, prevenindo Optionals aninhados. filter mantém o valor apenas se um predicado corresponder. Encadear map/filter/flatMap permite construir pipelines que fazem short-circuit no primeiro empty — muito mais limpo que verificações de null aninhadas.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// map: transform the value if present
Optional<Integer> length = name.map(String::length);  // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);

// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
    return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
    .flatMap(this::findEmail);  // Optional<email> not Optional<Optional<email>>

// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
    .filter(a -> a >= 18);  // Optional[25]
Optional<Integer> kid = Optional.of(10)
    .filter(a -> a >= 18);  // Optional.empty

// Chain transformations
String label = Optional.of("alice")
    .map(String::strip)
    .filter(s -> !s.isEmpty())
    .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
    .orElse("unknown");  // "Alice"

class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }

Anti-padrões a Evitar

Optional é projetado para tipos de retorno, não campos ou parâmetros. Ele não é Serializable e adiciona overhead como campo. Não use .get() sem verificar, e não use isPresent()+get() — isso é apenas verificação de null verbosa. Coleções já expressam vaziedade, então não as envolva em Optional. Use Optional como um sinal de tipo de retorno de que um valor pode estar ausente.

java
import java.util.Optional;

// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
    private Optional<String> name;  // DON'T
}

// GOOD: use plain field, return Optional from accessor
class Good {
    private String name;
    public Optional<String> getName() { return Optional.ofNullable(name); }
}

// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {}  // DON'T

// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }

// BAD: .get() without check
String x = findName().get();  // throws if empty

// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
    use(opt.get());  // just use ifPresent or map instead
}

// BAD: returning Optional from collections
public Optional<Item> find(...) {
    // Collections already express emptiness — return empty List, not Optional<List>
    return Optional.ofNullable(items);
}

Optional com Streams

Optional.stream() (Java 9+) produz um Stream de 0 ou 1 elemento, o que permite flatMap Optionals de um stream elegantemente. Esta é a maneira mais limpa de pular valores ausentes durante o processamento de stream. Ela evita o padrão verboso filter(isPresent).map(get) e mantém o pipeline declarativo.

java
import java.util.*;
import java.util.stream.*;

// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);

// Useful: flatMap Optional out of a stream
class User {
    String email;  // may be null
    User(String e) { email = e; }
    Optional<String> getEmail() { return Optional.ofNullable(email); }
}

List<User> users = List.of(
    new User("[email protected]"),
    new User(null),
    new User("[email protected]")
);

// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
    .flatMap(u -> u.getEmail().stream())
    .toList();  // [[email protected], [email protected]]

// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
    .map(User::getEmail)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .toList();
11

Streams API Aprofundado

Criando Streams

Streams podem ser criados a partir de coleções, arrays ou factories estáticas. iterate() e generate() produzem streams infinitos — sempre siga com limit(). iterate com um predicado (Java 9+) é mais seguro que iterate puro. IntStream/LongStream/DoubleStream evitam boxing para trabalho numérico. Streams são de uso único: uma vez que uma operação terminal executa, o stream é consumido.

java
import java.util.*;
import java.util.stream.*;

// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();

// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});

// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2);  // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1);  // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random);  // infinite
Stream<String> s10 = Stream.ofNullable(null);  // 0 or 1 element (Java 9+)

// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
    .limit(10)
    .toList();

// Numeric ranges
IntStream range = IntStream.range(0, 5);       // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

Operações Intermediárias

Operações intermediárias são lazy — elas não executam até uma operação terminal ser invocada. filter mantém elementos, map transforma 1:1, flatMap transforma 1:muitos. distinct/sorted/limit/skip são stateful. takeWhile/dropWhile (Java 9+) param no primeiro elemento não correspondente (ao contrário de filter, que examina tudo). Use peek para depuração, não para side effects em produção.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// filter: keep matching
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0).toList();

// map: transform
List<String> labels = nums.stream()
    .map(n -> "n=" + n).toList();

// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
    .stream().flatMap(List::stream).toList();  // [1,2,3]

// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();

// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();

// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();

// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();

// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();

Collectors: grouping & partitioning

Collectors.groupingBy é o SQL GROUP BY dos streams Java. A função classificadora define a chave; um downstream collector opcional processa cada grupo (counting, summing, mapping, etc.). partitioningBy é um caso especial com um predicado booleano (exatamente dois buckets). Passe um supplier TreeMap para chaves ordenadas. Esses compõem poderosamente — você pode construir agrupamentos multi-nível.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, String city, int age) {}

List<Person> people = List.of(
    new Person("Alice", "NYC", 30),
    new Person("Bob", "LA", 25),
    new Person("Carol", "NYC", 35),
    new Person("Dave", "LA", 40)
);

// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}

// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city, Collectors.counting()));

Map<String, Integer> sumAgeByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.summingInt(Person::age)));

Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.mapping(Person::name, Collectors.toList())));

// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 30));

// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
    .collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));

Redução & Estatísticas

reduce combina todos os elementos em um único valor — forneça uma identidade para segurança com streams vazios. summaryStatistics dá count/sum/min/max/avg em uma passagem. Collectors.joining é útil para construir strings delimitadas. teeing (Java 12+) executa dois collectors em paralelo e mescla seus resultados — útil quando você precisa dois agregados (como min e max) em uma passagem.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5);

// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum);          // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b);  // Optional[120]

// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()

// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);

// join strings
String joined = List.of("a", "b", "c").stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[a, b, c]"

// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
    Collectors.minBy(Comparator.naturalOrder()),
    Collectors.maxBy(Comparator.naturalOrder()),
    (mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));

Streams Numéricos

IntStream/LongStream/DoubleStream são especializações primitivas que evitam overhead de autoboxing — use-os para trabalho numérico. mapToInt/mapToLong/mapToDouble convertem streams de objeto para streams primitivos; boxed() volta. Streams primitivos têm operações terminal especializadas (sum, average, max) que retornam OptionalInt/Double para tratar streams vazios. Ótimos para pipelines numéricos sensíveis a desempenho.

java
import java.util.stream.*;
import java.util.*;

// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum();                       // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);

// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();

// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();

// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
    .mapToObj(i -> "item-" + i).toList();

// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();

// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);

// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
    .limit(10).mapToInt(a -> a[0]).boxed().toList();

List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}

Streams Paralelos

parallelStream divide trabalho pelo ForkJoinPool comum (dimensionado para núcleos de CPU). Use apenas para grandes conjuntos de dados com operações intensivas de CPU, stateless e independentes de ordem — para dados pequenos o overhead excede o benefício. Evite estado mutável compartilhado (causa races). I/O em streams paralelos bloqueia o pool compartilhado — use um ForkJoinPool personalizado para trabalho bloqueante. Meça antes de assumir que paralelo é mais rápido.

java
import java.util.*;
import java.util.stream.*;

// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
    .mapToInt(Integer::intValue).sum();

// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
    .filter(n -> n % 2 == 0).count();

// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
    .forEachOrdered(System.out::println);

// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
    .map(n -> n * 2).boxed().toList();

// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
    IntStream.range(0, 1000).parallel().sum()
).get();

// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered ops
12

Generics Aprofundado

Classes & Métodos Genéricos

Generics habilitam código reutilizável type-safe. Classes declaram parâmetros de tipo (<T>); métodos também podem (<T> antes do tipo de retorno). O operador diamond <> infere o tipo na construção. Generics são verificados em tempo de compilação — eles tornam coleções e APIs mais seguras capturando erros de tipo cedo em vez de em runtime via ClassCastException.

java
// Generic class
public class Box<T> {
    private T value;
    public void set(T v) { value = v; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // no cast needed

// Multiple type parameters
public class Pair<K, V> {
    private final K key;
    private final V value;
    public Pair(K k, V v) { key = k; value = v; }
    public K key() { return key; }
    public V value() { return value; }
}

Pair<String, Integer> p = new Pair<>("age", 30);

// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
    return list.get(0);
}

// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
    Map<K, V> m = new HashMap<>();
    for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
    return m;
}

Parâmetros de Tipo Bounded

Parâmetros de tipo bounded (<T extends Bound>) restringem quais tipos podem ser usados e permitem chamar métodos do bound. <T extends Number> significa T deve ser um Number ou subtipo. Múltiplos bounds usam & — no máximo uma classe (deve ser a primeira), o resto interfaces. Bounds são essenciais para escrever algoritmos que precisam de capacidades específicas (comparabilidade, operações numéricas).

java
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}

sum(List.of(1, 2, 3));        // Integer is a Number
sum(List.of(1.0, 2.5));       // Double is a Number
// sum(List.of("a"));          // compile error

// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}

public static <T extends Number & Comparable<T> & Serializable>
    T max(List<T> list) {
    T best = list.get(0);
    for (T t : list) if (t.compareTo(best) > 0) best = t;
    return best;
}

// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
    int len = 0;
    for (CharSequence c : items) len += c.length();  // can call .length()
    return len;
}

Wildcards: ?, extends, super

Wildcards tornam tipos genéricos flexíveis. ? extends T (covariante) permite ler T mas não escrever — use para producers. ? super T (contravariante) permite escrever T mas apenas ler Object — use para consumers. A regra PECS (Producer Extends, Consumer Super) orienta qual usar. copy(dest, src) é o exemplo clássico: dest é um consumer (super), src é um producer (extends).

java
import java.util.*;

// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
    for (Object o : list) System.out.println(o);
}

// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}
sum(List.of(1, 2, 3));     // List<Integer> OK
sum(List.of(1.0, 2.0));    // List<Double> OK
// nums.add(5);  // ERROR: can't add (don't know exact type)

// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); list.add(3);  // OK to add Integer
}
addNumbers(new ArrayList<Number>());  // OK
addNumbers(new ArrayList<Object>());  // OK
// Number n = list.get(0);  // only safe to read as Object

// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}

Type Erasure

Generics Java usam type erasure — tipos genéricos existem apenas em tempo de compilação; em runtime, List<String> e List<Integer> são ambos apenas List. Isso habilita compatibilidade com versões anteriores mas tem limites: você não pode fazer new T(), criar arrays genéricos, usar instanceof com generics, ou ter métodos sobrecarregados com assinaturas de mesmo erasure. Heap pollution ocorre quando casts unchecked colocam tipos errados em generics, adiando erros para runtime.

java
import java.util.*;

// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList

// You CANNOT do these due to erasure:
// new T()              — can't instantiate type param
// new T[]              — can't create generic array
// instanceof List<String>  — only instanceof List (raw)
// class MyException<T> extends Exception  — can't extend Throwable generically
// static T field       — no static generic fields

// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {}  // ERROR: same erasure

// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
    List<T> result = new ArrayList<>();
    for (Object o : items) {
        if (type.isInstance(o)) result.add(type.cast(o));
    }
    return result;
}

// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2);  // unchecked
// String s = polluted.get(0);  // ClassCastException at runtime

Métodos Genéricos & Inferência

Type inference permite que o compilador determine argumentos de tipo a partir do contexto (argumentos e tipo alvo), então você raramente os escreve explicitamente. O operador diamond <> é inferência para construtores. Target typing usa o tipo esperado da variável. Use uma testemunha de tipo explícita (Class.<T>method()) apenas quando a inferência não consegue resolver ambiguidade. Inferência faz código genérico ler tão limpo quanto código não genérico.

java
import java.util.*;

// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world");      // T inferred as String
Number n = pick(1, 2.0);                // T inferred as Number (common supertype)

// Target typing: inference uses the expected type
List<String> list = Collections.emptyList();  // T inferred from target

// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
    .map(Object::toString)              // Stream<String>
    .collect(Collectors.joining(","));  // inferred

// Explicit type witness (rarely needed)
Collections.<String>emptyList();

// Generic constructor
class Holder<T> {
    private T value;
    <U extends T> Holder(U init) { value = init; }  // constructor type param
    T get() { return value; }
}
Holder<Number> h = new Holder<>(42);  // U=Integer, T=Number

import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }

Interfaces Genéricas & Padrões

Interfaces genéricas (como Repository<T,ID>) definem contratos reutilizáveis. O padrão de bound auto-referenciante (class X implements Comparable<X>) garante que compareTo só aceita o mesmo tipo. O padrão type token (usar Class<T> como chave) contorna erasure para fornecer segurança de tipo em runtime em contêineres heterogêneos. Esses padrões são a espinha dorsal de frameworks como Spring Data.

java
// Generic interface
interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    void save(T entity);
}

// Implement with concrete types
class UserRepository implements Repository<User, Long> {
    public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
    public List<User> findAll() { return List.of(); }
    public void save(User entity) {}
}

// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
    int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
    private final double celsius;
    Temperature(double c) { celsius = c; }
    public int compareTo(Temperature other) {
        return Double.compare(celsius, other.celsius);
    }
}

// Generic builder pattern
class Builder<T> {
    private T value;
    public Builder<T> set(T v) { value = v; return this; }
    public T build() { return value; }
}

// Type token pattern for runtime type safety
class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> type, T value) { map.put(type, value); }
    public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}

record User(String name) {}
import java.util.Optional;
13

Annotations

Annotations Integradas

Annotations integradas do Java: @Override (captura typos em overriding — sempre use), @Deprecated (sinaliza que a API não deveria ser usada, com metadados since/forRemoval), @SuppressWarnings (silencia avisos específicos — use de forma estreita), @FunctionalInterface (impõe a regra SAM). Essas são as annotations do dia a dia que melhoram a segurança e documentação em tempo de compilação.

java
import java.util.*;

// @Override: declares intent to override (compiler checks)
class Animal {
    public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    public void sound() { System.out.println("Woof"); }
}

// @Deprecated: marks API as outdated
class OldApi {
    @Deprecated(since = "1.5", forRemoval = true)
    public void legacy() {}
}

// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();

// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }

// Common warning keys: unchecked, deprecation, rawtypes, null

// Java 17+ sealed-related
@Deprecated
class ToRemove {}

Annotations Personalizadas

Annotations personalizadas são declaradas com @interface. Membros parecem métodos mas são atributos de annotation — eles podem ter valores default. Use @Target para restringir onde se aplica (TYPE, METHOD, FIELD, etc.) e @Retention para controlar disponibilidade. Marker annotations (sem membros) apenas marcam elementos. Annotations elas mesmas não carregam comportamento — processadores (reflection, annotation tools) as leem e agem.

java
import java.lang.annotation.*;

// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    String value() default "";
    long timeout() default 0L;
}

// Use it
class MyTests {
    @Test
    public void quickCheck() {}

    @Test(timeout = 5000)
    public void slowCheck() {}

    @Test("custom-name")
    public void named() {}
}

// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
    String table() default "";
    String[] columns() default {};
}

@Entity(table = "users", columns = {"id", "name"})
class User {}

// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}

Retention & Target

@Retention controla por quanto tempo uma annotation sobrevive: SOURCE (apenas tempo de compilação, como @Override), CLASS (no bytecode mas não visível em runtime — o padrão), RUNTIME (acessível via reflection). @Target restringe onde uma annotation pode aparecer. Java 8+ adicionou TYPE_USE e TYPE_PARAMETER, permitindo anotar generics e casts (List<@NonNull String>). Escolha RUNTIME apenas se precisar de acesso via reflection.

java
import java.lang.annotation.*;

// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}

// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}

// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}

// ElementType targets
@Target(ElementType.TYPE)         // classes, interfaces, enums
@interface ForType {}

@Target(ElementType.METHOD)
@interface ForMethod {}

@Target(ElementType.FIELD)
@interface ForField {}

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}

// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names;  // example usage
List<@RuntimeVisible String> typed;

Lendo Annotations via Reflection

Annotations com retention RUNTIME podem ser lidas via reflection: isAnnotationPresent() verifica existência, getAnnotation() a recupera. É assim que frameworks (Spring, JUnit, JAX-RS) conectam comportamento declarativamente — você marca métodos/classes, o framework escaneia e faz dispatch. Annotation processing em tempo de compilação (annotation processors) é uma alternativa para geração de código sem custo de reflection em runtime.

java
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
    String path();
    String method() default "GET";
}

class Api {
    @Route(path = "/users", method = "GET")
    public void listUsers() {}

    @Route(path = "/users", method = "POST")
    public void createUser() {}
}

// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Route.class)) {
        Route r = m.getAnnotation(Route.class);
        System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
    }
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser

// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }

@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name());  // "orders"

Repeatable & Meta-Annotations

@Repeatable (Java 8+) permite aplicar a mesma annotation múltiplas vezes definindo uma annotation contêiner. @Inherited faz annotations propagarem para subclasses (apenas para annotations de nível de classe). @Documented inclui a annotation no Javadoc. @Target com ANNOTATION_TYPE cria meta-annotations (annotations que anotam outras annotations) — é assim que Spring constrói estereótipos de annotation composáveis como @RestController = @Controller + @ResponseBody.

java
import java.lang.annotation.*;

// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
    String cron();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
    Schedule[] value();  // container annotation
}

// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
    String cron();
}

// Now you can repeat it (Java 8+)
class Job {
    @Schedule2(cron = "0 0 * * *")
    @Schedule2(cron = "0 30 * * *")
    public void run() {}
}

// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)  // can only annotate other annotations
@interface TestCategory {}

@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}

// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {}  // Child also has @Persistent

// Documented: appears in Javadoc
@Documented
@interface PublicApi {}
14

Reflection

Objeto Class & Obtendo Classes

Todo tipo carregado tem um objeto Class único — o ponto de entrada para reflection. Obtenha-o via class literal (Type.class), instance.getClass(), ou Class.forName() (carregamento dinâmico, lança ClassNotFoundException). O objeto Class expõe name, modifiers, superclass, interfaces e verificações de tipo (isInterface, isArray, isEnum, isRecord). isAssignableFrom verifica relacionamentos polimórficos.

java
import java.lang.reflect.*;

// Three ways to get a Class object
Class<String> c1 = String.class;                  // class literal
Class<?> c2 = "hello".getClass();                 // from instance
Class<?> c3 = Class.forName("java.lang.String");  // by name (throws checked)

// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName());          // "java.lang.String"
System.out.println(c.getSimpleName());    // "String"
System.out.println(c.getPackage());       // "package java.lang"
System.out.println(c.getSuperclass());    // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers()));  // "public final"

// Check type relationships
System.out.println(c.isInterface());      // false
System.out.println(c.isArray());          // false
System.out.println(c.isEnum());           // false
System.out.println(c.isRecord());         // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c));  // true

// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive());  // true

Inspecionando Fields, Methods, Constructors

getDeclaredFields/Methods/Constructors retornam TODOS os membros (incluindo private) declarados apenas nesta classe. getFields/getMethods retornam apenas membros public mas incluem herdados. Para encontrar um membro específico, use getDeclaredField(name) ou getDeclaredMethod(name, paramTypes...) — tipos de parâmetro são necessários para desambiguar sobrecargas. Reflection contorna controle de acesso a menos que você chame setAccessible(true).

java
import java.lang.reflect.*;
import java.util.*;

class Sample {
    public String name;
    private int count;
    public Sample() {}
    public Sample(String n) { name = n; }
    private void secret() {}
    public int compute(int x) { return x * 2; }
}

Class<?> c = Sample.class;

// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName()
        + " (" + Modifier.toString(f.getModifiers()) + ")");
}

// Methods
for (Method m : c.getDeclaredMethods()) {
    System.out.println(m.getName()
        + " params=" + Arrays.toString(m.getParameterTypes())
        + " returns=" + m.getReturnType().getSimpleName());
}

// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
    System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}

// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);

Invocando Métodos & Criando Instâncias

Method.invoke(obj, args...) chama um método via reflection — sempre retorna Object, então faça cast do resultado. setAccessible(true) contorna verificações de acesso Java (membros private tornam-se acessíveis; pode exigir --add-opens em módulos). Constructor.newInstance() cria objetos — o equivalente via reflection de new. Array.newInstance cria arrays de um tipo de componente conhecido em runtime. Reflection é mais lento que chamadas diretas e contorna segurança em tempo de compilação.

java
import java.lang.reflect.*;

class Greeter {
    public String greet(String name) { return "Hello, " + name; }
    private String secret() { return "hidden"; }
}

Object obj = new Greeter();
Class<?> c = obj.getClass();

// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice");  // "Hello, Alice"

// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true);  // bypass access check
String s = (String) sec.invoke(obj);  // "hidden"

// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();

// With args
class Person {
    String name;
    public Person(String n) { name = n; }
    public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p);  // "Person(Bob)"

// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);

Lendo & Modificando Fields

Field.get(instance) lê o valor de um campo; Field.set(instance, value) o escreve. Para primitivos, use accessores type-specific (getInt/setInt) para evitar boxing. Campos estáticos recebem null como argumento de instância. setAccessible(true) é necessário para campos private. Acesso a campos via reflection é como bibliotecas de serialização (Jackson, Gson) e frameworks ORM leem/escrevem estado de objeto genericamente.

java
import java.lang.reflect.*;

class Config {
    public String env = "dev";
    private int retries = 3;
    public static String VERSION = "1.0";
}

Config cfg = new Config();
Class<?> c = cfg.getClass();

// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg);  // "dev"

// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg);  // 3
// For objects: Object val = field.get(instance);

// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env);     // "prod"

// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null);  // "1.0"
version.set(null, "2.0");

// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()

Proxies Dinâmicos & Casos de Uso

java.lang.reflect.Proxy cria proxies dinâmicos que implementam interfaces em runtime — um InvocationHandler intercepta cada chamada. É assim que Spring AOP, Hibernate lazy loading e Mockito mocks funcionam. Reflection alimenta a maioria dos frameworks Java (DI, ORM, serialização, testes) mas tem custos: mais lento que chamadas diretas, segurança de tipo mais fraca e restrições do module-system. Use quando precisar de flexibilidade em runtime, não para código ordinário.

java
import java.lang.reflect.*;
import java.util.*;

// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
    String getUser(long id);
    void deleteUser(long id);
}

// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
    private final Object target;
    LoggingHandler(Object t) { target = t; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("  -> " + result + " (" + (System.nanoTime() - start) + "ns)");
        return result;
    }
}

UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[]{UserService.class},
    new LoggingHandler(real)
);
proxied.getUser(42);  // logs the call and result

// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools

// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.
15

JDBC & Acesso a Banco de Dados

Connection & DriverManager

DriverManager.getConnection() abre uma conexão com banco de dados — sempre envolva em try-with-resources para evitar vazamentos. Desde JDBC 4, drivers se auto-registram via ServiceLoader, então Class.forName() raramente é necessário. O formato de URL varia por vendor. Use Properties para opções de conexão (SSL, timeouts). Em produção, prefira um pool de conexões (HikariCP) a chamadas diretas ao DriverManager.

java
import java.sql.*;

// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
    System.out.println("Connected: " + conn.getSchema());
    // ... use connection
}

// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");

// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test  (in-memory H2)

// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
    // ...
}

Statement vs PreparedStatement

Sempre use PreparedStatement em vez de Statement para qualquer consulta com parâmetros — previne SQL injection separando estrutura SQL de dados. Parâmetros são definidos por índice baseado em 1 com setters type-specific. PreparedStatements podem ser reutilizados (redefina params e execute novamente) e suportam batching (addBatch/executeBatch) para operações em massa. Statement é adequado apenas para SQL estático e confiável como DDL.

java
import java.sql.*;

// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     Statement st = c.createStatement()) {

    st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
    st.execute("INSERT INTO users VALUES (1, 'Alice')");

    // NEVER do this with user input — SQL injection!
    // st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}

// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     PreparedStatement ps = c.prepareStatement(sql)) {

    // Set parameters by index (1-based!)
    ps.setInt(1, 1);
    ps.setString(2, "Alice");
    ps.executeUpdate();

    // Reuse with different params
    ps.setInt(1, 2);
    ps.setString(2, "Bob");
    ps.executeUpdate();

    // Batch inserts
    for (int i = 3; i <= 100; i++) {
        ps.setInt(1, i);
        ps.setString(2, "user" + i);
        ps.addBatch();
    }
    ps.executeBatch();
}

ResultSet & Consultas

ResultSet é um cursor sobre linhas de consulta — chame next() para avançar (retorna false no fim). Leia colunas por nome (legível) ou índice baseado em 1. wasNull() distingue SQL NULL de um default primitivo (ex.: getInt retorna 0 para NULL). ResultSets padrão são forward-only; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE habilita acesso aleatório e updates in-place, embora isso raramente seja usado em apps modernos.

java
import java.sql.*;
import java.util.*;

record User(int id, String name, String email) {}

// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
     PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setBoolean(1, true);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {  // advance to next row
            users.add(new User(
                rs.getInt("id"),       // by column name
                rs.getString("name"),
                rs.getString("email")  // by column name (preferred)
            ));
        }
    }
}

// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")

// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous";  // distinguish NULL from real null

// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5);  // jump to row 5
rs.updateString("name", "newname");
rs.updateRow();  // persist change

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Transações & Batch

JDBC auto-commits cada instrução por padrão — defina autoCommit(false) para agrupar instruções em uma transação. Commit em sucesso, rollback em falha. Savepoints permitem rollback parcial dentro de uma transação. Níveis de isolamento controlam visibilidade de mudanças concorrentes (READ_COMMITTED é o default comum; SERIALIZABLE é o mais seguro mas mais lento). Sempre restaure autoCommit ou feche a conexão para evitar vazamento de estado de transação.

java
import java.sql.*;

// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
    c.setAutoCommit(false);  // start transaction
    try (PreparedStatement ps = c.prepareStatement(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
        ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate();  // debit
        ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate();  // credit
    }
    c.commit();  // commit both

    // If any step fails, rollback
} catch (SQLException e) {
    // connection auto-closed; rollback happens implicitly on close if not committed
}

// Explicit rollback pattern
try (Connection c = getConnection()) {
    c.setAutoCommit(false);
    try {
        // ... multiple statements
        c.commit();
    } catch (SQLException e) {
        c.rollback();  // undo all changes in this transaction
        throw e;
    }
}

// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp);  // undo only after savepoint
c.commit();

// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Connection Pooling (HikariCP)

Pools de conexão (HikariCP é o padrão de fato) mantêm conexões quentes e as reutilizam, evitando o custo de 10-100ms de abrir uma nova conexão TCP+auth por requisição. Configure max pool size (limitado pela capacidade do DB), timeouts e lifetime. Pegue com getConnection(), retorne fechando (ela volta para o pool, não é fechada). Sempre feche o DataSource no shutdown. No Spring Boot, HikariCP é auto-configurado.

java
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;

// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000);   // ms to wait for a connection
config.setIdleTimeout(600_000);        // ms before idle connections close
config.setMaxLifetime(1_800_000);      // ms max connection lifetime
config.setPoolName("app-pool");

HikariDataSource ds = new HikariDataSource(config);

// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
     PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getString("name"));
    }
}  // connection returns to pool here

// Shutdown pool when app stops
ds.close();

// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.

// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, Agroal
16

I/O & NIO Aprofundado

InputStream & OutputStream (Bytes)

InputStream/OutputStream lidam com bytes crus. Sempre envolva em variantes Buffered* — I/O sem buffer faz uma chamada de sistema por byte, o que é catastrófico para desempenho. read() retorna -1 no fim do stream. transferTo() (Java 9+) faz uma cópia em massa eficiente. readAllBytes() é conveniente mas carrega o stream inteiro na memória — apenas para arquivos pequenos. Sempre feche streams (try-with-resources).

java
import java.io.*;

// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
    int b;
    while ((b = in.read()) != -1) {  // -1 = end of stream
        // process byte
    }
}

// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
    byte[] buffer = new byte[8192];
    int read;
    while ((read = in.read(buffer)) != -1) {
        // process buffer[0..read]
    }
}

// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();

// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
    out.write(65);  // single byte
    out.write(new byte[]{66, 67, 68});
    out.flush();  // force buffered data to disk
}

// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
     OutputStream out = new FileOutputStream("dst.bin")) {
    in.transferTo(out);  // efficient bulk copy
}

// Standard streams
System.in.read();   // stdin (InputStream)
System.out.write(65);  // stdout (PrintStream)

Channels & Buffers (NIO)

NIO Channels + ByteBuffers são a alternativa de alto desempenho a streams. Buffers têm position/limit/capacity; flip() alterna do modo de escrita para leitura, clear() reinicia para escrita, compact() preserva dados não lidos. Direct buffers (allocateDirect) vivem fora do heap da JVM e evitam um passo de cópia para I/O grande. transferTo entre channels pode usar zero-copy em SOes suportados. Use NIO quando desempenho de streaming importa.

java
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;

// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // ByteBuffer: fixed-capacity block of bytes
    ByteBuffer buf = ByteBuffer.allocate(1024);
    int read = ch.read(buf);  // fill buffer from channel
    buf.flip();  // switch from write mode to read mode

    while (buf.hasRemaining()) {
        byte b = buf.get();
    }
    buf.clear();  // reset for next read (or compact() to preserve unread)
}

// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);

// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body});  // scatter

// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
     FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
    src.transferTo(0, src.size(), dst);
}

// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();

Operações de Path (NIO.2)

Path (NIO.2) substitui a antiga classe File. Operações de Path (getFileName, getParent, resolve, normalize, relativize) são matemática de string pura — elas não tocam o disco. Métodos Files.* interagem com o sistema de arquivos: size, timestamps, permissions, symlinks. normalize() limpa segmentos . e ... resolveSibling é útil para renomear (mesmo diretório, nome diferente). Prefira Path a File em código moderno.

java
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;

// Creating Path objects
Path p1 = Path.of("a/b/c.txt");          // relative
Path p2 = Path.of("/usr/local/bin");     // absolute
Path p3 = Paths.get("config.json");      // older API, same thing

// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName());    // "c.txt"
System.out.println(p1.getParent());      // "a/b"
System.out.println(p1.getRoot());        // "" (relative)
System.out.println(p2.getRoot());        // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize());      // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b")));  // "../.."

// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file));            // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));

// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);

// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);

Caminhamento de Diretórios & Árvores de Arquivos

Files.list() lista um nível; Files.walk() caminha recursivamente a árvore (retorna um Stream, deve fechar). Files.find() filtra por path e atributos durante o caminhamento. Para controle total, walkFileTree com um FileVisitor permite pular subárvores, tratar erros e agir antes/depois de visitar diretórios. Todos retornam Streams que mantêm handles de arquivo — sempre use try-with-resources. Use max depth para limitar caminhamentos caros.

java
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;

// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile)
           .map(Path::getFileName)
           .forEach(System.out::println);
}

// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
    walk.forEach(System.out::println);
}

// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
        (path, attrs) -> attrs.isRegularFile()
            && attrs.size() > 1_000_000
            && path.toString().endsWith(".log"))) {
    found.forEach(p -> System.out.println("Large log: " + p));
}

// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Visiting " + file);
        return FileVisitResult.CONTINUE;
    }
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().equals("target")) {
            return FileVisitResult.SKIP_SUBTREE;  // skip target dirs
        }
        return FileVisitResult.CONTINUE;
    }
});

import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;

WatchService & Eventos de Arquivo

WatchService recebe eventos de sistema de arquivos (create, modify, delete) para diretórios registrados. take() bloqueia até um evento chegar; pollEvents() os drena. Observe o diretório pai e filtre por context() para rastrear um arquivo específico. Eventos podem ser coalescidos ou perdidos (OVERFLOW). Observação recursiva exige registrar cada subdiretório. WatchService é nativo do SO (inotify no Linux, FSEvents no macOS) mas sua API é de baixo nível — considere bibliotecas para necessidades complexas.

java
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;

// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    Path dir = Path.of("/var/log");
    dir.register(watcher,
        ENTRY_CREATE,
        ENTRY_DELETE,
        ENTRY_MODIFY);

    System.out.println("Watching " + dir + "...");
    while (true) {
        WatchKey key = watcher.take();  // blocks until event
        for (WatchEvent<?> event : key.pollEvents()) {
            Path changed = dir.resolve((Path) event.context());
            System.out.println(event.kind() + " -> " + changed);

            if (event.kind() == OVERFLOW) continue;  // events lost

            // React to change
            if (event.kind() == ENTRY_CREATE) {
                System.out.println("New file: " + changed);
            }
        }
        if (!key.reset()) break;  // key no longer valid (dir deleted)
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.
17

Records & Pattern Matching

Básico de Records

Records (Java 16+) são portadores de dados imutáveis transparentes. O cabeçalho `record Name(Type1 f1, Type2 f2)` gera um construtor, accessors (f1(), f2() — não getF1()), equals, hashCode e toString. Eles são ideais para DTOs, objetos de valor e resultados de funções. O construtor compacto (apenas `{ ... }`) valida ou normaliza sem reatribuir campos. Records podem implementar interfaces mas não podem estender classes.

java
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}

// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)

Point p = new Point(3, 4);
System.out.println(p.x());          // 3 (accessor, not getX())
System.out.println(p.y());          // 4
System.out.println(p);              // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4)));  // true

// Immutable: no setters
// p.setX(5);  // no such method

// Custom record with validation
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age: " + value);
        }
    }
}

// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}

// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}

Construtores Compactos & Métodos Personalizados

O construtor compacto (`public Name { ... }`) executa antes da atribuição de campos — atribua ao parâmetro para normalizar, e o compilador o atribui ao campo. Records podem ter métodos adicionais e factories estáticas mas nenhum campo de instância além do cabeçalho. Construtores não-canônicos devem delegar ao canônico via this(...). Use factories estáticas para construção mais clara (Point.origin()) e para armazenar em cache instâncias comuns.

java
// Compact constructor: validation without reassigning
public record Email(String address) {
    public Email {
        if (!address.contains("@")) {
            throw new IllegalArgumentException("Bad email: " + address);
        }
        address = address.toLowerCase().strip();  // normalize
    }
}

// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
    public Money plus(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount + other.amount, currency);
    }
    public Money times(int factor) {
        return new Money(amount * factor, currency);
    }
    public static Money usd(double amt) { return new Money(amt, "USD"); }
}

// Canonical + non-canonical constructors
public record Range(int start, int end) {
    // Compact canonical (validation)
    public Range {
        if (start > end) throw new IllegalArgumentException();
    }
    // Non-canonical convenience constructor
    public Range(int end) {
        this(0, end);
    }
}

// Static factories are common on records
public record Point(int x, int y) {
    public static Point origin() { return new Point(0, 0); }
    public static Point of(int x, int y) { return new Point(x, y); }
}

Sealed Classes

Sealed classes/interfaces (Java 17+) restringem quais tipos podem estendê-las via cláusula `permits`. Cada subtipo permitido deve ser final, sealed ou non-sealed. Combinadas com records, elas formam algebraic data types (herança fechada + dados imutáveis). O benefício chave: o compilador conhece todos os subtipos, então expressões switch podem ser exaustivas sem um branch default — se você adicionar um novo subtipo, o compilador sinaliza cada switch que precisa atualização.

java
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
    permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {}  // open again

// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square sq -> sq.side() * sq.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // No default needed — compiler knows all subtypes
    };
}

// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)

// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}

Pattern Matching para instanceof

Pattern matching para instanceof (Java 16+) declara uma variável que é vinculada apenas quando o teste sucede, eliminando o cast explícito. O escopo da variável flui da verdade do padrão — usável em continuações && e após early returns. Java 21 adicionou pattern matching em switch (case Type var when guard), habilitando dispatch baseado em tipo com guards. Isso torna código de verificação de tipo muito mais conciso e menos propenso a erros.

java
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// Java 16+: pattern variable
if (obj instanceof String s) {
    System.out.println(s.length());  // s is in scope, no cast
}

// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
    System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited

// Negation scoping
if (!(obj instanceof String s)) {
    // s NOT in scope here
    return;
}
// s IS in scope here (the instanceof must have been true)

// Combining with other patterns
Object data = 42;
String desc = switch (data) {
    case Integer i when i > 0 -> "positive int: " + i;
    case Integer i -> "non-positive int: " + i;
    case String s -> "string: " + s;
    case null -> "null";
    default -> "other";
};

Switch Pattern Matching (Java 21)

Switch pattern matching (Java 21, final) permite fazer switch em tipos, desestruturar records e adicionar guards (when). Combinado com sealed types, o compilador verifica exaustividade — sem default necessário se todos os subtipos estiverem cobertos. Record patterns (case Point(int x, int y)) desestruturam em um passo. case null é explícito (sem NPE). Isso traz Java perto do pattern matching de ML/Scala para modelar domínios declarativamente.

java
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}

String describe(Event e) {
    return switch (e) {
        case Login l -> l.user() + " logged in";
        case Logout l -> l.user() + " logged out";
        case Message m when m.from().equals("system") -> "[system] " + m.text();
        case Message m -> m.from() + ": " + m.text();
        case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
        case Error(int code, String detail) -> "error " + code + ": " + detail;
        case null -> "no event";  // explicit null handling
    };
}

// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == y -> "diagonal";
        case Point(int x, int y) -> "point at (" + x + "," + y + ")";
        default -> "not a point";
    };
}

// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
    return switch (o) {
        case Box(Point(int x, int y)) -> "box at " + x + "," + y;
        default -> "unknown";
    };
}
18

Módulos (JPMS)

Básico de module-info.java

module-info.java declara um módulo (Java 9+ JPMS). requires adiciona uma dependência; exports torna pacotes acessíveis; opens permite acesso reflexivo (necessário para frameworks de serialização/DI); uses/provides conectam ServiceLoader. Sem esse arquivo, o código vive no classpath como um 'unnamed module' com comportamento legado. Módulos dão encapsulamento forte (apenas pacotes exportados são public) e configuração confiável (dependências explícitas).

java
// File: src/com.example.app/module-info.java
module com.example.app {
    requires java.sql;           // depends on java.sql module
    requires transitive com.example.lib;  // re-export (consumers see it too)
    requires static java.annotation;  // compile-time only (optional at runtime)

    exports com.example.app.api;       // public API visible to all
    exports com.example.app.internal to com.example.test;  // qualified export

    opens com.example.app.model to com.fasterxml.jackson.databind;  // reflection only
    opens com.example.app.dynamic;  // open to all for reflection

    uses com.example.app.spi.Plugin;  // service consumer
    provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin;  // service provider
}

// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration

// A module without module-info.java is an "unnamed module" (classpath behavior)

requires, exports, opens

requires declara uma dependência; requires transitive a propaga (use quando sua API pública expõe tipos desse módulo). exports torna pacotes public; exports to restringe a módulos nomeados (qualified exports). opens concede acesso reflexivo (deep reflection) — essencial para frameworks que fazem setAccessible(true). A distinção entre exports (API pública) e opens (reflection) é chave: encapsulamento forte é o padrão, e você opta por pacote.

java
// Module A: com.example.library
module com.example.library {
    // Public API anyone can use
    exports com.example.library.api;

    // Internal package: only visible to specific modules
    exports com.example.library.internal to com.example.app;

    // Allow reflection for frameworks (Jackson, JPA)
    opens com.example.library.model;

    // Only specific module can reflect
    opens com.example.library.config to com.example.app;

    // Dependencies
    requires java.logging;
    requires transitive java.sql;  // consumers of A also get java.sql
}

// Module B: com.example.app (consumer)
module com.example.app {
    requires com.example.library;  // use A's exported packages
    // Note: transitive means java.sql is also available here

    requires com.fasterxml.jackson.databind;
}

// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.

// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)

// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).

ServiceLoader & Services

ServiceLoader implementa o padrão SPI: uma interface em um módulo, implementações descobertas em runtime. O módulo API exporta a interface; módulos provider declaram `provides X with Y`; módulos consumer declaram `uses X`. ServiceLoader.load(X.class) encontra todos os providers no module path. Isso desacopla interface de implementação — drivers JDBC, backends de logging (SLF4J) e providers de Charset funcionam assim. Sem dependência de compilação na implementação.

java
// SPI (Service Provider Interface) pattern with modules

// 1. Define the service interface in an API module
module com.example.spi {
    exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
    String name();
    void run();
}

// 2. Implement in a provider module
module com.example.plugin.impl {
    requires com.example.spi;
    provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
    public String name() { return "default"; }
    public void run() { System.out.println("running"); }
}

// 3. Consume in an app module
module com.example.app {
    requires com.example.spi;
    uses com.example.spi.Plugin;  // declares intent to load services
}

// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
    System.out.println("Found: " + p.name());
    p.run();
}

// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.

Module Path vs Classpath

O module path (--module-path) contém JARs modulares com encapsulamento forte; o classpath (-cp) contém JARs legados como unnamed modules sem encapsulamento. JARs modulares funcionam em ambos. Automatic modules são JARs sem module-info colocados no module path — seu nome vem do nome do arquivo ou do atributo de manifesto Automatic-Module-Name. --add-opens é uma saída de escape para reflection legado que quebra sob encapsulamento forte.

java
// Compile a module
//   javac -d out/com.example.app \
//         --module-source-path src \
//         --module com.example.app

// Run a modular app
//   java --module-path out --module com.example.app/com.example.app.Main

// Package as a modular JAR (includes module-info.class)
//   jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .

// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation

// Mixing: modular JARs can be used on the classpath too (automatic module)
//   java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).

// Inspect a module JAR
//   jar --describe-module --file app.jar

// List observable modules
//   java --list-modules
//   java --describe-module java.sql

// Add opens at runtime for legacy reflection (escape hatch)
//   java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMED

jdeps & jlink (Runtimes Personalizados)

jdeps analisa bytecode para listar dependências de módulo — útil para migrar para módulos e encontrar deps não usadas. jlink cria um JRE personalizado contendo apenas os módulos que seu app precisa, produzindo um runtime self-contained, menor, com inicialização mais rápida. Isso é ideal para imagens Docker e instaladores: envie o app mais um JRE de 30-50MB em vez de exigir uma instalação de JDK de 300MB. Juntos, jdeps + jlink habilitam deployments Java enxutos e self-contained.

java
# jdeps: analyze dependencies (find unused, list required modules)

# List dependencies of a JAR
jdeps --module-path lib app.jar

# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar

# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql

# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
  --module-path "$JAVA_HOME/jmods:./out" \
  --add-modules com.example.app \
  --output ./custom-jre \
  --strip-debug \
  --compress=zip-6 \
  --no-header-files \
  --no-man-pages \
  --launcher app=com.example.app/com.example.app.Main

# The custom JRE is self-contained:
#   ./custom-jre/bin/app   # launches the app
#   ./custom-jre/bin/java  # the stripped JVM

# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods

# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)
19

Concorrência Aprofundado

Locks: ReentrantLock & ReadWriteLock

ReentrantLock oferece mais controle que synchronized: tryLock (non-blocking/timed), fairness, interruptibility e inspeção de estado de lock. Sempre unlock em finally. ReadWriteLock permite muitos leitores concorrentes mas escritores exclusivos — ótimo para caches de leitura pesada. StampedLock (Java 8+) adiciona leituras otimistas para ainda melhor throughput de leitura. Prefira synchronized para casos simples; use Lock quando precisar de seus recursos avançados.

java
import java.util.concurrent.locks.*;

// ReentrantLock: more flexible than synchronized
class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // MUST be in finally
        }
    }

    public int get() {
        lock.lock();
        try { return count; }
        finally { lock.unlock(); }
    }
}

// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
}

// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);

// ReadWriteLock: many readers OR one writer
class Cache {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final Lock read = rw.readLock();
    private final Lock write = rw.writeLock();
    private java.util.Map<String, String> data = new java.util.HashMap<>();

    public String get(String key) {
        read.lock();
        try { return data.get(key); }
        finally { read.unlock(); }
    }
    public void put(String key, String val) {
        write.lock();
        try { data.put(key, val); }
        finally { write.unlock(); }
    }
}

Coleções Concorrentes

ConcurrentHashMap é o map thread-safe workhorse — use compute/merge para updates atômicos em vez de check-then-act. CopyOnWriteArrayList é melhor para listas de leitura pesada, escrita rara (event listeners) — writes copiam o array. BlockingQueue é a espinha dorsal de pipelines producer-consumer (put bloqueia quando cheio, take bloqueia quando vazio). ConcurrentLinkedQueue é non-blocking e unbounded. Esses substituem wrappers synchronized (Collections.synchronizedX) que são coarse-grained e mais lentos.

java
import java.util.concurrent.*;
import java.util.*;

// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2);  // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1);  // atomic update
map.merge("a", 1, Integer::sum);  // add 1 atomically
Integer val = map.getOrDefault("a", 0);

// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x");  // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }

// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task");        // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS);  // timed
// Consumer
String task = queue.take();  // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);

// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");

// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();

import java.util.concurrent.TimeUnit;

CountDownLatch & CyclicBarrier

CountDownLatch é um gate one-shot — N threads fazem countdown, outras aguardam; não resettable. CyclicBarrier é reutilizável — threads esperam umas pelas outras em um ponto de encontro, com uma ação opcional quando todas chegam. Phaser é o mais flexível: parties variáveis, múltiplas fases, estrutura de árvore. Use latch para coordenação de startup, barrier para algoritmos paralelos multi-fase, phaser para contagens de participantes dinâmicas.

java
import java.util.concurrent.*;
import java.util.*;

// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());

for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
        results.add("worker-" + id);
        ready.countDown();  // signal done
    }).start();
}
ready.await();  // main thread blocks until count reaches 0
System.out.println("All done: " + results);

// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("--- phase complete ---"));

Runnable worker = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " phase 1");
        barrier.await();  // wait for all 3
        System.out.println(Thread.currentThread().getName() + " phase 2");
        barrier.await();  // reusable for next phase
    } catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();

// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register();  // dynamically add a party
phaser.arriveAndAwaitAdvance();

Semaphore & Exchanger

Semaphore controla acesso a N permits — acquire bloqueia até um estar disponível, release o retorna. Use para rate limiting, pools de conexão ou qualquer cenário de recurso bounded. tryAcquire oferece variantes non-blocking e timed. Exchanger permite que duas threads troquem valores em um ponto de encontro — nicho mas elegante para designs de pipeline onde duas threads trocam buffers. Ambos estão em java.util.concurrent e são de nível mais baixo que locks para alguns padrões de coordenação.

java
import java.util.concurrent.*;

// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5);  // 5 concurrent allowed

pool.acquire();  // blocks until a permit is available
try {
    // critical section (at most 5 threads here at once)
    System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
    pool.release();  // return the permit
}

// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
    try { /* work */ } finally { pool.release(); }
} else {
    System.out.println("Too busy, try later");
}

// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
    try { /* work */ } finally { pool.release(); }
}

// Use case: rate limiting, connection pools, parking lots

// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
    try {
        String got = ex.exchange("from-A");  // gives "from-A", receives "from-B"
        System.out.println("A got: " + got);
    } catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B");  // gives "from-B", receives "from-A"
System.out.println("B got: " + got);

import java.util.concurrent.TimeUnit;

CompletableFuture Avançado

CompletableFuture é a Promise do Java — componha trabalho async com thenApply (map), thenCompose (flatMap), thenCombine (zip dois). allOf espera todos, anyOf pelo primeiro. exceptionally recupera de erros; handle cobre ambos. orTimeout (Java 9+) cancela se demorar muito. Sempre passe um executor explícito — o commonPool padrão pode starvar sob trabalho bloqueante. Esta é a fundação do código async estilo reativo em Java.

java
import java.util.concurrent.*;
import java.util.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "hello", pool)            // async supplier
    .thenApply(String::toUpperCase)              // transform
    .thenCompose(s -> CompletableFuture.supplyAsync(  // flatMap
        () -> s + " world"))
    .thenApply(s -> s + "!");

System.out.println(future.join());  // "HELLO world!"

// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join());  // 30

// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
    CompletableFuture.supplyAsync(() -> 1, pool),
    CompletableFuture.supplyAsync(() -> 2, pool),
    CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();

// Error handling
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> { throw new RuntimeException("boom"); })
    .exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join());  // "fallback: boom"

// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
    .supplyAsync(() -> "ok")
    .handle((val, ex) -> ex == null ? val : "error");

// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
    .supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
    .orTimeout(1, TimeUnit.SECONDS)
    .exceptionally(ex -> "timed out");

Virtual Threads (Java 21)

Virtual threads (Java 21) são threads leves agendadas pela JVM em um pequeno pool de carrier (platform) threads. Quando uma virtual thread bloqueia em I/O, ela é suspensa e a carrier executa outra — então você pode ter milhões de operações bloqueantes concorrentes. Isso permite escrever código bloqueante simples em vez de cadeias reativas/async complexas. Use-as para workloads I/O-bound (HTTP handlers, DB calls); para trabalho CPU-bound, platform threads ou parallelStream ainda são apropriados.

java
import java.util.concurrent.*;
import java.util.*;

// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"

// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread);
});

// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
    // blocking I/O here is fine — virtual thread yields, not the OS thread
});

// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();

// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
    // Submit a million tasks — each gets its own virtual thread
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        final int id = i;
        futures.add(es.submit(() -> {
            Thread.sleep(100);  // blocking call is cheap on virtual threads
            return "done-" + id;
        }));
    }
    // virtual threads yield when blocking, so 1M concurrent is feasible
}

// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)

// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).
20

Collections Framework Aprofundado

Comparators & Sorting

Comparator.comparing(keyExtractor) constrói comparators a partir de uma função de chave — muito mais limpo que escrever lógica compare crua. thenComparing encadeia chaves de ordenação secundárias. nullsFirst/nullsLast tratam nulls com segurança. Use comparingInt/comparingLong/comparingDouble para evitar autoboxing. List.sort() ordena in-place (apenas listas mutáveis); Stream.sorted() retorna um novo stream ordenado. Comparators alimentam sorting, ordenação de TreeSet/TreeMap e operações de stream.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Carol", 30),
    new Person("Dave", 25)
);

// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();

// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();

// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
    Comparator.comparingInt(Person::age)
              .thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]

// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList();  // [null, a, b]

// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();

// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]

// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);

Coleções Unmodifiable & Imutáveis

List.of/Set.of/Map.of (Java 9+) criam coleções verdadeiramente imutáveis — sem nulls, sem mutações. Collections.unmodifiableX cria uma view somente leitura que ainda reflete mudanças na coleção backing. List.copyOf (Java 10+) faz uma cópia imutável independente. Arrays.asList é uma view de tamanho fixo de um array (set funciona, add/remove não). Escolha com base na necessidade: factories imutáveis para constantes, views unmodifiable para expor internals com segurança, copyOf para cópias defensivas.

java
import java.util.*;

// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d");  // throws UnsupportedOperationException
// immutable.set(0, "z");  // throws
// nulls not allowed in these immutable collections

// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c");  // throws
mutable.add("c");  // but changes to backing list ARE visible in view
System.out.println(view);  // [a, b, c]

// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable);  // independent immutable copy

// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x");  // OK (writes through to array)
// fixed.add("c");  // throws (size fixed)

// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));

Implementações de Queue & Deque

ArrayDeque é a implementação preferida de stack e queue — mais rápida que a legada Stack (que é synchronized) e LinkedList. PriorityQueue ordena elementos por um Comparator (min-heap por padrão) — use para agendamento, problemas top-K. Deque suporta ambas as extremidades; use addFirst/removeFirst para semântica de stack, addLast/removeFirst para semântica de queue. Para queues concorrentes, use as implementações java.util.concurrent (LinkedBlockingQueue, etc.).

java
import java.util.*;

// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek());  // "a" (head)
System.out.println(queue.poll());  // "a" (remove head)

// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst());  // "a"
System.out.println(deque.peekLast());   // "b"
deque.removeFirst(); deque.removeLast();

// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first");  // addFirst
stack.push("second");
System.out.println(stack.pop());  // "second" (LIFO)
System.out.println(stack.peek()); // "first"

// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll());  // 1 (smallest first)
System.out.println(pq.poll());  // 3

// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll());  // "a"

// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue use

Map merge, compute, getOrDefault

Esses métodos Map tornam padrões comuns atômicos e concisos. merge é o idioma de contagem de palavras — combina valor existente e novo, removendo a entrada se a função retornar null. computeIfAbsent é o padrão de lazy-cache (memoization). getOrDefault evita verificações de null. replaceAll transforma todos os valores. Esses são muito mais limpos que a dança check-then-act get/put, e são os blocos de construção para updates atômicos do ConcurrentHashMap em código concorrente.

java
import java.util.*;

Map<String, Integer> counts = new HashMap<>();

// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0);  // 0, no null

// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1);  // sets to 1
counts.putIfAbsent("a", 2);  // no change (already present)

// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1);  // increment

// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread

// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null);  // decrement, remove at 0

// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum);  // word count pattern
counts.merge("word", 1, Integer::sum);  // now 2
counts.merge("word", 1, Integer::sum);  // now 3

// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);

// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);

// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
    freq.merge(w, 1, Integer::sum);
}

Métodos Utilitários de Collections

Collections.* fornece utilitários clássicos: sort, binarySearch (requer entrada ordenada), shuffle, reverse, frequency, min/max. Factories singleton/empty retornam coleções imutáveis de elemento único ou vazias — prefira emptyList() a retornar null. nCopies é eficiente em memória (um elemento compartilhado). Wrappers synchronized existem para código legado mas prefira coleções java.util.concurrent. Wrappers checked capturam violações de tipo genérico em runtime, úteis para interop com raw types.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

// Sorting
Collections.sort(nums);                    // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder());      // List.sort (preferred)

// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4);  // index, or negative if absent

// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);

// Frequency & disjoint
int freq = Collections.frequency(nums, 1);  // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4));  // true

// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());

// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);

// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();

// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x");  // [x,x,x,x,x]

// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());

// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123);  // throws ClassCastException at the add site
21

Testes (JUnit 5 & Mockito)

Básico de JUnit 5

Annotations do JUnit 5 (Jupiter): @Test marca um teste; @BeforeEach/@AfterEach executam ao redor de cada teste; @BeforeAll/@AfterAll executam uma vez para a classe (devem ser static). @DisplayName customiza nomes de teste. @Disabled pula testes. assertThrows verifica exceções. Testes devem ser independentes — use @BeforeEach para resetar estado, não campos estáticos. JUnit 5 vive em org.junit.jupiter.api (diferente do org.junit do JUnit 4).

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calc;

    @BeforeAll
    static void setUpOnce() {
        // runs once before all tests (must be static)
        System.out.println("Starting CalculatorTest");
    }

    @BeforeEach
    void setUp() {
        // runs before each test
        calc = new Calculator();
    }

    @Test
    @DisplayName("2 + 2 should equal 4")
    void addsTwoNumbers() {
        assertEquals(4, calc.add(2, 2));
    }

    @Test
    void dividesByZeroThrows() {
        ArithmeticException ex = assertThrows(
            ArithmeticException.class,
            () -> calc.divide(1, 0)
        );
        assertEquals("Division by zero", ex.getMessage());
    }

    @AfterEach
    void tearDown() {
        // runs after each test (cleanup)
        calc = null;
    }

    @AfterAll
    static void tearDownOnce() {
        // runs once after all tests
    }

    @Disabled("until bug #42 is fixed")
    @Test
    void skippedTest() {}
}

class Calculator {
    int add(int a, int b) { return a + b; }
    int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("Division by zero");
        return a / b;
    }
}

Asserções

Asserções do JUnit 5: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (identidade). assertAll agrupa verificações para que todas executem mesmo se algumas falharem. assertTimeout falha testes lentos. Mensagens podem ser strings ou Suppliers (lazy — construídas apenas em falha, evitando concatenação de string quando testes passam). assertIterableEquals compara coleções ordenadas. Essas vêm de org.junit.jupiter.api.Assertions.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {

    @Test
    void equality() {
        assertEquals(4, 2 + 2);
        assertEquals(4, 2 + 2, "math is broken");
        assertNotEquals(5, 2 + 2);
    }

    @Test
    void booleans() {
        assertTrue(5 > 3);
        assertFalse(5 < 3, "5 should not be less than 3");
    }

    @Test
    void nullness() {
        assertNull(null);
        assertNotNull(new Object());
    }

    @Test
    void sameInstance() {
        String a = "x";
        assertSame(a, a);          // ==
        assertNotSame(a, new String("x"));
    }

    @Test
    void collections() {
        assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
        assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
    }

    @Test
    void groupedAssertions() {
        // All run even if one fails — reports all failures
        assertAll("person",
            () -> assertEquals("Alice", "Alice"),
            () -> assertEquals(30, 30),
            () -> assertNotNull("x")
        );
    }

    @Test
    void timeout() {
        // Fails if it takes longer than 100ms
        assertTimeout(Duration.ofMillis(100), () -> {
            Thread.sleep(10);
        });
        // assertTimeoutPreemptively: stops the task early (in another thread)
    }

    @Test
    void customMessage() {
        int result = 5;
        assertEquals(4, result, () -> "expected 4 but got " + result);
        // Supplier<String> — message built lazily only on failure
    }

    import java.time.Duration;
    import java.util.List;
}

Testes Parametrizados

Testes parametrizados executam a mesma lógica de teste com múltiplas entradas. @ValueSource fornece um array de argumento único. @CsvSource mapeia linhas CSV para múltiplos params. @MethodSource (mais flexível) usa um Stream<Arguments> estático. @EnumSource itera valores de enum. @NullAndEmptySource adiciona casos null/empty. Isso elimina métodos de teste copy-paste e torna testes data-driven limpos. Display names incluem automaticamente os parâmetros para diagnóstico fácil.

java
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void positiveNumbersArePositive(int n) {
        assertTrue(n > 0);
    }

    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t"})
    void blankStrings(String s) {
        assertTrue(s.isBlank());
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t"})
    void blankOrNull(String s) {
        assertTrue(s == null || s.isBlank());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5"
    })
    void addition(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
    void fromCsv(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @MethodSource("additionProvider")
    void fromMethod(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
    static java.util.stream.Stream<Arguments> additionProvider() {
        return java.util.stream.Stream.of(
            Arguments.of(1, 1, 2),
            Arguments.of(2, 3, 5)
        );
    }

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void allEnums(TimeUnit unit) {
        assertNotNull(unit);
    }
}

Lifecycle, Nested & Conditional

@Nested cria classes de teste internas que compartilham lifecycle — ótimo para estruturas BDD-style 'when X then Y' onde setup externo se aplica a testes internos. Annotations condicionais (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) pulam testes com base no ambiente. @Tag agrupa testes para execução seletiva (ex.: fast vs slow, unit vs integration). Classes nested não podem ter @BeforeAll (elas são non-static). Esses recursos tornam a organização de testes expressiva.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;

class LifecycleTest {

    @Test
    void topLevel() {}

    @Nested
    @DisplayName("when stack is empty")
    class WhenEmpty {
        @BeforeEach
        void setUp() { /* stack is empty here */ }

        @Test
        void throwsOnPop() {
            assertThrows(Exception.class, () -> {});
        }

        @Nested
        @DisplayName("after pushing one element")
        class AfterPush {
            @BeforeEach
            void push() { /* push one */ }

            @Test
            void popReturnsElement() {
                // ...
            }
        }
    }

    // Conditional execution
    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {}

    @Test
    @EnabledIfSystemProperty(named = "env", matches = "ci")
    void onlyInCi() {}

    @Test
    @EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
    void onlyWithPostgres() {}

    @Test
    @DisabledIf("customCondition")
    void conditional() {}
    static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }

    // Tagging for selective runs
    @Test
    @Tag("slow")
    void slowIntegrationTest() {}

    @Test
    @Tag("fast")
    void fastUnitTest() {}

    // Run only fast: mvn test -Dgroups=fast
    import org.junit.jupiter.api.condition.OS;
    import java.util.concurrent.TimeUnit;

Mockito Mocking

Mockito cria test doubles para dependências. @Mock cria um mock; @InjectMocks constrói um objeto real com mocks injetados. when(...).thenReturn(...) faz stub de valores de retorno; verify(...) verifica interações. Argument matchers (any(), eq(), argThat()) correspondem chamadas flexivelmente. Spies envolvem objetos reais (mocking parcial). O padrão Arrange-Act-Assert mantém testes legíveis. Mocking isola a unidade sob teste de suas dependências (banco de dados, rede, tempo).

java
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    UserRepository repo;  // mock dependency

    @InjectMocks
    UserService service;  // real service with mocks injected

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void returnsUserWhenFound() {
        // Arrange: stub the mock
        User alice = new User(1, "Alice");
        when(repo.findById(1)).thenReturn(alice);

        // Act
        String name = service.getUserName(1);

        // Assert
        assertEquals("Alice", name);
        verify(repo).findById(1);       // called once
        verify(repo, never()).findById(2);
    }

    @Test
    void throwsWhenNotFound() {
        when(repo.findById(99)).thenReturn(null);
        // or: when(repo.findById(99)).thenThrow(new RuntimeException());

        assertThrows(RuntimeException.class, () -> service.getUserName(99));
    }

    @Test
    void argumentMatchers() {
        when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
        // matchers: eq(), any(), anyInt(), contains(), argThat()

        service.getUserName(42);
        verify(repo).findById(intThat(n -> n > 0));
    }

    @Test
    void verifyInteractionDetails() {
        service.getUserName(1);
        verify(repo, times(1)).findById(1);
        verify(repo, atLeastOnce()).findById(anyInt());
        verifyNoMoreInteractions(repo);
    }

    @Test
    void spy_partialMock() {
        List<String> spy = spy(new ArrayList<>());
        spy.add("real");
        when(spy.size()).thenReturn(100);  // stub one method
        assertEquals(100, spy.size());     // stubbed
        assertEquals(1, spy.size());       // wait, this would be 100 too
    }
}

interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
    UserRepository repo;
    UserService(UserRepository r) { repo = r; }
    String getUserName(int id) {
        User u = repo.findById(id);
        if (u == null) throw new RuntimeException("not found");
        return u.name();
    }
}
22

Testes JUnit

Teste Básico

JUnit 5 usa @Test de org.junit.jupiter.api. assertEquals verifica se esperado é igual a atual. Outras asserções: assertTrue, assertThrows, assertAll.

java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
    @Test
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
    }
}

Testes Parametrizados

Testes parametrizados executam o mesmo teste com entradas diferentes. @ValueSource fornece argumentos únicos. @CsvSource fornece múltiplos argumentos. Reduz duplicação de testes.

java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }

@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calc.add(a, b));
}

Métodos de Lifecycle

@BeforeAll/@AfterAll executam uma vez por classe (devem ser static). @BeforeEach/@AfterEach executam ao redor de cada teste. Use para conexões de banco de dados e setup de mock.

java
class DbTest {
    @BeforeAll static void setupAll() { /* once before all */ }
    @AfterAll static void tearDownAll() { /* once after all */ }
    @BeforeEach void setup() { /* before each test */ }
    @AfterEach void tearDown() { /* after each test */ }
}

Asserções

assertAll executa todas as asserções mesmo se algumas falharem. assertThrows verifica que código lança uma exceção específica. Use assertTimeout para testes limitados por tempo.

java
@Test
void testAll() {
    assertAll("person",
        () -> assertEquals("Alice", p.getName()),
        () -> assertEquals(30, p.getAge())
    );
}
@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}

Mockito

@Mock cria objetos mock, @InjectMocks os injeta. when().thenReturn() faz stub de chamadas. verify() verifica se método foi chamado. Mockito é o framework de mocking padrão.

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @InjectMocks UserService service;
    @Test
    void testFind() {
        when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
        assertEquals("Alice", service.findUser(1).getName());
        verify(repo).findById(1);
    }
}
23

Maven/Gradle

Maven POM

Maven usa pom.xml. groupId/artifactId/version identificam o projeto. Dependências têm scope (compile, test, provided). Maven impõe estrutura de diretório padrão.

java
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Comandos Maven

Lifecycle do Maven: clean, compile, test, package, install, deploy. Cada fase executa fases precedentes. Use -DskipTests para pular testes.

java
mvn clean          # Remove target/
mvn compile        # Compile sources
mvn test           # Run tests
mvn package        # Build JAR
mvn install        # Install to local repo
mvn dependency:tree  # Show dependency tree

Build Gradle

Gradle usa build.gradle (Groovy) ou build.gradle.kts (Kotlin). implementation para deps de compilação, testImplementation para teste. Gradle é mais rápido que Maven.

java
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }

Comandos Gradle

Tasks do Gradle: build, test, run, clean. O wrapper (gradlew) garante versões consistentes. Use --parallel para builds de módulo paralelos.

java
gradle clean       # Clean build
gradle build       # Build + test
gradle test        # Run tests
gradle run         # Run application
gradle bootRun     # Run Spring Boot
gradle dependencies  # Show dependency tree

Projeto Multi-Módulo

Projetos multi-módulo dividem apps grandes. settings.gradle lista módulos. project(:core) cria dependências inter-módulo. Cada módulo tem seu próprio build.gradle.

java
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
    apply plugin: 'java'
    repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }
24

Básico de Spring

App Spring Boot

@SpringBootApplication habilita auto-configuração, component scanning e configuração. SpringApplication.run inicia o servidor embedded. Elimina configuração XML.

java
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

REST Controller

@RestController combina @Controller e @ResponseBody. @GetMapping, @PostMapping são atalhos. @PathVariable extrai params de URL, @RequestBody vincula JSON.

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    @PostMapping
    public User create(@RequestBody User user) {
        return userService.save(user);
    }
}

Injeção de Dependência

@Autowired injeta dependências. Injeção por construtor é recomendada (testável, imutável). @Service, @Repository, @Component são estereótipos para injeção.

java
@Service
public class UserService {
    private final UserRepository repo;
    @Autowired  // Constructor injection (recommended)
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

Configuração

@Configuration marca classes de config. @Bean declara beans gerenciados pelo Spring. @Primary torna um bean preferido. Use para classes de terceiros.

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    @Bean @Primary
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Application Properties

application.properties configura Spring Boot. Profiles habilitam config específica de ambiente. Ative com spring.profiles.active=dev. Use @Value ou @ConfigurationProperties.

java
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090
25

JDBC Aprofundado

Connection & Statement

DriverManager.getConnection estabelece uma conexão. Statement executa SQL estático. ResultSet itera resultados. Sempre feche recursos ou use try-with-resources.

java
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
    System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();

PreparedStatement

PreparedStatement previne SQL injection parametrizando consultas. Defina valores por índice (baseado em 1). try-with-resources fecha automaticamente. Melhora desempenho através de pré-compilação.

java
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, "Alice");
    ps.setString(2, "[email protected]");
    ps.executeUpdate();
}

Gerenciamento de Transações

setAutoCommit(false) inicia uma transação. commit persiste, rollback desfaz. Se qualquer instrução falhar, rollback para manter integridade. @Transactional trata isso no Spring.

java
conn.setAutoCommit(false);
try {
    stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
    stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Connection Pooling

Connection pooling reutiliza conexões. HikariCP é o pool mais rápido. maximumPoolSize limita conexões concorrentes. Sempre feche (retorna ao pool). Spring Boot configura HikariCP automaticamente.

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();

Metadados de ResultSet

ResultSetMetaData descreve estrutura de resultados: nomes de coluna, tipos, propriedades. Útil para acesso genérico a dados. Índices de coluna são baseados em 1.

java
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
    System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));
26

Utilitários de Concorrência

ExecutorService

ExecutorService gerencia pools de threads. submit retorna um Future para resultados async. get bloqueia até terminar (com timeout). Sempre faça shutdown do executor.

java
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();

CompletableFuture

CompletableFuture habilita programação async funcional. supplyAsync executa no ForkJoinPool. thenApply transforma, thenAccept consome, exceptionally trata erros.

java
CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> process(data))
    .thenAccept(result -> System.out.println(result))
    .exceptionally(ex -> { ex.printStackTrace(); return null; });

Coleções Concorrentes

ConcurrentHashMap é thread-safe sem locking completo. CopyOnWriteArrayList copia em escrita (leitura pesada). BlockingQueue suporta padrões producer-consumer.

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

CountDownLatch & CyclicBarrier

CountDownLatch espera por N threads (one-shot). CyclicBarrier espera por N threads então reinicia (reutilizável). Use latch para startup, barrier para computação em fases.

java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
    new Thread(() -> { work(); latch.countDown(); }).start();
latch.await();  // Wait for all

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));

Variáveis Atomic

Variáveis atomic fornecem operações thread-safe sem lock. compareAndSet habilita optimistic locking. LongAdder é mais rápido que AtomicLong para contadores de alta contenção.

java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();
27

Internos da JVM

Áreas de Memória

Memória da JVM: Heap (objetos, gerenciado por GC), Stack (chamadas de método, por thread), Metaspace (metadados de classe). Young Gen usa copying GC, Old Gen usa mark-sweep-compact.

java
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2g

Class Loading

Class loading é lazy. Bootstrap carrega Java core, Extension carrega extensões, Application carrega classpath. Inicializadores estáticos executam uma vez. Classloaders personalizados habilitam hot-reload.

java
// Bootstrap -> Extension -> Application classloaders
class MyClass {
    static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loading

Bytecode

Java compila para bytecode (baseado em stack). javap -c desmonta arquivos de classe. Cada instrução faz push/pop da operand stack. Java agents podem modificar bytecode em tempo de carregamento.

java
// javap -c MyClass.class
// Method int add(int, int):
//   iload_1      // Load local var 1
//   iload_2      // Load local var 2
//   iadd         // Add
//   ireturn      // Return int

Compilação JIT

JIT compila bytecode frequentemente executado para código nativo. Tiered compilation equilibra startup e desempenho de pico. Métodos hot são inlined e otimizados.

java
// JIT compiles hot methods to native code
// -XX:+PrintCompilation  // Show JIT activity
// -XX:CompileThreshold=10000  // Method call count
// Tiered: Interpreter -> C1 -> C2

Thread Dump

Thread dumps mostram todos os estados de thread e stack traces. Essencial para depurar deadlocks e hangs. jstack é a ferramenta de linha de comando. Procure por threads BLOCKED e WAITING.

java
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"
28

Garbage Collection

Algoritmos de GC

Serial GC para apps pequenos. Parallel GC maximiza throughput. G1 GC equilibra throughput e latência (padrão). ZGC fornece pausas sub-milissegundo para heaps grandes.

java
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGC

Tuning do G1 GC

G1 divide o heap em regiões. MaxGCPauseMillis define um target de pausa soft. G1 prioriza regiões com mais lixo. Use GCViewer ou GCEasy para analisar logs.

java
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime

Vazamentos de Memória

Vazamentos de memória são causados por retenção não intencional de objetos. Coleções estáticas, recursos não fechados e registros de listener são comuns. jmap mostra contagens de objetos. Analise hprof com MAT.

java
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>

Weak References

WeakReference permite GC quando não há strong references. SoftReference sobrevive até pressão de memória. Chaves de WeakHashMap são weak. Use para caches que não devem impedir GC.

java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value");  // Entry removed when key is GC'd

Finalization

finalize() está deprecated (imprevisível, lento). A API Cleaner (Java 9+) fornece melhor cleanup. try-with-resources é preferido para cleanup determinístico.

java
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;
    Resource() {
        cleanable = Cleaner.create().register(this, () -> cleanup());
    }
    public void close() { cleanable.clean(); }
}
29

Stream Collectors

Grouping By

groupingBy particiona elementos por um classificador. O segundo argumento é um downstream collector para agregação. counting, averaging, summing são downstream collectors comuns.

java
Map<String, List<Person>> byCity =
    people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
    people.stream().collect(Collectors.groupingBy(
        Person::getCity, Collectors.counting()));

Partitioning

partitioningBy divide em dois grupos (true/false). Mais eficiente que groupingBy para chaves booleanas. Resultado sempre tem ambas as chaves. Downstream collectors agregam cada partição.

java
Map<Boolean, List<Person>> partition =
    people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
    people.stream().collect(Collectors.partitioningBy(
        p -> p.getAge() >= 18, Collectors.counting()));

Joining

joining concatena strings com delimitador, prefixo e sufixo opcionais. Elementos devem ser strings; use map primeiro. Usa StringBuilder internamente.

java
String names = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
    .map(p -> p.getName() + "=" + p.getAge())
    .collect(Collectors.joining("\n", "[", "]"));

Reducing

reducing realiza uma operação de fold. A versão de três argumentos recebe identity, mapper, reducer. A versão de dois argumentos retorna Optional. Use quando collectors padrão são insuficientes.

java
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
    .collect(Collectors.reducing((p1, p2) ->
        p1.getAge() > p2.getAge() ? p1 : p2));

Collector Personalizado

Collector.of cria collectors personalizados: supplier, accumulator, combiner, finisher. O combiner mescla resultados parciais para streams paralelos. Útil para formatos de saída especializados.

java
Collector<Person, ?, String> toJson = Collector.of(
    StringBuilder::new,
    (sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
    StringBuilder::append,
    StringBuilder::toString
);
30

Armadilhas Comuns

Integer Caching

Java armazena em cache valores Integer de -128 a 127. == compara referências, não valores. Para Integer fora do range de cache, == retorna false. Sempre use .equals() para Integer.

java
Integer a = 127; Integer b = 127;
System.out.println(a == b);  // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d);  // false (not cached)
System.out.println(c.equals(d));  // true

Imutabilidade de String

Strings são imutáveis: métodos como concat retornam novas strings. Esquecer de atribuir o resultado é um bug comum. Use StringBuilder para concatenação repetida.

java
String s = "Hello";
s.concat(" World");  // Returns new string, s unchanged
System.out.println(s);  // "Hello"
// Use StringBuilder for mutation
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // Mutates in place

Equals & HashCode

Objetos iguais devem ter hash codes iguais. HashMap e HashSet usam hashCode para bucketing e equals para comparação. Se você sobrescrever equals, você DEVE sobrescrever hashCode.

java
class Person {
    String name;
    public boolean equals(Object o) {
        if (!(o instanceof Person)) return false;
        return name.equals(((Person)o).name);
    }
    public int hashCode() { return name.hashCode(); }
}

Checked vs Unchecked

Exceções checked devem ser declaradas ou capturadas (IOException, SQLException). Unchecked (RuntimeException) não exigem tratamento. Evite capturar Exception amplo.

java
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");

Vazamentos de Recurso

Recursos devem ser fechados. try-with-resources (Java 7+) fecha automaticamente AutoCloseable. Sem ele, exceções causam vazamentos. Nunca deixe um recurso não fechado.

java
// BAD: resource leak
FileInputStream fis = new FileInputStream("file.txt");
// If exception here, fis never closed
// GOOD: try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // Use resource
}  // Auto-closed even on exception
31

Padrões de Design

Singleton

Double-checked locking com volatile garante inicialização lazy thread-safe. volatile previne reordenação de instruções. Enum singleton é mais simples: public enum Singleton { INSTANCE; }.

java
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

Builder

O padrão Builder trata objetos com muitos parâmetros opcionais. Evita construtores telescópicos. O Builder interno coleta parâmetros fluentemente. build() cria o objeto imutável.

java
public class Pizza {
    private final String size;
    private final boolean cheese;
    private Pizza(Builder b) { size = b.size; cheese = b.cheese; }
    public static class Builder {
        private String size; private boolean cheese;
        public Builder size(String s) { size = s; return this; }
        public Builder cheese(boolean c) { cheese = c; return this; }
        public Pizza build() { return new Pizza(this); }
    }
}

Strategy

O padrão Strategy encapsula algoritmos intercambiáveis. O contexto delega à interface strategy. Evita grandes cadeias if-else. Segue o princípio open-closed.

java
interface PaymentStrategy { void pay(double amount); }
class CreditCard implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Card: " + amount); }
}
class Cart { private PaymentStrategy strategy; void checkout() { strategy.pay(100); } }

Observer

O padrão Observer define uma dependência um-para-muitos. Quando o subject muda, todos os observers são notificados. Usado em MVC, sistemas de eventos e programação reativa.

java
interface Observer { void update(String event); }
class Subject {
    private List<Observer> observers = new ArrayList<>();
    void subscribe(Observer o) { observers.add(o); }
    void notify(String event) { observers.forEach(o -> o.update(event)); }
}

Factory Method

Factory Method define uma interface para criar objetos mas deixa subclasses decidirem qual classe instanciar. Desacopla código cliente de classes concretas.

java
abstract class Document { abstract void open(); }
class PDF extends Document { void open() { /* ... */ } }
abstract class DocFactory { abstract Document create(); }
class PDFFactory extends DocFactory { Document create() { return new PDF(); } }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.