Programación I

Programación – Los Arreglos en Java

Los Arreglos en Java ⚙️

1. Introducción

En el estudio del lenguaje Java, los arreglos —también denominados arrays— constituyen una de las primeras estructuras de datos que permiten comprender el modo en que los programas administran grandes volúmenes de información. Hasta este punto, los programas que Ud. ha desarrollado probablemente manipulan variables simples, es decir, contenedores individuales que guardan un único dato: un número, una palabra o un valor lógico.

Sin embargo, en la mayoría de los problemas reales es necesario manejar colecciones de datos del mismo tipo, como una lista de calificaciones, las temperaturas diarias de una semana o los tiempos obtenidos por distintos corredores en una competencia. Intentar resolver tales situaciones declarando una variable para cada dato sería poco práctico e ineficiente.

Los arreglos surgen precisamente para organizar la información de manera estructurada y accesible, aprovechando los principios de orden, homogeneidad y secuencia que caracterizan la memoria del computador.

En este documento se estudiará el concepto de arreglo desde una doble perspectiva:

  • Lógica, comprendiendo su función en el diseño de algoritmos y su relación con las estructuras de control.
  • Técnica, analizando su representación en la memoria de la máquina virtual de Java (JVM) y la forma en que los programas interactúan con el hardware subyacente.

💡 Dominar el uso de los arreglos constituye un paso esencial hacia el pensamiento computacional avanzado. Permite organizar los datos, recorrerlos con precisión, realizar cálculos repetitivos y sentar las bases para comprender estructuras más complejas, como listas dinámicas o matrices multidimensionales.


2. El problema de los datos múltiples 🤔

Para comprender la necesidad de los arreglos, observe el siguiente ejemplo. Supóngase que se desea calcular el promedio de calificaciones de cinco estudiantes.

int nota1 = 8;
int nota2 = 6;
int nota3 = 9;
int nota4 = 7;
int nota5 = 10;

double promedio = (nota1 + nota2 + nota3 + nota4 + nota5) / 5.0;
System.out.println("El promedio es: " + promedio);

Este código cumple su propósito, pero resulta evidente que no es escalable. Si el grupo tuviera treinta o cien estudiantes, sería necesario declarar decenas de variables y modificar múltiples líneas de código cada vez que se agregara o eliminara un dato.

El arreglo resuelve esta limitación al proporcionar un único identificador capaz de agrupar varios valores del mismo tipo bajo una estructura ordenada. De este modo, cada elemento puede ser accedido mediante un número entero denominado índice.


3. Definición conceptual de un arreglo 🧱

Un arreglo es una colección ordenada de elementos del mismo tipo, almacenados de forma contigua en la memoria y accesibles individualmente mediante un índice numérico. En Java, los arreglos poseen tres características fundamentales que conviene memorizar, ya que definen su comportamiento interno:

  • Tamaño fijo: El tamaño de un arreglo se establece en el momento de su creación y no puede modificarse durante la ejecución del programa. Esta característica permite a la máquina virtual reservar de inmediato el espacio exacto en memoria, optimizando el acceso y el rendimiento.
  • Homogeneidad de tipo: Todos los elementos que componen un arreglo deben pertenecer al mismo tipo de dato: int, double, String, boolean, entre otros. Esto garantiza que cada posición ocupe el mismo espacio en memoria y que el compilador pueda calcular la dirección de cualquier elemento de forma precisa.
  • Acceso por índice base cero: Los índices de un arreglo comienzan siempre en 0, no en 1. Por tanto, en un arreglo de N elementos, el primer elemento ocupa la posición 0 y el último la posición N-1. Este detalle es de gran relevancia práctica, ya que un error de un solo índice puede provocar fallos en tiempo de ejecución.

Podemos representar conceptualmente un arreglo como una secuencia de casilleros numerados:

Índice 0 1 2 3 4
Valor 8 6 9 7 10

Cada casillero contiene un valor y todos juntos conforman una estructura accesible a través de su nombre, por ejemplo:

int[] notas = {8, 6, 9, 7, 10};

Así, notas[0] representa el primer elemento del arreglo (8) y notas[4] el último (10).


4. Ventajas pedagógicas y computacionales 👍

El uso de arreglos proporciona beneficios tanto para el diseño del programa como para la comprensión del funcionamiento interno del computador:

  • Eficiencia: al almacenar los datos de manera contigua, la memoria puede ser recorrida con rapidez por el procesador.
  • Orden y claridad: se simplifica la escritura de algoritmos repetitivos y se reduce la posibilidad de errores humanos.
  • Abstracción: el programador se concentra en el conjunto de datos como una entidad unificada, sin preocuparse por la ubicación física de cada valor.
  • Reutilización: el mismo código puede adaptarse fácilmente a distintos tamaños de arreglo, lo cual favorece la escalabilidad del programa.

Ejemplo introductorio

El siguiente programa utiliza un arreglo para calcular el promedio de calificaciones de manera generalizada:

public class PromedioNotas {
    public static void main(String[] args) {
        int[] notas = {8, 6, 9, 7, 10};
        int suma = 0;

        // Recorremos el arreglo para sumar las notas
        for (int i = 0; i < notas.length; i++) {
            suma += notas[i]; // Acumulamos cada nota en la variable suma
        }

        // Calculamos el promedio (importante usar double para la división)
        double promedio = (double) suma / notas.length;

        System.out.println("El promedio es: " + promedio);
    }
}

Observe que el código es más compacto, legible y adaptable: si el número de calificaciones cambia, basta con modificar el contenido del arreglo. La propiedad notas.length nos da el tamaño del arreglo automáticamente.


5. El ciclo de vida de un arreglo en Java: Declaración, Creación e Inicialización 🔄

Todo arreglo en Java atraviesa tres etapas fundamentales antes de poder ser utilizado en un programa: declaración, creación e inicialización. Comprender claramente estas fases es esencial, ya que cada una representa un momento distinto en la relación entre el programa y la memoria del sistema.

5.1 Declaración: el nombre y el tipo

La declaración de un arreglo informa al compilador que se utilizará una estructura de datos capaz de almacenar múltiples elementos del mismo tipo. En esta fase no se reserva todavía memoria, sino que se crea una referencia (un nombre simbólico) que podrá apuntar a un conjunto de valores en el futuro.

