Guía de Java: De 0 a 100

Guía de Java: De 0 a 100

Desde la primera línea de código hasta producción: instalación, ecosistema, frameworks, proyectos reales y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:66 MINUTOS DE LECTURA

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

  1. Descarga el instalador Eclipse Temurin JDK 21 LTS (MSI) desde adoptium.net
  2. Ejecuta el MSI y marca “Set JAVA_HOME” durante la instalación — esto es crítico
  3. Verifica: abre cmd o PowerShell y ejecuta java --version
  4. Alternativa vía winget: winget install EclipseAdoptium.Temurin.21.JDK
  5. 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 @Test y assertEquals

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
  • enum con 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 R
  • Predicate<T> — prueba una condición (devuelve boolean)
  • Consumer<T> — consume un valor sin devolver resultado
  • Supplier<T> — provee un valor sin tomar argumentos
  • UnaryOperator<T> — Function donde T y R son el mismo tipo
  • BinaryOperator<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:

ParadigmaSoportado desdeUso principal
ImperativoJava 1.0Bloques básicos, bucles, condicionales
POOJava 1.0Modelado de dominio, arquitectura
FuncionalJava 8Procesamiento de datos, pipelines
ProcedimentalJava 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)
  • var se resuelve en compilación — no tiene impacto en runtime
  • instanceof es 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 var para tipos que no son obvios en el contexto (var result = getData() donde no se ve el tipo)

🏗️ Metodología

  • Usa var para tipos obvios en el lado derecho (var list = new ArrayList<String>())
  • Usa Integer/Long wrappers 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.

🔗 Oracle — Autoboxing

Sintaxis básica

PrimitivoTamañoWrapperRangoDefaultCache
byte8 bitsByte-128 a 1270-128 a 127
short16 bitsShort-32.768 a 32.7670-128 a 127
int32 bitsInteger-2³¹ a 2³¹-10-128 a 127
long64 bitsLong-2⁶³ a 2⁶³-10L-128 a 127
float32 bitsFloat±3.4E-380.0fNo
double64 bitsDouble±1.7E-3080.0dNo
boolean~1 bitBooleantrue/falsefalsetrue/false
char16 bitsCharacter0 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 para long/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) da true, pero con 128 da false
  • El autoboxing en bucles grandes es costoso: cada suma += i crea un nuevo Integer
  • Comparator.comparingInt() evita autoboxing vs Comparator.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 Integer para contadores en bucles — usa int

🏗️ 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+: IntList de 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.

🔗 Oracle — Strings

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
  • StringBuilder es 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 StringBuilder para 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

  • var es 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 var cuando el tipo es obvio por el inicializador
  • ⚠️ No uses var para parámetros de métodos, campos de clase o tipos de retorno
  • ❌ No uses var si oscurece la legibilidad (var result = complexMethod())

🏗️ Metodología

  • IDE-friendly: en IntelliJ, var se expande al tipo real con Ctrl+Click
  • Usa var para 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.

🔗 Oracle — Records

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 final con campos private 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 @Data para 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).

🔗 Oracle — Enum Types

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 indexados
  • EnumMap y EnumSet son 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 switch sobre ordinal() — usa el enum directamente

🏗️ Metodología

  • Constantes simples: enum sin campos
  • Constantes con comportamiento: enum con métodos abstractos implementados por cada valor
  • Singleton seguro: enum con 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í”.

🔗 Oracle — Optional

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

  • Optional es 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”
  • null para 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/else es O(1) por condición — evaluación lazy (cortocircuito)
  • switch tradicional usa tablas de salto (lookupswitch/tableswitch) — O(1)
  • switch expression 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 break en 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.

🔗 Oracle — Loops

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

  • for clásico sobre array: tan rápido como C (la JVM optimiza los bounds checks)
  • for-each sobre colecciones: usa Iterator internamente — ligero overhead
  • forEach con 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-each sobre for clá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: for clásico
  • Transformaciones de datos: Stream API (más declarativo)
  • Bucles infinitos con condición de salida: while (true) con break

🔗 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

🔗 Oracle — Exceptions

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 try sin excepción tiene overhead mínimo (la JVM optimiza)
  • No uses excepciones para control de flujo normal
  • try-with-resources genera bytecode más complejo que un try/finally manual, pero es más seguro

✅ Buenas prácticas

  • ✅ Sé específico con los tipos de excepción que capturas
  • ⚠️ No captures Exception gené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.

🔗 OpenJDK — Project Jigsaw

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)
  • jlink puede 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.

🔗 Oracle — File I/O (NIO.2)

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 grandes
  • FileChannel con MappedByteBuffer ofrece el mejor rendimiento para archivos muy grandes
  • WatchService usa eventos del SO (inotify en Linux) — overhead mínimo
  • Las operaciones de NIO.2 son generalmente más rápidas que java.io por 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_8 explí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: WatchService para 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.

