Skip to content

UD4 - Estructuras de Datos: Organizando la Información

Introducción: De una Pila de Papeles a una Biblioteca Digital

Imagina que eres el desarrollador de una nueva app de música, llamémosla "DevTune". Al principio, con 10 usuarios, podrías guardar sus playlists favoritas en variables sueltas. Pero, ¿qué pasa cuando tienes 10 millones de usuarios? ¿Crearías 10 millones de variables? ¡Imposible! Tu código sería un caos inmanejable, como una habitación llena de papeles hasta el techo. Necesitas un sistema para organizar esa información.

En la unidad anterior, UD3 - Programación modular, aprendimos a crear funciones, que son como "recetas" para realizar tareas. Pero una buena receta necesita ingredientes bien organizados. Las estructuras de datos son precisamente eso: los sistemas de estanterías, archivadores y catálogos que nos permiten guardar, encontrar y gestionar nuestros "ingredientes" (los datos) de manera eficiente.

En este tema, pasaremos de tener datos sueltos a construir nuestra propia biblioteca digital. Empezaremos con las estanterías más básicas (Arrays) y avanzaremos hasta sistemas de catálogo inteligentes (Colecciones como Listas, Conjuntos y Mapas) que nos permitirán manejar cantidades masivas de información sin volvernos locos. ¡Bienvenido a la arquitectura de la información!

Conceptos Fundamentales

Antes de construir, necesitamos conocer nuestras herramientas. Estos son los pilares sobre los que se asienta toda la gestión de datos en programación.

Definición: Estructura de Datos

Es una forma especializada de organizar, procesar, recuperar y almacenar datos en una computadora. Piensa en ella como un contenedor diseñado para un tipo específico de información y optimizado para ciertas operaciones. No es lo mismo una caja de zapatos para guardar fotos (acceso lento y desordenado) que un álbum de fotos (acceso rápido y ordenado).

Definición: Inmutabilidad vs. Mutabilidad

Un objeto inmutable es como una fotografía impresa: una vez creada, no se puede cambiar. Si quieres una versión diferente, tienes que tomar una foto nueva. Un objeto mutable es como un documento de texto: puedes editarlo, borrar partes y añadir contenido en cualquier momento sin necesidad de crear un archivo nuevo. Esta diferencia es CRUCIAL para el rendimiento y la seguridad.

Definición: Colección (Collection)

Es un objeto que agrupa múltiples elementos en una sola unidad. Las colecciones se utilizan para almacenar, recuperar, manipular y comunicar datos agregados. Son los "muebles" de nuestra biblioteca digital: estanterías (Listas), vitrinas de objetos únicos (Conjuntos), etc.



Arrays: Los Archivadores Fijos

El array es la estructura de datos más fundamental. Es como un archivador con un número fijo de cajones, cada uno etiquetado con un número (su índice). Es increíblemente rápido para acceder a un cajón si sabes su número, pero tiene una gran limitación: no puedes añadir más cajones una vez que lo has comprado.

Analogía Laboral: El Array

Un array es como una huevera. Tiene un número fijo de huecos (su tamaño), y cada hueco está diseñado para contener exactamente un huevo (el mismo tipo de dato). Puedes acceder instantáneamente al tercer huevo, pero no puedes meter un séptimo huevo en una huevera de seis.

Arrays Unidimensionales

Es la forma más simple de un array: una única fila de elementos.

Declaración e Inicialización

En Java, declarar e inicializar un array es un proceso de dos pasos: reservar el espacio y (opcionalmente) llenarlo.

// Google Java Style Guide

package com.example.estructuras;

public class DemoArrays {
    public static void main(String[] args) {
        // 1. Declaración y creación (reserva de memoria)
        // Crea un array para 5 números enteros.
        // Por defecto, se inicializan a 0 para tipos numéricos.
        int[] puntuaciones = new int[5];

        // 2. Asignación de valores por índice
        puntuaciones[0] = 95; // El primer elemento está en el índice 0
        puntuaciones[1] = 87;
        puntuaciones[2] = 100;
        puntuaciones[3] = 73;
        puntuaciones[4] = 91; // El último elemento está en tamaño - 1

        System.out.println("La puntuación del jugador 3 es: " + puntuaciones[2]); // Acceso directo

        // Declaración e inicialización en una sola línea (cuando sabes los valores)
        String[] nombresVideojuegos = {"The Witcher 3", "Cyberpunk 2077", "Elden Ring"};
        System.out.println("Mi juego favorito es: " + nombresVideojuegos[0]);
    }
}

