Guía de Java: De 0 a 100
¿Qué es Java?
Java es un lenguaje de programación orientado a objetos, de tipado estático y fuerte, creado por James Gosling en Sun Microsystems y lanzado en 1995. Su filosofía central es “Write Once, Run Anywhere” (WORA): el código se compila a bytecode que se ejecuta en la Java Virtual Machine (JVM), lo que permite que un mismo programa corra en Windows, Linux, macOS y cualquier plataforma que tenga una JVM.
Historia breve:
- 1991: James Gosling inicia el proyecto “Oak” en Sun Microsystems (inspirado en C++ pero más simple)
- 1995: Lanzamiento oficial de Java 1.0 con el lema WORA
- 2006: Sun libera Java como open source bajo licencia GPL — nace OpenJDK
- 2009: Oracle adquiere Sun Microsystems por 7.400 millones de dólares
- 2014: Java 8 — el cambio más grande: lambdas, Streams API, Optional, nueva API de fecha
- 2017: Java 9 — módulos (JPMS / Project Jigsaw), JShell
- 2018+: Ciclo de release cada 6 meses (Java 10, 11 LTS, 12, 13, 14, 15, 16, 17 LTS, 18, 19, 20, 21 LTS…)
¿Dónde se usa?
- Empresarial / Backend: Spring Boot, Jakarta EE — bancos, seguros, gobierno, comercio electrónico
- Android: El desarrollo nativo de Android usa Java (y Kotlin, que corre en la JVM). Más de 2500 millones de dispositivos activos
- Big Data: Apache Hadoop, Apache Spark, Apache Kafka, Elasticsearch, Apache Flink — todos escritos en Java
- DevOps / Infraestructura: Jenkins, Gradle, Maven, Tomcat, WildFly, Nexus, SonarQube
- IoT / Embebido: Java ME (Micro Edition), Raspberry Pi con Java
- Juegos: Minecraft (el videojuego más vendido de la historia) está escrito en Java
¿Quién lo usa? Google (Android, backend), Netflix (microservicios en JVM, Spring Cloud), Amazon (AWS SDK, backend), LinkedIn (backend original en Java), Twitter (migró partes a JVM), Spotify (backend con Java/Kotlin), Uber (sistema de tarifas en Java), NASA (instrumentos de control en Marte).
Fuente: Oracle — History of Java, OpenJDK — About, Dev.java — What is Java?
Prerrequisitos
Antes de empezar, necesitas:
- Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
- El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)
Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.
¿Cómo empezar? Instalación en todos los entornos
SDKMAN (recomendado para Linux/macOS/WSL)
SDKMAN es el gestor de versiones de JDK más usado. Instala, cambia y gestiona múltiples JDKs al instante, similar a pyenv o nvm.
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk list java
sdk install java 21.0.2-temurin
sdk install java 17.0.10-temurin
sdk default java 21.0.2-temurin
java --version
javac --version
Windows
- Descarga el instalador Eclipse Temurin JDK 21 LTS (MSI) desde adoptium.net
- Ejecuta el MSI y marca “Set JAVA_HOME” durante la instalación — esto es crítico
- Verifica: abre
cmdo PowerShell y ejecutajava --version - Alternativa vía winget:
winget install EclipseAdoptium.Temurin.21.JDK - Alternativa vía chocolatey:
choco install openjdk21
macOS
- Homebrew (recomendado):
brew install openjdk@21 sudo ln -sfn /usr/local/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-21.jdk - Xcode Command Line Tools:
xcode-select --install(necesario para algunas herramientas) - Verifica:
java --version
Linux (Debian/Ubuntu)
sudo apt update
sudo apt install wget apt-transport-https
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
sudo echo "deb https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt update && sudo apt install temurin-21-jdk
sudo apt install openjdk-21-jdk
Para Fedora/RHEL: sudo dnf install java-21-openjdk-devel
jabba — gestor de versiones como pyenv
curl -sL https://github.com/shyiko/jabba/raw/main/install.sh | bash
source ~/.jabba/jabba.sh
jabba install openjdk@1.21
jabba use openjdk@1.21
Docker
Con Maven (build multi-stage):
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Con Gradle:
FROM gradle:8-jdk21 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
RUN gradle build -x test
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Usa eclipse-temurin:21-jre como base de runtime (~150 MB). Evita openjdk:latest para builds reproducibles.
Alias útiles para el día a día
Añade a tu ~/.bashrc o ~/.zshrc:
alias jvs='java --version'
alias jcompile='javac -d target/classes -sourcepath src/main/java'
alias jrun='java -cp target/classes'
alias jmvn='mvn clean install -DskipTests'
alias jgradle='gradle build -x test'
alias jshell='jshell'
alias jspring='mvn spring-boot:run'
Fuente: SDKMAN docs, Adoptium docs, Docker Official Images — Eclipse Temurin
Escala de aprendizaje: de 0 a 100
Nivel 0-15: Fundamentos absolutos
Qué aprender:
- Sintaxis:
public class Main { public static void main(String[] args) }, variables primitivas,System.out.println() - Condicionales:
if/else if/else, operadores - Bucles:
for(clásico y enhanced),while - Arrays:
int[] nums = {1, 2, 3}, acceso por índice,length - Strings:
length(),charAt(),substring(),equals()vs== - Testing: JUnit 5 básico con
@TestyassertEquals
Proyecto: Adivina el número — programa elige aleatorio (Random), usuario adivina con pistas “mayor/menor”. Contador de intentos. Try-catch para entrada no numérica.
Nivel 15-30: POO, colecciones, archivos y algoritmos
Qué aprender:
- Clases, objetos, constructores,
this,new - Encapsulación:
private+ getters/setters ArrayList,HashMap,HashSet- Archivos:
Path/Files(java.nio.file) - Algoritmos:
Collections.sort(),Arrays.sort(),binarySearch, tabla Big O - Testing:
@ParameterizedTest,@TempDir
Proyecto: Gestor de tareas CLI — añadir, listar, completar, eliminar tareas. Persistencia en JSON. Clase Tarea con id, descripción, completado.
Nivel 30-45: Herencia, interfaces y excepciones
Qué aprender:
- Herencia:
extends,super(),@Override - Clases abstractas e interfaces
- Polimorfismo de subtipo
- Excepciones: checked vs unchecked
enumcon campos, Records (JDK 16+)Optional<T>
Proyecto: Sistema de inventario — Producto (record), Inventario con HashMap. Interface Exportable. Excepciones personalizadas.
Nivel 45-60: Testing, lambdas, streams, genéricos
Qué aprender:
- JUnit 5 avanzado + Mockito
- Lambdas, method references
- Streams API: filter, map, flatMap, collect, reduce, groupingBy
- Genéricos: clases, métodos, wildcards
- Algoritmos avanzados: PriorityQueue, ArrayDeque, Collections.binarySearch
Proyecto: Analizador de ventas — procesa CSV con Streams. Tests con JUnit 5 y Mockito.
Nivel 60-75: Spring Boot y APIs REST
Qué aprender:
- Spring Initializr (start.spring.io)
@RestController,@RequestMapping,@GetMapping/@PostMapping- Spring Data JPA:
@Entity, repositorios, query methods - DTOs con Records, Bean Validation
Proyecto: API de recetas — Spring Boot + JPA + H2. CRUD recetas e ingredientes.
Nivel 75-90: Producción, Docker y multimedia
Qué aprender:
- Docker multi-stage para Spring Boot, Docker Compose
- Testcontainers para tests de integración
- CI/CD con GitHub Actions
- Logging: SLF4J + Logback, MDC
- Métricas con Micrometer + Prometheus
- Spring Security + JWT
Proyecto: Dashboard de monitorización — API + WebSocket. JPA + PostgreSQL. Redis caché. Docker Compose.
Nivel 90-100: Microservicios y arquitectura
Qué aprender:
- Spring Cloud: Eureka, Gateway, Config Server
- Circuit Breaker: Resilience4j
- Distributed tracing: OpenTelemetry
- GraalVM native-image
- JFR, Async Profiler
Proyecto: Sistema de notificaciones distribuido — microservicios con Eureka, Gateway, Kafka, Resilience4j, OpenTelemetry.
Primeros pasos y configuración del entorno
Si prefieres ver directamente el “Hola Mundo” en Java, consulta El Atlas del Hola Mundo.
Compilación y ejecución manual
// Hola.java
public class Hola {
public static void main(String[] args) {
System.out.println("Hola mundo");
}
}
javac Hola.java # Genera Hola.class (bytecode)
java Hola # Ejecuta en la JVM
JShell (REPL interactivo, JDK 9+)
Java incluye un REPL desde Java 9. Ejecuta jshell en terminal y escribe directamente:
jshell> System.out.println("Hola mundo")
jshell> 2 + 2
jshell> var lista = List.of(1, 2, 3)
jshell> lista.stream().map(x -> x * x).forEach(System.out::println)
jshell> record Punto(int x, int y) {}
jshell> new Punto(3, 4)
jshell> /exit
Para salir: /exit o Ctrl+D.
Estructura de proyecto Maven
mvn archetype:generate -DgroupId=com.miempresa \
-DartifactId=mi-proyecto \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
mi-proyecto/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/com/miempresa/
│ │ └── resources/
│ └── test/
│ └── java/com/miempresa/
Estructura de proyecto Gradle
gradle init --type java-application --project-name mi-proyecto --package com.miempresa
mi-proyecto/
├── build.gradle.kts
├── settings.gradle.kts
├── src/
│ ├── main/java/com/miempresa/
│ └── test/java/com/miempresa/
Editores recomendados
Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.
Configuración inicial del proyecto (Maven pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.miempresa</groupId>
<artifactId>mi-proyecto</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Fuente: Maven docs, Gradle docs, Dev.java — JShell
Paradigmas de programación en Java
Java es un lenguaje multiparadigma. Aunque nació como lenguaje orientado a objetos, soporta varios estilos de programación.
Imperativo (statement-oriented)
El paradigma base: instrucciones secuenciales que modifican el estado del programa. Cada sentencia termina en ; y se ejecuta en orden una tras otra.
int suma = 0;
for (int i = 1; i <= 10; i++) {
suma += i;
}
System.out.println(suma);
Es el estilo más intuitivo para quienes empiezan. El programador dicta paso a paso cómo se hace cada operación. Es el paradigma dominante en lenguajes como C, Pascal y el Java más básico.
POO (puro — casi todo es objeto)
Java fue diseñado desde el inicio como lenguaje orientado a objetos. Salvo los tipos primitivos (int, boolean, double, etc.), todo es un objeto. El código se organiza en clases que encapsulan datos (atributos) y comportamiento (métodos).
public class Coche {
private String modelo;
public Coche(String modelo) {
this.modelo = modelo;
}
public void arrancar() {
System.out.println(modelo + " arrancó");
}
}
Características clave:
- Encapsulación: ocultar el estado interno mediante modificadores de acceso (
private) - Herencia simple: una clase solo puede extender una superclase (
extends) - Polimorfismo: el mismo mensaje produce comportamiento distinto según el objeto receptor
- Composición: objetos que contienen otros objetos (favorecida sobre herencia)
Funcional parcial (Java 8+)
Java no es un lenguaje funcional puro, pero desde Java 8 incorpora elementos del paradigma funcional:
- Lambdas: funciones anónimas como ciudadanos de primera clase —
(x) -> x * 2 - Streams API: procesamiento declarativo de colecciones (filter, map, reduce)
- Functional interfaces: interfaces con un único método abstracto, anotadas con
@FunctionalInterface - Method references: acceso directo a métodos como si fueran funciones —
System.out::println - Inmutabilidad fomentada: Records,
List.of(),Stream.toList()
list.forEach(x -> System.out.println(x)); // Lambda
list.forEach(System.out::println); // Method reference
var resultado = personas.stream()
.filter(p -> p.edad() >= 18)
.map(Persona::nombre)
.sorted()
.toList();
Las interfaces funcionales principales están en java.util.function:
Function<T,R>— transforma T en RPredicate<T>— prueba una condición (devuelve boolean)Consumer<T>— consume un valor sin devolver resultadoSupplier<T>— provee un valor sin tomar argumentosUnaryOperator<T>— Function donde T y R son el mismo tipoBinaryOperator<T>— combina dos T en un T
Procedimental histórico
Antes de que se asentara el paradigma OOP, el código Java se escribía a menudo de forma procedimental: métodos static en clases utilitarias, sin instanciar objetos. Hoy solo se ve en código legacy, métodos main() y clases de utilidades (Math, Collections).
public class UtilMath {
public static int sumar(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(sumar(3, 4));
}
}
Resumen de paradigmas:
| Paradigma | Soportado desde | Uso principal |
|---|---|---|
| Imperativo | Java 1.0 | Bloques básicos, bucles, condicionales |
| POO | Java 1.0 | Modelado de dominio, arquitectura |
| Funcional | Java 8 | Procesamiento de datos, pipelines |
| Procedimental | Java 1.0 (histórico) | Legacy, utilidades estáticas |
Fuente: Java Language Specification, Oracle — Lambda Expressions
Tipos de datos y variables
Sistema de tipos
¿Qué es?
Java tiene tipado estático y fuerte: el tipo de cada variable se conoce en tiempo de compilación y no se realizan conversiones implícitas entre tipos incompatibles. El sistema de tipos es nominal: dos clases son compatibles si una extiende/implementa explícitamente a la otra. Es como una máquina expendedora: cada producto tiene un código exacto y no puedes meter un código de otro producto para obtenerlo.
🔗 Wikipedia — Tipado estático, Java Language Spec
Sintaxis básica
int x = 42; // tipo explícito
var y = "hola"; // inferencia local (JDK 10+), compila a String
// x = "texto"; // Error de compilación: tipos incompatibles
String saludo(String nombre) {
return "Hola " + nombre;
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TiposTest {
@Test
void tipadoEstatico() {
int x = 42;
assertEquals(42, x);
// x = "texto"; // No compila
}
@Test
void inferenciaVar() {
var texto = "hola";
assertTrue(texto instanceof String);
}
@Test
void tipadoFuerte() {
// assertThrows(Exception.class, () -> { "42" + 1; });
// En Java no hay concatenación implícita peligrosa como en JS
String resultado = "42" + 1; // "421" — String, no número
assertEquals("421", resultado);
}
}
💡 Memoria y rendimiento
- Los tipos primitivos se almacenan directamente en el stack (o dentro de objetos en el heap)
- Los wrappers (
Integer,Long) son objetos en el heap con overhead (~16-24 bytes) varse resuelve en compilación — no tiene impacto en runtimeinstanceofes O(1) gracias al caché de tipos de la JVM
✅ Buenas prácticas
- ✅ Usa primitivos (
int,long,double) para valores que no necesitan ser null — más rápidos y ocupan menos memoria - ⚠️ No confundas
==con.equals():==compara referencias en objetos,.equals()compara valor - ❌ No uses
varpara tipos que no son obvios en el contexto (var result = getData()donde no se ve el tipo)
🏗️ Metodología
- Usa
varpara tipos obvios en el lado derecho (var list = new ArrayList<String>()) - Usa
Integer/Longwrappers solo cuando necesites nullabilidad (campos de entidad JPA, genéricos) - En APIs públicas, prefiere tipos explícitos sobre
var
🔗 Para saber más
Primitivos vs Wrappers
¿Qué es?
Cada tipo primitivo tiene su correspondiente wrapper (clase envolvente) en el heap. Los primitivos viven en el stack; los wrappers son objetos en el heap. Es como la diferencia entre llevar el dinero en efectivo en el bolsillo (stack) vs tenerlo en una caja fuerte (heap): más seguro pero más lento de acceder.
Sintaxis básica
| Primitivo | Tamaño | Wrapper | Rango | Default | Cache |
|---|---|---|---|---|---|
byte | 8 bits | Byte | -128 a 127 | 0 | -128 a 127 |
short | 16 bits | Short | -32.768 a 32.767 | 0 | -128 a 127 |
int | 32 bits | Integer | -2³¹ a 2³¹-1 | 0 | -128 a 127 |
long | 64 bits | Long | -2⁶³ a 2⁶³-1 | 0L | -128 a 127 |
float | 32 bits | Float | ±3.4E-38 | 0.0f | No |
double | 64 bits | Double | ±1.7E-308 | 0.0d | No |
boolean | ~1 bit | Boolean | true/false | false | true/false |
char | 16 bits | Character | 0 a 65.535 | ‘\u0000’ | 0 a 127 |
int primitivo = 42; // Stack
Integer wrapper = 42; // Heap + autoboxing
int vuelta = wrapper; // Unboxing
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PrimitivosTest {
@Test
void autoboxing() {
Integer wrapper = 42; // autoboxing: int → Integer
int primitivo = wrapper; // unboxing: Integer → int
assertEquals(42, primitivo);
}
@Test
void integerCache() {
assertSame(Integer.valueOf(127), Integer.valueOf(127));
assertNotSame(Integer.valueOf(128), Integer.valueOf(128));
}
@Test
void rendimientoAutoboxing() {
// NO hagas esto en producción — crea 1M de objetos
Integer suma = 0;
for (int i = 0; i < 1000; i++) {
suma += i;
}
assertEquals(499500, suma);
}
}
💡 Memoria y rendimiento
- Primitivo en stack: 4 bytes para
int, 8 paralong/double - Wrapper en heap: ~16-24 bytes de overhead de objeto + los datos
- Integer caching:
Integer.valueOf()cachea valores -128 a 127.Integer.valueOf(127) == Integer.valueOf(127)datrue, pero con 128 dafalse - El autoboxing en bucles grandes es costoso: cada
suma += icrea un nuevoInteger Comparator.comparingInt()evita autoboxing vsComparator.comparing()
✅ Buenas prácticas
- ✅ Siempre usa
.equals()para comparar wrappers, nunca== - ⚠️ En colecciones, el autoboxing es necesario pero ten cuidado con el rendimiento
- ❌ No uses
Integerpara contadores en bucles — usaint
🏗️ Metodología
- Primitivos: datos numéricos locales, contadores, flags
- Wrappers: colecciones genéricas (
List<Integer>), JPA (nullable), APIs que requieren Object - Para colecciones de primitivos en Java 21+:
IntListde Eclipse Collections o arrays
🔗 Para saber más
String
¿Qué es?
String es un objeto inmutable: cualquier operación que parezca modificarlo (concat, replace, substring) crea una nueva cadena. Java almacena las cadenas literales en un String Pool dentro del heap. Es como un sello de goma: cada vez que cambias el texto, necesitas un sello nuevo.
Sintaxis básica
String literal = "hola"; // String Pool
String objeto = new String("hola"); // Heap (evitar)
String internada = literal.intern();
String concat = "a" + "b"; // El compilador optimiza literales
String a = "hello";
String b = "hello";
System.out.println(a == b); // true (misma referencia en pool)
String c = new String("hello");
System.out.println(a == c); // false (objeto distinto en heap)
// StringBuilder — para concatenaciones múltiples
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("linea ").append(i).append("\n");
}
String resultado = sb.toString();
// Text Blocks (JDK 15+)
String json = """
{
"nombre": "Ana",
"email": "ana@email.com"
}
""";
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class StringTest {
@Test
void inmutable() {
String s = "hola";
String resultado = s.toUpperCase();
assertEquals("hola", s);
assertEquals("HOLA", resultado);
}
@Test
void stringPool() {
String a = "hello";
String b = "hello";
assertSame(a, b);
}
@Test
void stringBuilder() {
var sb = new StringBuilder();
sb.append("a").append("b").append("c");
assertEquals("abc", sb.toString());
}
@Test
void textBlock() {
String json = """
{"clave": "valor"}
""".trim();
assertEquals("{\"clave\": \"valor\"}", json);
}
}
💡 Memoria y rendimiento
- Las cadenas literales se almacenan en el String Pool (internado automático)
new String("hola")crea un objeto en el heap fuera del pool — evitarlo- La concatenación con
+en bucles es O(n²) porque crea un nuevo String en cada iteración StringBuilderes la opción eficiente para concatenaciones múltiples (O(n))String.intern()permite meter una cadena en el pool manualmente, pero es costoso
✅ Buenas prácticas
- ✅ Usa
StringBuilderpara construir cadenas en bucles - ⚠️ No uses
==para comparar cadenas — usa.equals() - ❌ No uses
new String("literal")— es redundante y evita el pool - ✅ Prefiere Text Blocks para JSON, SQL, HTML multilínea
🏗️ Metodología
- Cadenas constantes: literales (String Pool)
- Construcción dinámica:
StringBuilder - Thread-safe:
StringBuffer(sincronizado, más lento) - Parsing:
String.split(),String.format(),String.join()
🔗 Para saber más
var (inferencia de tipo local, JDK 10+)
¿Qué es?
var infiere el tipo de la variable a partir del inicializador en compilación. No es tipado dinámico — el tipo se deduce en tiempo de compilación y es inmutable después. Es como cuando dices “dame eso” señalando: el contexto determina qué es exactamente.
🔗 Oracle — Local Variable Type Inference
Sintaxis básica
var lista = new ArrayList<String>(); // ArrayList<String>
var mapa = new HashMap<String, Integer>();
var resultado = calcularAlgo();
// ❌ No compila:
// var x; // necesita inicializador
// var y = null; // tipo ambiguo
// var z = (a, b) -> a+b; // lambda sin tipo explícito
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class VarTest {
@Test
void inferencia() {
var texto = "hola";
assertInstanceOf(String.class, texto);
}
@Test
void varConGenericos() {
var lista = new java.util.ArrayList<String>();
lista.add("test");
assertEquals(1, lista.size());
}
}
💡 Memoria y rendimiento
vares puramente sintáctico — el compilador reemplaza por el tipo exacto- No tiene impacto en memoria ni rendimiento en runtime
- El bytecode generado es idéntico a escribir el tipo explícito
✅ Buenas prácticas
- ✅ Usa
varcuando el tipo es obvio por el inicializador - ⚠️ No uses
varpara parámetros de métodos, campos de clase o tipos de retorno - ❌ No uses
varsi oscurece la legibilidad (var result = complexMethod())
🏗️ Metodología
- IDE-friendly: en IntelliJ,
varse expande al tipo real con Ctrl+Click - Usa
varpara tipos genéricos complejos (Map<String, List<Integer>>) - En APIs públicas, preferir tipos explícitos para documentación
🔗 Para saber más
Records (JDK 16+)
¿Qué es?
Los Records son clases inmutables generadas automáticamente por el compilador. Proporcionan constructor, getters, equals(), hashCode(), toString() sin boilerplate. Es como pedir una hamburguesa en un restaurante de menú fijo: ya viene con todo incluido, no tienes que pedir cada ingrediente por separado.
Sintaxis básica
public record Persona(String nombre, int edad) {
// Compact constructor — validación
public Persona {
if (edad < 0)
throw new IllegalArgumentException("Edad negativa");
}
// Métodos adicionales
public String saludo() {
return "Hola, soy " + nombre;
}
}
// Uso
var p = new Persona("Ana", 30);
p.nombre(); // "Ana" (getter)
p.toString(); // "Persona[nombre=Ana, edad=30]"
p.equals(new Persona("Ana", 30)); // true
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class RecordTest {
@Test
void crearYAcceder() {
var p = new Persona("Ana", 30);
assertEquals("Ana", p.nombre());
assertEquals(30, p.edad());
}
@Test
void equalsPorValor() {
var a = new Persona("Ana", 30);
var b = new Persona("Ana", 30);
assertEquals(a, b);
}
@Test
void validacion() {
assertThrows(IllegalArgumentException.class,
() -> new Persona("Ana", -1));
}
}
💡 Memoria y rendimiento
- Los Records son
finalcon camposprivate final— la JVM puede optimizarlos agresivamente - Al ser inmutables, son seguros en concurrencia sin sincronización
- Sin overhead de librerías externas (vs Lombok
@Data) - El bytecode generado es similar al de una clase escrita manualmente
✅ Buenas prácticas
- ✅ Usa Records para DTOs, value objects, claves en mapas, respuestas API
- ⚠️ Los Records no pueden extender otras clases — no son para jerarquías de dominio
- ❌ No uses Records si necesitas setters o mutabilidad
🏗️ Metodología
- Records para datos inmutables de transporte (DTOs, eventos)
- Clases normales para entidades con identidad mutable (JPA
@Entity) - Lombok
@Datapara clases con muchos campos que necesitan mutabilidad
🔗 Para saber más
Enums
¿Qué es?
En Java, enum es una clase completa. Pueden tener campos, métodos, constructores, implementar interfaces. Son inherentemente serializables y resistentes a reflection. Es como un menú de restaurante: tiene opciones fijas (los valores del enum) pero cada una puede tener sus propios ingredientes (campos) y preparación (métodos).
Sintaxis básica
public enum Dia {
LUNES(true), MARTES(true), MIERCOLES(true),
JUEVES(true), VIERNES(true), SABADO(false), DOMINGO(false);
private final boolean laborable;
Dia(boolean laborable) { this.laborable = laborable; }
public boolean esLaborable() { return laborable; }
}
Dia hoy = Dia.LUNES;
System.out.println(hoy.esLaborable()); // true
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class EnumTest {
@Test
void valores() {
assertTrue(Dia.LUNES.esLaborable());
assertFalse(Dia.DOMINGO.esLaborable());
}
@Test
void valueOf() {
assertEquals(Dia.LUNES, Dia.valueOf("LUNES"));
}
@Test
void values() {
assertEquals(7, Dia.values().length);
}
}
💡 Memoria y rendimiento
- Los enums son singletons por definición — solo una instancia por valor en toda la JVM
- La comparación con
==es segura y O(1) (misma referencia) ordinal()devuelve la posición (0-based) — útil para arrays indexadosEnumMapyEnumSetson las implementaciones más eficientes para claves enum
✅ Buenas prácticas
- ✅ Usa enums para conjuntos fijos de constantes relacionadas
- ⚠️ No uses
ordinal()para lógica de negocio — puede cambiar al reordenar - ❌ No uses
switchsobreordinal()— usa el enum directamente
🏗️ Metodología
- Constantes simples:
enumsin campos - Constantes con comportamiento:
enumcon métodos abstractos implementados por cada valor - Singleton seguro:
enumcon una sola instancia (Effective Java Item 3)
🔗 Para saber más
Optional (Java 8+)
¿Qué es?
Contenedor que puede o no contener un valor. Alternativa declarativa a null. Es como una caja que puede estar vacía: en vez de devolver null y que el que llama tenga que acordarse de comprobarlo, devuelves una caja que dice explícitamente “puede no haber nada aquí”.
Sintaxis básica
Optional<String> opt = Optional.ofNullable(obtenerNombre());
opt.map(String::toUpperCase)
.filter(s -> s.length() > 3)
.orElse("defecto");
Optional.ofNullable(obtenerNombre())
.map(String::toUpperCase)
.orElseThrow(() -> new IllegalStateException("No hay valor"));
obtenerNombre().ifPresent(n -> System.out.println("Hola " + n));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OptionalTest {
@Test
void orElse() {
var opt = Optional.ofNullable(null);
assertEquals("defecto", opt.orElse("defecto"));
}
@Test
void mapYFilter() {
var opt = Optional.of("hola mundo");
var resultado = opt.map(String::toUpperCase)
.filter(s -> s.length() > 5)
.orElse("corto");
assertEquals("HOLA MUNDO", resultado);
}
@Test
void orElseThrow() {
var opt = Optional.empty();
assertThrows(IllegalStateException.class,
() -> opt.orElseThrow(() -> new IllegalStateException("no hay")));
}
}
💡 Memoria y rendimiento
Optionales un objeto con overhead (~16-24 bytes)- No uses Optional para campos de clase (serialización problemática)
Optional.empty()devuelve un singleton — no crea objetos nuevos- Las operaciones
map()/filter()crean nuevos objetos Optional en cada paso
✅ Buenas prácticas
- ✅ Úsalo para valores de retorno que pueden estar ausentes
- ⚠️ No uses Optional para: parámetros de métodos, campos de clase, colecciones, serialización
- ❌ No llames
.get()sin verificar.isPresent()— derrota el propósito
🏗️ Metodología
- Optional como retorno de métodos: “puede o no haber resultado”
nullpara ausencia programática (no hay resultado porque no debía haberlo)- Excepciones para errores que deben manejarse
orElseThrow()cuando la ausencia es un error
🔗 Para saber más
Memoria: Stack vs Heap
¿Qué es?
Java gestiona la memoria en dos áreas principales: el Stack (pila) y el Heap (montículo). Cada hilo tiene su propio stack (~1 MB); el heap es compartido por todos los hilos. Las variables locales y las referencias viven en el stack; los objetos y arrays viven en el heap, gestionados por el Garbage Collector. Es como una oficina: tu escritorio (stack) tiene tus notas actuales, y el archivo (heap) guarda todos los documentos.
🔗 Oracle — JVM Memory Management
Sintaxis básica
Stack (por hilo ~1MB) Heap (compartido)
┌─────────────────────┐ ┌─────────────────────────┐
│ Variables locales │ │ Objetos, arrays │
│ Referencias a │────ref────▶│ String Pool │
│ objetos del heap │ │ Integer Cache (-128~127)│
│ Parámetros │ │ Generational GC │
│ Se libera al salir │ │ Gestionado por GC │
│ del método │ │ Configurable (-Xms/-Xmx)│
└─────────────────────┘ └─────────────────────────┘
public void ejemplo() {
int x = 5; // x → stack (valor directo)
String s = "hola"; // s (ref) → stack; "hola" → heap (String Pool)
Persona p = new Persona("Ana", 30); // p (ref) → stack; Persona → heap
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemoriaTest {
@Test
void stackVsHeap() {
int primitivo = 5; // stack
String literal = "hola"; // ref → stack, "hola" → pool
Integer wrapper = 42; // ref → stack, 42 → heap (cached)
assertAll(
() -> assertEquals(5, primitivo),
() -> assertEquals("hola", literal),
() -> assertEquals(42, wrapper)
);
}
}
💡 Memoria y rendimiento
- Stack overflow: bucles recursivos sin condición de salida (~1MB por hilo, configurable con
-Xss) - Heap overflow: crear más objetos de los que la memoria disponible puede contener
- Las variables en stack se liberan automáticamente al salir del método (sin intervención del GC)
- Los objetos en heap sobreviven hasta que el GC determina que no hay referencias
-Xms(heap inicial) y-Xmx(heap máximo) son las flags de configuración principales
✅ Buenas prácticas
- ✅ Prefiere primitivos sobre wrappers para reducir presión en el heap
- ⚠️ No crees objetos innecesariamente dentro de bucles
- ❌ No confíes en
finalize()para liberar recursos — está deprecated desde Java 9
🏗️ Metodología
- Objetos pequeños y temporales: se asignan en Eden (Young Gen) y se recolectan rápidamente
- Objetos longevos: promocionados a Old Gen después de varias colecciones
- Strings: el String Pool reduce duplicados pero retiene cadenas que ya no se usan
🔗 Para saber más
Control de flujo y modularidad
Condicionales
¿Qué es?
Las estructuras condicionales permiten ejecutar diferentes bloques de código según condiciones booleanas. Java ofrece if/else, switch (tradicional y expresión), y pattern matching en switch. Es como un semáforo: el camino que tomas depende del color que veas.
🔗 Wikipedia — Estructuras de control
Sintaxis básica
if (condicion) {
// se ejecuta si condicion es true
} else if (otraCondicion) {
// ...
} else {
// ...
}
Switch tradicional (JDK 1+):
switch (dia) {
case "LUNES":
case "MARTES":
System.out.println("Entre semana");
break;
case "SABADO":
case "DOMINGO":
System.out.println("Fin de semana");
break;
default:
System.out.println("Inválido");
}
Switch expression (JDK 14+):
String tipo = switch (dia) {
case "LUNES", "MARTES", "MIERCOLES", "JUEVES", "VIERNES" -> "laborable";
case "SABADO", "DOMINGO" -> "finde";
case null -> "nulo";
default -> "invalido";
};
Switch pattern matching (JDK 21+ stable):
static String formatear(Object obj) {
return switch (obj) {
case Integer i -> "Entero: " + i;
case String s when s.length() > 10 -> "String largo";
case String s -> "String: " + s;
case null -> "nulo";
default -> "desconocido";
};
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CondicionalesTest {
@Test
void ifElse() {
int edad = 18;
String categoria;
if (edad < 12) categoria = "niño";
else if (edad < 18) categoria = "adolescente";
else categoria = "adulto";
assertEquals("adulto", categoria);
}
@Test
void switchExpression() {
String dia = "LUNES";
String tipo = switch (dia) {
case "LUNES", "MARTES", "MIERCOLES", "JUEVES", "VIERNES" -> "laborable";
case "SABADO", "DOMINGO" -> "finde";
default -> "invalido";
};
assertEquals("laborable", tipo);
}
@Test
void patternMatching() {
assertEquals("Entero: 42", formatear(42));
assertEquals("String: hola", formatear("hola"));
assertEquals("nulo", formatear(null));
}
}
💡 Memoria y rendimiento
if/elsees O(1) por condición — evaluación lazy (cortocircuito)switchtradicional usa tablas de salto (lookupswitch/tableswitch) — O(1)switchexpression es eficiente: el compilador genera bytecode optimizado- Pattern matching en switch: la JVM optimiza los patrones
✅ Buenas prácticas
- ✅ Prefiere switch expression sobre if/else encadenados para múltiples valores de una variable
- ⚠️ No olvides
breaken switch tradicional — el fall-through causa bugs - ❌ No uses switch sobre
ordinal()de enums — usa el enum directamente - ✅ Usa pattern matching para instanceof + cast (JDK 16+)
🏗️ Metodología
- 1-2 condiciones:
if/else - 3+ condiciones sobre el mismo valor:
switch - Validaciones complejas: pattern matching
- Dispatch dinámico: polimorfismo
🔗 Para saber más
Bucles
¿Qué es?
Los bucles ejecutan un bloque de código repetidamente. Java ofrece for (clásico y enhanced), while, do-while, y forEach con Streams. Es como una cinta transportadora: los elementos pasan uno tras otro y tú haces algo con cada uno.
Sintaxis básica
// for clásico
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// enhanced for (for-each, JDK 5+)
for (String s : lista) {
System.out.println(s);
}
// while
while (condicion) {
// se ejecuta mientras condicion sea true
}
// do-while (se ejecuta al menos una vez)
do {
// se ejecuta al menos una vez
} while (condicion);
// forEach con Stream
lista.forEach(s -> System.out.println(s));
lista.forEach(System.out::println);
// forEach con Map
mapa.forEach((k, v) -> System.out.println(k + " -> " + v));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BuclesTest {
@Test
void forClasico() {
int suma = 0;
for (int i = 1; i <= 5; i++) suma += i;
assertEquals(15, suma);
}
@Test
void forEach() {
var lista = java.util.List.of("a", "b", "c");
var resultado = new StringBuilder();
for (String s : lista) resultado.append(s);
assertEquals("abc", resultado.toString());
}
@Test
void whileTest() {
int i = 0;
while (i < 3) i++;
assertEquals(3, i);
}
}
💡 Memoria y rendimiento
forclásico sobre array: tan rápido como C (la JVM optimiza los bounds checks)for-eachsobre colecciones: usaIteratorinternamente — ligero overheadforEachcon lambda: overhead de crear un Consumer, pero el JIT inlinea- Los bucles con autoboxing dentro son costosos: crean objetos en cada iteración
✅ Buenas prácticas
- ✅ Prefiere
for-eachsobreforclásico para colecciones - ⚠️ No modifiques la colección mientras la iteras —
ConcurrentModificationException - ❌ No uses
for (int i = 0; i < list.size(); i++)— guarda el tamaño en una variable - ✅ Extrae condiciones invariantes fuera del bucle
🏗️ Metodología
- Procesamiento simple de colecciones:
for-each - Necesidad de índice:
forclásico - Transformaciones de datos:
Stream API(más declarativo) - Bucles infinitos con condición de salida:
while (true)conbreak
🔗 Para saber más
Excepciones
¿Qué es?
Java diferencia entre checked (verificadas en compilación) y unchecked (runtime). Las checked exceptions obligan al programador a manejarlas; las unchecked (subclases de RuntimeException) no. Es como la diferencia entre un aviso de mantenimiento programado (checked) y una avería imprevista (unchecked).
Throwable
├── Error (unchecked) — OutOfMemoryError, StackOverflowError
└── Exception
├── RuntimeException (unchecked) — NullPointerException, IllegalArgumentException
└── IOException (checked) — FileNotFoundException, SocketException
Sintaxis básica
// Checked — debes manejarlas
try {
var reader = new FileReader("archivo.txt");
} catch (FileNotFoundException e) {
System.err.println("Archivo no encontrado");
}
// Unchecked — no obligatorio
public void dividir(int a, int b) {
if (b == 0) throw new IllegalArgumentException("No dividir por cero");
}
// Try-with-resources (JDK 7+)
try (var br = new BufferedReader(new FileReader("archivo.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
// Multi-catch (JDK 7+)
try {
// código
} catch (IOException | SQLException e) {
log.error("Error", e);
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ExcepcionesTest {
@Test
void excepcionEsperada() {
assertThrows(IllegalArgumentException.class,
() -> { throw new IllegalArgumentException("error"); });
}
@Test
void mensajeExcepcion() {
var ex = assertThrows(IllegalArgumentException.class,
() -> dividir(10, 0));
assertTrue(ex.getMessage().contains("cero"));
}
@Test
void sinExcepcion() {
assertDoesNotThrow(() -> dividir(10, 2));
}
}
private int dividir(int a, int b) {
if (b == 0) throw new IllegalArgumentException("No dividir por cero");
return a / b;
}
💡 Memoria y rendimiento
- Construir una excepción es caro (rellenar el stack trace, ~50μs)
- El bloque
trysin excepción tiene overhead mínimo (la JVM optimiza) - No uses excepciones para control de flujo normal
try-with-resourcesgenera bytecode más complejo que untry/finallymanual, pero es más seguro
✅ Buenas prácticas
- ✅ Sé específico con los tipos de excepción que capturas
- ⚠️ No captures
Exceptiongenérica sin razón - ❌ No tragues excepciones con
catch (Exception e) { /* vacío */ } - ✅ Usa try-with-resources para cualquier recurso que implemente
AutoCloseable
🏗️ Metodología
- Checked exceptions: errores recuperables que quien llama debe manejar (IOException, SQLException)
- Unchecked exceptions: errores de programación (NullPointerException, IllegalArgumentException)
- Excepciones personalizadas: cuando necesitas transmitir información específica del error
- Validación de entrada: lanza
IllegalArgumentException
🔗 Para saber más
Módulos y paquetes
¿Qué es?
Un paquete (package) agrupa clases relacionadas. Un módulo (module-info.java, JPMS, JDK 9+) encapsula paquetes y declara dependencias explícitas. Es la unidad de organización del código en Java. Es como una biblioteca: los paquetes son las estanterías y los módulos son las salas que agrupan estanterías temáticas.
Sintaxis básica
package com.miempresa.app;
import java.util.List;
import static java.lang.Math.PI; // import estático
module-info.java (JPMS, JDK 9+):
module com.miempresa.app {
requires com.miempresa.datos;
requires transitive java.sql;
exports com.miempresa.app.api;
opens com.miempresa.app.internal;
provides com.miempresa.app.spi.Plugin
with com.miempresa.app.impl.PluginImpl;
uses com.miempresa.app.spi.Plugin;
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PaquetesTest {
@Test
void importsFuncionan() {
var lista = new java.util.ArrayList<String>();
lista.add("test");
assertTrue(lista instanceof java.util.List);
}
}
💡 Memoria y rendimiento
- Los imports no tienen impacto en rendimiento — son purely sintácticos
- JPMS verifica dependencias en compilación y runtime (no en caliente)
jlinkpuede crear runtime mínimo incluyendo solo los módulos necesarios (~30-40 MB)
✅ Buenas prácticas
- ✅ Usa imports explícitos, no
import package.* - ⚠️ No tengas clases con el mismo nombre que clases de la stdlib
- ❌ No crees paquetes con más de 20-30 clases — divide por responsabilidad
🏗️ Metodología
- Paquetes por capa:
controller,service,repository,model - Paquetes por feature:
usuario,pedido,producto(más modular) - Módulos: para librerías y aplicaciones grandes donde quieras encapsular APIs internas
🔗 Para saber más
Sistema de archivos
java.nio.file (NIO.2, JDK 7+)
¿Qué es?
La API moderna y recomendada para manejo de archivos, introducida en Java 7 como parte de NIO.2. Proporciona operaciones fluidas, streaming, watches y mapeo de memoria. Es como tener un asistente de archivos que puede leer, escribir, copiar y vigilar cambios sin que tengas que hacer el trabajo pesado.
Sintaxis básica
import java.nio.file.*;
import java.nio.file.attribute.*;
Path ruta = Path.of("datos", "config.properties");
Path absoluta = Path.of("/home/user", "app", "config.yml");
// Leer y escribir (JDK 11+)
String contenido = Files.readString(path);
Files.writeString(path, "Hola mundo");
List<String> lineas = Files.readAllLines(path);
Files.write(path, List.of("línea 1", "línea 2"));
// Copiar, mover, borrar
Files.copy(origen, destino, StandardCopyOption.REPLACE_EXISTING);
Files.move(origen, destino, StandardCopyOption.ATOMIC_MOVE);
Files.deleteIfExists(path);
// Recorrer archivos
try (var stream = Files.walk(Path.of("src"))) {
stream.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Archivos grandes con Stream perezoso
try (var lines = Files.lines(Path.of("grande.csv"))) {
lines.filter(l -> !l.startsWith("#"))
.map(l -> l.split(","))
.limit(100)
.forEach(arr -> System.out.println(arr[0]));
}
// Atributos
var attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Tamaño: " + attrs.size());
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.*;
class FileTest {
@TempDir
Path tempDir;
@Test
void escribirYLeer() throws Exception {
var archivo = tempDir.resolve("test.txt");
Files.writeString(archivo, "contenido");
assertEquals("contenido", Files.readString(archivo));
}
@Test
void copiar() throws Exception {
var origen = tempDir.resolve("a.txt");
var destino = tempDir.resolve("b.txt");
Files.writeString(origen, "datos");
Files.copy(origen, destino, StandardCopyOption.REPLACE_EXISTING);
assertTrue(Files.exists(destino));
}
@Test
void walk() throws Exception {
Files.createDirectory(tempDir.resolve("sub"));
Files.writeString(tempDir.resolve("sub/archivo.txt"), "contenido");
try (var stream = Files.walk(tempDir)) {
long count = stream.count();
assertTrue(count >= 2);
}
}
}
💡 Memoria y rendimiento
Files.readString()carga todo el archivo en memoria — peligroso para archivos grandes (>100MB)Files.lines()es lazy: lee bajo demanda, ideal para archivos grandesFileChannelconMappedByteBufferofrece el mejor rendimiento para archivos muy grandesWatchServiceusa eventos del SO (inotify en Linux) — overhead mínimo- Las operaciones de NIO.2 son generalmente más rápidas que
java.iopor el buffering nativo
✅ Buenas prácticas
- ✅ Siempre usa try-with-resources con streams de archivos
- ⚠️ No uses
readString()para archivos que no caben en memoria - ❌ No ignores el encoding — especifica
StandardCharsets.UTF_8explícitamente - ✅ Prefiere NIO.2 sobre java.io para nuevo código
🏗️ Metodología
- Archivos pequeños:
Files.readString()/writeString() - Archivos grandes:
Files.lines()para streaming línea por línea - Alto rendimiento binario:
FileChannel+MappedByteBuffer - Vigilar cambios:
WatchServicepara hot-reload, procesamiento de archivos entrantes
🔗 Para saber más
Algoritmos y estructuras de datos
Ordenación
¿Qué es?
Java incluye algoritmos de ordenación optimizados: Timsort (para objetos, estable, O(n log n)) y Dual-Pivot Quicksort (para primitivos, inestable, O(n log n)). Es como tener un archivador que ordena tus documentos automáticamente en cuanto se lo pides.
Sintaxis básica
import java.util.*;
var list = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9));
Collections.sort(list); // Timsort — O(n log n), estable
Collections.reverse(list);
Collections.shuffle(list);
int[] array = {3, 1, 4, 1, 5, 9};
Arrays.sort(array); // Dual-Pivot Quicksort
Arrays.parallelSort(array); // ForkJoinPool
// Comparadores
records.sort(Comparator.comparing(Persona::edad));
records.sort(Comparator.comparing(Persona::edad).reversed());
records.sort(Comparator.comparing(Persona::apellido)
.thenComparing(Persona::nombre));
// Primitivos (evitan autoboxing)
records.sort(Comparator.comparingInt(Persona::edad));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OrdenacionTest {
@Test
void sortLista() {
var list = new ArrayList<>(List.of(3, 1, 2));
Collections.sort(list);
assertEquals(List.of(1, 2, 3), list);
}
@Test
void sortArray() {
int[] arr = {3, 1, 2};
Arrays.sort(arr);
assertArrayEquals(new int[]{1, 2, 3}, arr);
}
@Test
void comparadorPersonalizado() {
record Persona(String nombre, int edad) {}
var personas = new ArrayList<>(List.of(
new Persona("Ana", 30), new Persona("Luis", 25)));
personas.sort(Comparator.comparing(Persona::edad));
assertEquals(25, personas.get(0).edad());
}
@Test
void sortEstable() {
record Par(int key, String val) {}
var datos = new ArrayList<>(List.of(
new Par(1, "a"), new Par(2, "b"), new Par(1, "c")));
datos.sort(Comparator.comparing(Par::key));
assertEquals("a", datos.get(0).val());
assertEquals("c", datos.get(2).val());
}
}
💡 Memoria y rendimiento
Collections.sort()(Timsort): O(n log n) peor caso, O(n) mejor caso (datos ya ordenados)Arrays.sort()(Dual-Pivot Quicksort): O(n log n) promedioArrays.parallelSort(): divide el array, ordena en paralelo y fusionasorted()con Stream crea una nueva lista (O(n) memoria extra)list.sort()ordena in-place (O(1) memoria extra)- Timsort es estable: elementos iguales mantienen su orden relativo
✅ Buenas prácticas
- ✅ Usa
Comparator.comparing()con method references — evita errores - ⚠️
parallelSort()solo es beneficioso para arrays grandes (>5000 elementos) - ❌ No implementes tu propio algoritmo de ordenación — el de Java es mejor
- ✅ Usa
Comparator.comparingInt()para evitar autoboxing con primitivos
🏗️ Metodología
- Ordenación simple:
Collections.sort()olist.sort() - Streams:
.sorted(Comparator.comparing(...)) - Arrays primitivos:
Arrays.sort()oArrays.parallelSort() - Top-N elementos: PriorityQueue o Stream
.limit(N)después de sorted
🔗 Para saber más
Conceptos clave explicados a fondo
JVM, JRE, bytecode y compilación JIT
¿Qué es?
El ecosistema Java tiene tres capas: JDK (kit de desarrollo con compilador javac), JRE (runtime con la JVM y librerías) y JVM (máquina virtual que ejecuta bytecode). La JVM convierte bytecode en código máquina mediante compilación JIT (Just-In-Time). Es como un traductor simultáneo: el JDK es el diccionario, la JRE es el traductor, y la JVM es el intérprete que habla directamente con el oyente.
int suma = 1 + 2;
// javac lo compila a bytecode: iconst_1, iconst_2, iadd, istore_1
// La JVM interpreta bytecode. Las partes "calientes" (hot spots)
// se compilan a código nativo mediante JIT
Sintaxis básica
.java --(javac)--> .class (bytecode) --(JVM)--> código máquina
jlink — runtime mínimo:
jlink --module-path $JAVA_HOME/jmods:target/classes \
--add-modules com.miempresa.app \
--output runtime-minimo --compress 2 --strip-debug
./runtime-minimo/bin/java com.miempresa.app.Main
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class JVMTest {
@Test
void bytecodeExecution() {
int suma = 1 + 2;
assertEquals(3, suma);
}
@Test
void jitOptimization() {
long inicio = System.nanoTime();
long suma = 0;
for (int i = 0; i < 1_000_000; i++) {
suma += i;
}
long fin = System.nanoTime();
assertTrue(suma > 0);
System.out.println("Tiempo (ms): " + (fin - inicio) / 1_000_000);
// La primera ejecución es más lenta (interpretación)
// La JVM compila JIT los hot spots
}
}
💡 Memoria y rendimiento
- La compilación JIT ocurre cuando un método se ejecuta muchas veces (umbral configurable:
-XX:CompileThreshold=10000) - AOT con GraalVM: compila Java a nativo con
native-image. Arranque instantáneo (~0.1s vs ~3s), menos memoria (~10 MB vs ~100 MB) - El JIT puede hacer inline de métodos virtuales y eliminar bounds checking
- JVM warm-up: las primeras ejecuciones son más lentas hasta que el JIT compila los hot spots
✅ Buenas prácticas
- ✅ Perfila tu aplicación con JMH antes de optimizar
- ⚠️ El warm-up de la JVM afecta benchmarks — usa JMH que incluye warm-up iterations
- ❌ No asumas que el bytecode se ejecuta igual en todas las JVMs (HotSpot vs OpenJ9 vs GraalVM)
🏗️ Metodología
- Apps server: JVM HotSpot con G1GC es la opción por defecto más equilibrada
- Serverless/FaaS: GraalVM Native Image para arranque instantáneo
- Microservicios con restricciones de memoria: GraalVM (menor footprint)
🔗 Para saber más
Garbage Collection
¿Qué es?
La JVM gestiona la memoria automáticamente mediante el Garbage Collector. El heap se organiza en generaciones: Young (Eden + S0 + S1) → Old → Metaspace. Los objetos nuevos se asignan en Eden; los que sobreviven varias colecciones se promocionan a Old. La mayoría de los objetos muere joven (weak generational hypothesis). Es como un servicio de limpieza de oficinas: cada noche se tira la basura de las papeleras (Eden), y cada mes se revisan los archivos (Old) para ver qué se puede eliminar.
Sintaxis básica
java -XX:+UseZGC -jar app.jar
java -XX:+UseShenandoahGC -jar app.jar
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
| Recolector | JDK | Descripción | Ideal para |
|---|---|---|---|
| G1 GC | Default JDK 9+ | Divide heap en regiones (~1-32 MB), prioriza las más sucias. Pausas predecibles | Uso general |
| ZGC | JDK 15+ (prod) | Pausas < 1ms, heaps hasta 16 TB. Coloreado de punteros | Latencia ultrabaja |
| Shenandoah | JDK 21+ (stable) | Compactación concurrente, pausas ultrabajas | Apps sensibles a latencia |
| Serial GC | Siempre | Un solo hilo para todo el GC. Pausas largas pero mínimo overhead | Apps pequeñas, monohilo |
| Parallel GC | JDK 8 default | Múltiples hilos para Young y Old GC | Throughput máximo |
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.management.*;
class GCTest {
@Test
void memoriaDisponible() {
var mem = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
assertTrue(mem.getMax() > 0);
System.out.println("Heap max: " + mem.getMax() / 1024 / 1024 + " MB");
}
}
💡 Memoria y rendimiento
- Fugas de memoria en Java: listeners no desregistrados, ClassLoader leaks (redeploys en Tomcat),
String.intern()excesivo, conexiones no cerradas - El GC Stop-The-World (STW) pausa todos los hilos de la aplicación
- ZGC y Shenandoah minimizan las pausas STW (concurrentes)
- Herramientas: VisualVM, JProfiler, Eclipse MAT, JFR (built-in), Async Profiler
✅ Buenas prácticas
- ✅ Configura
-Xmsy-Xmxiguales para evitar redimensiones del heap - ⚠️ Si ves pausas largas de GC, prueba ZGC o Shenandoah
- ❌ No llames a
System.gc()— ignora la sugerencia o empeora el rendimiento - ✅ Monitorea el GC con
-Xlog:gc*en JDK 17+
🏗️ Metodología
- Apps con requisitos de latencia: ZGC o Shenandoah
- Apps batch/throughput: Parallel GC
- Microservicios con memoria limitada: GraalVM Native Image (sin GC)
- Monitoreo: JFR + GC logs + Prometheus/Grafana con JMX Exporter
🔗 Para saber más
POO clásica: clases, herencia, interfaces y polimorfismo
¿Qué es?
Java es un lenguaje puramente orientado a objetos (casi todo es objeto, salvo primitivos). El sistema de tipos es nominal: dos clases son compatibles si una extiende/implementa explícitamente a la otra. Es como un árbol genealógico: cada clase conoce a sus ancestros y descendientes.
Sintaxis básica
// Clase y encapsulación
public class Persona {
private String nombre;
private int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}
// Herencia simple y clase abstracta
public abstract class Animal {
protected String nombre;
public Animal(String nombre) { this.nombre = nombre; }
public abstract String hacerSonido();
public void dormir() { System.out.println(nombre + " durmiendo..."); }
}
public class Perro extends Animal {
public Perro(String nombre) { super(nombre); }
@Override public String hacerSonido() { return "Guau"; }
}
// Interfaces
public interface Volador {
void volar();
default void planear() { System.out.println("Planeando..."); }
static boolean esAve(Object obj) { return obj instanceof Volador; }
}
// Polimorfismo
Animal a = new Perro("Rex");
System.out.println(a.hacerSonido()); // Guau
// Pattern Matching for instanceof (JDK 16+)
if (a instanceof Perro perro) {
perro.moverCola();
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class POOTest {
@Test
void encapsulacion() {
var p = new Persona("Ana", 30);
assertEquals("Ana", p.getNombre());
}
@Test
void polimorfismo() {
Animal a = new Perro("Rex");
assertEquals("Guau", a.hacerSonido());
}
@Test
void patternMatchingInstanceof() {
Animal a = new Perro("Rex");
if (a instanceof Perro perro) {
assertNotNull(perro);
}
}
}
💡 Memoria y rendimiento
- Cada objeto tiene overhead de cabecera (~12-16 bytes en JVM de 64 bits con compressed OOPs)
- La herencia añade una tabla de métodos virtuales (vtable) por clase
- El JIT puede inline métodos virtuales cuando detecta que solo hay una implementación
- Pattern matching (
instanceofcon variable) no tiene overhead adicional
✅ Buenas prácticas
- ✅ Favorece composición sobre herencia (Effective Java Item 18)
- ⚠️ No crees jerarquías de herencia profundas (>3 niveles)
- ❌ No uses herencia solo para reutilizar código — prefiere composición o delegación
- ✅ Usa interfaces para definir contratos, clases abstractas para implementación base
🏗️ Metodología
- “es un”: herencia (
Perro extends Animal) - “tiene un”: composición (
Coche contiene Motor) - “puede hacer”: interfaz (
Volador,Exportable) - Para DTOs/value objects: Records (JDK 16+)
🔗 Para saber más
Genéricos, wildcards y type erasure
¿Qué es?
Los genéricos son invariantes y se implementan mediante type erasure: en runtime List<String> y List<Integer> son ambos List. Los genéricos proporcionan seguridad de tipos en compilación sin overhead en runtime. Es como una máquina de envasado: la etiqueta (tipo) solo importa durante el envasado; después todas las cajas son iguales.
Sintaxis básica
public class Caja<T> {
private T contenido;
public void guardar(T contenido) { this.contenido = contenido; }
public T obtener() { return contenido; }
}
Caja<String> caja = new Caja<>();
caja.guardar("Hola");
String valor = caja.obtener(); // Sin cast
// Wildcards
void sumar(List<? extends Number> nums) { /* solo lectura */ }
void addInts(List<? super Integer> lista) { /* solo escritura */ }
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class GenericosTest {
@Test
void cajaGenerica() {
var caja = new Caja<String>();
caja.guardar("test");
assertEquals("test", caja.obtener());
}
@Test
void wildcardExtends() {
var nums = java.util.List.of(1, 2, 3);
double suma = 0;
for (Number n : nums) suma += n.doubleValue();
assertEquals(6.0, suma);
}
@Test
void typeErasure() {
var listaStr = new java.util.ArrayList<String>();
var listaInt = new java.util.ArrayList<Integer>();
// En runtime ambos son ArrayList
assertTrue(listaStr.getClass() == listaInt.getClass());
}
}
💡 Memoria y rendimiento
- Type erasure: en compilación
Tse reemplaza porObject(o el bound) - No puedes hacer
new T()niinstanceof Caja<String>— la información de tipo se pierde - Los genéricos no tienen overhead en runtime (todo se resuelve en compilación)
- Los wildcards
? extendsy? superson conceptos de compilación — sin impacto en runtime
✅ Buenas prácticas
- ✅ PECS: Producer Extends, Consumer Super
- ⚠️ No uses raw types (
Listsin parámetro de tipo) — el compilador da warning - ❌ No uses genéricos con tipos primitivos — necesitas wrappers
- ✅ Usa method references con genéricos (
Function.identity())
🏗️ Metodología
- Clases genéricas: cuando el algoritmo no depende del tipo concreto
- Wildcards: APIs que deben ser flexibles en los tipos aceptados
- Type bounds:
<T extends Comparable<T>>para ordenación
🔗 Para saber más
Streams API y Optional
¿Qué es?
Streams (Java 8+) es una API para procesamiento declarativo de datos. Las operaciones son lazy (no se ejecutan hasta que hay una operación terminal). Optional es un contenedor que puede o no contener un valor, diseñado para evitar NullPointerException. Streams es como una cadena de montaje: cada trabajador hace una operación y pasa el resultado al siguiente.
Sintaxis básica
// Streams
list.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.distinct()
.sorted()
.limit(10)
.toList();
// Collectors
Collectors.joining(", ")
Collectors.groupingBy(String::length)
Collectors.partitioningBy(s -> s.length() > 5)
// reduce
int suma = nums.stream().reduce(0, Integer::sum);
// Optional
Optional.ofNullable(obtenerNombre())
.map(String::toUpperCase)
.orElse("Por defecto")
.orElseThrow(() -> new IllegalStateException("No hay valor"));
obtenerNombre().ifPresent(n -> System.out.println("Hola " + n));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.stream.*;
class StreamsTest {
@Test
void filterMap() {
var result = Stream.of("hola", "mundo", "java")
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.toList();
assertEquals(2, result.size());
}
@Test
void groupingBy() {
var words = Stream.of("java", "kotlin", "c", "rust", "go");
var grouped = words.collect(Collectors.groupingBy(String::length));
assertTrue(grouped.containsKey(4));
}
@Test
void reduce() {
int suma = Stream.of(1, 2, 3, 4, 5)
.reduce(0, Integer::sum);
assertEquals(15, suma);
}
@Test
void optional() {
var resultado = Optional.ofNullable(null)
.orElse("defecto");
assertEquals("defecto", resultado);
}
}
💡 Memoria y rendimiento
- Las operaciones intermedias (filter, map) son lazy — no se ejecutan hasta la terminal
toList()(JDK 16+) crea una lista inmutable — más eficiente quecollect(Collectors.toList())parallelStream()puede mejorar rendimiento en datos grandes y operaciones CPU-bound- Optional no está diseñado para alto rendimiento en bucles — cada operación crea un nuevo Optional
✅ Buenas prácticas
- ✅ Prefiere
toList()(JDK 16+) sobrecollect(Collectors.toList()) - ⚠️ No uses
parallelStream()sin medir — el overhead de paralelización puede empeorar el rendimiento - ❌ No uses Stream para bucles simples —
for-eaches más legible - ✅ Usa
.orElseThrow()en vez de.get()(más seguro)
🏗️ Metodología
- Transformaciones de datos: Streams API
- Valores ausentes: Optional
- Procesamiento paralelo:
parallelStream()solo si es CPU-bound y datos grandes - Bucles simples con efectos secundarios:
for-each
🔗 Para saber más
Excepciones: checked vs unchecked
¿Qué es?
Java diferencia entre checked (verificadas en compilación) y unchecked (runtime). Las checked obligan al programador a manejarlas con try/catch o declararlas con throws. Es como la diferencia entre una inspección técnica de tu coche (checked: sabes que va a pasar y tienes que prepararte) y un pinchazo inesperado (unchecked: pasa sin avisar).
Sintaxis básica
// Checked
try {
var reader = new FileReader("archivo.txt");
} catch (FileNotFoundException e) {
System.err.println("Archivo no encontrado");
}
// Unchecked
public void dividir(int a, int b) {
if (b == 0) throw new IllegalArgumentException("No dividir por cero");
}
// Try-with-resources (JDK 7+)
try (var br = new BufferedReader(new FileReader("archivo.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ExceptionsTest {
@Test
void uncheckedEsperada() {
assertThrows(IllegalArgumentException.class,
() -> dividir(10, 0));
}
@Test
void tryWithResources() {
assertDoesNotThrow(() -> {
try (var br = new java.io.BufferedReader(
new java.io.StringReader("hola"))) {
assertEquals("hola", br.readLine());
}
});
}
}
💡 Memoria y rendimiento
- Construir una excepción es caro (~50μs) por el stack trace
- Try-with-resources genera bytecode más complejo, pero el recurso se cierra siempre
- Stack trace filling puede desactivarse con
-XX:-StackTraceInThrowable
✅ Buenas prácticas
- ✅ Captura excepciones específicas, no
Exceptiongenérica - ⚠️ No uses excepciones para control de flujo normal
- ❌ No tragues excepciones sin registrarlas
- ✅ Usa try-with-resources para cualquier recurso AutoCloseable
🏗️ Metodología
- Checked: errores recuperables esperados
- Unchecked: errores de programación
- Personalizadas: cuando necesitas datos adicionales del error
🔗 Para saber más
POO y patrones de diseño
Sistema multimedia completo
Este ejemplo une todos los conceptos de POO en un sistema de biblioteca multimedia.
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
// ── Record y Enum ──
enum FormatoVideo { MP4, WEBM, AVI, MKV }
record Metadatos(String titulo, int duracionSegundos,
LocalDateTime fechaCreacion, List<String> tags) {
public Metadatos {
if (duracionSegundos <= 0)
throw new IllegalArgumentException("Duración debe ser positiva");
if (tags == null) tags = List.of();
}
public Metadatos(String titulo, int duracionSegundos) {
this(titulo, duracionSegundos, LocalDateTime.now(), List.of());
}
public String duracionFormateada() {
return String.format("%d:%02d",
duracionSegundos / 60, duracionSegundos % 60);
}
@Override public String toString() {
return titulo + " (" + duracionFormateada() + ")";
}
}
// ── Clase abstracta ──
abstract class Contenido {
protected final Metadatos metadatos;
private int reproducciones;
public Contenido(Metadatos metadatos) {
this.metadatos = metadatos;
this.reproducciones = 0;
}
public Metadatos getMetadatos() { return metadatos; }
public int getReproducciones() { return reproducciones; }
public String reproducir() {
reproducciones++;
return "▶️ Reproduciendo " + metadatos.titulo() + "...";
}
public abstract String obtenerInfo();
@Override public String toString() {
return "[" + getClass().getSimpleName() + "] " + metadatos;
}
}
// ── Herencia simple ──
class Audio extends Contenido {
private final int bitrateKbps;
public Audio(Metadatos metadatos, int bitrateKbps) {
super(metadatos);
this.bitrateKbps = bitrateKbps;
}
@Override public String obtenerInfo() {
return String.format("🎵 %s | %s | %dkbps | %d reps",
metadatos.titulo(), metadatos.duracionFormateada(),
bitrateKbps, getReproducciones());
}
}
class Video extends Contenido {
private final String resolucion;
private final FormatoVideo formato;
private boolean subtitulos;
public Video(Metadatos metadatos, String resolucion,
FormatoVideo formato) {
super(metadatos);
this.resolucion = resolucion;
this.formato = formato;
}
public void setSubtitulos(boolean v) { this.subtitulos = v; }
@Override public String obtenerInfo() {
return String.format("🎬 %s | %s | %s | %s",
metadatos.titulo(), resolucion, formato,
subtitulos ? "CC" : "Sin subtítulos");
}
}
// ── Interface ──
interface Exportable {
default String exportar(String formato) {
if ("json".equals(formato))
return "{ \"tipo\": \"" + getClass().getSimpleName() + "\" }";
return "Exportación a " + formato + " no soportada";
}
}
// ── Herencia + Interface ──
class Podcast extends Audio implements Exportable {
private final int episodio;
private final String anfitrion;
public Podcast(Metadatos m, int bitrate, int ep, String anfitrion) {
super(m, bitrate);
this.episodio = ep;
this.anfitrion = anfitrion;
}
@Override public String obtenerInfo() {
return String.format("🎙️ Ep.%d: %s | con %s | %s",
episodio, metadatos.titulo(), anfitrion,
super.obtenerInfo());
}
}
// ── Composición ──
class ListaReproduccion {
private final String nombre;
private final List<Contenido> items = new ArrayList<>();
public ListaReproduccion(String nombre) { this.nombre = nombre; }
public void agregar(Contenido item) { items.add(item); }
public List<String> infoCompleta() {
return items.stream().map(Contenido::obtenerInfo).toList();
}
public List<String> reproducirTodo() {
return items.stream().map(Contenido::reproducir).toList();
}
public int size() { return items.size(); }
@Override public String toString() {
return "📋 " + nombre + " (" + items.size() + " items)";
}
}
Conceptos demostrados:
| Concepto | Cómo aparece |
|---|---|
| Clase abstracta | Contenido con abstract obtenerInfo() |
| Herencia simple | Audio extends Contenido, Video extends Contenido |
| Interface | Exportable con método default |
| Herencia + Interface | Podcast extends Audio implements Exportable |
| Encapsulación | private int reproducciones, getters públicos |
| Polimorfismo | obtenerInfo() distinto según clase concreta |
| Composición | ListaReproduccion contiene List<Contenido> |
| Record | Metadatos con validación en compact constructor |
| Enum | FormatoVideo con valores constantes |
| Streams API | items.stream().map(...).toList() |
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class POOCompletoTest {
@Test
void polimorfismo() {
var cancion = new Audio(
new Metadatos("Bohemian Rhapsody", 354), 320);
var pelicula = new Video(
new Metadatos("Inception", 8880), "4K", FormatoVideo.MKV);
assertTrue(cancion.obtenerInfo().contains("Bohemian"));
assertTrue(pelicula.obtenerInfo().contains("Inception"));
}
@Test
void composicion() {
var lista = new ListaReproduccion("Favoritos");
lista.agregar(new Audio(new Metadatos("Test", 100), 128));
assertEquals(1, lista.size());
}
}
💡 Memoria y rendimiento
- Cada objeto en Java tiene overhead de cabecera (~12-16 bytes con compressed OOPs)
- La herencia añade una vtable por clase (tamaño fijo O(n) donde n es número de métodos virtuales)
- Records son
finaly pueden optimizarse mejor que clases mutables - Los patrones de diseño pueden añadir overhead si se usan en exceso
✅ Buenas prácticas
- ✅ Favorece composición sobre herencia
- ⚠️ No abuses de patrones de diseño — YAGNI
- ❌ No implementes un patrón si una función/lambda es suficiente
🏗️ Metodología
- Estrategias de negocio: Strategy (lambdas)
- Eventos: Observer (PropertyChangeListener o eventos personalizados)
- Creación compleja: Builder (Lombok @Builder)
- Singleton: enum con una instancia (Effective Java Item 3)
- Adaptación: Adapter (wrapping de APIs externas)
🔗 Para saber más
Polimorfismo en detalle
¿Qué es?
Java tiene tres formas de polimorfismo: por subtipo (herencia de interfaces/clases), paramétrico (genéricos) y ad-hoc (overloading de métodos). Es como un control remoto universal: el mismo botón “play” funciona en un DVD, una consola o un servicio de streaming, pero cada uno hace algo diferente internamente.
Sintaxis básica
Subtipo (herencia):
interface Sonido { String sonido(); }
class Perro implements Sonido { public String sonido() { return "Guau"; } }
class Gato implements Sonido { public String sonido() { return "Miau"; } }
void hacerSonido(Sonido s) { System.out.println(s.sonido()); }
Paramétrico (genéricos):
interface Forma<T extends Number> { double area(); }
class Circulo implements Forma<Double> {
private final double radio;
public Circulo(double radio) { this.radio = radio; }
public double area() { return Math.PI * radio * radio; }
}
void mostrarArea(Forma<?> f) {
System.out.printf("Área: %.2f%n", f.area());
}
Ad-hoc (overloading):
class Impresora {
void imprimir(String s) { System.out.println("Texto: " + s); }
void imprimir(int n) { System.out.println("Número: " + n); }
}
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PolimorfismoTest {
@Test
void subtipo() {
Sonido s = new Perro();
assertEquals("Guau", s.sonido());
}
@Test
void parametrico() {
var c = new Circulo(5);
assertTrue(c.area() > 0);
}
}
💡 Memoria y rendimiento
- Subtipo: la vtable se consulta en cada llamada — overhead mínimo
- Paramétrico: type erasure — no hay overhead en runtime
- Ad-hoc: se resuelve en compilación — sin overhead
- El JIT puede desvirtualizar e inlinear métodos cuando detecta monomorfismo
✅ Buenas prácticas
- ✅ Prefiere interfaces sobre clases abstractas para definir contratos
- ⚠️ El overloading puede ser confuso con tipos primitivos y wrappers
- ❌ No uses instanceof para simular polimorfismo — el polimorfismo es mejor
🏗️ Metodología
- Subtipo: comportamiento que varía por tipo de objeto
- Paramétrico: algoritmos independientes del tipo de dato
- Ad-hoc: operaciones que difieren por tipo de argumento
🔗 Para saber más
Interacción con contenido multimedia
javax.imageio (Imágenes)
¿Qué es?
La API estándar de Java para leer y escribir imágenes. Soporta PNG, JPEG, BMP, WBMP, GIF. Para otros formatos (WebP, TIFF, SVG) usa extensiones como TwelveMonkeys o Apache Commons Imaging.
Sintaxis básica
import javax.imageio.*;
import java.awt.image.BufferedImage;
import java.io.File;
// Leer imagen
BufferedImage img = ImageIO.read(new File("foto.jpg"));
System.out.println("Dimensiones: " + img.getWidth() + "x" + img.getHeight());
// Escribir imagen
ImageIO.write(img, "PNG", new File("salida.png"));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
import javax.imageio.*;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
class MultimediaTest {
@Test
void crearYGuardarImagen(@TempDir Path tempDir) throws Exception {
var img = new BufferedImage(100, 50, BufferedImage.TYPE_INT_RGB);
var archivo = tempDir.resolve("test.png").toFile();
assertTrue(ImageIO.write(img, "PNG", archivo));
assertTrue(archivo.exists());
}
}
💡 Memoria y rendimiento
- Una imagen 4K (3840x2160) en RGB ocupa ~25 MB en RAM
ImageIOestá escrito en Java puro — más lento que librerías nativas- Para procesamiento intensivo, usa
BufferedImagecon acceso directo a píxeles (getRGB/setRGB)
✅ Buenas prácticas
- ✅ Usa TwelveMonkeys para soporte WebP y TIFF
- ⚠️ No proceses imágenes muy grandes (>10000px) sin redimensionar
- ❌ No guardes en JPEG sin especificar calidad
🏗️ Metodología
- Procesamiento simple de imágenes: ImageIO + BufferedImage
- Renderizado 2D: Java2D (Graphics2D)
- Visión artificial: OpenCV Java bindings
- Redimensionado rápido: Thumbnailator
🔗 Para saber más
Bases de datos
JDBC
¿Qué es?
JDBC (Java Database Connectivity) es la API estándar para conectar Java con bases de datos relacionales. Proporciona una interfaz uniforme para diferentes motores de BD mediante drivers.
Sintaxis básica
try (var conn = DriverManager.getConnection(url, user, pass);
var stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE IF NOT EXISTS usuarios (" +
"id SERIAL PRIMARY KEY, nombre VARCHAR(100))");
try (var ps = conn.prepareStatement(
"INSERT INTO usuarios (nombre) VALUES (?)")) {
ps.setString(1, "Ana");
ps.executeUpdate();
}
try (var rs = stmt.executeQuery("SELECT * FROM usuarios")) {
while (rs.next())
System.out.println(rs.getString("nombre"));
}
}
Spring Data JPA
@Entity
public class Usuario {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
@Column(unique = true) private String email;
private int edad;
}
public interface UsuarioRepo extends JpaRepository<Usuario, Long> {
Optional<Usuario> findByEmail(String email);
}
@Service
public class UsuarioService {
private final UsuarioRepo repo;
public UsuarioService(UsuarioRepo repo) { this.repo = repo; }
// ...
}
| Librería | Tipo | Para qué |
|---|---|---|
| Hibernate | ORM | Estándar JPA. Cache L2, lazy loading |
| Spring Data JPA | Abstracción JPA | Repositorios automáticos |
| jOOQ | Query builder type-safe | SQL fluido, genera código desde BD |
| MyBatis | SQL Mapper | SQL explícito en XML |
| H2 | BD embebida | En memoria, perfecta para tests |
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.sql.*;
class DATest {
@Test
void h2EnMemoria() throws Exception {
try (var conn = DriverManager.getConnection("jdbc:h2:mem:test")) {
conn.createStatement().execute("CREATE TABLE t (id INT)");
conn.createStatement().execute("INSERT INTO t VALUES (1)");
var rs = conn.createStatement().executeQuery("SELECT COUNT(*) FROM t");
rs.next();
assertEquals(1, rs.getInt(1));
}
}
}
Fuente: Spring Data JPA docs, Hibernate docs
WebSockets y mensajería
Spring WebSocket
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/mensajes")
public Mensaje enviar(Mensaje msg) {
return new Mensaje("Sistema", "Echo: " + msg.texto());
}
}
Cuándo usar cada uno:
- Jakarta WebSocket: estándar, ligero
- Spring WebSocket: ecosistema Spring, STOMP, seguridad
- Netty: máximo rendimiento (Kafka, Elasticsearch lo usan)
Threading, concurrencia y paralelismo
ExecutorService y Virtual Threads
¿Qué es?
Java ofrece múltiples modelos de concurrencia: Threads (clásico, pesado), ExecutorService (pool de hilos, Java 5+), CompletableFuture (composición asíncrona, Java 8+), y Virtual Threads (hilos ligeros, JDK 21 LTS). Es como tener diferentes tamaños de herramientas para diferentes trabajos.
Sintaxis básica
// ExecutorService
ExecutorService fixed = Executors.newFixedThreadPool(4);
Future<String> future = fixed.submit(() -> {
Thread.sleep(1000);
return "Resultado";
});
String resultado = future.get(2, TimeUnit.SECONDS);
fixed.shutdown();
// CompletableFuture
CompletableFuture.supplyAsync(() -> obtenerPrecio("AAPL"))
.thenApply(p -> p * 0.85)
.thenAccept(System.out::println)
.exceptionally(ex -> { log.error("Error", ex); return null; });
// Virtual Threads (JDK 21 LTS)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // No bloquea: VT se parkea
return "ok";
});
}
}
Thread vt = Thread.ofVirtual()
.name("mi-vt")
.start(() -> System.out.println("Hola desde VT"));
🧪 Cómo probarlo
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.*;
class ConcurrenciaTest {
@Test
void executorService() throws Exception {
var executor = Executors.newSingleThreadExecutor();
var future = executor.submit(() -> 42);
assertEquals(42, future.get());
executor.shutdown();
}
@Test
void completableFuture() throws Exception {
var result = CompletableFuture.supplyAsync(() -> 21)
.thenApply(n -> n * 2)
.get();
assertEquals(42, result);
}
}
💡 Memoria y rendimiento
- Threads del SO: ~1MB por hilo — no escalan más allá de miles
- Virtual Threads: ~100 bytes por hilo — puedes crear millones
- Virtual Threads son ideales para I/O-bound (servidores web, BD, APIs)
- Para CPU-bound, sigue siendo mejor usar
parallelStream()oForkJoinPool
✅ Buenas prácticas
- ✅ Usa Virtual Threads para tareas I/O-bound (JDK 21+)
- ⚠️ No uses
synchronizedcon Virtual Threads — usaReentrantLock - ❌ No combines Virtual Threads con pools de hilos fijos
- ✅ Prefiere
CompletableFuturepara composición de tareas asíncronas
🏗️ Metodología
- I/O-bound (HTTP, BD, APIs): Virtual Threads (JDK 21+) o CompletableFuture
- CPU-bound (cálculos): parallelStream, ForkJoinPool
- Tareas programadas: ScheduledExecutorService
- Productor-consumidor: BlockingQueue + ExecutorService
🔗 Para saber más
Testing y calidad
Frameworks y herramientas
| Framework | Propósito | Async | CLI |
|---|---|---|---|
| JUnit 5 | Unitario e integración | Sí | mvn test / gradle test |
| Mockito | Mocks y stubs | Sí | Incluido en test runtime |
| AssertJ | Aserciones fluidas | No | Librería de aserciones |
| JaCoCo | Cobertura de código | No | mvn jacoco:report |
Cómo testear cada concepto
Variables y tipos → aserciones simples
@Test
void tipos() {
assertInstanceOf(Integer.class, 42);
assertInstanceOf(String.class, "hola");
}
Control de flujo → boundary testing con parametrización
@ParameterizedTest
@CsvSource({"0,cero", "1,positivo", "-1,negativo"})
void testSigno(int input, String expected) {
String result;
if (input > 0) result = "positivo";
else if (input < 0) result = "negativo";
else result = "cero";
assertEquals(expected, result);
}
POO → mocks con Mockito
@Test
void testConMock() {
var repo = mock(UsuarioRepo.class);
when(repo.findById(1L)).thenReturn(Optional.of(new Usuario()));
var service = new UsuarioService(repo);
assertNotNull(service.obtener(1L));
}
File I/O → temp directories con JUnit 5
@TempDir
Path tempDir;
@Test
void testArchivo() throws Exception {
var f = tempDir.resolve("test.txt");
Files.writeString(f, "contenido");
assertEquals("contenido", Files.readString(f));
}
Parametrización avanzada
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testPositivos(int n) {
assertTrue(n > 0);
}
@ParameterizedTest
@MethodSource("proveedorArgumentos")
void testConMetodo(String input, int expected) {
assertEquals(expected, input.length());
}
static Stream<Arguments> proveedorArgumentos() {
return Stream.of(
Arguments.of("hola", 4),
Arguments.of("mundo", 5)
);
}
Cobertura y CI
Configuración JaCoCo en pom.xml:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
mvn verify # tests + cobertura
mvn jacoco:report # generar reporte HTML
# Abrir target/site/jacoco/index.html
GitHub Actions:
- name: Run tests with coverage
run: mvn verify
- name: Upload coverage
uses: codecov/codecov-action@v3
Umbrales recomendados: 70-80% de cobertura de instrucciones para proyectos en producción.
Conceptos avanzados para Java
Project Loom (Virtual Threads)
¿Qué es?
Project Loom introduce virtual threads (hilos ligeros) gestionados por la JVM, no por el SO. Permite crear millones de hilos sin agotar la memoria. Es como tener un organizador de tareas que puede gestionar miles de pendientes con solo unos pocos empleados.
Sintaxis básica
// Crear un virtual thread
Thread vt = Thread.ofVirtual()
.name("mi-vt")
.start(() -> System.out.println("Hola desde VT"));
// ExecutorService con virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // Se parkea, no bloquea
return "ok";
});
}
}
Spring Boot 3.2+: spring.threads.virtual.enabled=true
GraalVM Native Image
Compila Java a binario nativo con native-image. Arranque instantáneo (~0.1s), menos memoria (~10 MB).
JMH (Java Microbenchmark Harness)
Framework oficial para benchmarks precisos en Java. Incluye warm-up, evita optimizaciones del JIT.
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void benchmarkSuma() {
int suma = 0;
for (int i = 0; i < 100; i++) suma += i;
}
mvn clean install
java -jar target/benchmarks.jar
🔗 JMH docs
JMM (Java Memory Model)
Define cómo los hilos interactúan a través de la memoria y qué garantías de visibilidad ofrece. Palabras clave: volatile (visibilidad), synchronized (atomicidad + visibilidad), happens-before.
Proyecto final integrador: Analizador de logs HTTP
Este proyecto combina file I/O, algoritmos, POO, testing y red. Ejecutable de principio a fin.
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;
public record LogEntry(
String ip, String timestamp, String metodo,
String ruta, int status, int tamano
) {}
class LogParser {
private static final Pattern PATRON = Pattern.compile(
"(?<ip>[\\d.]+).*\\[(?<ts>[^\\]]+)\\].*" +
"\"(?<metodo>\\w+)\\s+(?<ruta>\\S+).*\".*" +
"(?<status>\\d+)\\s+(?<tamano>\\d+)"
);
private final Path archivo;
public LogParser(Path archivo) { this.archivo = archivo; }
public List<LogEntry> parsear() throws IOException {
return Files.lines(archivo)
.map(PATRON::matcher)
.filter(Matcher::matches)
.map(m -> new LogEntry(
m.group("ip"), m.group("ts"), m.group("metodo"),
m.group("ruta"), Integer.parseInt(m.group("status")),
Integer.parseInt(m.group("tamano"))
))
.toList();
}
}
class LogAnalyzer {
private final List<LogEntry> entradas;
public LogAnalyzer(List<LogEntry> entradas) {
this.entradas = entradas;
}
public List<String> topIps(int n) {
return entradas.stream()
.collect(Collectors.groupingBy(LogEntry::ip, Collectors.counting()))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(n)
.map(Map.Entry::getKey)
.toList();
}
public Map<String, Long> erroresPorRuta() {
return entradas.stream()
.filter(e -> e.status() >= 400)
.collect(Collectors.groupingBy(
LogEntry::ruta, Collectors.counting()));
}
public Map<String, Object> resumen() {
return Map.of(
"total", entradas.size(),
"ips_unicas", entradas.stream().map(LogEntry::ip).distinct().count(),
"top_ips", topIps(5),
"errores", erroresPorRuta()
);
}
}
Tests
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.*;
class LogAnalyzerTest {
@TempDir
Path tempDir;
@Test
void parsearLog() throws Exception {
var log = "192.168.1.1 - - [10/Oct/2023:13:55:36] " +
"\"GET /index.html HTTP/1.1\" 200 2326";
var f = tempDir.resolve("test.log");
Files.writeString(f, log);
var parser = new LogParser(f);
var entradas = parser.parsear();
assertEquals(1, entradas.size());
assertEquals("192.168.1.1", entradas.get(0).ip());
}
@Test
void analizarLogs() {
var entradas = List.of(
new LogEntry("1.1.1.1", "ts", "GET", "/", 200, 100),
new LogEntry("1.1.1.1", "ts", "GET", "/", 404, 50),
new LogEntry("2.2.2.2", "ts", "POST", "/api", 200, 200)
);
var analyzer = new LogAnalyzer(entradas);
var resumen = analyzer.resumen();
assertEquals(3, resumen.get("total"));
assertEquals(2L, resumen.get("ips_unicas"));
}
}
Despliegue a producción
Build para producción
# Maven
mvn package -DskipTests
# Gradle
gradle build -x test
Docker multi-stage
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
CMD ["java", "-jar", "app.jar"]
Hosting
- Fly.io — despliegue con
flyctl launch - Railway — conecta tu repo y despliega automáticamente
- Heroku — despliegue con Git o Container Registry
CI/CD (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: mvn package -DskipTests
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Hacks y tips de productividad
1. var para inferencia local (JDK 10+)
var lista = new ArrayList<String>();
var mapa = new HashMap<String, List<Integer>>();
2. Try-with-resources con múltiples recursos
try (var reader = Files.newBufferedReader(path);
var writer = Files.newBufferedWriter(output)) {
reader.lines().map(String::toUpperCase).forEach(line -> {
writer.write(line);
writer.newLine();
});
}
3. Text Blocks (JDK 15+)
String json = """
{ "nombre": "Ana", "email": "ana@email.com" }
""";
4. instanceof con pattern matching (JDK 16+)
if (obj instanceof String s && s.length() > 5) {
System.out.println("String largo: " + s);
}
5. Switch pattern matching (JDK 21+)
return switch (obj) {
case Integer i -> "Entero: " + i;
case String s when s.length() > 10 -> "String largo";
case String s -> "String: " + s;
case null -> "nulo";
default -> "desconocido";
};
6. Records para DTOs
public record UsuarioResponse(Long id, String nombre, String email) {}
7. Optional.orElseThrow() con excepción personalizada
var tarea = repo.findById(id)
.orElseThrow(() -> new TareaNoEncontradaException(id));
8. @Builder de Lombok
@Data
@Builder
public class Config {
private String host;
private int puerto;
@Builder.Default private boolean ssl = true;
}
9. SLF4J + Logback con Lombok @Slf4j
log.debug("Procesando usuario: {}", id);
log.info("Operación completada");
log.error("Error procesando pedido {}", pedidoId, exception);
10. jlink para runtime mínimo
jlink --module-path $JAVA_HOME/jmods:target/classes \
--add-modules com.miempresa.app \
--output mi-app --compress 2 --strip-debug
./mi-app/bin/java com.miempresa.app.Main
Canales y recursos en español
YouTube
- Programación ATS — Curso de Java desde cero, POO intuitiva
- Píldoras Informáticas — Java SE + Java EE, Swing, JDBC
- CodigoFacilito — Java básico a avanzado, Spring Boot, Hibernate
- MoureDev — Java, Kotlin, Spring Boot, proyectos prácticos
- OpenWebinars — Cursos profesionales de Java, Spring, arquitectura
- TodoCode — Java, POO, Spring Boot, testing con JUnit
Comunidades
- r/Java (Reddit) — Noticias, discusiones, proyectos
- Stack Overflow en español — Etiqueta
java - Discord Java en español — Canales de ayuda y mentoría
Blogs y newsletters
- Baeldung (baeldung.com) — Tutoriales Java #1
- Java Code Geeks (javacodegeeks.com)
- InfoQ Java (infoq.com/java)
Repositorios
⚠️ Errores comunes
Si algo falla, lo más probable es que esté en esta tabla:
| Error | Causa | Solución |
|---|---|---|
Cannot find symbol | Variable, método o clase no declarado, o falta un import | Añade el import correspondiente; revisa la ortografía y el scope |
Exception in thread "main" java.lang.NullPointerException | Accedes a un objeto null (método, campo, array) | Usa Optional, valida con != null o usa el operador ?. (Java 16+) |
'javac' is not recognized as an internal or external command | JDK no instalado o JAVA_HOME / PATH no configurado | Instala JDK y configura JAVA_HOME y PATH en variables de entorno |
ClassNotFoundException | La clase indicada no está en el classpath al ejecutar | Verifica que el JAR está en classpath; en Maven/Gradle ejecuta clean package |
package X does not exist (error de compilación) | Falta una dependencia en pom.xml o build.gradle, o el import es incorrecto | Añade la dependencia en el gestor y ejecuta mvn install / gradle build |
error: incompatible types: X cannot be converted to Y | Asignas un tipo a otro sin casting o conversión explícita | Añade el cast adecuado o usa métodos de conversión (Integer.parseInt, etc.) |
Error: Could not find or load main class X | El classpath no incluye la clase principal o el nombre del paquete es incorrecto | Verifica el FQN de la clase y que bin/ o target/classes está en classpath |
🔗 Guías relacionadas
- Kotlin de 0 a 100 — la alternativa moderna sobre la JVM: Guía de Kotlin: De 0 a 100
- C# de 0 a 100 — otro lenguaje de tipado fuerte con POO completa: Guía de C# y .NET: De 0 a 100
- Docker — containeriza tus apps Java: Docker: instalación y primeros pasos
Referencias y documentación oficial
- Docs oficiales: docs.oracle.com/en/java
- OpenJDK: openjdk.org — Código fuente, JEPs
- Dev.java: dev.java — Tutorial oficial moderno
- Spring docs: docs.spring.io
- Maven: maven.apache.org
- Baeldung: baeldung.com
Fuentes de esta guía
- Oracle — Java Documentation oficial
- Dev.java — Learn Java
- OpenJDK — JEP Index
- Baeldung — Tutoriales
- Spring Boot — Documentación
- Spring Cloud — Documentación
- Hibernate — Documentación
- Effective Java (Joshua Bloch)
- Java Concurrency in Practice (Brian Goetz)
- Modern Java in Action (Raoul-Gabriel Urma)
- r/Java
- Awesome Java (GitHub)
🏆 Retos Relacionados
Pon a prueba lo aprendido con estos desafíos:
