Java compila tu código a bytecode, que ejecuta la máquina virtual de Java (JVM) en cualquier sistema. Es uno de los lenguajes más usados en empresas y bancos, en servidores y en el ecosistema de Android. Es más estricto que Python o JavaScript, y eso ayuda a detectar errores pronto.
Aplicaciones de empresa y servidores
Aplicaciones Android
Procesamiento de datos a gran escala
Para empezar: Instala un JDK (por ejemplo, desde dev.java). Los fragmentos de las lecciones puedes probarlos en jshell, la consola interactiva que viene con el JDK, o dentro del main del primer ejemplo.
Nivel 1 Básico
1
Lección 1 · Básico
Tu primer programa
En Java todo el código vive dentro de una clase, y el programa empieza en el método main. System.out.println() escribe una línea en la consola.
El compilador javac traduce el código a bytecode y la JVM lo ejecuta. Desde Java 11 puedes hacer las dos cosas de una vez con java Main.java.
Ejemplo práctico
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println("¡Hola, mundo!");
System.out.println(2 + 3); // 5
}
}
// En la terminal: java Main.java
Ejercicio
Completa la instrucción que escribe una línea.
System.out.("Hola");
Ver la solución
Respuesta:println. System.out.println() escribe el texto y salta de línea.
2
Lección 2 · Básico
Variables y tipos
Java es de tipado estático: cada variable tiene un tipo fijo. Los más comunes son int (enteros), double (decimales), boolean y String (texto, con comillas dobles).
Con var el compilador deduce el tipo a partir del valor, pero sigue siendo fijo: no puedes guardar después un texto en una variable que empezó siendo un número.
Ejemplo práctico
int unidades = 3;
double precio = 19.99;
boolean enStock = true;
String producto = "Auriculares";
var total = unidades * precio; // double
System.out.println(producto + ": " + total);
Ejercicio
¿Qué tipo usarías para guardar 19,99?
Ver la solución
Respuesta: double. int solo guarda enteros; para decimales se usa double.
3
Lección 3 · Básico
Condiciones y switch
if y else funcionan como en otros lenguajes, con la condición entre paréntesis y el bloque entre llaves. Los textos se comparan con .equals(), no con ==.
La expresión switch con flechas (Java 14 o posterior) elige un valor según varios casos de forma compacta.
Ejemplo práctico
String dia = "sábado";
if (dia.equals("sábado") || dia.equals("domingo")) {
System.out.println("Fin de semana");
}
int horas = switch (dia) {
case "sábado", "domingo" -> 0;
case "viernes" -> 6;
default -> 8;
};
Ejercicio
¿Cómo se comprueba si dos textos son iguales en Java?
Ver la solución
Respuesta: a.equals(b). == compara si son el mismo objeto en memoria; .equals() compara su contenido.
Encuentra los errores
Este fragmento debería mostrar «Fin de semana» y no mostrar «Aprobado» (la nota es 4). Tiene 2 errores clásicos de Java.
Los errores
Línea 4:== compara si son el mismo objeto en memoria, no si el texto es igual; aquí son dos objetos distintos y da false. Los textos se comparan con equals. Corrección: if (dia.equals("sábado")) {
Línea 7: El ; después de la condición cierra el if sin hacer nada, y el bloque de llaves se ejecuta siempre, apruebes o no. Corrección: if (nota >= 5) {
Usa siempre equals (o equalsIgnoreCase) para comparar textos, y no pongas ; entre el if (…) y su llave.
Ver la solución
Línea 4:== compara si son el mismo objeto en memoria, no si el texto es igual; aquí son dos objetos distintos y da false. Los textos se comparan con equals. Corrección: if (dia.equals("sábado")) {
Línea 7: El ; después de la condición cierra el if sin hacer nada, y el bloque de llaves se ejecuta siempre, apruebes o no. Corrección: if (nota >= 5) {
4
Lección 4 · Básico
Arrays y bucles
Un array tiene un tamaño fijo y un único tipo: int[] notas = {7, 8, 9};. Las posiciones empiezan en 0 y .length dice cuántas hay.
El for clásico usa un contador; el for mejorado (for (int n : notas)) recorre cada elemento directamente.
Ejemplo práctico
int[] notas = {7, 8, 9};
int suma = 0;
for (int nota : notas) {
suma += nota;
}
System.out.println("Media: " + (double) suma / notas.length); // 8.0
for (int i = 3; i > 0; i--) {
System.out.println(i);
}
Ejercicio
En int[] notas = {7, 8, 9}, ¿qué vale notas[1]?
Ver la solución
Respuesta: 8. La posición 0 es la primera (7), así que la 1 es la segunda: 8.
Escribe el código
Escribe el método static int maximo(int[] numeros): devuelve el número más alto del array (que siempre tiene al menos un elemento). Recórrelo con un bucle, sin usar Arrays.stream ni Collections.max.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
static int maximo(int[] numeros) {
int mayor = numeros[0];
for (int n : numeros) {
if (n > mayor) {
mayor = n;
}
}
return mayor;
}
Se toma el primer elemento como candidato y se recorre el array: cada vez que aparece un número mayor, pasa a ser el nuevo candidato.
Ver la solución
static int maximo(int[] numeros) {
int mayor = numeros[0];
for (int n : numeros) {
if (n > mayor) {
mayor = n;
}
}
return mayor;
}
Se toma el primer elemento como candidato y se recorre el array: cada vez que aparece un número mayor, pasa a ser el nuevo candidato.
Nivel 2 Intermedio
5
Lección 5 · Intermedio
Métodos
Un método es una función dentro de una clase. Se declara con el tipo que devuelve (void si no devuelve nada), su nombre y sus parámetros con tipo.
Los métodos static pertenecen a la clase y se llaman sin crear un objeto, como main.
Respuesta:void. void indica que el método no devuelve ningún valor.
Escribe el código
Escribe static boolean esPrimo(int n): devuelve true si n es primo (solo es divisible por 1 y por sí mismo) y false si no. Los números menores que 2 no son primos.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
static boolean esPrimo(int n) {
if (n < 2) {
return false;
}
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
Basta con probar divisores hasta la raíz cuadrada de n (i * i <= n): si n tuviera un divisor mayor, también tendría otro menor que ya se habría encontrado.
Ver la solución
static boolean esPrimo(int n) {
if (n < 2) {
return false;
}
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
Basta con probar divisores hasta la raíz cuadrada de n (i * i <= n): si n tuviera un divisor mayor, también tendría otro menor que ya se habría encontrado.
6
Lección 6 · Intermedio
Clases, objetos y records
Una clase define los datos (atributos) y el comportamiento (métodos) de un tipo de objeto. El constructor, con el mismo nombre que la clase, prepara cada objeto creado con new.
Para clases que solo guardan datos, un record (Java 16 o posterior) genera solo el constructor, los métodos de acceso, equals y toString.
Ejemplo práctico
class Cuenta {
private double saldo;
Cuenta(double saldoInicial) {
saldo = saldoInicial;
}
void ingresar(double cantidad) { saldo += cantidad; }
double getSaldo() { return saldo; }
}
record Producto(String nombre, double precio) {}
Cuenta c = new Cuenta(100);
c.ingresar(50);
System.out.println(c.getSaldo()); // 150.0
System.out.println(new Producto("Ratón", 12.5)); // Producto[nombre=Ratón, precio=12.5]
Ejercicio
¿Qué palabra crea un objeto nuevo a partir de una clase?
Ver la solución
Respuesta: new. new Clase(...) reserva el objeto y ejecuta su constructor.
7
Lección 7 · Intermedio
Colecciones: List y Map
Las colecciones crecen según lo necesites. ArrayList es una lista ordenada y HashMap guarda pares clave–valor. Se indica el tipo de los elementos entre < y >.
List.of() y Map.of() crean colecciones que no se pueden modificar, útiles para datos fijos.
Ejemplo práctico
import java.util.*;
List<String> tareas = new ArrayList<>();
tareas.add("Comprar pan");
tareas.add("Llamar al médico");
Map<String, Integer> stock = new HashMap<>();
stock.put("teclado", 10);
stock.put("raton", 0);
System.out.println(tareas.size()); // 2
System.out.println(stock.getOrDefault("webcam", 0)); // 0
Ejercicio
Necesitas buscar el precio de un producto por su nombre. ¿Qué colección usas?
Ver la solución
Respuesta: HashMap. Un Map asocia cada clave (el nombre) con su valor (el precio).
Encuentra los errores
Este fragmento debería mostrar «3 0» (tres nombres y la edad 0 para Luis, que no está en el mapa), pero se detiene con un error. Tiene 2 fallos.
Los errores
Línea 1:List.of crea una lista inmutable: add lanzaría UnsupportedOperationException. Para añadir elementos, cópiala en un ArrayList. Corrección: List<String> nombres = new ArrayList<>(List.of("Ana", "Luis"));
Línea 6:get devuelve null si la clave no existe, y al guardarlo en un int salta un NullPointerException. getOrDefault da un valor por defecto. Corrección: int edadLuis = edades.getOrDefault("Luis", 0);
List.of, Set.of y Map.of son muy cómodos para datos fijos, pero no se pueden modificar.
Ver la solución
Línea 1:List.of crea una lista inmutable: add lanzaría UnsupportedOperationException. Para añadir elementos, cópiala en un ArrayList. Corrección: List<String> nombres = new ArrayList<>(List.of("Ana", "Luis"));
Línea 6:get devuelve null si la clave no existe, y al guardarlo en un int salta un NullPointerException. getOrDefault da un valor por defecto. Corrección: int edadLuis = edades.getOrDefault("Luis", 0);
Nivel 3 Avanzado
8
Lección 8 · Avanzado
Herencia e interfaces
Una clase puede heredar de otra con extends y reutilizar su código, pero solo de una. Una interfaz define métodos que las clases se comprometen a tener con implements, y una clase puede implementar varias.
Así puedes tratar objetos distintos de la misma forma: una lista de Figura puede contener círculos y rectángulos.
Ejemplo práctico
interface Figura {
double area();
}
record Circulo(double radio) implements Figura {
public double area() { return Math.PI * radio * radio; }
}
record Rectangulo(double ancho, double alto) implements Figura {
public double area() { return ancho * alto; }
}
List<Figura> figuras = List.of(new Circulo(1), new Rectangulo(2, 3));
for (Figura f : figuras) System.out.println(f.area());
Ejercicio
Completa para que la clase cumpla la interfaz Figura.
class Cuadrado Figura { … }
Ver la solución
Respuesta:implements. Una clase implementa una interfaz con implements; hereda de otra clase con extends.
9
Lección 9 · Avanzado
Excepciones
Cuando algo falla, Java lanza una excepción. Con try y catch la capturas; finally se ejecuta siempre. Algunas excepciones (las comprobadas, como IOException) el compilador te obliga a tratarlas o declararlas.
try-with-resources cierra automáticamente archivos y conexiones al terminar.
Ejemplo práctico
import java.nio.file.*;
import java.io.IOException;
try (var lineas = Files.lines(Path.of("datos.txt"))) {
System.out.println(lineas.count() + " líneas");
} catch (IOException e) {
System.out.println("No se pudo leer: " + e.getMessage());
}
try {
int n = Integer.parseInt("doce");
} catch (NumberFormatException e) {
System.out.println("No es un número");
}
Ejercicio
¿Qué bloque se ejecuta siempre, haya error o no?
Ver la solución
Respuesta: finally. finally se ejecuta al salir del try, tanto si hubo excepción como si no.
10
Lección 10 · Avanzado
Lambdas y streams
Una lambda es una función corta que se pasa como dato: p -> p.precio() > 10. Los streams encadenan operaciones sobre colecciones: filtrar, transformar, ordenar y reunir el resultado.
El código queda más declarativo: dices qué quieres obtener, no cómo recorrer la lista paso a paso.
Ejemplo práctico
record Producto(String nombre, double precio) {}
var productos = List.of(
new Producto("Teclado", 25.9),
new Producto("Ratón", 12.5),
new Producto("Cable", 4.99));
List<String> caros = productos.stream()
.filter(p -> p.precio() > 10)
.map(Producto::nombre)
.sorted()
.toList(); // Java 16+
System.out.println(caros); // [Ratón, Teclado]
Ejercicio
¿Qué operación de un stream se queda solo con algunos elementos?
Ver la solución
Respuesta: filter. filter conserva los elementos para los que la lambda devuelve true.
11
Lección 11 · Avanzado
Texto a fondo: StringBuilder, format y bloques de texto
Los String no se pueden modificar: cada + en un bucle crea un texto nuevo. Para construir un texto a trozos, StringBuilder va añadiendo con append() sobre el mismo objeto y, al final, toString() da el resultado.
String.format (o el método formatted) mete valores en una plantilla: %s para texto, %d para enteros y %.2f para decimales con dos cifras. Los bloques de texto entre """ escriben textos de varias líneas tal cual.
Ejemplo práctico
public class Textos {
public static void main(String[] args) {
StringBuilder lista = new StringBuilder();
for (int i = 1; i <= 3; i++) {
lista.append("Tarea ").append(i).append("\n");
}
System.out.print(lista.toString());
String linea = String.format("%s pagó %.2f € en %d compras", "Ana", 45.5, 3);
System.out.println(linea); // Ana pagó 45,50 € en 3 compras (con idioma español)
String json = """
{
"nombre": "Ana",
"activo": true
}
""";
System.out.print(json);
}
}
Escribe el código
Escribe static String repetir(String texto, int veces) que devuelva el texto repetido veces veces seguidas, construyéndolo con un StringBuilder y un bucle.
Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.
Una solución posible
static String repetir(String texto, int veces) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < veces; i++) {
sb.append(texto);
}
return sb.toString();
}
append va añadiendo sobre el mismo objeto, y toString() convierte el resultado en un String. (Java también tiene texto.repeat(veces), que hace justo esto.)
Ver la solución
static String repetir(String texto, int veces) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < veces; i++) {
sb.append(texto);
}
return sb.toString();
}
append va añadiendo sobre el mismo objeto, y toString() convierte el resultado en un String. (Java también tiene texto.repeat(veces), que hace justo esto.)
12
Lección 12 · Avanzado
Optional: cuando un valor puede faltar
Un método que busca algo puede no encontrarlo. Devolver null obliga a quien lo llama a acordarse de comprobarlo, y si se le olvida aparece un NullPointerException. Optional<T> hace explícito que el valor puede no estar.
Con orElse(valor) das un valor por defecto, con map transformas el contenido si existe y con ifPresent ejecutas algo solo cuando hay valor. Evita llamar a get() sin comprobar antes.
Ejemplo práctico
import java.util.*;
public class Buscador {
static Optional<String> buscarCorreo(Map<String, String> agenda, String nombre) {
return Optional.ofNullable(agenda.get(nombre));
}
public static void main(String[] args) {
Map<String, String> agenda = Map.of("Ana", "ana@ejemplo.es");
System.out.println(buscarCorreo(agenda, "Ana").orElse("sin correo")); // ana@ejemplo.es
System.out.println(buscarCorreo(agenda, "Luis").orElse("sin correo")); // sin correo
buscarCorreo(agenda, "Ana")
.map(String::toUpperCase)
.ifPresent(c -> System.out.println("Enviar a " + c)); // Enviar a ANA@EJEMPLO.ES
}
}
Ejercicio
buscarCorreo(agenda, "Luis") devuelve un Optional vacío. ¿Qué muestra .orElse("sin correo")?
Ver la solución
Respuesta: «sin correo». orElse devuelve el contenido si existe y, si el Optional está vacío, el valor que le pasas.
13
Lección 13 · Avanzado
enum con datos y switch moderno
Un enum es un tipo con un conjunto cerrado de valores, como los planes de una suscripción. Cada valor puede llevar datos propios (se pasan a un constructor) y métodos, así el precio vive junto al plan y no en un if repartido por el código.
El switch con flechas (case X ->) devuelve un valor y no necesita break. Si lo usas con un enum, el compilador comprueba que cubres todos los casos y te avisa si añades un valor nuevo y te olvidas de tratarlo.
Ejemplo práctico
public class Suscripciones {
enum Plan {
BASICO(5), PRO(12), EMPRESA(30);
private final int precio;
Plan(int precio) {
this.precio = precio;
}
int precioAnual() {
return precio * 12;
}
}
static String descripcion(Plan plan) {
return switch (plan) {
case BASICO -> "Para empezar";
case PRO -> "Para profesionales";
case EMPRESA -> "Para equipos";
};
}
public static void main(String[] args) {
for (Plan p : Plan.values()) {
System.out.println(p + ": " + descripcion(p) + ", " + p.precioAnual() + " €/año");
}
// BASICO: Para empezar, 60 €/año
// PRO: Para profesionales, 144 €/año
// EMPRESA: Para equipos, 360 €/año
}
}
Encuentra los errores
Este código no compila: tiene 2 errores. Uno impide guardar el precio de cada plan y otro deja un caso sin tratar en el switch.
Los errores
Línea 4:precio = precio asigna el parámetro a sí mismo y el campo final se queda sin valor (error de compilación). Hay que distinguirlos con this. Corrección: Plan(int precio) { this.precio = precio; }
Línea 10: Un switch que devuelve valor tiene que cubrir todos los valores del enum: falta PRO. Corrección: case BASICO -> "Para empezar"; case PRO -> "Para profesionales";
Que el compilador detecte el caso que falta es una de las grandes ventajas de combinar enum y switch.
Ver la solución
Línea 4:precio = precio asigna el parámetro a sí mismo y el campo final se queda sin valor (error de compilación). Hay que distinguirlos con this. Corrección: Plan(int precio) { this.precio = precio; }
Línea 10: Un switch que devuelve valor tiene que cubrir todos los valores del enum: falta PRO. Corrección: case BASICO -> "Para empezar"; case PRO -> "Para profesionales";
14
Lección 14 · Avanzado
Fechas y horas con java.time
El paquete java.time representa fechas (LocalDate), horas (LocalTime), ambas juntas (LocalDateTime) y duraciones. Sus objetos no se modifican: plusDays(30) devuelve una fecha nueva y deja la original igual.
ChronoUnit.DAYS.between(a, b) cuenta los días entre dos fechas y DateTimeFormatter.ofPattern("dd/MM/yyyy") convierte una fecha en texto con el formato que quieras (o lee un texto con ese formato con parse).
Respuesta:of. LocalDate.of(año, mes, día) crea una fecha; los meses van del 1 al 12.
15
Lección 15 · Avanzado
Pruebas automáticas con JUnit
JUnit es la biblioteca de pruebas más usada en Java. Cada prueba es un método con la anotación @Test que llama a tu código y comprueba el resultado con assertEquals(esperado, obtenido), assertTrue o assertThrows.
Las herramientas de construcción como Maven y Gradle ejecutan todas las pruebas con una orden (mvn test, gradle test) y te dicen cuáles fallan. Si las ejecutas antes de cada cambio importante, detectas los errores al momento.
Ejemplo práctico
// src/main/java/Calculadora.java
public class Calculadora {
static int dividir(int a, int b) {
if (b == 0) throw new IllegalArgumentException("No se puede dividir entre 0");
return a / b;
}
}
// src/test/java/CalculadoraTest.java (con JUnit 5 añadido al proyecto)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculadoraTest {
@Test
void divideNumerosExactos() {
assertEquals(4, Calculadora.dividir(12, 3));
}
@Test
void entreCeroLanzaError() {
assertThrows(IllegalArgumentException.class, () -> Calculadora.dividir(5, 0));
}
}
Ejercicio
En assertEquals(4, Calculadora.dividir(12, 3)), ¿qué es el 4?
Ver la solución
Respuesta: El valor que esperas obtener. El primer argumento es el valor esperado y el segundo, el que devuelve tu código. Si no coinciden, la prueba falla y JUnit muestra ambos.
¿Has terminado? Sigue practicando
Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.
* Porcentaje de personas que dijeron haber usado Java en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.