Aprende Java

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.

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.

La clase sigue estando ahí, la escribe el compilador por ti. Si compilas ese archivo con 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.
Para probar cosas sueltas sin crear archivos está 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.

De aquí sale el error de principiante más repetido: llamar a 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.

Un detalle que casi nadie sabe. Para que el mensaje diga el nombre de la variable, la clase tiene que estar compilada con información de depuración. Comprobado: con 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.