Sintaxis general:

tipo[] nombre;

Ejemplo:

int[] edades;

En este ejemplo, se declara un arreglo denominado edades que podrá contener valores enteros (int). Hasta este momento, el arreglo no existe físicamente en memoria; solo se ha definido un nombre que funcionará como una referencia.

5.2 Creación: la reserva de memoria

Para que el arreglo adquiera existencia concreta dentro del programa, es necesario crear la estructura en la memoria utilizando el operador new. En este paso, la JVM (Java Virtual Machine) reserva un bloque contiguo de memoria en el heap, suficiente para almacenar la cantidad especificada de elementos.

Sintaxis general:

nombre = new tipo[tamaño];

Ejemplo:

edades = new int[5];

Aquí, el arreglo edades es creado con cinco casilleros de tipo entero. Cada uno de ellos será inicializado automáticamente con el valor 0, ya que Java asigna valores por defecto según el tipo de dato.

Valores por defecto más frecuentes:

Tipo de dato Valor por defecto
int, long, short, byte 0
double, float 0.0
boolean false
char ‘\u0000’ (carácter nulo)
String u objetos null

Es importante comprender que la variable edades no contiene los valores directamente, sino una referencia al bloque de memoria donde se encuentran almacenados.

5.3 Inicialización: la asignación de valores

Una vez creado el arreglo, es posible asignar valores a cada una de sus posiciones utilizando el operador de índice [ ].

Ejemplo:

edades[0] = 15;
edades[1] = 16;
edades[2] = 17;
edades[3] = 15;
edades[4] = 16;

De esta manera, cada casillero adquiere un valor concreto. También puede realizarse la inicialización en una sola línea, utilizando llaves {} al momento de declarar el arreglo:

int[] edades = {15, 16, 17, 15, 16};

Esta forma es más concisa y útil cuando los valores son conocidos de antemano.

5.4 Representación en memoria: stack y heap 💾

Para comprender el funcionamiento interno de los arreglos, resulta necesario observar cómo la JVM gestiona la memoria. En términos generales, la memoria de un programa Java se divide en dos grandes áreas:

  • Stack (pila): almacena las variables locales (dentro de métodos) y las referencias a objetos/arreglos.
  • Heap (montículo): almacena los objetos y arreglos creados dinámicamente con new.

Cuando se ejecuta la instrucción:

int[] edades = new int[5];

la JVM realiza lo siguiente:

  1. Reserva en el heap un bloque de memoria contiguo para cinco enteros.
  2. Almacena en el stack (dentro del método donde se declaró) una referencia que apunta al inicio de ese bloque en el heap.
  3. La variable edades contiene esa referencia (dirección de memoria), no los datos en sí.

Podemos representarlo de forma esquemática:

Stack Heap —— ————————— edades ───────────> [ 0 | 0 | 0 | 0 | 0 ] (Referencia) (Bloque de 5 enteros en la dirección 0xA320) ————————— Dirección 0xA320

Este modelo permite que múltiples variables apunten al mismo arreglo si se asignan entre sí las referencias. Por ejemplo:

int[] grupoA = {10, 12, 14};
int[] grupoB = grupoA;       // grupoB ahora apunta al MISMO arreglo que grupoA

En este caso, grupoA y grupoB apuntan al mismo bloque de memoria. Si se modifica un elemento mediante grupoB[1] = 20;, el cambio también se reflejará al acceder a grupoA[1], ya que ambas referencias comparten el mismo objeto en el heap.

5.5 Errores frecuentes ❌