Creación de array

Recorriendo Arrays

Para procesar todos los elementos de un array, usamos bucles. El bucle for clásico nos da control sobre el índice, mientras que el for-each (o bucle mejorado) es más simple si solo necesitamos el valor.

// Google Java Style Guide

package com.example.estructuras;

public class RecorrerArrays {
    public static void main(String[] args) {
        String[] nombresVideojuegos = {"The Witcher 3", "Cyberpunk 2077", "Elden Ring", "Baldur's Gate 3"};

        // Opción 1: Bucle for clásico
        // Útil cuando necesitas el índice
        System.out.println("--- Bucle for clásico ---");
        for (int i = 0; i < nombresVideojuegos.length; i++) {
            System.out.println("Juego en posición " + i + ": " + nombresVideojuegos[i]);
        }

        // Opción 2: Bucle for-each (mejorado)
        // Más limpio y seguro si solo necesitas el valor
        System.out.println("\n--- Bucle for-each ---");
        for (String juego : nombresVideojuegos) {
            System.out.println("Juego: " + juego);
        }
    }
}

Paso por Referencia vs. Valor

Este es uno de los conceptos que más confunden al principio, pero es vital.

¡Atención! Confusión a la vista

En Java, los tipos primitivos (int, double, boolean...) se pasan a las funciones por valor (se hace una copia). Sin embargo, los objetos (y los arrays SON objetos en Java) se pasan por referencia (se pasa la "dirección de memoria" del objeto original). ¡Esto significa que una función PUEDE modificar tu array original!

// Google Java Style Guide

package com.example.estructuras;

import java.util.Arrays;

public class PasoPorReferencia {

    public static void main(String[] args) {
        int[] inventario = {10, 20, 5}; // Pociones, Flechas, Bombas
        System.out.println("Inventario antes de la misión: " + Arrays.toString(inventario));

        // Enviamos el inventario a una misión
        gastarRecursosEnMision(inventario);

        // ¡El inventario original ha cambiado!
        System.out.println("Inventario después de la misión: " + Arrays.toString(inventario));
    }

    /**
     * Esta función modifica el array que recibe como parámetro.
     * @param recursos El array de inventario del jugador.
     */
    public static void gastarRecursosEnMision(int[] recursos) {
        System.out.println(" -> Usando recursos en la misión...");
        recursos[1] -= 5; // Gastamos 5 flechas
        recursos[2] -= 1; // Usamos 1 bomba
        // No necesitamos un 'return', el cambio afecta al array original.
    }
}
sequenceDiagram
    participant main as "main()"
    participant gastarRecursos as "gastarRecursosEnMision()"

    main->>main: int[] inventario = {10, 20, 5};
    Note right of main: 'inventario' apunta a la dirección de memoria 0x123

    main->>gastarRecursos: gastarRecursosEnMision(inventario);
    Note left of gastarRecursos: El parámetro 'recursos' ahora también<br>apunta a la misma dirección 0x123.<br>¡No es una copia del contenido!

    gastarRecursos->>gastarRecursos: recursos[1] -= 5;
    Note right of main: El array en 0x123 ahora es {10, 15, 5}

    gastarRecursos-->>main: La función termina.
    main->>main: System.out.println(inventario);
    Note right of main: Se imprime el array modificado {10, 15, 4}

La Clase java.util.Arrays

Java nos da una clase de utilidad fantástica para trabajar con arrays. No tienes que reinventar la rueda para tareas comunes.

// Google Java Style Guide

package com.example.estructuras;

import java.util.Arrays;

public class ClaseArraysUtil {
    public static void main(String[] args) {
        int[] puntuaciones = {95, 87, 100, 73, 91, 87};

        // 1. Convertir a String para imprimirlo fácilmente
        System.out.println("Puntuaciones: " + Arrays.toString(puntuaciones));

        // 2. Ordenar el array
        Arrays.sort(puntuaciones);
        System.out.println("Puntuaciones ordenadas: " + Arrays.toString(puntuaciones));

        // 3. Búsqueda binaria (¡el array DEBE estar ordenado!)
        // Devuelve el índice del elemento, o un número negativo si no lo encuentra.
        int indiceDe100 = Arrays.binarySearch(puntuaciones, 100);
        System.out.println("El 100 está en el índice: " + indiceDe100);

        // 4. Rellenar un array con un valor
        int[] inventarioLleno = new int[5];
        Arrays.fill(inventarioLleno, 99); // Llenar el inventario con 99 pociones
        System.out.println("Inventario lleno: " + Arrays.toString(inventarioLleno));

        // 5. Copiar un array
        int[] puntuacionesCopia = Arrays.copyOf(puntuaciones, puntuaciones.length);
        System.out.println("Copia del array: " + Arrays.toString(puntuacionesCopia));
    }
}

