Primeros pasos
Hello World
Cada programa de Java comienza desde main(). El nombre del archivo debe coincidir con el nombre de la clase pública (Main.java → Main.class). javac compila a bytecode (.class), java lo ejecuta en la JVM. System.out.println imprime a stdout; printf soporta especificadores de formato (%s, %d, %f, %n para nueva línea).
// 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)Variables y tipos primitivos
Java tiene 8 tipos primitivos (int, long, double, float, boolean, char, byte, short) y tipos de referencia (String, arrays, objetos). Usa 'final' para constantes. 'var' (Java 10+) infiere el tipo en tiempo de compilación — úsalo para variables locales con tipos obvios. Los guiones bajos en números (100_000) mejoran la legibilidad.
// 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>Clases envoltorio y boxing
Las clases envoltorio (Integer, Double, Boolean, etc.) son versiones objeto de los primitivos. El autoboxing/unboxing convierte automáticamente. Integer almacena en caché los valores -128 a 127, así que == funciona para números pequeños pero falla para números mayores — siempre usa .equals(). Los wrappers son necesarios para las Collections (que no pueden contener primitivos).
// 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 comparisonPaquetes e imports
Los paquetes organizan las clases y evitan conflictos de nombres. Convención: nombre de dominio invertido (com.example.app). Importa clases específicas o usa comodines (*). Los imports estáticos traen constantes y métodos (Math.PI, Math.sqrt). Los nombres completamente calificados funcionan sin imports pero son verbosos. El paquete java.lang se importa automáticamente.
// 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.channelEntrada y salida
System.out (stdout), System.err (stderr), System.in (stdin). Scanner es la forma más fácil de leer entrada de consola — analiza tokens (nextInt, nextDouble, nextLine). Siempre cierra Scanner para liberar recursos. Los argumentos de línea de comandos están en args[] (args[0] es el primer argumento, no el nombre del programa como en C).
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"Strings y formato
Métodos de String
Los Strings son inmutables — los métodos devuelven nuevos strings. Siempre usa .equals() para comparación de contenido (== compara referencias). compareTo() devuelve negativo/cero/positivo para ordenamiento (útil para ordenar). split() devuelve un String[]. Para strings mutables, usa StringBuilder.
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 y concatenación
La concatenación de strings con + crea un nuevo String cada vez (ineficiente en bucles). StringBuilder es mutable y eficiente para construir strings incrementalmente. StringBuffer es la versión thread-safe (raramente necesaria). String.join() combina con un delimitador. Java 11+ añade repeat() para multiplicación de strings.
// 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"Formato de strings
printf/format usa especificadores de formato estilo C: %d (int), %f (float), %s (string), %c (char), %b (boolean), %x (hex). Ancho (%5d), alineación izquierda (%-5d), relleno con ceros (%05d), precisión (%.2f). %n es la nueva línea de la plataforma. Los bloques de texto (Java 15+) con triple comilla permiten strings multilínea sin escape.
// 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
}
""";Expresiones regulares
Java regex usa Pattern (compilado) y Matcher (aplicado a la entrada). Los métodos de String (matches, split, replaceAll) son atajos convenientes. Las barras invertidas deben duplicarse en literales de string de Java (\\d para \d). Los grupos se capturan con paréntesis y se referencian como $1, $2 en reemplazos. Siempre compila los patrones una vez si se usan repetidamente.
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 y Math
La clase Math proporciona funciones matemáticas estáticas. Math.random() devuelve 0.0-1.0. Para más control, usa java.util.Random (con semilla) o java.security.SecureRandom (criptográfico). Integer/Double tienen métodos utilitarios estáticos. Ten cuidado con la precisión de coma flotante — usa BigDecimal para cálculos financieros.
// 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();Flujo de control
If / Else
Java if/else funciona como C/C++. Las condiciones deben ser booleanas — no hay truthy/falsy como en JavaScript (0 y strings no vacíos NO son truthy). El operador ternario (cond ? a : b) es una expresión, no una declaración. Usa llaves incluso para cuerpos de una sola línea (mejor práctica de estilo de código).
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 booleanSwitch y expresiones
El switch tradicional tiene fall-through (usa break). Las expresiones switch de Java 14+ (->) no tienen fall-through y pueden devolver valores. Usa comas para múltiples etiquetas case (case 1, 2, 3). 'yield' devuelve un valor de un bloque complejo. Las expresiones switch son exhaustivas — necesitas un default o todos los casos para enums.
// 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;
};Bucles
Java tiene bucles for, while y do-while. El for mejorado (for-each) funciona con arrays y cualquier Iterable. break sale del bucle; continue salta a la siguiente iteración. Para colecciones, prefiere for-each o streams sobre bucles indexados. do-while se ejecuta al menos una vez (raramente usado).
// 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 y continue con etiqueta
Las etiquetas (outer:) permiten break/continue de bucles externos desde bucles anidados. Esto raramente se necesita — extraer a un método con return suele ser más limpio. Las etiquetas se colocan antes del bucle, seguidas de dos puntos. break label sale del bucle etiquetado; continue label salta a su siguiente iteración.
// 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
Los arrays tienen longitud fija (usa ArrayList para dinámicos). Arrays.sort() ordena in place. Arrays.toString() da una representación legible. Arrays.copyOf() crea una copia con nueva longitud. Para arrays multidimensionales, cada fila puede tener diferente longitud (arrays irregulares). Usa Arrays para métodos utilitarios en arrays.
// 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 2Métodos y funciones
Definición de métodos
Los métodos de Java siempre están dentro de una clase. 'static' significa que el método pertenece a la clase (llama sin instancia). El tipo de retorno (int, String, void) se declara antes del nombre. Los parámetros son tipados. Java no tiene valores de parámetros por defecto — usa sobrecarga de métodos en su lugar.
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étodos
La sobrecarga de métodos permite múltiples métodos con el mismo nombre pero diferentes listas de parámetros (tipo, cantidad u orden). Java resuelve las sobrecargas en tiempo de compilación basándose en los tipos de argumentos. La sobrecarga es común para constructores y métodos utilitarios. Es diferente de la sobreescritura (que involucra herencia y despacho en runtime).
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 y paso por valor
Varargs (Type... name) permiten argumentos variables, recibidos como un array. Java siempre es paso por valor: los primitivos se copian, las referencias a objetos se copian (pero apuntan al mismo objeto). Así que modificar un parámetro dentro de un método no afecta la variable del llamador, pero modificar el objeto al que apunta sí lo hace.
// 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
}Recursión
La recursión es cuando un método se llama a sí mismo. Siempre ten un caso base para detener. Java no optimiza la recursión de cola (a diferencia de algunos lenguajes), así que la recursión profunda puede causar StackOverflowError. Para recursión profunda o crítica de rendimiento, conviértela a iteración. La memoización (caché de resultados) puede acelerar soluciones recursivas como Fibonacci.
// 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 insteadExpresiones lambda
Las lambdas (Java 8+) son funciones anónimas. El tipo es una interfaz funcional (un método abstracto). Comunes: Function<T,R> (entrada→salida), Predicate<T> (test booleano), Consumer<T> (consume, sin retorno), Supplier<T> (produce, sin entrada). Las referencias a métodos (String::length) son abreviatura de lambdas que llaman a un solo método.
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"Clases y OOP
Clase y constructor
Las clases son plantillas para objetos. Los campos mantienen el estado, los métodos definen el comportamiento. Los constructores inicializan nuevos objetos (usa 'this' para distinguir campos de parámetros). @Override indica que un método sobrescribe un método de superclase (toString es de Object). Encapsulación: campos private, getters/setters public.
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 acceso y static
Modificadores de acceso: public (en todas partes), private (solo clase), protected (clase + subclases + paquete), default/package-private (mismo paquete). Los miembros static pertenecen a la clase, no a instancias — compartidos entre todos los objetos. Los inicializadores static se ejecutan una vez cuando la clase carga. Usa static para constantes (static final), métodos utilitarios y contadores.
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 callHerencia y super
Java usa 'extends' para herencia de clases (solo herencia simple). super() llama al constructor padre (debe ser la primera línea). @Override indica sobreescritura de método (polimorfismo en runtime). Un Dog ES-A Animal. Usa herencia para relaciones 'es-un'; usa composición (tiene-un) para reutilización de código. Java 17+ soporta clases sealed para restringir la herencia.
// 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 methodClases abstractas e interfaces
Las clases abstractas no pueden instanciarse y pueden tener tanto métodos abstractos (sin cuerpo) como concretos. Los interfaces definen contratos — todos los métodos son public abstract por defecto. Java 8+ permite métodos default (con cuerpo) y métodos static en interfaces. Una clase extiende una clase abstracta pero puede implementar múltiples interfaces. Usa clases abstractas para código compartido, interfaces para contratos.
// 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 y casting
Polimorfismo: una referencia padre puede contener un objeto hijo. Las llamadas a métodos despachan a la implementación del objeto actual (polimorfismo en runtime). Usa instanceof antes de downcasting para evitar ClassCastException. Java 16+ pattern matching (instanceof Circle c) combina check y cast. Sobrescribe equals() y hashCode() juntos para comportamiento correcto en colecciones.
// 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 representationRecords y enums
Los Records (Java 16+) son clases de datos inmutables — el compilador genera constructor, getters, equals, hashCode y toString. Úsalos para DTOs y objetos valor. Los enums son constantes type-safe que pueden tener campos, métodos y constructores. Los enums implementan Comparable y tienen métodos values() y valueOf(). Ambos son esenciales para el Java moderno.
// 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(); // DOWNColecciones y genéricos
List (ArrayList y LinkedList)
ArrayList está respaldado por un array (get/set rápido, insert/delete lento en medio). LinkedList está respaldado por una lista doblemente enlazada (insert/delete rápido en extremos, acceso aleatorio lento). List.of() crea listas inmutables. Usa ArrayList para la mayoría de casos; LinkedList solo para operaciones frecuentes en extremos. Ambos implementan la interfaz List.
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 referenceSet (HashSet y TreeSet)
Set almacena elementos únicos. HashSet es el más rápido pero desordenado. TreeSet mantiene elementos ordenados (orden natural o Comparator). LinkedHashSet mantiene el orden de inserción. Operaciones de Set: addAll (unión), retainAll (intersección), removeAll (diferencia). Para objetos personalizados en un HashSet, sobrescribe equals() y hashCode().
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 y TreeMap)
Map almacena pares clave-valor. HashMap es el más rápido (desordenado). TreeMap ordena por claves. LinkedHashMap mantiene el orden de inserción. getOrDefault evita null checks. compute/merge son potentes para actualizar valores. Para claves personalizadas, sobrescribe equals() y hashCode(). Map.of() crea mapas inmutables (Java 9+).
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 y Deque
Queue es FIFO (primero en entrar, primero en salir). Deque es de doble extremo (puede add/remove desde ambos extremos). PriorityQueue ordena elementos por orden natural o un Comparator (min-heap por defecto). Para una pila, usa ArrayDeque (push/pop) en lugar de la clase legacy Stack. ArrayDeque es más rápido que LinkedList para operaciones queue/deque.
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)Genéricos
Los genéricos habilitan colecciones y clases type-safe. <T> es un parámetro de tipo. Los tipos acotados (<T extends Comparable<T>>) restringen a tipos con cierto comportamiento. Wildcards: ? (cualquiera), ? extends T (covariante, solo lectura), ? super T (contravariante, solo escritura). Los genéricos usan type erasure — los tipos se verifican en tiempo de compilación, se borran en runtime.
// 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 superclassIterators y Comparable
Iterator permite remoción segura durante la iteración (it.remove()). ListIterator añade recorrido bidireccional y set/add. Comparable define el orden natural (compareTo). Comparator define orden personalizado (comparing, comparingInt, reversed, thenComparing). Usa Comparator.comparing() para ordenamiento fluido. Collections.sort() usa el orden natural.
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 ComparableStreams y funcional
Fundamentos de Stream
Los Streams (Java 8+) proporcionan procesamiento declarativo de datos. Créalos con .stream() (colecciones) o Stream.of(). Las operaciones intermedias (filter, map, sorted) son lazy — se ejecutan solo cuando se llama a una operación terminal (collect, reduce, count, forEach). toList() (Java 16+) es una alternativa concisa a collect(Collectors.toList()).
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();Operaciones de Stream
sorted() ordena elementos (natural o con Comparator). distinct() elimina duplicados. limit(n)/skip(n) paginan. flatMap aplana streams anidados — esencial para transformaciones uno-a-muchos. peek() es para depuración (side effects). groupingBy crea mapas agrupando elementos por un clasificador. Los Streams son lazy — las operaciones se encadenan eficientemente.
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 y reducción
Los Collectors proporcionan ricas operaciones de reducción: joining (concatenar strings), groupingBy (agrupar por clave), partitioningBy (dividir por booleano), toMap (crear un mapa), summarizingInt (estadísticas: count, sum, min, max, average). Los Collectors pueden componerse (groupingBy con collector downstream). Estos reemplazan bucles verbosos con one-liners declarativos.
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> es un contenedor que puede o no contener un valor. Fuerza el manejo explícito de la ausencia — no más NullPointerException. Usa of() para valores no nulos, ofNullable() para posiblemente nulos. Encadena con map/flatMap/filter. Nunca uses get() sin isPresent() — prefiere orElse/orElseThrow. Optional está diseñado para tipos de retorno, no para campos.
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 funcionales
Las interfaces funcionales tienen exactamente un método abstracto (pueden tener múltiples métodos default). @FunctionalInterface es opcional pero documenta la intención. Java proporciona muchas en java.util.function: Function, Predicate, Consumer, Supplier, más variantes Bi- y primitivas. Úsalas en lugar de crear interfaces personalizadas cuando sea posible. Habilitan expresiones lambda y referencias a métodos.
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));
}
}Excepciones y I/O
Try / Catch / Finally
try/catch/finally maneja excepciones. finally siempre se ejecuta (úsalo para limpieza). Multi-catch (catch A | B) maneja múltiples excepciones juntas. Try-with-resources cierra automáticamente cualquier AutoCloseable (archivos, conexiones, streams) — preferido sobre la limpieza manual con finally. Los recursos se cierran en orden inverso de declaración.
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
Las excepciones checked (extends Exception) deben declararse con 'throws' o capturarse — el compilador lo exige. Úsalas para condiciones recuperables (archivo no encontrado, error de red). Las excepciones unchecked (extends RuntimeException) no necesitan declaración — úsalas para errores de programación (null pointer, argumento inválido). El debate: las checked exceptions fuerzan el manejo pero pueden saturar el código; muchos frameworks prefieren unchecked.
// 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); }
}File I/O (NIO.2)
NIO.2 (java.nio.file) es la API moderna de archivos. Files.readString/writeString (Java 11+) son convenientes para texto. Files.lines() devuelve un Stream lazy — eficiente para archivos grandes (debe cerrarse con try-with-resources). Path.of() reemplaza la vieja clase File. Files.createDirectories() crea la ruta completa. Siempre maneja IOException.
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 y Writer (texto)
BufferedReader/Writer son eficientes para I/O de texto (el buffering reduce system calls). PrintWriter ofrece formato estilo printf. Scanner analiza entrada (nextInt, nextDouble, nextLine). InputStreamReader conecta byte streams con character streams (especifica charset para no-UTF-8). Siempre usa try-with-resources para asegurar que los streams se cierren.
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);Fecha y hora (java.time)
java.time (Java 8+) es la API moderna de fecha/hora, reemplazando el viejo Date/Calendar. LocalDate (solo fecha), LocalTime (solo hora), LocalDateTime (ambos), ZonedDateTime (con zona horaria). Todos son inmutables y thread-safe. Usa Period para diferencias de fecha, Duration para diferencias de tiempo. DateTimeFormatter para parsear/formatear. Instant para timestamps de máquina (UTC).
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);Fundamentos de concurrencia
Concurrencia en Java: Thread (bajo nivel), ExecutorService (pools de threads — preferido), CompletableFuture (composición async, como Promises). parallelStream() usa el ForkJoinPool para procesamiento paralelo. Los bloques synchronized protegen estado compartido. Las variables Atomic (AtomicInteger, etc.) proporcionan operaciones thread-safe sin locks. Para concurrencia compleja, usa colecciones de java.util.concurrent (ConcurrentHashMap, BlockingQueue).
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);Expresiones lambda
Sintaxis básica de lambda
Las lambdas (Java 8+) son implementaciones concisas de interfaces funcionales. Sintaxis: (params) -> expresión o (params) -> { declaraciones; }. El compilador infiere los tipos de parámetros del tipo objetivo. Las lambdas de un solo parámetro pueden omitir paréntesis; las de cero parámetros necesitan paréntesis vacíos. Las lambdas habilitan programación funcional y son la columna vertebral de la Streams API.
// 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 funcionales
Una interfaz funcional tiene exactamente un método abstracto (tipo SAM). La anotación @FunctionalInterface hace que el compilador lo exija. Las lambdas solo pueden apuntar a interfaces funcionales. Los métodos default y static están permitidos y no rompen la regla de un solo método. Esta es la base que hace que las lambdas funcionen en el sistema de tipos de 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 funcionales integradas
java.util.function proporciona ~40 interfaces funcionales listos para usar, así que raramente escribes los tuyos. Los cuatro centrales: Function (transformar), Predicate (test), Consumer (consumir), Supplier (producir). Las variantes Bi- toman dos argumentos. Las variantes primitivas (IntFunction, ToIntFunction, etc.) evitan overhead de autoboxing. Úsalas en lugar de crear interfaces personalizadas.
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;Referencias a métodos
Las referencias a métodos (::) son abreviatura de lambdas que solo llaman a un método. Cuatro tipos: static (Class::static), instancia vinculada (obj::method), instancia no vinculada (Class::method — el primer parámetro se convierte en receptor), y constructor (Class::new). Úsalas cuando un lambda solo reenvía a un método — son más legibles. De lo contrario, quédate con lambdas explícitas.
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 variables (effectively final)
Las lambdas pueden capturar variables locales, pero deben ser final o 'effectively final' (nunca reasignadas). Esto es porque las lambdas pueden sobrevivir al stack frame. Para solucionarlo, usa un array de un solo elemento o un objeto AtomicInteger/holder. Los campos de instancia y static no tienen tal restricción. 'this' dentro de un lambda se refiere a la instancia de la clase envolvente, no al lambda mismo.
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"
}Referencias a constructores
Las referencias a constructores (ClassName::new) crean nuevas instancias concisamente. Útiles con Collectors.toCollection() para elegir el tipo de resultado, con creación de arrays (Type[]::new), y en patrones factory. Para records y objetos inmutables, las referencias a constructores son la forma idiomática de construir copias. Se combinan naturalmente con objetivos Function/Supplier.
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));Optional y null safety
Creando Optional
Optional es un contenedor que puede o no contener un valor. Usa empty() para ningún valor, of() cuando estés seguro de que el valor es no nulo (lanza NPE de lo contrario), y ofNullable() cuando null es posible. Optional fuerza a los llamadores a manejar explícitamente el caso de ausencia. Nunca devuelvas null donde se espera Optional — eso derrota el propósito.
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();Consumiendo valores de forma segura
Prefiere ifPresent/ifPresentOrElse sobre isPresent+get. orElse devuelve un default constante; orElseGet toma un Supplier así el default se calcula lazy (importante cuando el default es costoso). orElseThrow convierte ausencia en excepción. El objetivo es nunca llamar .get() a ciegas — eso reintroduce el riesgo de NPE que Optional estaba destinado a eliminar.
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 con map y flatMap
map transforma el valor contenido (Optional<T> -> Optional<R>). flatMap se usa cuando la función de mapeo misma devuelve un Optional, previniendo Optionals anidados. filter mantiene el valor solo si un predicado coincide. Encadenar map/filter/flatMap te permite construir pipelines que short-circuit en el primer vacío — mucho más limpio que null checks anidados.
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-patrones a evitar
Optional está diseñado para tipos de retorno, no para campos o parámetros. No es Serializable y añade overhead como campo. No uses .get() sin verificar, y no uses isPresent()+get() — eso es solo null-checking verboso. Las colecciones ya expresan vacío, así que no las envuelvas en Optional. Usa Optional como señal de tipo de retorno de que un valor puede estar ausente.
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 con streams
Optional.stream() (Java 9+) produce un Stream de 0 o 1 elemento, lo que te permite flatMap Optionals fuera de un stream elegantemente. Esta es la forma más limpia de saltar valores ausentes durante el procesamiento de streams. Evita el patrón verboso filter(isPresent).map(get) y mantiene el pipeline declarativo.
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();Streams API en profundidad
Creando streams
Los streams pueden crearse desde colecciones, arrays o factories estáticos. iterate() y generate() producen streams infinitos — siempre síguelos con limit(). iterate con un predicado (Java 9+) es más seguro que iterate puro. IntStream/LongStream/DoubleStream evitan boxing para trabajo numérico. Los streams son de un solo uso: una vez que se ejecuta una operación terminal, el stream se consume.
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,5Operaciones intermedias
Las operaciones intermedias son lazy — no se ejecutan hasta que se invoca una operación terminal. filter mantiene elementos, map transforma 1:1, flatMap transforma 1:muchos. distinct/sorted/limit/skip son stateful. takeWhile/dropWhile (Java 9+) se detienen en el primer elemento no coincidente (a diferencia de filter, que escanea todo). Usa peek para depuración, no para side effects en producción.
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 y partitioning
Collectors.groupingBy es el SQL GROUP BY de los streams de Java. La función clasificadora define la clave; un collector downstream opcional procesa cada grupo (counting, summing, mapping, etc.). partitioningBy es un caso especial con un predicado booleano (exactamente dos buckets). Pasa un supplier de TreeMap para claves ordenadas. Estos se componen poderosamente — puedes construir agrupaciones multi-nivel.
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()));Redución y estadísticas
reduce combina todos los elementos en un solo valor — proporciona una identidad para seguridad con streams vacíos. summaryStatistics da count/sum/min/max/avg en una pasada. Collectors.joining es útil para construir strings delimitados. teeing (Java 12+) ejecuta dos collectors en paralelo y combina sus resultados — útil cuando necesitas dos agregados (como min y max) en una pasada.
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 son especializaciones primitivas que evitan overhead de autoboxing — úsalos para trabajo numérico. mapToInt/mapToLong/mapToDouble convierten streams de objetos a streams primitivos; boxed() va de vuelta. Los streams primitivos tienen operaciones terminal especializadas (sum, average, max) que devuelven OptionalInt/Double para manejar streams vacíos. Geniales para pipelines numéricos sensibles al rendimiento.
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 el trabajo a través del ForkJoinPool común (dimensionado a núcleos de CPU). Solo úsalo para datasets grandes con operaciones CPU-intensivas, stateless e independientes del orden — para datos pequeños el overhead excede el beneficio. Evita estado mutable compartido (causa races). El I/O en streams paralelos bloquea el pool compartido — usa un ForkJoinPool personalizado para trabajo bloqueante. Mide antes de asumir que paralelo es más rápido.
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 opsGenéricos en profundidad
Clases y métodos genéricos
Los genéricos habilitan código reutilizable type-safe. Las clases declaran parámetros de tipo (<T>); los métodos también pueden (<T> antes del tipo de retorno). El operador diamond <> infiere el tipo en construcción. Los genéricos se verifican en tiempo de compilación — hacen las colecciones y APIs más seguras capturando errores de tipo temprano en lugar de en runtime vía ClassCastException.
// 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 acotados
Los parámetros de tipo acotados (<T extends Bound>) restringen qué tipos pueden usarse y te permiten llamar métodos del bound. <T extends Number> significa que T debe ser un Number o subtipo. Múltiples bounds usan & — como máximo una clase (debe ser primera), el resto interfaces. Los bounds son esenciales para escribir algoritmos que necesitan capacidades específicas (comparabilidad, operaciones numéricas).
// 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
Los wildcards hacen los tipos genéricos flexibles. ? extends T (covariante) te permite leer T pero no escribir — úsalo para productores. ? super T (contravariante) te permite escribir T pero solo leer Object — úsalo para consumidores. La regla PECS (Producer Extends, Consumer Super) guía cuál usar. copy(dest, src) es el ejemplo clásico: dest es un consumidor (super), src es un productor (extends).
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
Los genéricos de Java usan type erasure — los tipos genéricos existen solo en tiempo de compilación; en runtime, List<String> y List<Integer> son ambos solo List. Esto habilita compatibilidad hacia atrás pero tiene límites: no puedes hacer new T(), crear arrays genéricos, usar instanceof con genéricos, o tener métodos sobrecargados con mismas firmas borradas. La heap pollution ocurre cuando casts unchecked ponen tipos incorrectos en genéricos, aplazando errores a runtime.
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 runtimeMétodos genéricos e inferencia
La inferencia de tipos deja al compilador determinar argumentos de tipo desde el contexto (argumentos y tipo objetivo), así que raramente los escribes explícitamente. El operador diamond <> es inferencia para constructores. El target typing usa el tipo esperado de la variable. Usa un type witness explícito (Class.<T>method()) solo cuando la inferencia no pueda resolver ambigüedad. La inferencia hace que el código genérico se lea tan limpiamente como el no genérico.
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 y patrones genéricos
Las interfaces genéricas (como Repository<T,ID>) definen contratos reutilizables. El patrón de bound auto-referenciado (class X implements Comparable<X>) asegura que compareTo solo acepte el mismo tipo. El patrón type token (usar Class<T> como clave) sortea el erasure para proporcionar type safety en runtime en contenedores heterogéneos. Estos patrones son la columna vertebral de frameworks como Spring Data.
// 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;Anotaciones
Anotaciones integradas
Anotaciones integradas de Java: @Override (captura typos en sobreescritura — siempre úsalo), @Deprecated (señala que la API no debería usarse, con metadatos since/forRemoval), @SuppressWarnings (silencia warnings específicos — úsalo de forma limitada), @FunctionalInterface (exige la regla SAM). Estas son las anotaciones cotidianas que mejoran la seguridad y documentación en tiempo de compilación.
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 {}Anotaciones personalizadas
Las anotaciones personalizadas se declaran con @interface. Los miembros parecen métodos pero son atributos de anotación — pueden tener valores default. Usa @Target para restringir dónde aplica (TYPE, METHOD, FIELD, etc.) y @Retention para controlar disponibilidad. Las anotaciones marker (sin miembros) solo etiquetan elementos. Las anotaciones mismas no llevan comportamiento — los procesadores (reflection, herramientas de anotación) las leen y actúan.
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 y Target
@Retention controla cuánto sobrevive una anotación: SOURCE (solo tiempo de compilación, como @Override), CLASS (en bytecode pero no visible en runtime — el default), RUNTIME (accesible vía reflection). @Target restringe dónde puede aparecer una anotación. Java 8+ añadió TYPE_USE y TYPE_PARAMETER, permitiendo anotar genéricos y casts (List<@NonNull String>). Elige RUNTIME solo si necesitas acceso por reflection.
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;Lectura de anotaciones vía reflection
Las anotaciones con retención RUNTIME pueden leerse vía reflection: isAnnotationPresent() verifica existencia, getAnnotation() la recupera. Así es como los frameworks (Spring, JUnit, JAX-RS) conectan comportamiento declarativamente — etiquetas métodos/clases, el framework escanea y despacha. El procesamiento de anotaciones en tiempo de compilación (annotation processors) es una alternativa para generación de código sin costo de reflection en runtime.
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 y meta-anotaciones
@Repeatable (Java 8+) te permite aplicar la misma anotación múltiples veces definiendo una anotación contenedora. @Inherited hace que las anotaciones se propaguen a subclases (solo para anotaciones a nivel de clase). @Documented incluye la anotación en Javadoc. @Target con ANNOTATION_TYPE crea meta-anotaciones (anotaciones que anotan otras anotaciones) — así es como Spring construye estereotipos de anotación componibles como @RestController = @Controller + @ResponseBody.
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 {}Reflection
Objeto Class y obtención de clases
Cada tipo cargado tiene un objeto Class único — el punto de entrada a reflection. Obténlo vía class literal (Type.class), instance.getClass(), o Class.forName() (carga dinámica, lanza ClassNotFoundException). El objeto Class expone name, modifiers, superclass, interfaces y verificaciones de tipo (isInterface, isArray, isEnum, isRecord). isAssignableFrom verifica relaciones polimórficas.
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()); // trueInspección de campos, métodos, constructores
getDeclaredFields/Methods/Constructors devuelven TODOS los miembros (incluyendo privados) declarados solo en esta clase. getFields/getMethods devuelven solo miembros public pero incluyen heredados. Para encontrar un miembro específico, usa getDeclaredField(name) o getDeclaredMethod(name, paramTypes...) — los tipos de parámetro son necesarios para desambiguar sobrecargas. Reflection sortea el control de acceso a menos que llames setAccessible(true).
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);Invocación de métodos y creación de instancias
Method.invoke(obj, args...) llama a un método reflectivamente — siempre devuelve Object, así que haz cast del resultado. setAccessible(true) sortea las verificaciones de acceso de Java (los miembros privados se vuelven accesibles; puede requerir --add-opens en módulos). Constructor.newInstance() crea objetos — el equivalente reflexivo de new. Array.newInstance crea arrays de un tipo de componente conocido en runtime. Reflection es más lento que llamadas directas y sortea la seguridad en tiempo de compilación.
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);Lectura y modificación de campos
Field.get(instance) lee el valor de un campo; Field.set(instance, value) lo escribe. Para primitivos, usa accessores específicos de tipo (getInt/setInt) para evitar boxing. Los campos static toman null como argumento de instancia. setAccessible(true) se requiere para campos privados. El acceso a campos por reflection es cómo las librerías de serialización (Jackson, Gson) y los frameworks ORM leen/escriben estado de objetos genéricamente.
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 y casos de uso
java.lang.reflect.Proxy crea proxies dinámicos que implementan interfaces en runtime — un InvocationHandler intercepta cada llamada. Así es como funcionan Spring AOP, la carga lazy de Hibernate y los mocks de Mockito. Reflection alimenta la mayoría de frameworks de Java (DI, ORM, serialización, testing) pero tiene costos: más lento que llamadas directas, seguridad de tipo más débil y restricciones del module-system. Úsalo cuando necesitas flexibilidad en runtime, no para código ordinario.
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.JDBC y acceso a base de datos
Connection y DriverManager
DriverManager.getConnection() abre una conexión a base de datos — siempre envuélvela en try-with-resources para evitar leaks. Desde JDBC 4, los drivers se auto-registran vía ServiceLoader, así que Class.forName() raramente se necesita. El formato URL varía por vendor. Usa Properties para opciones de conexión (SSL, timeouts). En producción, prefiere un pool de conexiones (HikariCP) sobre llamadas directas a DriverManager.
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
Siempre usa PreparedStatement sobre Statement para cualquier consulta con parámetros — previene SQL inyección separando la estructura SQL de los datos. Los parámetros se establecen por índice basado en 1 con setters específicos de tipo. Los PreparedStatements pueden reusarse (re-set params y ejecutar de nuevo) y soportan batching (addBatch/executeBatch) para operaciones bulk. Statement está bien solo para SQL estático y confiable como DDL.
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 y consultas
ResultSet es un cursor sobre filas de consulta — llama next() para avanzar (devuelve false al final). Lee columnas por nombre (legible) o índice basado en 1. wasNull() distingue SQL NULL de un default primitivo (ej., getInt devuelve 0 para NULL). Los ResultSets por defecto son forward-only; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE habilita acceso aleatorio y updates in-place, aunque raramente se usa en apps modernas.
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", "");
}Transacciones y batch
JDBC auto-committea cada statement por defecto — set autoCommit(false) para agrupar statements en una transacción. Commit en éxito, rollback en fallo. Los savepoints permiten rollback parcial dentro de una transacción. Los niveles de aislamiento controlan visibilidad de cambios concurrentes (READ_COMMITTED es el default común; SERIALIZABLE es el más seguro pero el más lento). Siempre restaura autoCommit o cierra la conexión para evitar leaking de estado de transacción.
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)
Los pools de conexiones (HikariCP es el estándar de facto) mantienen conexiones calientes y las reusan, evitando el costo de 10-100ms de abrir una nueva conexión TCP+auth por petición. Configura max pool size (limitado por capacidad de DB), timeouts y lifetime. Pide con getConnection(), devuelve cerrando (va al pool, no se cierra). Siempre cierra el DataSource en shutdown. En Spring Boot, HikariCP se auto-configura.
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, AgroalI/O y NIO en profundidad
InputStream y OutputStream (bytes)
InputStream/OutputStream manejan bytes crudos. Siempre envuélvelos en variantes Buffered* — I/O sin buffering hace un system call por byte, lo cual es catastrófico para el rendimiento. read() devuelve -1 al end-of-stream. transferTo() (Java 9+) hace una copia bulk eficiente. readAllBytes() es conveniente pero carga el stream entero en memoria — solo para archivos pequeños. Siempre cierra streams (try-with-resources).
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 y buffers (NIO)
NIO Channels + ByteBuffers son la alternativa de alto rendimiento a streams. Los buffers tienen position/limit/capacity; flip() cambia de modo escritura a lectura, clear() resetea para escritura, compact() preserva datos no leídos. Los buffers directos (allocateDirect) viven fuera del heap de la JVM y evitan un paso de copia para I/O grande. transferTo entre channels puede usar zero-copy en SOes soportados. Usa NIO cuando el rendimiento de streaming importa.
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();Operaciones de Path (NIO.2)
Path (NIO.2) reemplaza la vieja clase File. Las operaciones de Path (getFileName, getParent, resolve, normalize, relativize) son matemática de strings pura — no tocan el disco. Los métodos Files.* interactúan con el filesystem: size, timestamps, permissions, symlinks. normalize() limpia segmentos . y ... resolveSibling es útil para renombrar (mismo directorio, diferente nombre). Prefiere Path sobre File en código moderno.
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);Recorrido de directorios y árboles de archivos
Files.list() lista un nivel; Files.walk() recorre el árbol recursivamente (devuelve un Stream, debe cerrarse). Files.find() filtra por path y atributos durante el recorrido. Para control total, walkFileTree con un FileVisitor te permite saltar subárboles, manejar errores y actuar antes/después de visitar directorios. Todos devuelven Streams que mantienen file handles — siempre usa try-with-resources. Usa max depth para limitar recorridos costosos.
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 y eventos de archivos
WatchService recibe eventos del filesystem (create, modify, delete) para directorios registrados. take() bloquea hasta que llega un evento; pollEvents() los drena. Observa el directorio padre y filtra por context() para rastrear un archivo específico. Los eventos pueden coalescer o perderse (OVERFLOW). El recursive watching requiere registrar cada subdirectorio. WatchService es OS-native (inotify en Linux, FSEvents en macOS) pero su API es de bajo nivel — considera librerías para necesidades complejas.
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.Records y pattern matching
Fundamentos de records
Los Records (Java 16+) son transportes de datos inmutables transparentes. La cabecera `record Name(Type1 f1, Type2 f2)` genera un constructor, accessores (f1(), f2() — no getF1()), equals, hashCode y toString. Son ideales para DTOs, objetos valor y resultados de funciones. El constructor compacto (solo `{ ... }`) valida o normaliza sin reasignar campos. Los records pueden implementar interfaces pero no extender clases.
// 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 {}Constructores compactos y métodos personalizados
El constructor compacto (`public Name { ... }`) se ejecuta antes de la asignación de campos — asigna al parámetro para normalizar, y el compilador lo asigna al campo. Los records pueden tener métodos adicionales y factories estáticas pero no campos de instancia más allá de la cabecera. Los constructores no canónicos deben delegar al canónico vía this(...). Usa factories estáticas para construcción más clara (Point.origin()) y para cachear instancias comunes.
// 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); }
}Clases sealed
Las clases/interfaces sealed (Java 17+) restringen qué tipos pueden extenderlas vía la cláusula `permits`. Cada subtipo permitido debe ser final, sealed o non-sealed. Combinados con records, forman tipos de datos algebraicos (herencia cerrada + datos inmutables). El beneficio clave: el compilador conoce todos los subtipos, así que las expresiones switch pueden ser exhaustivas sin un branch default — si añades un nuevo subtipo, el compilador marca cada switch que necesita actualización.
// 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 una variable que se vincula solo cuando el test tiene éxito, eliminando el cast explícito. El alcance de la variable fluye desde la verdad del patrón — utilizable en continuaciones && y después de early returns. Java 21 añadió pattern matching en switch (case Type var when guard), habilitando despacho basado en tipo con guards. Esto hace el código de verificación de tipo mucho más conciso y menos propenso a errores.
// 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) te permite switchear sobre tipos, destruir records y añadir guards (when). Combinado con tipos sealed, el compilador verifica exhaustividad — no se necesita default si todos los subtipos están cubiertos. Los record patterns (case Point(int x, int y)) destruyen en un paso. El case null es explícito (no NPE). Esto acerca a Java al pattern matching de ML/Scala para modelar dominios declarativamente.
// 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";
};
}Módulos (JPMS)
Fundamentos de module-info.java
module-info.java declara un módulo (Java 9+ JPMS). requires añade una dependencia; exports hace paquetes accesibles; opens permite acceso reflexivo (necesario para frameworks de serialización/DI); uses/provides conectan ServiceLoader. Sin este archivo, el código vive en el classpath como un 'unnamed module' con comportamiento legacy. Los módulos dan encapsulación fuerte (solo los paquetes exportados son public) y configuración confiable (dependencias explícitas).
// 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 una dependencia; requires transitive la propaga (úsalo cuando tu API pública expone tipos de ese módulo). exports hace paquetes public; exports to restringe a módulos nombrados (qualified exports). opens otorga acceso reflexivo (deep reflection) — esencial para frameworks que hacen setAccessible(true). La distinción entre exports (API pública) y opens (reflection) es clave: la encapsulación fuerte es el default, y optas por paquete.
// 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 y servicios
ServiceLoader implementa el patrón SPI: un interfaz en un módulo, implementaciones descubiertas en runtime. El módulo API exporta el interfaz; los módulos provider declaran `provides X with Y`; los módulos consumer declaran `uses X`. ServiceLoader.load(X.class) encuentra todos los providers en el module path. Esto desacopla interfaz de implementación — los drivers JDBC, backends de logging (SLF4J) y providers de Charset funcionan así. Sin dependencia de compile-time en la implementación.
// 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
El module path (--module-path) contiene JARs modulares con encapsulación fuerte; el classpath (-cp) contiene JARs legacy como unnamed modules sin encapsulación. Los JARs modulares funcionan en ambos. Los automatic modules son JARs sin module-info colocados en el module path — su nombre viene del filename o del atributo manifest Automatic-Module-Name. --add-opens es una vía de escape para reflection legacy que rompe bajo encapsulación fuerte.
// 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-UNNAMEDjdeps y jlink (runtimes personalizados)
jdeps analiza bytecode para listar dependencias de módulo — útil para migrar a módulos y encontrar deps no usadas. jlink crea un JRE personalizado conteniendo solo los módulos que tu app necesita, produciendo un runtime autocontenido, más pequeño y de inicio más rápido. Esto es ideal para imágenes Docker e installers: envía la app más un JRE de 30-50MB en lugar de requerir una instalación JDK de 300MB. Juntos, jdeps + jlink habilitan deployments Java lean y autocontenidos.
# 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)Concurrencia en profundidad
Locks: ReentrantLock y ReadWriteLock
ReentrantLock ofrece más control que synchronized: tryLock (non-blocking/timed), fairness, interruptibility e inspección de estado de lock. Siempre unlock en finally. ReadWriteLock permite muchos lectores concurrentes pero escritores exclusivos — genial para caches de lectura pesada. StampedLock (Java 8+) añade lecturas optimistas para aún mejor throughput de lectura. Prefiere synchronized para casos simples; usa Lock cuando necesites sus características avanzadas.
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(); }
}
}Colecciones concurrentes
ConcurrentHashMap es el map thread-safe workhorse — usa compute/merge para updates atómicos en lugar de check-then-act. CopyOnWriteArrayList es mejor para listas de lectura pesada y escritura rara (event listeners) — las escrituras copian el array. BlockingQueue es la columna vertebral de pipelines productor-consumidor (put bloquea cuando está lleno, take bloquea cuando vacío). ConcurrentLinkedQueue es non-blocking y unbounded. Estos reemplazan wrappers synchronized (Collections.synchronizedX) que son coarse-grained y más lentos.
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 y CyclicBarrier
CountDownLatch es una puerta one-shot — N threads hacen countdown, otros esperan; no reseteable. CyclicBarrier es reutilizable — los threads se esperan mutuamente en un punto de encuentro, con una acción opcional cuando todos llegan. Phaser es el más flexible: parties variables, múltiples fases, estructura de árbol. Usa latch para coordinación de startup, barrier para algoritmos paralelos multi-fase, phaser para conteos dinámicos de participantes.
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 y Exchanger
Semaphore controla acceso a N permisos — acquire bloquea hasta que uno está disponible, release lo devuelve. Úsalo para rate limiting, pools de conexiones o cualquier escenario de recurso limitado. tryAcquire ofrece variantes non-blocking y timed. Exchanger deja que dos threads intercambien valores en un punto de encuentro — nicho pero elegante para diseños de pipeline donde dos threads intercambian buffers. Ambos están en java.util.concurrent y son de más bajo nivel que locks para algunos patrones de coordinación.
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 avanzado
CompletableFuture es el Promise de Java — compón trabajo async con thenApply (map), thenCompose (flatMap), thenCombine (zip dos). allOf espera a todos, anyOf al primero. exceptionally recupera de errores; handle cubre ambos. orTimeout (Java 9+) cancela si toma demasiado. Siempre pasa un executor explícito — el commonPool por defecto puede starvarse bajo trabajo bloqueante. Esta es la base del código async estilo reactivo en 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)
Los virtual threads (Java 21) son threads ligeros programados por la JVM en un pequeño pool de carrier threads (plataforma). Cuando un virtual thread bloquea en I/O, se suspende y el carrier ejecuta otro — así puedes tener millones de operaciones bloqueantes concurrentes. Esto te permite escribir código bloqueante simple en lugar de cadenas reactivas/async complejas. Úsalos para workloads I/O-bound (HTTP handlers, DB calls); para trabajo CPU-bound, los platform threads o parallelStream siguen siendo apropiados.
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).Collections Framework en profundidad
Comparators y ordenamiento
Comparator.comparing(keyExtractor) construye comparadores desde una función clave — mucho más limpio que escribir lógica compare cruda. thenComparing encadena claves de ordenamiento secundarias. nullsFirst/nullsLast manejan nulls de forma segura. Usa comparingInt/comparingLong/comparingDouble para evitar autoboxing. List.sort() ordena in place (solo listas mutables); Stream.sorted() devuelve un nuevo stream ordenado. Los comparadores alimentan ordenamiento, ordenamiento de TreeSet/TreeMap y operaciones de stream.
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);Colecciones inmutables y no modificables
List.of/Set.of/Map.of (Java 9+) crean colecciones verdaderamente inmutables — sin nulls, sin mutaciones. Collections.unmodifiableX crea una vista de solo lectura que aún refleja cambios en la colección de respaldo. List.copyOf (Java 10+) hace una copia inmutable independiente. Arrays.asList es una vista de tamaño fijo de un array (set funciona, add/remove no). Elige según necesidad: factories inmutables para constantes, vistas no modificables para exponer internals de forma segura, copyOf para copias defensivas.
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));Implementaciones de Queue y Deque
ArrayDeque es la implementación preferida de stack y queue — más rápida que el legacy Stack (que es synchronized) y LinkedList. PriorityQueue ordena elementos por un Comparator (min-heap por defecto) — úsalo para scheduling, problemas top-K. Deque soporta ambos extremos; usa addFirst/removeFirst para semántica de stack, addLast/removeFirst para semántica de queue. Para queues concurrentes, usa las implementaciones de java.util.concurrent (LinkedBlockingQueue, etc.).
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 useMap merge, compute, getOrDefault
Estos métodos de Map hacen patrones comunes atómicos y concisos. merge es el idiom de conteo de palabras — combina valor existente y nuevo, removiendo la entrada si la función devuelve null. computeIfAbsent es el patrón de lazy-cache (memoization). getOrDefault evita null checks. replaceAll transforma todos los valores. Estos son mucho más limpios que el baile check-then-act get/put, y son los building blocks para los updates atómicos de ConcurrentHashMap en código concurrente.
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 utilitarios de Collections
Collections.* proporciona utilidad clásica: sort, binarySearch (requiere entrada ordenada), shuffle, reverse, frequency, min/max. Las factories singleton/empty devuelven colecciones inmutables de un solo elemento o vacías — prefiere emptyList() sobre devolver null. nCopies es eficiente en memoria (un elemento compartido). Los wrappers synchronized existen para código legacy pero prefiere colecciones de java.util.concurrent. Los wrappers checked capturan violaciones de tipo genérico en runtime, útiles para interop con raw types.
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 siteTesting (JUnit 5 y Mockito)
Fundamentos de JUnit 5
Anotaciones de JUnit 5 (Jupiter): @Test marca un test; @BeforeEach/@AfterEach se ejecutan alrededor de cada test; @BeforeAll/@AfterAll se ejecutan una vez para la clase (deben ser static). @DisplayName personaliza nombres de test. @Disabled salta tests. assertThrows verifica excepciones. Los tests deben ser independientes — usa @BeforeEach para resetear estado, no campos static. JUnit 5 vive en org.junit.jupiter.api (diferente del org.junit de JUnit 4).
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;
}
}Assertions
Assertions de JUnit 5: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (identidad). assertAll agrupa checks para que todos se ejecuten incluso si algunos fallan. assertTimeout falla tests lentos. Los mensajes pueden ser strings o Suppliers (lazy — construidos solo en fallo, evitando concatenación de strings cuando los tests pasan). assertIterableEquals compara colecciones ordenadas. Estos vienen de org.junit.jupiter.api.Assertions.
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;
}Tests parametrizados
Los tests parametrizados ejecutan la misma lógica de test con múltiples entradas. @ValueSource proporciona un array de un solo argumento. @CsvSource mapea filas CSV a múltiples parámetros. @MethodSource (el más flexible) usa un Stream<Arguments> static. @EnumSource itera valores enum. @NullAndEmptySource añade casos null/vacío. Esto elimina métodos de test copy-paste y hace el data-driven testing limpio. Los display names auto-incluyen los parámetros para diagnóstico fácil.
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 y conditional
@Nested crea clases de test internas que comparten lifecycle — genial para estructuras estilo BDD 'when X then Y' donde el setup outer aplica a tests inner. Las anotaciones condicionales (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) saltan tests basándose en el entorno. @Tag agrupa tests para ejecución selectiva (ej., fast vs slow, unit vs integration). Las clases nested no pueden tener @BeforeAll (son non-static). Estas características hacen la organización de tests expresiva.
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;Mocking con Mockito
Mockito crea test doubles para dependencias. @Mock crea un mock; @InjectMocks construye un objeto real con mocks inyectados. when(...).thenReturn(...) stubbea valores de retorno; verify(...) verifica interacciones. Los argument matchers (any(), eq(), argThat()) coinciden llamadas flexiblemente. Los spies envuelven objetos reales (mocking parcial). El patrón Arrange-Act-Assert mantiene los tests legibles. El mocking aísla la unidad bajo test de sus dependencias (base de datos, red, tiempo).
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();
}
}Testing con JUnit
Test básico
JUnit 5 usa @Test de org.junit.jupiter.api. assertEquals verifica que expected equals actual. Otras assertions: assertTrue, assertThrows, assertAll.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
@Test
void testAdd() {
assertEquals(5, calc.add(2, 3));
}
}Tests parametrizados
Los tests parametrizados ejecutan el mismo test con diferentes entradas. @ValueSource proporciona argumentos únicos. @CsvSource proporciona múltiples argumentos. Reduce la duplicación de tests.
@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 se ejecutan una vez por clase (deben ser static). @BeforeEach/@AfterEach se ejecutan alrededor de cada test. Úsalos para conexiones de base de datos y setup de mocks.
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 */ }
}Assertions
assertAll ejecuta todas las assertions incluso si algunas fallan. assertThrows verifica que el código lanza una excepción específica. Usa assertTimeout para tests limitados en tiempo.
@Test
void testAll() {
assertAll("person",
() -> assertEquals("Alice", p.getName()),
() -> assertEquals(30, p.getAge())
);
}
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}Mockito
@Mock crea objetos mock, @InjectMocks los inyecta. when().thenReturn() stubbea llamadas. verify() verifica que un método fue llamado. Mockito es el framework de mocking estándar.
@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);
}
}Maven/Gradle
Maven POM
Maven usa pom.xml. groupId/artifactId/version identifican el proyecto. Las dependencias tienen scope (compile, test, provided). Maven impone una estructura de directorios estándar.
<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 de Maven
Ciclo de vida de Maven: clean, compile, test, package, install, deploy. Cada fase ejecuta las fases precedentes. Usa -DskipTests para saltar tests.
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 treeBuild de Gradle
Gradle usa build.gradle (Groovy) o build.gradle.kts (Kotlin). implementation para deps de compilación, testImplementation para test. Gradle es más rápido que Maven.
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 de Gradle
Tareas de Gradle: build, test, run, clean. El wrapper (gradlew) asegura versiones consistentes. Usa --parallel para builds de módulos paralelos.
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 treeProyecto multi-módulo
Los proyectos multi-módulo dividen apps grandes. settings.gradle lista los módulos. project(:core) crea dependencias inter-módulo. Cada módulo tiene su propio build.gradle.
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
apply plugin: 'java'
repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }Fundamentos de Spring
App de Spring Boot
@SpringBootApplication habilita auto-configuración, component scanning y configuración. SpringApplication.run inicia el servidor embebido. Elimina la configuración XML.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}Controlador REST
@RestController combina @Controller y @ResponseBody. @GetMapping, @PostMapping son atajos. @PathVariable extrae parámetros de URL, @RequestBody vincula JSON.
@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);
}
}Inyección de dependencias
@Autowired inyecta dependencias. La inyección por constructor es recomendada (testeable, inmutable). @Service, @Repository, @Component son estereotipos para inyección.
@Service
public class UserService {
private final UserRepository repo;
@Autowired // Constructor injection (recommended)
public UserService(UserRepository repo) {
this.repo = repo;
}
}Configuración
@Configuration marca clases de configuración. @Bean declara beans gestionados por Spring. @Primary hace un bean preferido. Úsalo para clases de terceros.
@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. Los profiles habilitan configuración específica por entorno. Actívalos con spring.profiles.active=dev. Usa @Value o @ConfigurationProperties.
# 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=9090JDBC en profundidad
Connection y Statement
DriverManager.getConnection establece una conexión. Statement ejecuta SQL estático. ResultSet itera resultados. Siempre cierra recursos o usa try-with-resources.
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 previene SQL injection parametrizando consultas. Establece valores por índice (basado en 1). try-with-resources cierra automáticamente. Mejora el rendimiento mediante pre-compilación.
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();
}Gestión de transacciones
setAutoCommit(false) inicia una transacción. commit persiste, rollback deshace. Si cualquier statement falla, rollback para mantener integridad. @Transactional maneja esto en Spring.
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
El connection pooling reutiliza conexiones. HikariCP es el pool más rápido. maximumPoolSize limita conexiones concurrentes. Siempre cierra (devuelve al pool). Spring Boot configura HikariCP automáticamente.
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();Metadata de ResultSet
ResultSetMetaData describe la estructura del resultado: nombres de columna, tipos, propiedades. Útil para acceso a datos genérico. Los índices de columna son basados en 1.
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));Utilidades de concurrencia
ExecutorService
ExecutorService gestiona pools de threads. submit devuelve un Future para resultados async. get bloquea hasta completar (con timeout). Siempre shutdown el executor.
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 programación async funcional. supplyAsync se ejecuta en ForkJoinPool. thenApply transforma, thenAccept consume, exceptionally maneja errores.
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> { ex.printStackTrace(); return null; });Colecciones concurrentes
ConcurrentHashMap es thread-safe sin locking completo. CopyOnWriteArrayList copia en escritura (lectura pesada). BlockingQueue soporta patrones productor-consumidor.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);CountDownLatch y CyclicBarrier
CountDownLatch espera a N threads (one-shot). CyclicBarrier espera a N threads luego resetea (reutilizable). Usa latch para startup, barrier para computación por fases.
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"));Variables atómicas
Las variables atómicas proporcionan operaciones thread-safe sin locks. compareAndSet habilita optimistic locking. LongAdder es más rápido que AtomicLong para contadores de alta contención.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();Internals de la JVM
Áreas de memoria
Memoria de la JVM: Heap (objetos, gestionado por GC), Stack (llamadas a métodos, por thread), Metaspace (metadatos de clase). Young Gen usa copying GC, Old Gen usa mark-sweep-compact.
// 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 -Xmx2gCarga de clases
La carga de clases es lazy. Bootstrap carga Java core, Extension carga extensiones, Application carga el classpath. Los inicializadores static se ejecutan una vez. Los classloaders personalizados habilitan hot-reload.
// Bootstrap -> Extension -> Application classloaders
class MyClass {
static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loadingBytecode
Java compila a bytecode (basado en stack). javap -c desensambla archivos de clase. Cada instrucción push/pop el operand stack. Los Java agents pueden modificar bytecode en tiempo de carga.
// 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 intCompilación JIT
JIT compila bytecode frecuentemente ejecutado a código nativo. La compilación tiered balancea startup y rendimiento pico. Los métodos hot se inlinean y optimizan.
// JIT compiles hot methods to native code
// -XX:+PrintCompilation // Show JIT activity
// -XX:CompileThreshold=10000 // Method call count
// Tiered: Interpreter -> C1 -> C2Thread dump
Los thread dumps muestran todos los estados de threads y stack traces. Esenciales para depurar deadlocks y hangs. jstack es la herramienta de línea de comandos. Busca threads BLOCKED y WAITING.
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"Garbage Collection
Algoritmos de GC
Serial GC para apps pequeñas. Parallel GC maximiza throughput. G1 GC balancea throughput y latencia (default). ZGC proporciona pausas sub-milisegundo para heaps grandes.
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGCTuning de G1 GC
G1 divide el heap en regiones. MaxGCPauseMillis establece un objetivo suave de pausa. G1 prioriza regiones con más basura. Usa GCViewer o GCEasy para analizar logs.
# 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,uptimeMemory leaks
Los memory leaks son causados por retención no intencional de objetos. Las colecciones static, recursos no cerrados y registros de listeners son comunes. jmap muestra conteos de objetos. Analiza hprof con MAT.
// 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 cuando no existen strong references. SoftReference sobrevive hasta presión de memoria. Las claves de WeakHashMap son weak. Úsalas para caches que no deberían prevenir GC.
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'dFinalization
finalize() está deprecado (impredecible, lento). La API Cleaner (Java 9+) proporciona mejor limpieza. try-with-resources es preferido para limpieza determinista.
// 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(); }
}Stream Collectors
Grouping By
groupingBy particiona elementos por un clasificador. El segundo argumento es un collector downstream para agregación. counting, averaging, summing son collectors downstream comunes.
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 en dos grupos (true/false). Más eficiente que groupingBy para claves booleanas. El resultado siempre tiene ambas claves. Los collectors downstream agregan cada partición.
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 con delimitador, prefijo y sufijo opcionales. Los elementos deben ser strings; usa map primero. Usa StringBuilder internamente.
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 una operación fold. La versión de tres argumentos toma identity, mapper, reducer. La versión de dos argumentos devuelve Optional. Úsalo cuando los collectors estándar son insuficientes.
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 crea collectors personalizados: supplier, accumulator, combiner, finisher. El combiner combina resultados parciales para streams paralelos. Útil para formatos de salida especializados.
Collector<Person, ?, String> toJson = Collector.of(
StringBuilder::new,
(sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
StringBuilder::append,
StringBuilder::toString
);Pitfalls comunes
Integer caching
Java cachea valores Integer de -128 a 127. == compara referencias, no valores. Para Integer fuera del rango de caché, == devuelve false. Siempre usa .equals() para Integer.
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)); // trueInmutabilidad de String
Los strings son inmutables: métodos como concat devuelven nuevos strings. Olvidar asignar el resultado es un bug común. Usa StringBuilder para concatenación repetida.
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 placeEquals y HashCode
Objetos iguales deben tener hash codes iguales. HashMap y HashSet usan hashCode para bucketing y equals para comparación. Si sobrescribes equals, DEBES sobrescribir hashCode.
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
Las excepciones checked deben declararse o capturarse (IOException, SQLException). Las unchecked (RuntimeException) no requieren manejo. Evita capturar Exception amplio.
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");Resource leaks
Los recursos deben cerrarse. try-with-resources (Java 7+) cierra automáticamente AutoCloseable. Sin él, las excepciones causan leaks. Nunca dejes un recurso sin cerrar.
// 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 exceptionPatrones de diseño
Singleton
Double-checked locking con volatile asegura inicialización lazy thread-safe. volatile previene reordering de instrucciones. Enum singleton es más simple: public enum Singleton { INSTANCE; }.
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
El patrón Builder maneja objetos con muchos parámetros opcionales. Evita constructores telescópicos. El Builder interno recolecta parámetros fluentemente. build() crea el objeto inmutable.
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
El patrón Strategy encapsula algoritmos intercambiables. El contexto delega al interfaz strategy. Evita grandes cadenas if-else. Sigue el principio open-closed.
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
El patrón Observer define una dependencia uno-a-muchos. Cuando el subject cambia, todos los observers son notificados. Usado en MVC, sistemas de eventos y programación reactiva.
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 un interfaz para crear objetos pero deja que las subclases decidan qué clase instanciar. Desacopla el código cliente de las clases concretas.
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(); } }Fragmentos de Java relacionados
Copy-paste ready code for common tasks.
Operaciones de String y StringBuilder
Manipular strings con split, join, substring y StringBuilder en Java.
Stream API
Procesar colecciones usando la Stream API.
Expresiones Lambda
Simplificar código con expresiones Lambda.
Optional
Manejar valores null de forma elegante.
Operaciones de Colecciones
Operaciones comunes sobre List, Set y Map.
Manejo de Excepciones
try-catch-finally y excepciones personalizadas.
File IO
Operaciones de lectura y escritura de archivos.
Threads
Crear y gestionar threads.
Utilidades de Concurrencia
Utilidades comunes en el paquete de concurrencia.
Anotaciones
Anotaciones personalizadas y uso.
Genéricos
Clases y métodos genéricos.
Reflection
Obtener información de clase en tiempo de ejecución.
Serialización
Serialización y deserialización de objetos.
Fecha y Hora
API de Fecha-Hora de Java 8+.
Expresiones Regulares
Pattern y Matcher.
JDBC
Conexión y operaciones de base de datos.
Cliente HTTP
Cliente HTTP de Java 11+.
Record
Clases record de Java 14+.
Pattern Matching
Pattern matching con instanceof.
Sealed Classes
Sealed classes de Java 17+.
Text Blocks
Strings multilínea de Java 15+.
Was this helpful?