Java tiene treinta años y sigue siendo el lenguaje con el que está escrito buena parte del software que mueve dinero, pacientes y aviones. También arrastra fama de ceremonioso, y buena parte de esa fama ya no es cierta: la versión actual deja escribir un programa completo en tres líneas. Este curso empieza por ahí y llega hasta un proyecto con pruebas.
La ruta: diez lecciones
Están pensadas para leerse en orden: cada una usa lo de la anterior. Las dos primeras están aquí mismo, en esta página; y las otras ocho tienen la suya.
- 1Instalación y primer programaEl JDK, ejecutar un archivo sin compilar a mano y por qué «hola mundo» ya no ocupa cinco líneas.En esta página
- 2Valores, referencias y nullLa distinción que explica la mitad de los errores de un principiante, y el fallo más famoso del lenguaje.En esta página
3Tipos, variables y métodosPrimitivos y objetos, var, conversiones, parámetros y sobrecarga.Leer lección →
4Control de flujo y arraysif, for, while y el switch moderno, que ya es una expresión.Leer lección →
5Clases, objetos y recordsCampos, constructores, encapsulación, y cómo un record quita cien líneas de ruido.Leer lección →
6Herencia, interfaces y selladoQué heredar y qué no, interfaces con métodos por defecto y clases selladas.Leer lección →
7Colecciones y genéricosList, Map y Set, los genéricos, y por qué equals y hashCode van juntos.Leer lección →
8Excepciones y OptionalComprobadas y no comprobadas, try-with-resources y cómo dejar de devolver null.Leer lección →
9Streams y lambdasFunciones como valores, map, filter y collect, y cuándo no usarlos.Leer lección →
10Un proyecto de verdadPaquetes, pruebas con JUnit y empaquetar un jar que se pueda ejecutar.Leer lección →
Por qué Java se siente distinto
Si vienes de Python o de JavaScript, lo primero que choca es que el compilador va antes que tú. No puedes ejecutar algo que no encaje: un tipo que no corresponde, una variable sin valor, un método que no existe. Lo que en otros lenguajes se descubre en la línea 300 a las dos de la madrugada, aquí no llega a arrancar.
Lo segundo es que todo vive dentro de una clase. No hay funciones sueltas flotando en un archivo. Durante años eso obligaba a escribir un andamiaje considerable para imprimir una línea, y es el origen de la fama del lenguaje. Eso ha cambiado, y es por donde vamos a empezar.
Y lo tercero, que no se ve pero lo explica todo: Java no se compila a instrucciones del procesador, sino a bytecode para una máquina virtual. Por eso el mismo archivo compilado corre igual en Linux, en Windows y en un servidor que no has visto nunca. Y por eso el primer arranque es lento y el programa se va volviendo más rápido según corre: la máquina virtual va observando qué partes se usan más y las traduce a código nativo sobre la marcha.
Lección 1: el primer programa
Necesitas un JDK. En Debian o Ubuntu, sudo apt install openjdk-25-jdk; en Fedora, sudo dnf install java-25-openjdk-devel; y si prefieres no depender de la distribución, Adoptium publica binarios para todo. Comprueba que está:
java -version
salidaopenjdk version "25.0.4.1" 2026-08-18 LTS OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS) OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
Ese 25 importa. Es la versión con soporte a largo plazo actual, y lo que viene a continuación necesita como mínimo la 25.
Hola mundo, en 2026
Esto es un programa de Java completo. Guárdalo como Hola.java:
void main() {
IO.println("Hola desde un archivo compacto");
}
java Hola.java
salidaHola desde un archivo compacto
Tres líneas, sin class, sin public static, sin String[] args y sin compilar a mano. Dos cosas lo hacen posible, y conviene saber cuál es cuál.
La primera es que desde hace años java acepta directamente un archivo fuente: lo compila en memoria y lo ejecuta, sin dejar un .class por ahí. Para aprender y para guiones sueltos es justo lo que hace falta.
La segunda es nueva: los archivos fuente compactos, que llegaron definitivamente en Java 25. Permiten omitir la clase y escribir un main que no es estático, y traen IO.println e IO.readln a mano, sin tener que recordar System.out.
javac Hola.java y miras lo que sale con javap, aparece:
javap -p Compacto.classCompiled from "Compacto.java"
final class Compacto {
Compacto();
void main();
}
Una clase normal, con el nombre del archivo, y un main de instancia en vez de estático. No hay magia: hay azúcar sintáctico.
La forma que vas a ver en todas partes
Lo de arriba es lo nuevo. Esto es lo que hay escrito en los últimos treinta años, y lo que te vas a encontrar en cualquier proyecto:
public class Clasico {
public static void main(String[] args) {
System.out.println("Hola desde el Java de siempre");
}
}
salidaHola desde el Java de siempre
Hay que saber leerlo, así que vale la pena desarmarlo pieza a pieza:
| Trozo | Qué dice |
|---|---|
public class Clasico |
Una clase visible desde fuera. El archivo tiene que llamarse igual: Clasico.java. |
static |
Pertenece a la clase, no a un objeto. Hace falta porque al arrancar todavía no existe ningún objeto. |
void |
No devuelve nada. |
String[] args |
Los argumentos de la línea de órdenes. Casi nunca se usan, y había que escribirlos igual. |
System.out.println |
La salida estándar. IO.println es un atajo para lo mismo. |
Leer algo del teclado
void main() {
String nombre = IO.readln("¿Cómo te llamas? ");
IO.println("Hola, " + nombre + ".");
}
salida¿Cómo te llamas? Ana Hola, Ana.
jshell, que viene con el JDK: escribes una expresión, pulsas Enter y te dice el resultado. Es el mejor sitio para resolver una duda de dos minutos.Lección 2: valores, referencias y null
Esta es la idea que, si se entiende el primer día, ahorra meses de desconcierto. En Java hay dos clases de cosas, y se comportan de forma distinta al copiarlas.
Los primitivos —int, double, boolean, char y tres más— guardan el valor directamente. Todo lo demás es un objeto, y la variable no guarda el objeto: guarda una referencia a él. Copiar la variable copia la referencia, no el objeto.
// un primitivo: la variable ES el valor
int a = 10;
int b = a;
b = 99;
IO.println("a = " + a + ", b = " + b);
// un objeto: la variable es una referencia AL valor
int[] x = {1, 2, 3};
int[] y = x;
y[0] = 99;
IO.println("x[0] = " + x[0] + ", y[0] = " + y[0]);
IO.println("¿son el mismo objeto? " + (x == y));
salidaa = 10, b = 99 x[0] = 99, y[0] = 99 ¿son el mismo objeto? true
Con el int, cambiar b no toca a a. Con el array, cambiar y[0] cambia también x[0], porque nunca hubo dos arrays: hay uno y dos nombres apuntándolo.
Ahora bien, no todos los objetos se dejan modificar:
String s = "hola";
String t = s;
t = t.toUpperCase();
IO.println("s = " + s + ", t = " + t);
salidas = hola, t = HOLA
Aquí s no cambia, y no es una excepción a la regla: es que toUpperCase() no modifica la cadena, devuelve una nueva. Los String en Java son inmutables. La variable t pasó a apuntar a otro objeto; el primero sigue intacto.
texto.trim() o texto.replace(...) y esperar que texto cambie. No cambia. Hay que recoger lo que devuelven: texto = texto.trim();.null, y el mensaje que ya no es inútil
Una referencia puede no apuntar a nada. Eso es null, y usarla revienta:
String texto = null;
IO.println("longitud: " + texto.length());
salidaException in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "texto" is null at Nulo.main(Nulo.java:3)
Durante veinte años ese mensaje decía solo «NullPointerException» y la línea, y te tocaba adivinar cuál de las cinco cosas de la línea era nula. Desde hace unas cuantas versiones dice qué fue nulo y qué intentabas hacer con ello.
javac Nulo.java a secas, el mensaje dice because "<local1>" is null; con javac -g Nulo.java, dice because "texto" is null. Las herramientas de construcción suelen activarlo, pero si alguna vez te sale un <local1>, ya sabes por qué.Y una trampa que no es de Java, es de los decimales
var cuantos = 3; var precio = 19.90; IO.println(cuantos + " x " + precio + " = " + (cuantos * precio));
salida3 x 19.9 = 59.699999999999996
No es un fallo del lenguaje: double es coma flotante binaria y 19,90 no se puede representar exacto, igual que 1/3 no se puede escribir con decimales finitos. Para dinero se usa BigDecimal, y para mostrar un resultado, String.format("%.2f", total). Lo veremos en la lección 3.
Fíjate también en el var: no convierte a Java en un lenguaje dinámico. El compilador deduce el tipo una vez, al declarar, y a partir de ahí es tan fijo como si lo hubieras escrito.
El compilador como red de seguridad
Conviene acostumbrarse pronto a leer sus mensajes, porque son buenos. Dos que vas a ver mucho:
int cuantos = "tres";
javacNoCompila.java:2: error: incompatible types: String cannot be converted to int
int cuantos = "tres";
^
1 error
int n; IO.println(n);
javacSinInit.java:3: error: variable n might not have been initialized
IO.println(n);
^
1 error
El segundo es más interesante de lo que parece. Java no te deja leer una variable local a la que nunca asignaste nada: no hay «valor basura» ni cero implícito. Es el mismo tipo de red que ponen Rust o Go, y es la razón de que un programa que compila en Java ya tenga descartada una familia entera de errores.
Cómo está hecho este curso
Todo el código que aparece aquí se compiló y se ejecutó con Java 25.0.4.1 LTS en un contenedor limpio antes de publicarlo, y las salidas están copiadas de esa ejecución, no escritas de memoria. Cuando algo dice «esto no compila», es porque se intentó compilar y se copió el error literal: los mensajes de javac de arriba son los de verdad.
Las lecciones son cortas a propósito. La idea es que se puedan leer en un rato y que el código se pueda copiar, pegar y ejecutar sin montar un proyecto.
¿Buscas un ejemplo concreto en vez de una lección? En Ejemplos de Java hay dos docenas de recetas sueltas: archivos, XML, Excel, JDBC, expresiones regulares. Y si quieres comparar cómo resuelven lo mismo otros lenguajes, están el curso de Rust y el curso de Go.