Arrays Multidimensionales

Si un array unidimensional es una fila, uno multidimensional es una tabla o una cuadrícula. El más común es el de dos dimensiones (2D), perfecto para representar tableros de juego, mapas, imágenes, etc.

Analogía Laboral

Un array 2D es como una hoja de cálculo de Excel. Tienes filas y columnas, y para acceder a una celda específica, necesitas ambas coordenadas (por ejemplo, C5).

Creación de matrices

Ejemplo de matriz

Ejemplo de matriz

// Google Java Style Guide

package com.example.estructuras;

public class DemoArrays2D {
    public static void main(String[] args) {
        // Un tablero de Tres en Raya (3x3)
        // ' ' -> Vacío, 'X' -> Jugador 1, 'O' -> Jugador 2
        char[][] tablero = new char[3][3];

        // Inicializar con espacios vacíos
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                tablero[i][j] = ' ';
            }
        }

        // Algunas jugadas
        tablero[0][0] = 'X'; // Fila 0, Columna 0
        tablero[1][1] = 'O'; // Fila 1, Columna 1
        tablero[0][1] = 'X'; // Fila 0, Columna 1

        // Imprimir el tablero
        imprimirTablero(tablero);
    }

    public static void imprimirTablero(char[][] tablero) {
        for (int i = 0; i < tablero.length; i++) {
            for (int j = 0; j < tablero[i].length; j++) {
                System.out.print("[" + tablero[i][j] + "]");
            }
            System.out.println(); // Nueva línea para la siguiente fila
        }
    }
}

Accediendo al índice de la matriz.

Índice de matriz

Ejemplo de índice

Obteniendo la longitud de una dimensión:

Longitud de matriz

Matrices irregulares:

Matrices irregulares

Reflexionemos sobre Arrays

  1. Si un array se pasa por referencia, ¿qué implicaciones de seguridad podría tener esto en una aplicación grande donde muchas partes del código acceden al mismo array de datos sensibles (por ejemplo, datos de usuario)?
  2. Arrays.binarySearch() es mucho más rápido que un bucle for para buscar, pero tiene un prerrequisito. ¿Cuál es y qué coste tiene? ¿En qué escenario no valdría la pena usarlo a pesar de su velocidad?
  3. Imagina que estás desarrollando un clon de "Buscaminas". ¿Cómo usarías un array 2D para representar el tablero? ¿Qué tipo de datos almacenarías en cada celda?

¡Claro que sí! Aquí tienes una versión parafraseada y extendida de ese concepto, con el código adaptado a la Google Java Style Guide y con un enfoque más didáctico, como lo explicaríamos en clase.


Bucles for-each: La Forma Elegante de Recorrer Colecciones

Aunque el bucle for tradicional con un índice es potente, a veces solo queremos recorrer todos los elementos de un array sin preocuparnos por las posiciones. Para esto, Java nos ofrece una sintaxis más limpia y menos propensa a errores: el bucle for-each o bucle mejorado.

Recorriendo Arrays de una Dimensión

Para arrays simples o unidimensionales, el for-each nos permite procesar cada elemento de manera directa.

// Google Java Style Guide

import java.util.Arrays;

public class RecorridoElegante1D {
    public static void main(String[] args) {
        // Ejemplo 1: Iterar sobre los elementos con un bucle for-each.
        int[] puntuacionesPartida = {1, 4, 5, 6, 7, 8, 3, 8};

        System.out.print("Puntuaciones obtenidas: ");
        for (int puntuacion : puntuacionesPartida) {
            // En cada iteración, 'puntuacion' toma el valor de un elemento del array.
            System.out.print(puntuacion); 
        }
        System.out.println(); // Imprime 14567838

        // Ejemplo 2: Imprimir el contenido completo de un array.
        // La forma más rápida y recomendada para depurar o visualizar un array.
        int[] inventarioItems = {1, 4, 5, 6};
        System.out.println("Contenido del inventario: " + Arrays.toString(inventarioItems)); // Imprime [1, 4, 5, 6]
    }
}

