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:
- Reserva en el heap un bloque de memoria contiguo para cinco enteros.
- Almacena en el stack (dentro del método donde se declaró) una referencia que apunta al inicio de ese bloque en el heap.
-
La variable
edadescontiene esa referencia (dirección de memoria), no los datos en sí.
Podemos representarlo de forma esquemática:
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 nullSolución: incluirnumeros = 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ímitesEn 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 esn - 1. Esto suele causar errores en los buclesfor, 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 emplearArrays.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 variablesumano 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 / 2da7en lugar de7.5). -
Colocar mal el límite del bucle. Usar
<= datos.lengthen lugar de< datos.lengthprovoca 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
- 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.
-
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;:
Explique con sus palabras por qué el cambio realizado a través de la variableint[] numeros = {3, 6, 9}; int[] copia = numeros; copia[1] = 12;copiatambién afecta al arreglo cuando se accede a él mediante la variablenumeros. ¿Cuántos arreglos existen realmente en el heap? -
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 (probablementeArrayIndexOutOfBoundsException). 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.lengthen los bucles). - 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:
- Se crea una instancia de
Termometrocon un valor inicial de 20.0. - Se imprime por consola su estado inicial usando
toString()yestado(). - Se utiliza un bucle
whileque se ejecuta 3 veces para llamar al métodosubir(5.0)en cada repetición. - 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:
- Crea un objeto
Bateriacon un nivel inicial del 35%. - Muestra el estado inicial.
- Inicia un bucle
whileque se repetirá mientras el nivel de la batería sea mayor o igual a 10. - Dentro del bucle, intenta consumir un 15% de la batería en cada iteración y muestra el estado actual.
- 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:
- Crea una instancia de
CuentaBancariacon número 123 y saldo inicial de 500. - Realiza un depósito de 700 y muestra el nuevo saldo.
- Intenta retirar 200 y muestra si la operación fue exitosa.
- Usa un bucle
whilepara ejecutar el métodomantenimiento()dos veces, mostrando el cargo aplicado y el saldo resultante en cada paso. - 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:
- Crea un objeto
Semaforoiniciando en color 0 (ROJO) y 0 segundos. - Muestra el estado inicial.
- Ejecuta un bucle
while5 veces. En cada pasada, llama aavanzar()para cambiar el color y luego actualiza los segundos del objeto usandogetSegundos()ysetSegundos(). - 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 ---