🔗 Wikipedia — Timsort

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) promedio
  • Arrays.parallelSort(): divide el array, ordena en paralelo y fusiona
  • sorted() 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() o list.sort()
  • Streams: .sorted(Comparator.comparing(...))
  • Arrays primitivos: Arrays.sort() o Arrays.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

🔗 Oracle — JVM docs

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.

🔗 Oracle — GC Tuning

Sintaxis básica

java -XX:+UseZGC -jar app.jar
java -XX:+UseShenandoahGC -jar app.jar
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
RecolectorJDKDescripciónIdeal para
G1 GCDefault JDK 9+Divide heap en regiones (~1-32 MB), prioriza las más sucias. Pausas predeciblesUso general
ZGCJDK 15+ (prod)Pausas < 1ms, heaps hasta 16 TB. Coloreado de punterosLatencia ultrabaja
ShenandoahJDK 21+ (stable)Compactación concurrente, pausas ultrabajasApps sensibles a latencia
Serial GCSiempreUn solo hilo para todo el GC. Pausas largas pero mínimo overheadApps pequeñas, monohilo
Parallel GCJDK 8 defaultMúltiples hilos para Young y Old GCThroughput 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 -Xms y -Xmx iguales 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.

🔗 Oracle — OOP Concepts

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 (instanceof con 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.

🔗 Oracle — Generics Tutorial

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 T se reemplaza por Object (o el bound)
  • No puedes hacer new T() ni instanceof 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 ? extends y ? super son conceptos de compilación — sin impacto en runtime

✅ Buenas prácticas

  • ✅ PECS: Producer Extends, Consumer Super
  • ⚠️ No uses raw types (List sin 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.

🔗 Oracle — Streams

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 que collect(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+) sobre collect(Collectors.toList())
  • ⚠️ No uses parallelStream() sin medir — el overhead de paralelización puede empeorar el rendimiento
  • ❌ No uses Stream para bucles simples — for-each es 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).

🔗 Oracle — Exceptions

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 Exception gené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:

ConceptoCómo aparece
Clase abstractaContenido con abstract obtenerInfo()
Herencia simpleAudio extends Contenido, Video extends Contenido
InterfaceExportable con método default
Herencia + InterfacePodcast extends Audio implements Exportable
Encapsulaciónprivate int reproducciones, getters públicos
PolimorfismoobtenerInfo() distinto según clase concreta
ComposiciónListaReproduccion contiene List<Contenido>
RecordMetadatos con validación en compact constructor
EnumFormatoVideo con valores constantes
Streams APIitems.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 final y 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.

🔗 Wikipedia — Polimorfismo

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.

🔗 Oracle — Image I/O

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
  • ImageIO está escrito en Java puro — más lento que librerías nativas
  • Para procesamiento intensivo, usa BufferedImage con 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.

🔗 Oracle — JDBC

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íaTipoPara qué
HibernateORMEstándar JPA. Cache L2, lazy loading
Spring Data JPAAbstracción JPARepositorios automáticos
jOOQQuery builder type-safeSQL fluido, genera código desde BD
MyBatisSQL MapperSQL explícito en XML
H2BD embebidaEn 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.

🔗 Oracle — Concurrency

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() o ForkJoinPool

✅ Buenas prácticas

  • ✅ Usa Virtual Threads para tareas I/O-bound (JDK 21+)
  • ⚠️ No uses synchronized con Virtual Threads — usa ReentrantLock
  • ❌ No combines Virtual Threads con pools de hilos fijos
  • ✅ Prefiere CompletableFuture para 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

FrameworkPropósitoAsyncCLI
JUnit 5Unitario e integraciónSímvn test / gradle test
MockitoMocks y stubsSíIncluido en test runtime
AssertJAserciones fluidasNoLibrería de aserciones
JaCoCoCobertura de códigoNomvn 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.

🔗 OpenJDK — Project Loom

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).

🔗 GraalVM docs

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.

🔗 JSR 133 — JMM


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

Repositorios


⚠️ Errores comunes

Si algo falla, lo más probable es que esté en esta tabla:

ErrorCausaSolución
Cannot find symbolVariable, método o clase no declarado, o falta un importAñade el import correspondiente; revisa la ortografía y el scope
Exception in thread "main" java.lang.NullPointerExceptionAccedes 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 commandJDK no instalado o JAVA_HOME / PATH no configuradoInstala JDK y configura JAVA_HOME y PATH en variables de entorno
ClassNotFoundExceptionLa clase indicada no está en el classpath al ejecutarVerifica 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 incorrectoAñade la dependencia en el gestor y ejecuta mvn install / gradle build
error: incompatible types: X cannot be converted to YAsignas un tipo a otro sin casting o conversión explícitaAñade el cast adecuado o usa métodos de conversión (Integer.parseInt, etc.)
Error: Could not find or load main class XEl classpath no incluye la clase principal o el nombre del paquete es incorrectoVerifica el FQN de la clase y que bin/ o target/classes está en classpath

🔗 Guías relacionadas

Referencias y documentación oficial

Fuentes de esta guía


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
COMENTARIOS:

📋 Contenido