¡Usa Arrays.toString()!

Cuando quieras ver rápidamente qué contiene un array, Arrays.toString() es tu mejor amigo. Si intentas imprimir el array directamente (System.out.println(miArray)), Java te mostrará su referencia en memoria (algo como [I@7f31245a), ¡lo cual no es nada útil para depurar!

Recorriendo Arrays de dos Dimensiones

La elegancia del for-each se extiende a los arrays multidimensionales. Simplemente anidamos los bucles: el bucle exterior recorrerá las filas, y el interior, las columnas de cada fila.

// Google Java Style Guide

import java.util.Arrays;

public class RecorridoElegante2D {
    public static void main(String[] args) {
        // Ejemplo 1: Iterar sobre cada elemento de una matriz.
        int[][] matrizCoordenadas = {{1, 4, 5}, {6, 7, 8}, {3, 8}};

        System.out.print("Coordenadas procesadas: ");
        for (int[] fila : matrizCoordenadas) { // El primer bucle obtiene cada array-fila.
            for (int coordenada : fila) {      // El segundo bucle obtiene cada elemento de esa fila.
                System.out.print(coordenada);
            }
        }
        System.out.println(); // Imprime 14567867838

        // Ejemplo 2: Imprimir una matriz fila por fila.
        // Una técnica muy común para visualizar tableros o cuadrículas.
        int[][] tableroJuego = {{3, 8, 5}, {4, 1, 8, 4}, {5, 2}};
        System.out.println("Estado del tablero:");
        for (int[] fila : tableroJuego) {
            System.out.println(Arrays.toString(fila));
        }
        // Imprime:
        // [3, 8, 5]
        // [4, 1, 8, 4]
        // [5, 2]
    }
}



Argumentos de Línea de Comandos: El String[] args del main

Seguro que desde el primer "Hola Mundo" has visto esta cabecera: public static void main(String[] args). La hemos usado sin pensar mucho en ella, pero es la puerta de entrada para que nuestros programas interactúen con el mundo exterior desde el momento en que se ejecutan.

Anatomía del Método main

  • public: Es accesible desde cualquier lugar.
  • static: Se puede ejecutar sin necesidad de crear un objeto de la clase. Es el punto de partida.
  • void: No devuelve ningún valor una vez que termina.
  • String[] args: ¡Aquí está la clave! Es un array de Strings llamado args (de "argumentos"). Este array recoge cualquier texto que le pasemos a nuestro programa en la consola justo después del comando de ejecución.

Piensa en ello como darle instrucciones a tu programa antes de que empiece a trabajar.

¿Cómo Funciona?

Cuando ejecutas un programa Java desde la terminal, todo lo que escribes después del nombre de la clase (o del .jar) se divide por los espacios y se almacena en el array args.

graph TD
    subgraph "Terminal"
        A["java EjecutarPrograma.java Juan 1990"]
    end

    subgraph "Array 'args' en Memoria"
        B["args[0] = 'Juan'"]
        C["args[1] = '1990'"]
    end

    A --> B;
    A --> C;

Ejemplo: Un Saludo Personalizado

Veamos cómo usar estos argumentos para crear un saludo que se adapta a lo que el usuario escribe en la consola.

// Google Java Style Guide
package com.example.estructuras;

public class SaludoPersonalizado {

    public static void main(String[] args) {
        if (args.length > 0) {
            // Si el array 'args' tiene elementos, los usamos.
            System.out.print("¡Hola");
            for (String nombre : args) {
                System.out.print(" " + nombre);
            }
            System.out.println("! Bienvenido al sistema.");
        } else {
            // Si no se pasaron argumentos, mostramos un saludo genérico.
            System.out.println("¡Hola, mundo! Bienvenido al sistema.");
        }
    }
}

Ejecuciones en la terminal:

# Sin argumentos
$ java SaludoPersonalizado.java
¡Hola, mundo! Bienvenido al sistema.

# Con un argumento
$ java SaludoPersonalizado.java Ana
¡Hola Ana! Bienvenido al sistema.

# Con múltiples argumentos
$ java SaludoPersonalizado.java Ana Soto
¡Hola Ana Soto! Bienvenido al sistema.

Todo es un String

Recuerda que args es un String[]. Si pasas un número como java MiPrograma 3, el valor args[0] será el string "3", no el número entero 3. Si quieres usarlo para operaciones matemáticas, tendrás que convertirlo explícitamente usando Integer.parseInt(args[0]). ¡Esto es una fuente muy común de errores (NumberFormatException) si el usuario no introduce un número válido!



Varargs: Parámetros Flexibles

¿Y si quisieras crear una función que sume números, pero no sabes de antemano si el usuario querrá sumar dos, tres o veinte números? Aquí es donde entran los argumentos variables o varargs.

Definición: Varargs

Es una característica de Java que permite a un método aceptar un número variable de argumentos del mismo tipo. Es "azúcar sintáctico" (una forma más cómoda de escribir algo) que, por debajo, convierte los argumentos en un array.

La sintaxis es simple: se añaden tres puntos (...) después del tipo de dato del último parámetro del método.

Reglas de Oro de los Varargs: 1. Se convierte en un array: Dentro del método, la variable varargs se trata como un array normal. 2. Solo uno por método: Un método solo puede tener un parámetro varargs. 3. Siempre al final: El parámetro varargs debe ser el último en la lista de parámetros del método.

Ejemplo: Una Calculadora de Sumas Flexible

// Google Java Style Guide
package com.example.estructuras;

public class CalculadoraFlexible {

    public static void main(String... args) { // ¡El main también puede usar varargs!
        System.out.println("Suma 1: " + sumar()); // 0
        System.out.println("Suma 2: " + sumar(10, 20)); // 30
        System.out.println("Suma 3: " + sumar(5, 15, 25, 55)); // 100
    }

    /**
     * Suma una cantidad variable de números enteros.
     * @param numeros Los números a sumar.
     * @return La suma total.
     */
    public static int sumar(int... numeros) {
        int total = 0;
        // Dentro del método, 'numeros' es tratado como un int[]
        for (int numero : numeros) {
            total += numero;
        }
        return total;
    }
}



Copiando Arrays: No es lo que Parece

Aquí llegamos a uno de los conceptos más importantes y que más errores provoca al principio: la diferencia entre copiar una referencia y clonar un objeto.

Copia por Referencia: La Llave Compartida

Como vimos antes, los arrays son objetos. Cuando haces arrayB = arrayA;, no estás creando un nuevo array. Estás creando una segunda "etiqueta" o "puntero" que apunta exactamente al mismo objeto en memoria.

Analogía Laboral

Imagina que arrayA es un enlace a un documento de Google Docs. Si le envías ese enlace a un compañero (arrayB = arrayA), ambos estáis viendo y editando el mismo documento. Si tu compañero borra un párrafo, tú también verás que ha desaparecido.

// Google Java Style Guide
package com.example.estructuras;

import java.util.Arrays;

public class CopiaPorReferencia {
    public static void main(String[] args) {
        int[] inventarioOriginal = {10, 20, 30};
        int[] inventarioAsignado = inventarioOriginal; // No es una copia, es una referencia

        System.out.println("Original antes del cambio: " + Arrays.toString(inventarioOriginal));

        // Modificamos el segundo array
        inventarioAsignado[0] = 99;

        // ¡El original también ha cambiado!
        System.out.println("Original después del cambio: " + Arrays.toString(inventarioOriginal));
        System.out.println("Asignado después del cambio: " + Arrays.toString(inventarioAsignado));
    }
}
graph TD
    subgraph "Memoria"
        direction TB
        A["Array en 0x123: [10, 20, 30]"]
    end

    B["inventarioOriginal"] --> A
    C["inventarioAsignado"] --> A

    subgraph "Código"
        D["inventarioAsignado[0] = 99;"]
    end

    D --> E["Array en 0x123: [99, 20, 30]"]

    A -->|"Actualizado en memoria"| E

Clonado de Arrays: Creando una Copia Real

Para crear una copia independiente de un array, necesitamos clonarlo, es decir, crear un nuevo objeto en memoria con el mismo contenido.

1. El Método clone()

Es la forma más directa, pero a veces puede ser menos legible. Crea una copia superficial (suficiente para arrays de primitivos, pero ¡cuidado con arrays de objetos!).

2. System.arraycopy()

Un método de bajo nivel, muy rápido pero más verboso. Te da un control total sobre qué parte del array copiar y dónde.

// System.arraycopy(origen, posOrigen, destino, posDestino, longitud);

3. Arrays.copyOf() (Recomendado)

La forma más moderna, clara y flexible de clonar un array.

// Google Java Style Guide
package com.example.estructuras;

import java.util.Arrays;

public class ClonadoDeArrays {
    public static void main(String[] args) {
        int[] primos = {2, 3, 5, 7, 11};
        int[] copiaReferencia = primos; // Referencia, no copia

        // --- Creando una copia real e independiente ---
        int[] copiaClonada = primos.clone();

        // Modificamos las copias
        copiaReferencia[0] = 99; // Esto afectará al original
        copiaClonada[1] = 101;   // Esto NO afectará al original

        System.out.println("Array Original:    " + Arrays.toString(primos)); // [99, 3, 5, 7, 11]
        System.out.println("Copia Referencia:  " + Arrays.toString(copiaReferencia)); // [99, 3, 5, 7, 11]
        System.out.println("Copia Clonada:     " + Arrays.toString(copiaClonada)); // [2, 101, 5, 7, 11]
    }
}



Ordenación y Búsqueda: Eficiencia en Acción

Una de las razones principales para usar estructuras de datos es poder encontrar información rápidamente. Y para buscar rápido, primero hay que ordenar.

La Regla de Oro de la Búsqueda

Trabajar con datos ordenados es exponencialmente más eficiente que con datos desordenados. Es la diferencia entre encontrar un nombre en una agenda telefónica (ordenada) y encontrarlo en una pila de tarjetas de visita tiradas al azar.

Arrays.sort(): El Poder de la Ordenación

Aunque existen muchos algoritmos de ordenación (Burbuja, Inserción, Quicksort...), en la práctica casi siempre usaremos el método Arrays.sort(). Java utiliza un algoritmo de ordenación dual-pivot Quicksort altamente optimizado, que es extremadamente rápido para la mayoría de los casos.

// Google Java Style Guide
package com.example.estructuras;

import java.util.Arrays;

public class OrdenacionArrays {
    public static void main(String[] args) {
        int[] puntuaciones = {95, 87, 100, 73, 91, 87};
        System.out.println("Puntuaciones desordenadas: " + Arrays.toString(puntuaciones));

        Arrays.sort(puntuaciones);

        System.out.println("Puntuaciones ordenadas:    " + Arrays.toString(puntuaciones));
    }
}

Arrays.binarySearch(): Buscando a la Velocidad de la Luz

Una vez que un array está ordenado, podemos usar uno de los algoritmos de búsqueda más eficientes que existen: la búsqueda binaria.

Búsqueda Binaria: Divide y Vencerás

En lugar de buscar elemento por elemento (búsqueda lineal), la búsqueda binaria va al punto medio del array. 1. Si el elemento del medio es el que buscas, ¡listo! 2. Si es mayor, sabes que tu objetivo solo puede estar en la mitad inferior. 3. Si es menor, solo puede estar en la mitad superior. Repites este proceso, dividiendo el rango de búsqueda por la mitad en cada paso. Esto es increíblemente rápido.

// Google Java Style Guide
package com.example.estructuras;

import java.util.Arrays;

public class BusquedaBinaria {
    public static void main(String[] args) {
        String[] usuarios = {"ana", "juan", "luis", "maria", "pedro", "sara"};

        // ¡Importante! La búsqueda binaria REQUIERE que el array esté ordenado.
        // En este caso, ya lo está alfabéticamente.

        String usuarioABuscar = "maria";
        int indice = Arrays.binarySearch(usuarios, usuarioABuscar);

        if (indice >= 0) {
            System.out.println("'" + usuarioABuscar + "' encontrada en la posición: " + indice);
        } else {
            System.out.println("'" + usuarioABuscar + "' no ha sido encontrada.");
        }
    }
}
graph LR
    A["Array Ordenado: [ana, juan, luis, maria, pedro, sara]"] --> B{Buscar 'maria'};
    B --> C{"1. Mirar en el medio (índice 2): 'luis'"};
    C --> D{"'maria' > 'luis', descartar la mitad inferior"};
    D --> E["Nuevo rango: [maria, pedro, sara]"];
    E --> F{"2. Mirar en el medio del nuevo rango: 'pedro'"};
    F --> G{"'maria' < 'pedro', descartar la mitad superior"};
    G --> H["Nuevo rango: [maria]"];
    H --> I{"3. Mirar en el medio: 'maria'"};
    I --> J["¡Elemento encontrado en 3 pasos!"];

Reflexionemos sobre Copias y Ordenación

  1. Si clonas un array de objetos (por ejemplo, Articulo[]), ¿los objetos Articulo dentro del nuevo array son copias o son referencias a los objetos originales? ¿Qué implicaciones tiene esto? (Esto se conoce como copia superficial vs. profunda).
  2. Estás desarrollando una función que recibe un array de datos, necesita ordenarlo para una operación interna, pero no debe modificar el array original que te pasaron. ¿Cómo implementarías esta función de forma segura?
  3. Si Arrays.binarySearch() devuelve un número negativo (ej: -5), ¿qué información útil nos está dando ese número sobre dónde debería estar el elemento si existiera? (¡Investiga la documentación!).

Visualizando Algoritmos

Animación Algoanim
Animación algotithm-visualizer



Strings: Más que un Array de Caracteres

Aunque un String parece un simple texto, en Java es un objeto inmutable y muy potente, con una gran cantidad de métodos útiles. Tratarlo como un simple array de caracteres es quedarse muy corto.

La Inmutabilidad: El Superpoder Oculto

Inmutable = A prueba de cambios accidentales

Una vez que creas un String, su contenido no puede ser modificado. Cualquier método que parezca que lo modifica (toUpperCase, replace, etc.) en realidad devuelve un nuevo objeto String con el cambio.

¿Por qué es esto una ventaja?
* Seguridad: Si pasas un String a una función, tienes la garantía de que no lo cambiará.
* Rendimiento: El sistema puede reutilizar Strings idénticos en memoria (lo que se conoce como "String Pool").
* Concurrencia: Es seguro usar Strings en entornos multihilo sin miedo a que un hilo modifique el valor que otro está leyendo.

// Google Java Style Guide

package com.example.estructuras;

public class InmutabilidadString {
    public static void main(String[] args) {
        String saludo = "hola";
        System.out.println("Saludo original: " + saludo);

        saludo.toUpperCase(); // Esta línea NO cambia el string 'saludo'

        System.out.println("Saludo después de toUpperCase(): " + saludo); // Sigue siendo "hola"

        // Para capturar el cambio, debemos asignarlo a una variable (puede ser la misma)
        String saludoMayusculas = saludo.toUpperCase();
        System.out.println("Nuevo string en mayúsculas: " + saludoMayusculas);

        saludo = saludo.toUpperCase(); // Reasignamos la variable 'saludo' al nuevo objeto
        System.out.println("Saludo original reasignado: " + saludo);
    }
}

Métodos Esenciales de String

La clase String viene cargada de herramientas. Aquí tienes las más comunes:

Método Descripción Ejemplo de Uso
length() Devuelve el número de caracteres. "hola".length() devuelve 4
charAt(int index) Devuelve el carácter en una posición específica. "java".charAt(1) devuelve 'a'
substring(int begin, int end) Extrae una subcadena. "desarrollo".substring(2, 5) devuelve "sar"
indexOf(String str) Busca la primera ocurrencia de una subcadena. "hola mundo".indexOf("mun") devuelve 5
equals(Object anObject) Compara el contenido de dos strings (sensible a mayúsculas). "Java".equals("java") devuelve false
equalsIgnoreCase(...) Compara el contenido ignorando mayúsculas/minúsculas. "Java".equalsIgnoreCase("java") devuelve true
replace(char old, char new) Reemplaza todas las ocurrencias de un carácter. "hola".replace('o', 'a') devuelve "hala"
split(String regex) Divide el string en un array usando un delimitador. "1,2,3".split(",") devuelve {"1", "2", "3"}
trim() Elimina los espacios en blanco al principio y al final. " hola ".trim() devuelve "hola"

StringBuilder: El Gemelo Mutable

Si necesitas construir un String a base de muchas piezas (por ejemplo, en un bucle), usar el operador + es muy ineficiente porque crea un nuevo objeto String en cada iteración.

El Asesino Silencioso del Rendimiento

Concatenar strings en un bucle con + es una de las peores prácticas de rendimiento en Java. Es como si para añadir un ladrillo a una pared, ¡reconstruyeras la pared entera desde cero cada vez!

Para estos casos, usamos StringBuilder, que es mutable y mucho más eficiente.

// Google Java Style Guide

package com.example.estructuras;

public class DemoStringBuilder {
    public static void main(String[] args) {
        // MALA PRÁCTICA: Ineficiente
        String listaNombres = "";
        String[] nombres = {"Ana", "Juan", "Maria"};
        for (String nombre : nombres) {
            listaNombres += nombre + ", "; // Crea un nuevo objeto String en cada iteración
        }
        System.out.println("Mala práctica: " + listaNombres);

        // BUENA PRÁCTICA: Eficiente
        StringBuilder sb = new StringBuilder();
        for (String nombre : nombres) {
            sb.append(nombre).append(", "); // Modifica el objeto interno, muy rápido
        }
        // Al final, lo convertimos a un String inmutable
        String listaNombresEficiente = sb.toString();
        System.out.println("Buena práctica: " + listaNombresEficiente);
    }
}

Reflexionemos sobre Strings

  1. En una aplicación web, los parámetros de una URL (ej: ?user=admin&pass=123) se reciben como String. ¿Por qué la inmutabilidad de los String es una característica de seguridad importante en este contexto?
  2. Estás procesando un archivo CSV (valores separados por comas) muy grande, línea por línea. Cada línea es un String. ¿Qué método de la clase String sería fundamental para poder trabajar con los datos de cada columna?
  3. Imagina que tienes que generar un informe en formato JSON o XML que es muy largo y se construye a partir de cientos de piezas de datos de una base de datos. ¿Usarías String con el operador + o StringBuilder? Justifica tu respuesta.



Aplicación en el Mundo Real

Puede que los arrays y los strings parezcan básicos, pero son los cimientos sobre los que se construyen aplicaciones increíblemente complejas. No son solo para ejercicios de clase; son el pan de cada día de un desarrollador.

  • Desarrollo de Videojuegos y Gráficos: ¿Alguna vez has jugado a un juego de estrategia por turnos o un RPG con un mapa de casillas? Ese mapa es, en esencia, un array bidimensional (int[][] mapGrid). Cada celda contiene un código que representa el terreno: 0 para hierba, 1 para agua, 2 para una montaña. Cuando tu personaje se mueve, el juego simplemente está cambiando valores en las coordenadas de esa matriz. De igual manera, una imagen digital no es más que un array 2D de píxeles, donde cada elemento almacena un valor de color.

  • Herramientas de Línea de Comandos (CLI): ¿Usas git? Cuando escribes git commit -m "Mi primer commit", estás usando argumentos de línea de comandos. El programa git recibe un String[] args donde args[0] es "commit", args[1] es "-m", y args[2] es "Mi primer commit". Herramientas de automatización, scripts de servidor y muchas utilidades de desarrollo dependen de este mecanismo para funcionar sin una interfaz gráfica.

  • Procesamiento de Datos y Ficheros: Cuando una aplicación necesita procesar un fichero de configuración (.ini, .properties) o un archivo de datos como un CSV, es habitual leer el fichero línea por línea. Cada línea es un String. Métodos como split(";") son cruciales para dividir esa línea en un array de Strings, donde cada elemento es un dato de una columna, listo para ser procesado o insertado en una base de datos.

  • Generación de Contenido Dinámico: Imagina que estás construyendo el motor de un blog. Un post se compone de un título, un cuerpo, comentarios, tags, etc. Para generar el HTML final que se envía al navegador, no concatenas Strings con + (¡nunca en un bucle!). Usas un StringBuilder para añadir eficientemente el <h1> del título, los <p> del cuerpo, los <li> de los comentarios, etc. Es la única forma de hacerlo con un rendimiento aceptable cuando el contenido es grande y complejo.



Para Saber Más

Dominar estas estructuras es un viaje continuo. Si quieres profundizar, te recomiendo encarecidamente estos recursos de alta calidad que te darán una visión más profunda y profesional.

  1. Java Platform SE 21 Documentation - Arrays: La fuente oficial de la verdad. Aquí encontrarás todos los detalles técnicos sobre cómo funcionan los arrays en Java, directamente de Oracle. Es denso, pero es la documentación de referencia. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Arrays.html

  2. GeeksforGeeks - Varargs (Variable Arguments) in Java: Un artículo muy claro y con excelentes ejemplos que explora los varargs en profundidad, incluyendo las reglas y los casos de uso más comunes. Imprescindible para escribir código más flexible. https://www.geeksforgeeks.org/variable-arguments-varargs-in-java/

  3. Baeldung - A Guide to System.arraycopy() in Java: Baeldung es un recurso de cabecera para cualquier desarrollador Java. Este artículo desglosa uno de los métodos de copia más rápidos y potentes, explicando cuándo y por qué deberías usarlo sobre otras alternativas. https://www.baeldung.com/java-system-arraycopy




---

EJERCICIOS


=> Ejercicios de Estructuras de Datos