Los estudiantes que se inician en el uso de arreglos suelen cometer algunos errores típicos que conviene revisar detenidamente:

  • No crear el arreglo antes de usarlo.
    int[] numeros;
    numeros[0] = 5;   // Error: NullPointerException - 'numeros' es null
    Solución: incluir numeros = new int[tamaño]; antes de la asignación.
  • Acceder a índices fuera del rango permitido.
    int[] datos = new int[3]; // Índices válidos: 0, 1, 2
    datos[3] = 10;   // Error: Índice 3 está fuera de los límites
    En este caso, la JVM lanzará una excepción: ArrayIndexOutOfBoundsException.
  • Confundir el tamaño del arreglo con su último índice. Si un arreglo tiene longitud n, el último índice válido es n - 1. Esto suele causar errores en los bucles for, especialmente si se utiliza <= en lugar de <.
    for (int i = 0; i <= datos.length; i++) { // Error: i llegará a ser igual a length
  • Intentar imprimir el arreglo directamente. Al escribir System.out.println(edades);, la consola mostrará algo como [I@1b6d3586 (tipo y dirección de memoria), no los valores.
    Para imprimir su contenido, es necesario recorrerlo (con un bucle) o emplear Arrays.toString(edades);.

Ejemplo práctico: declaración completa

import java.util.Arrays; // Necesario para usar Arrays.toString()

public class Edades {
    public static void main(String[] args) {
        // 1. Declaración y 2. Creación
        int[] edades = new int[5];

        // 3. Inicialización (asignación de valores)
        edades[0] = 15;
        edades[1] = 16;
        edades[2] = 17;
        edades[3] = 15;
        edades[4] = 16;

        // Impresión usando Arrays.toString()
        System.out.println("Edades registradas: " + Arrays.toString(edades));
    }
}

Salida:

Edades registradas: [15, 16, 17, 15, 16]

Este ejemplo ilustra el ciclo completo: se declara, se crea, se inicializa y finalmente se imprime el arreglo de forma legible.


6. Recorridos, operaciones y aplicaciones de los arreglos 🚶‍♂️

Una vez comprendido cómo se declaran, crean e inicializan los arreglos, el paso siguiente consiste en procesar sus elementos mediante estructuras de control. En la práctica, esto significa recorrer el arreglo, leer o modificar sus valores y aplicar sobre ellos operaciones de cálculo o comparación.

Los arreglos permiten aplicar algoritmos repetitivos de manera sencilla, gracias a que todos sus elementos comparten el mismo tipo y están dispuestos en posiciones consecutivas dentro de la memoria. Por ello, los bucles —en especial for y for-each— son herramientas indispensable para operar con ellos.

6.1 Recorrido clásico con bucle for

El bucle for resulta el mecanismo más común para recorrer un arreglo, ya que su estructura permite controlar con precisión el índice de cada elemento.

Ejemplo básico:

int[] edades = {15, 16, 17, 15, 16};
System.out.println("Recorriendo el arreglo 'edades':");
for (int i = 0; i < edades.length; i++) { // i va de 0 hasta tamaño - 1
    System.out.println("Elemento en posición " + i + ": " + edades[i]);
}

Salida:

Recorriendo el arreglo 'edades':
Elemento en posición 0: 15
Elemento en posición 1: 16
Elemento en posición 2: 17
Elemento en posición 3: 15
Elemento en posición 4: 16

💡 El uso de la propiedad .length permite que el recorrido se adapte automáticamente al tamaño del arreglo, evitando errores y favoreciendo la reutilización del código.

6.2 Recorrido simplificado con for-each (for mejorado)

Java ofrece una versión simplificada del bucle for, conocida como for mejorado o for-each, cuyo propósito es recorrer todos los elementos sin utilizar índices explícitos.

Ejemplo:

int[] edades = {15, 16, 17, 15, 16};
System.out.println("Recorriendo con for-each:");
for (int edad : edades) { // Para cada 'edad' en el arreglo 'edades'
    System.out.println("Edad: " + edad);
}

Salida:

Recorriendo con for-each:
Edad: 15
Edad: 16
Edad: 17
Edad: 15
Edad: 16

Esta estructura es especialmente útil cuando no se necesita conocer la posición del elemento, sino únicamente su valor. No obstante, debe tenerse presente que no permite modificar directamente los valores del arreglo, ya que la variable del bucle (edad) es una copia temporal del valor en cada iteración.

6.3 Operaciones frecuentes sobre arreglos 🧮

El trabajo cotidiano con arreglos suele implicar operaciones de cálculo o búsqueda. A continuación, se presentan algunos ejemplos clásicos que permiten ejercitar la lógica algorítmica.

6.3.1 Cálculo de suma y promedio

int[] notas = {8, 6, 9, 7, 10};
int suma = 0; // Inicializar el acumulador
for (int i = 0; i < notas.length; i++) {
    suma += notas[i]; // suma = suma + notas[i]
}
double promedio = (double) suma / notas.length; // Usar (double) para división decimal
System.out.println("Suma: " + suma);
System.out.println("Promedio: " + promedio);

Análisis: Este programa acumula la suma de todas las notas en la variable suma, luego divide el total entre la cantidad de elementos (notas.length). El uso de (double) antes de suma asegura que la división se realice con decimales, evitando la truncación propia de los enteros.

6.3.2 Búsqueda de un valor (Búsqueda Secuencial)

int[] codigos = {101, 102, 103, 104, 105};
int buscado = 104;
boolean encontrado = false; // Bandera para saber si lo encontramos
int posicion = -1; // Para guardar la posición (-1 si no se encuentra)

for (int i = 0; i < codigos.length; i++) {
    if (codigos[i] == buscado) {
        encontrado = true;
        posicion = i;
        break; // Salir del bucle una vez encontrado
    }
}

if (encontrado) {
    System.out.println("Código " + buscado + " encontrado en la posición " + posicion);
} else {
    System.out.println("Código " + buscado + " no encontrado.");
}

Análisis: Este algoritmo recorre el arreglo comparando cada elemento con el valor buscado. Cuando lo encuentra, actualiza la bandera encontrado, guarda la posición y interrumpe el bucle con break (ya no necesita seguir buscando). Es un ejemplo clásico de búsqueda secuencial, método adecuado para conjuntos pequeños o no ordenados.

6.3.3 Obtención del valor máximo y mínimo

int[] temperaturas = {18, 22, 19, 25, 21};
int max = temperaturas[0]; // Suponemos que el primero es el máximo inicialmente
int min = temperaturas[0]; // Suponemos que el primero es el mínimo inicialmente

// Empezamos desde el segundo elemento (índice 1)
for (int i = 1; i  max) { // Si encontramos uno mayor...
        max = temperaturas[i]; // ...lo actualizamos como nuevo máximo
    }
    if (temperaturas[i] < min) { // Si encontramos uno menor...
        min = temperaturas[i]; // ...lo actualizamos como nuevo mínimo
    }
}

System.out.println("Temperatura máxima: " + max);
System.out.println("Temperatura mínima: " + min);

Análisis: El algoritmo establece como punto de partida (máximo y mínimo) el primer elemento. Luego, recorre el resto del arreglo (desde el índice 1) y compara cada elemento para actualizar, si corresponde, el valor máximo y mínimo encontrados hasta el momento. Este tipo de proceso desarrolla la capacidad de razonamiento secuencial, esencial en la programación estructurada.

6.4 Procesamiento condicional de datos ✅

Los arreglos permiten combinar estructuras repetitivas (bucles) y condicionales (if) para generar resultados más complejos. Por ejemplo, puede contarse cuántos elementos cumplen una determinada condición:

int[] notas = {8, 5, 9, 7, 4, 10};
int aprobados = 0; // Contador de aprobados

for (int nota : notas) { // Usamos for-each para simplificar
    if (nota >= 6) { // Condición: nota mayor o igual a 6 (aprobado)
        aprobados++; // Incrementamos el contador
    }
}

System.out.println("Cantidad de aprobados: " + aprobados);

En este caso, el programa recorre el arreglo verificando cuántas notas son iguales o mayores a 6. El contador aprobados se incrementa cada vez que la condición se cumple.

6.5 Ejercicio integrador: estadísticas simples

El siguiente ejemplo combina varios de los conceptos vistos:

import java.util.Arrays; // Para imprimir el arreglo fácilmente

public class Estadisticas {
    public static void main(String[] args) {
        int[] datos = {8, 6, 9, 7, 10, 5};

        int suma = 0;
        int max = datos[0]; // Asumir máximo inicial
        int min = datos[0]; // Asumir mínimo inicial

        // Recorrer con for-each para sumar, encontrar max y min
        for (int valor : datos) {
            suma += valor;
            if (valor > max) max = valor;
            if (valor < min) min = valor;
        }

        double promedio = (double) suma / datos.length;

        System.out.println("Valores: " + Arrays.toString(datos));
        System.out.println("Suma: " + suma);
        System.out.println("Promedio: " + promedio);
        System.out.println("Máximo: " + max);
        System.out.println("Mínimo: " + min);
    }
}

Este programa calcula la suma, el promedio, el máximo y el mínimo en un solo recorrido, demostrando la eficiencia y versatilidad de los arreglos como estructura de datos.

6.6 Errores lógicos frecuentes 🤯

Al trabajar con arreglos, el error no siempre se manifiesta como una excepción (error que detiene el programa); a veces el programa se ejecuta, pero el resultado es incorrecto (error lógico). Algunos ejemplos típicos:

  • Sumar antes de inicializar el contador o acumulador. Olvidar asignar suma = 0; antes del bucle genera resultados erróneos porque la variable suma no empieza desde cero.
  • Olvidar el uso del cast (conversión) al calcular promedios. Sin (double), la división entre enteros descarta los decimales (ej: 15 / 2 da 7 en lugar de 7.5).
  • Colocar mal el límite del bucle. Usar <= datos.length en lugar de < datos.length provoca un acceso fuera de rango (ArrayIndexOutOfBoundsException) en la última iteración.
  • Modificar el arreglo dentro del bucle sin intención. Una asignación mal colocada puede alterar datos que debían conservarse para cálculos posteriores.

🧠 El análisis de estos errores desarrolla la competencia en pensamiento crítico, pues exige verificar la coherencia entre lo que el programa hace y lo que se pretende que haga.


7. Los arreglos y la memoria: Abstracción, Eficiencia y Sinergia con el Hardware 🧠↔️💻

El estudio de los arreglos no puede considerarse completo sin comprender su relación con la memoria del computador y con el modo en que el procesador accede a los datos. Detrás de cada instrucción de Java existe un conjunto de operaciones a nivel de máquina que hacen posible que el programa funcione. Este conocimiento permite valorar los principios de abstracción y eficiencia que orientan a la programación moderna.

7.1 La abstracción en Java y la JVM

El lenguaje Java fue diseñado para ofrecer un entorno de programación independiente del hardware, gracias a la acción de la Java Virtual Machine (JVM). Cuando se escribe un arreglo en Java, el programador no necesita saber en qué dirección exacta de la memoria (RAM) se almacenan los datos, ni cómo el procesador los manipula. Esa complejidad está abstraída (oculta) por la JVM, que traduce las instrucciones de Java a código de máquina adecuado para el sistema operativo y el procesador en uso.

Desde el punto de vista del programador, un arreglo es una entidad lógica compuesta por posiciones numeradas. Desde el punto de vista de la JVM, es una estructura contigua de celdas que se crean en el heap y a las que se accede mediante cálculos de desplazamiento de memoria.

Por ejemplo, cuando se ejecuta la instrucción:

int[] datos = new int[5];

la JVM traduce internamente esta orden en una solicitud de memoria contigua para cinco valores enteros (por ejemplo, 5 * 4 bytes = 20 bytes), reservando además espacio para almacenar metadatos, como el tamaño del arreglo (5) y su tipo (int).

7.2 Acceso contiguo y cálculo de direcciones

Los arreglos son especialmente eficientes porque sus elementos se encuentran almacenados de manera contigua (uno al lado del otro) en la memoria. Esto permite que el procesador (o la JVM) calcule la dirección de cualquier elemento con una simple operación aritmética:

Dirección del elemento i = Dirección base + (i * tamaño del elemento en bytes)

Esta relación lineal evita búsquedas o saltos innecesarios, lo que convierte al arreglo en una estructura de acceso directo (se puede ir directamente a cualquier elemento si se conoce su índice). En contraste, otras estructuras, como las listas enlazadas (que se verán más adelante), requieren seguir «punteros» entre posiciones no contiguas, lo cual puede consumir más tiempo.

7.3 Comparación con los arreglos en C (Otro lenguaje)

Históricamente, la idea de arreglo proviene de lenguajes como C o Fortran, en los cuales el programador debía gestionar directamente la memoria. En C, un arreglo es esencialmente un bloque fijo de memoria y su nombre actúa como una dirección base (un «puntero»). El acceso se realiza mediante aritmética de punteros, sin protección alguna contra errores de índice.

Ejemplo en C:

int numeros[5];
numeros[7] = 10;   // ¡Peligro! Acceso fuera de rango. El programa puede fallar o corromper memoria sin aviso.

En Java, en cambio, la máquina virtual controla el acceso y genera una excepción (ArrayIndexOutOfBoundsException) si se intenta superar los límites del arreglo. Esta protección evita la corrupción de memoria y refuerza la confiabilidad del sistema, aunque introduce una leve sobrecarga de procesamiento (la JVM tiene que verificar el índice).

⚖️ Java sacrifica una pequeña porción de rendimiento (velocidad) en favor de la seguridad y la estabilidad del programa, manteniendo un equilibrio entre la abstracción y la eficiencia.

7.4 La interacción con la RAM y el procesador (CPU)

Desde una perspectiva física, los arreglos son gestionados dentro de la memoria principal (RAM), que funciona como un espacio temporal para los datos que el programa está usando. Cada posición del arreglo ocupa un número determinado de bytes (ej: 4 bytes para un int). El procesador (CPU) utiliza sus registros internos para calcular la dirección del elemento solicitado, lo copia temporalmente en una memoria intermedia muy rápida denominada caché, y ejecuta la operación correspondiente (leer, escribir, sumar, etc.).

Este proceso ocurre millones de veces por segundo. La contigüidad de los arreglos permite que el procesador anticipe los accesos: si se accede a datos[i], es probable que luego se acceda a datos[i+1], por lo que puede cargar varios elementos cercanos en caché al mismo tiempo (esto se llama localidad espacial). De esta manera, los arreglos favorecen el aprovechamiento óptimo del hardware y el rendimiento general del programa.

7.5 El papel del Recolector de Basura (Garbage Collector – GC) 🗑️

En Java, los arreglos son objetos dinámicos creados en el heap. Cuando un arreglo (o cualquier objeto) deja de estar referenciado por ninguna variable activa en el programa (por ejemplo, la variable que lo apuntaba sale de su ámbito o se le asigna null), la JVM lo marca como inaccesible. Posteriormente, un proceso automático llamado Garbage Collector (GC) libera la memoria ocupada por esos objetos inaccesibles, dejándola disponible para nuevas creaciones.

Este proceso automático evita fugas de memoria (memory leaks) y errores comunes en lenguajes como C o C++, donde la liberación de memoria debe hacerse manualmente. No obstante, el programador debe tener presente que crear y descartar arreglos muy grandes frecuentemente puede consumir recursos significativos hasta que el GC actúe, lo cual puede influir en el rendimiento.

7.6 Arrays como puente entre la lógica y el hardware

El estudio de los arreglos permite al estudiante comprender un principio esencial de la informática:

🌉 Todo programa es una traducción progresiva entre el pensamiento humano (la lógica, el algoritmo) y las operaciones físicas del hardware (la memoria, el procesador).

Desde el punto de vista lógico, el arreglo es un conjunto ordenado de valores. Desde el punto de vista físico, es una secuencia de celdas eléctricas que almacenan cargas binarias (0s y 1s) en la memoria RAM. La programación consiste en establecer un puente conceptual entre ambos niveles mediante reglas precisas y un lenguaje formal (como Java).

Así, al trabajar con arreglos, el estudiante no solo aprende una estructura de datos, sino que desarrolla una visión integrada de cómo la abstracción, la organización y la eficiencia son principios compartidos por la mente humana y la máquina.


8. Actividades y reflexión final ✍️

8.1 Actividades propuestas

  1. Modelización de datos: Declare un arreglo que almacene las edades (enteros) de los 11 integrantes titulares de un equipo de fútbol. Ingrese edades de ejemplo. Calcule la edad promedio del equipo y determine la edad más alta y la más baja entre los titulares. Imprima los resultados.
  2. Simulación de memoria (Stack y Heap): Dibuje en su cuaderno (o usando una herramienta gráfica) un esquema del stack y del heap que represente el estado de la memoria durante la ejecución del siguiente código, justo después de la línea copia[1] = 12;:
    int[] numeros = {3, 6, 9};
    int[] copia = numeros;
    copia[1] = 12;
    Explique con sus palabras por qué el cambio realizado a través de la variable copia también afecta al arreglo cuando se accede a él mediante la variable numeros. ¿Cuántos arreglos existen realmente en el heap?
  3. Análisis de errores (Excepciones): Escriba un programa simple que declare y cree un arreglo de tamaño 3 (new int[3]). Luego, intente asignar un valor al índice 5 (arreglo[5] = 100;). Ejecute el programa y observe la excepción que se genera en la consola (probablemente ArrayIndexOutOfBoundsException). Investigue qué significa el mensaje de error y proponga al menos una forma de prevenir este tipo de error en sus programas (por ejemplo, usando .length en los bucles).
  4. Comparación conceptual (Java vs. C): Explique en un breve texto (3-4 frases) las principales diferencias entre cómo se manejan los arreglos en Java y en C, considerando aspectos como la seguridad (control de límites), la gestión de memoria (creación y destrucción) y el nivel de abstracción del hardware. ¿Qué ventajas y desventajas ve en cada enfoque?

8.2 Cierre conceptual

El dominio de los arreglos constituye un punto de inflexión en el aprendizaje de la programación. Permite pasar del manejo aislado de variables simples al tratamiento de estructuras organizadas de datos, lo que habilita la resolución de problemas reales de mayor complejidad.

El arreglo enseña a pensar de manera sistemática, a recorrer datos de forma ordenada, a aplicar operaciones repetitivas y a visualizar la relación entre la lógica del algoritmo y la arquitectura física del computador. Comprender su funcionamiento en la JVM y su interacción con la memoria (stack, heap, GC) desarrolla una competencia clave del pensamiento computacional: la capacidad de modelar procesos que el hardware puede ejecutar eficientemente.

🚀 El paso siguiente será el estudio de las estructuras dinámicas, como ArrayList, que amplían las capacidades del arreglo tradicional al permitir modificar su tamaño en tiempo de ejecución. Sin embargo, la comprensión profunda del arreglo simple (estático) seguirá siendo la base fundamental sobre la cual se construyen todas las demás estructuras de datos.


Solución al Ejercicio: El Termómetro 🌡️

A continuación se presenta el código Java que modela un termómetro, aplicando estructuras de control if para validar datos y determinar estados.


Clase Termometro.java

Esta clase modela el comportamiento de un termómetro. Contiene la lógica para almacenar, modificar y evaluar la temperatura.

Atributo y Constructor

Atributo temperatura: Se declara como private para proteger el estado interno del objeto (encapsulamiento). Usamos el tipo double porque la temperatura puede tener valores decimales.

Constructor Termometro(double tempInicial): Permite crear un objeto `Termometro` con un valor inicial. Es una buena práctica usar el método setTemperatura dentro del constructor para asegurar que incluso el valor inicial cumpla con las validaciones de rango.

public class Termometro {

    private double temperatura;

    public Termometro(double tempInicial) {
        this.setTemperatura(tempInicial);
    }

Getters y Setters

getTemperatura(): Un método público simple que devuelve el valor actual del atributo temperatura.

setTemperatura(double nuevaTemperatura): Este método es clave. Usa una estructura if para verificar si el valor recibido está dentro del rango válido (entre -100 y 200). Solo si la condición es verdadera, actualiza el atributo. Si no, ignora la operación, cumpliendo con el requisito del ejercicio.

    public double getTemperatura() {
        return this.temperatura;
    }

    public void setTemperatura(double nuevaTemperatura) {
        if (nuevaTemperatura >= -100 && nuevaTemperatura <= 200) {
            this.temperatura = nuevaTemperatura;
        } else {
            // fuera de rango
        }
    }

Métodos Específicos

subir(double delta): Utiliza un if simple para asegurarse de que la temperatura solo pueda aumentar (delta > 0). Reutiliza el método setTemperatura para que el nuevo valor también pase por la validación de rango.

estado(): Implementa la lógica de decisión con una estructura if-else anidada. Evalúa la temperatura en orden descendente para determinar si el estado es «CALOR», «TEMPLADO» o «FRIO».

    public void subir(double delta) {
        if (delta > 0) {
            this.setTemperatura(this.temperatura + delta);
        }
    }

    public String estado() {
        if (this.temperatura >= 30) {
            return "CALOR 🔥";
        } else {
            if (this.temperatura >= 15) {
                return "TEMPLADO 😊";
            } else {
                return "FRIO ❄️";
            }
        }
    }

Método toString

El método toString() proporciona una representación en texto del objeto. Es fundamental para mostrar información de manera rápida y clara, facilitando la depuración y las pruebas.

    @Override
    public String toString() {
        return "Termómetro [Temperatura=" + this.temperatura + "°C]";
    }
}

Clase Principal.java

Esta clase contiene el método main, el punto de entrada de nuestro programa. Su único propósito es crear un objeto de la clase Termometro y probar sus métodos para verificar que funcionan como se espera.

El flujo es el siguiente:

  1. Se crea una instancia de Termometro con un valor inicial de 20.0.
  2. Se imprime por consola su estado inicial usando toString() y estado().
  3. Se utiliza un bucle while que se ejecuta 3 veces para llamar al método subir(5.0) en cada repetición.
  4. Finalmente, se imprime el estado final del termómetro para verificar que los cambios se aplicaron correctamente.
public class Principal {

    public static void main(String[] args) {
        
        System.out.println("--- INICIO DE LA PRUEBA ---");
        Termometro miTermometro = new Termometro(20.0);
        
        System.out.println("Estado inicial: " + miTermometro.toString());
        System.out.println("Sensación: " + miTermometro.estado());
        System.out.println("---------------------------");

        int contador = 0;
        System.out.println("Subiendo la temperatura 3 veces...");
        
        while (contador < 3) {
            miTermometro.subir(5.0);
            System.out.println("Paso " + (contador + 1) + ": " + miTermometro.getTemperatura() + "°C");
            contador = contador + 1;
        }
        
        System.out.println("---------------------------");
        
        System.out.println("Estado final: " + miTermometro.toString());
        System.out.println("Sensación final: " + miTermometro.estado());
        System.out.println("--- FIN DE LA PRUEBA ---");
    }
}

Salida Esperada en Consola

Al ejecutar la clase Principal, el resultado que se debe mostrar en la consola es el siguiente:

--- INICIO DE LA PRUEBA ---
Estado inicial: Termómetro [Temperatura=20.0°C]
Sensación: TEMPLADO 😊
---------------------------
Subiendo la temperatura 3 veces...
Paso 1: 25.0°C
Paso 2: 30.0°C
Paso 3: 35.0°C
---------------------------
Estado final: Termómetro [Temperatura=35.0°C]
Sensación final: CALOR 🔥
--- FIN DE LA PRUEBA ---

Solución al Ejercicio: La Batería del Teléfono 🔋

Este código modela el comportamiento de la batería de un teléfono, asegurando que su nivel se mantenga siempre entre 0 y 100, y gestionando la carga y el consumo.


Clase Bateria.java

Esta clase representa la batería. Define sus propiedades y las acciones que se pueden realizar sobre ella.

Atributo y Constructor

Atributo nivel: Se declara como private para protegerlo. Usamos int ya que el nivel de batería se maneja como un porcentaje entero.

Constructor Bateria(int nivelInicial): Inicializa el objeto. Llama internamente al método setNivel para asegurarse de que el valor inicial esté dentro del rango válido (0-100).

public class Bateria {

    private int nivel;

    public Bateria(int nivelInicial) {
        this.setNivel(nivelInicial);
    }

Getters y Setters

getNivel(): Devuelve el nivel actual de la batería.

setNivel(int nuevoNivel): Este método es el guardián del atributo. Se asegura de que el nivel nunca sea menor que 0 ni mayor que 100. Si se intenta asignar un valor fuera de ese rango, lo ajusta al límite más cercano (0 o 100). Esto se conoce como «sanitizar» o «clampear» el valor.

    public int getNivel() {
        return this.nivel;
    }

    public void setNivel(int nuevoNivel) {
        if (nuevoNivel > 100) {
            this.nivel = 100;
        } else if (nuevoNivel < 0) {
            this.nivel = 0;
        } else {
            this.nivel = nuevoNivel;
        }
    }

Métodos Específicos

cargar(int porc): Suma un porcentaje al nivel actual. Primero, verifica que el porcentaje a cargar sea positivo. Luego, calcula el nuevo nivel y utiliza setNivel para asignarlo. El setNivel se encarga automáticamente de que, si la suma supera 100, el nivel quede en 100.

consumir(int porc): Implementa una lógica if-else. Si el nivel actual es suficiente para cubrir el consumo, resta el porcentaje y devuelve true. De lo contrario, no hace ningún cambio en el nivel y devuelve false.

modoAhorro(): Es un método booleano simple. Devuelve true si el nivel es menor que 20, y false en cualquier otro caso. Es una forma directa de consultar un estado derivado del atributo principal.

    public void cargar(int porc) {
        if (porc > 0) {
            this.setNivel(this.nivel + porc);
        }
    }

    public boolean consumir(int porc) {
        if (this.nivel >= porc) {
            this.setNivel(this.nivel - porc);
            return true;
        } else {
            return false;
        }
    }

    public boolean modoAhorro() {
        if (this.nivel < 20) {
            return true;
        } else {
            return false;
        }
        // Versión simplificada: return this.nivel < 20;
    }

Método toString

El método toString() devuelve una cadena que representa el estado del objeto de forma legible para un humano.

    @Override
    public String toString() {
        return "Bateria [" + this.nivel + "%]";
    }
}

Clase Principal.java

Esta clase se utiliza para probar la lógica de la clase Bateria y simular su uso.

El flujo de la prueba es el siguiente:

  1. Crea un objeto Bateria con un nivel inicial del 35%.
  2. Muestra el estado inicial.
  3. Inicia un bucle while que se repetirá mientras el nivel de la batería sea mayor o igual a 10.
  4. Dentro del bucle, intenta consumir un 15% de la batería en cada iteración y muestra el estado actual.
  5. Una vez que el bucle termina (porque el nivel baja de 10), muestra el estado final y verifica si el modo de ahorro está activado.
public class Principal {

    public static void main(String[] args) {
        
        System.out.println("--- PRUEBA DE BATERÍA ---");
        Bateria miBateria = new Bateria(35);
        System.out.println("Estado inicial: " + miBateria.toString());
        System.out.println("-------------------------");

        // Bucle while que se ejecuta mientras haya suficiente batería (>=10%)
        // Esto simula el uso del teléfono a lo largo del tiempo.
        int paso = 1;
        while (miBateria.getNivel() >= 10) {
            System.out.println("Paso " + paso + ": Consumiendo 15%...");
            miBateria.consumir(15);
            System.out.println(" -> Nivel actual: " + miBateria.toString());
            paso++;
        }

        System.out.println("-------------------------");
        System.out.println("Batería baja, saliendo del bucle...");
        System.out.println("Estado final: " + miBateria.toString());
        System.out.println("¿Modo ahorro activado?: " + miBateria.modoAhorro());
        System.out.println("--- FIN DE LA PRUEBA ---");
    }
}

Salida Esperada en Consola

Al ejecutar la clase Principal, la salida debe ser la siguiente:

--- PRUEBA DE BATERÍA ---
Estado inicial: Bateria [35%]
-------------------------
Paso 1: Consumiendo 15%...
 -> Nivel actual: Bateria [20%]
Paso 2: Consumiendo 15%...
 -> Nivel actual: Bateria [5%]
-------------------------
Batería baja, saliendo del bucle...
Estado final: Bateria [5%]
¿Modo ahorro activado?: true
--- FIN DE LA PRUEBA ---

Solución al Ejercicio: Cuenta Bancaria 🏦

Este código implementa una clase para representar una cuenta bancaria, con operaciones básicas y lógica de mantenimiento basada en el saldo.


Clase CuentaBancaria.java

Define la estructura y el comportamiento de una cuenta, incluyendo sus atributos y las operaciones que se pueden realizar.

Atributos y Constructor

Atributos: Se definen numeroCuenta y saldo como private para mantener el encapsulamiento. El número de cuenta es un int y el saldo un double para manejar decimales.

Constructor: Recibe el número y el saldo inicial para crear un nuevo objeto CuentaBancaria, asignando estos valores a los atributos correspondientes.

public class CuentaBancaria {
    
    private int numeroCuenta;
    private double saldo;

    public CuentaBancaria(int numero, double saldoInicial) {
        this.numeroCuenta = numero;
        this.saldo = saldoInicial;
    }

Getters y Setters

Se incluyen los métodos estándar get y set para cada atributo, permitiendo un acceso controlado a los datos del objeto desde el exterior.

    public int getNumeroCuenta() {
        return this.numeroCuenta;
    }

    public void setNumeroCuenta(int numeroCuenta) {
        this.numeroCuenta = numeroCuenta;
    }

    public double getSaldo() {
        return this.saldo;
    }

    public void setSaldo(double saldo) {
        this.saldo = saldo;
    }

Métodos Específicos

depositar(double monto): Usa un if simple para verificar que el monto a depositar sea positivo antes de agregarlo al saldo.

retirar(double monto): Implementa una estructura if-else. La condición verifica dos cosas: que el monto sea positivo y que haya saldo suficiente. Si ambas se cumplen, actualiza el saldo y devuelve true. De lo contrario, no hace nada y devuelve false.

mantenimiento(): Usa un if anidado para determinar el cargo a aplicar según el saldo actual. Una vez determinado el cargo, lo resta del saldo y devuelve el valor del cargo que fue aplicado. Este método modifica el estado del objeto y además informa sobre el cambio realizado.

    public void depositar(double monto) {
        if (monto > 0) {
            this.saldo = this.saldo + monto;
        }
    }

    public boolean retirar(double monto) {
        if (monto > 0 && this.saldo >= monto) {
            this.saldo = this.saldo - monto;
            return true;
        } else {
            return false;
        }
    }

    public double mantenimiento() {
        double cargo;
        if (this.saldo >= 10000) {
            cargo = 0;
        } else {
            if (this.saldo >= 1000) {
                cargo = 50;
            } else {
                cargo = 100;
            }
        }
        this.saldo = this.saldo - cargo;
        return cargo;
    }

Método toString

Devuelve una cadena de texto que resume la información principal del objeto: su número de cuenta y su saldo actual.

    @Override
    public String toString() {
        return "Cuenta [Nro=" + this.numeroCuenta + ", Saldo=$" + this.saldo + "]";
    }
}

Clase Principal.java

Esta clase se encarga de probar la funcionalidad de CuentaBancaria, simulando una secuencia de operaciones.

El flujo de la prueba es el siguiente:

  1. Crea una instancia de CuentaBancaria con número 123 y saldo inicial de 500.
  2. Realiza un depósito de 700 y muestra el nuevo saldo.
  3. Intenta retirar 200 y muestra si la operación fue exitosa.
  4. Usa un bucle while para ejecutar el método mantenimiento() dos veces, mostrando el cargo aplicado y el saldo resultante en cada paso.
  5. Finalmente, imprime el estado completo de la cuenta usando toString().
public class Principal {

    public static void main(String[] args) {
        
        System.out.println("--- PRUEBA DE CUENTA BANCARIA ---");
        CuentaBancaria miCuenta = new CuentaBancaria(123, 500.0);
        System.out.println("Cuenta creada: " + miCuenta.toString());
        
        System.out.println("\nDepositando $700...");
        miCuenta.depositar(700);
        System.out.println("Saldo actual: $" + miCuenta.getSaldo());

        System.out.println("\nRetirando $200...");
        boolean retiroExitoso = miCuenta.retirar(200);
        System.out.println("¿Retiro fue posible?: " + retiroExitoso);

        int contador = 0;
        while (contador < 2) {
            System.out.println("\n--- Aplicando mantenimiento " + (contador + 1) + " ---");
            double cargoAplicado = miCuenta.mantenimiento();
            System.out.println("Cargo aplicado: $" + cargoAplicado);
            System.out.println("Saldo restante: $" + miCuenta.getSaldo());
            contador++;
        }

        System.out.println("\n---------------------------------");
        System.out.println("Estado final de la cuenta: " + miCuenta.toString());
        System.out.println("--- FIN DE LA PRUEBA ---");
    }
}

Salida Esperada en Consola

Al ejecutar la clase Principal, la salida en consola será la siguiente:

--- PRUEBA DE CUENTA BANCARIA ---
Cuenta creada: Cuenta [Nro=123, Saldo=$500.0]

Depositando $700...
Saldo actual: $1200.0

Retirando $200...
¿Retiro fue posible?: true

--- Aplicando mantenimiento 1 ---
Cargo aplicado: $50.0
Saldo restante: $950.0

--- Aplicando mantenimiento 2 ---
Cargo aplicado: $100.0
Saldo restante: $850.0

---------------------------------
Estado final de la cuenta: Cuenta [Nro=123, Saldo=$850.0]
--- FIN DE LA PRUEBA ---

Solución al Ejercicio: El Semáforo 🚦

Esta es la nueva versión del código que modela un semáforo. Se ajusta al diagrama UML proporcionado, utilizando números para los colores y añadiendo el atributo segundos.


Clase Semaforo.java

Esta clase encapsula la lógica de un semáforo, incluyendo su estado actual (color y segundos) y las transiciones entre colores.

Atributos y Constructor

Atributos: Se definen color y segundos como private para proteger el estado del objeto. El color se representa con un número entero (0=Rojo, 1=Amarillo, 2=Verde), como indica el comentario.

Constructor: Ahora recibe dos parámetros, colorInicial y segundosIniciales, para inicializar ambos atributos al crear el objeto.

public class Semaforo {

    // 0=ROJO, 1=AMARILLO, 2=VERDE
    private int color;
    private int segundos;

    public Semaforo(int colorInicial, int segundosIniciales) {
        this.setColor(colorInicial); // Reutilizamos el setter para validar
        this.setSegundos(segundosIniciales);
    }

Getters y Setters

setColor(int nuevoColor): Usa un if para asegurarse de que solo se puedan asignar valores válidos (0, 1 o 2). Si el valor es inválido, no se realiza ningún cambio.

setSegundos(int nuevosSegundos): También incluye una validación simple para evitar valores negativos de tiempo.

    public int getColor() {
        return this.color;
    }

    public void setColor(int nuevoColor) {
        if (nuevoColor >= 0 && nuevoColor = 0) {
            this.segundos = nuevosSegundos;
        }
    }

Métodos Específicos

avanzar(): Este método implementa la lógica del ciclo del semáforo. Usa una estructura if-else if-else para determinar el color actual y cambiarlo al siguiente en la secuencia definida: 0 (Rojo) -> 2 (Verde) -> 1 (Amarillo) -> 0 (Rojo).

puedeCruzarPeaton(): Devuelve true únicamente si el color del semáforo es 0 (Rojo), y false en los demás casos.

    public void avanzar() {
        if (this.color == 0) { // Si está en ROJO
            this.color = 2;   // Pasa a VERDE
        } else if (this.color == 2) { // Si está en VERDE
            this.color = 1;         // Pasa a AMARILLO
        } else if (this.color == 1) { // Si está en AMARILLO
            this.color = 0;         // Pasa a ROJO
        }
    }

    public boolean puedeCruzarPeaton() {
        if (this.color == 0) { // 0 es ROJO
            return true;
        } else {
            return false;
        }
    }

Método toString

Para que la salida sea legible, este método convierte el número interno del color a su nombre en texto. Además, ahora incluye el valor del atributo segundos.

    @Override
    public String toString() {
        String colorTexto;
        if (this.color == 0) {
            colorTexto = "ROJO 🔴";
        } else if (this.color == 1) {
            colorTexto = "AMARILLO 🟡";
        } else {
            colorTexto = "VERDE 🟢";
        }
        return "Semaforo [Color=" + colorTexto + ", Segundos=" + this.segundos + "s]";
    }
}

Clase Principal.java

Esta clase se actualizó para probar la nueva versión de la clase Semaforo, incluyendo la gestión de los segundos.

El flujo de la prueba es el siguiente:

  1. Crea un objeto Semaforo iniciando en color 0 (ROJO) y 0 segundos.
  2. Muestra el estado inicial.
  3. Ejecuta un bucle while 5 veces. En cada pasada, llama a avanzar() para cambiar el color y luego actualiza los segundos del objeto usando getSegundos() y setSegundos().
  4. Al terminar, imprime el estado final del objeto y si el peatón puede cruzar.
public class Principal {

    public static void main(String[] args) {
        
        System.out.println("--- SIMULACIÓN DE SEMÁFORO (v2) ---");
        Semaforo miSemaforo = new Semaforo(0, 0); // Inicia en ROJO (0) y 0 segundos
        
        System.out.println("Estado inicial: " + miSemaforo.toString());
        System.out.println("------------------------------------");

        int contador = 0;
        while (contador < 5) {
            miSemaforo.avanzar();
            // Actualizamos los segundos DENTRO del objeto
            miSemaforo.setSegundos(miSemaforo.getSegundos() + 10); 
            
            System.out.println("Paso " + (contador + 1) + ": " + miSemaforo.toString());
            contador++;
        }
        
        System.out.println("\n------------------------------------");
        System.out.println("Fin de la simulación.");
        System.out.println("Estado final: " + miSemaforo.toString());
        System.out.println("¿Puede cruzar el peatón ahora?: " + miSemaforo.puedeCruzarPeaton());
        System.out.println("--- FIN DE LA PRUEBA ---");
    }
}

Salida Esperada en Consola

Al ejecutar la nueva clase Principal, el resultado que se debe mostrar en la consola es el siguiente:

--- SIMULACIÓN DE SEMÁFORO (v2) ---
Estado inicial: Semaforo [Color=ROJO 🔴, Segundos=0s]
------------------------------------
Paso 1: Semaforo [Color=VERDE 🟢, Segundos=10s]
Paso 2: Semaforo [Color=AMARILLO 🟡, Segundos=20s]
Paso 3: Semaforo [Color=ROJO 🔴, Segundos=30s]
Paso 4: Semaforo [Color=VERDE 🟢, Segundos=40s]
Paso 5: Semaforo [Color=AMARILLO 🟡, Segundos=50s]

------------------------------------
Fin de la simulación.
Estado final: Semaforo [Color=AMARILLO 🟡, Segundos=50s]
¿Puede cruzar el peatón ahora?: false
--- FIN DE LA PRUEBA ---