El manejo de datos constituye la piedra angular del desarrollo de software. Es mediante esto que se genera información para la toma de decisiones.
Un manejo eficiente de los datos es, por lo tanto, un elemento fundamental en cualquier programa y, para ello, es importante conocer las diferentes formas existentes de agruparlos y estructurarlos.
En este tema conocerás algunas estructuras de datos esenciales que te permitirán organizar y generar información dentro de un programa.
Recuerda, sin embargo, que esto es solo la piedra angular, ya que existen otras formas de manejar los datos. Dar un paso a la vez te permitirá comprender mejor las diferentes necesidades que dan sentido a estas y otras estructuras de datos.
Existen diferentes maneras de organizar los datos que habrán de ser procesados y generalmente la estructura que se utilice suele estar con concordancia con el tipo de algoritmo que será implementado.
Arreglos (Arrays).
La primera estructura que estudiarás es el arreglo. De acuerdo con Boyarsky (2020), un arreglo es un área de memoria ubicada en la Heap memory con espacio suficiente para almacenar un número determinado de elementos.
En Java podemos declarar arreglos de la siguiente manera: el tipo de dato de los elementos que contendrá y un par de corchetes; estos son la marca inequívoca de que se trata de un arreglo y no de otro tipo:
int[] ArrA; |
Tabla 1. Muestra la declaración de un arreglo de elementos tipo int.
Como puedes observar, se trata de una variable de referencia, por lo que esta almacenará la dirección de memoria dentro de la heap memory donde se encuentre el espacio reservado para el arreglo una vez que este se haya inicializado.
Hay varias maneras de inicializar un arreglo. La más simple sería mediante la palabra reservada new, seguida del tipo de dato del arreglo, y colocando la capacidad del arreglo dentro de corchetes.
int[] ArrA = new int[5]; |
Tabla 2. Inicialización de un arreglo con elementos tipo int.
Una vez hecho esto, se reservarán cinco espacios contiguos en la heap memory y su dirección de memoria se almacenará en la variable ubicada en la stack memory, como se muestra en la siguiente imagen.
Figura 1. Representación gráfica de un arreglo en memoria.
Los arreglos son estructuras de datos indexados, lo que significa que cada elemento dentro de un arreglo tiene una posición numerada y es simple acceder a él conociendo su índice.
Los índices comienzan su numeración en 0 y continúan de manera ascendente. Nunca encontrarás un valor negativo como índice de un arreglo.
arrA[0] = 2; |
Tabla 3. Asignación de valores a un arreglo.
Por otro lado, para comprender esta estructura es importante que tengas presente que no es lo mismo el índice que indica la posición de un elemento que el elemento mismo. El elemento será del tipo indicado en la declaración del arreglo y, en caso de ser numérico, el valor puede ser cualquiera que sea válido. Así, el ejemplo de arriba se vería en memoria de la siguiente manera:
Figura 2. Representación de un arreglo con valores almacenados.
De la misma manera, para acceder a un elemento dentro de un arreglo se deben usar los índices.
System.out.println (ArrA[0]); System.out.println (ArrA[3]); System.out.println (ArrA[5]); System.out.println (ArrA[-1]); |
Tabla 4. Lectura de valores de un arreglo.
En Java no es posible modificar el tamaño de un arreglo, por lo que una vez que todos los espacios son ocupados es necesario crear un nuevo arreglo con el nuevo tamaño deseado y copiar todos los elementos al nuevo arreglo.
De la misma manera, no es posible eliminar elementos, por lo que, si se desea compactar elementos quitando uno de en medio, es necesario recorrer “manualmente” los elementos ubicados a la derecha de dicho elemento una posición a la izquierda y, de esta manera, eliminarlo. Sin embargo, la cantidad de elementos se mantendrá igual.
Para conocer el tamaño de un arreglo, en Java se utiliza el método length disponible en cualquier elemento. Esta propiedad es de solo lectura, por lo que no puede modificarse.
System.out.println(arrA.length); |
Tabla 5. Impresión de la longitud de un arreglo.
Listas enlazadas (ArrayLists).
En ocasiones no es posible conocer con anticipación la cantidad de elementos que se van a manejar durante la ejecución de un programa y, para esos casos, dispones de la clase ArrayList, la cual es en esencia un objeto “envoltorio” para un arreglo con una serie de funciones predefinidas en el mismo lenguaje que facilitan su uso.
Para crear un objeto ArrayList, se debe especificar el tipo de dato de los elementos que contendrá dentro de paréntesis angulares que se forman mediante el signo de menor qué < y mayor qué >. Un ArrayList es un objeto convencional, por lo que es necesario auxiliarse de la palabra reservada new, seguido del método constructor que se tiene el mismo nombre la clase. Este es acompañado de paréntesis angulares y, finalmente, un par de paréntesis convencionales.
Observa como el tipo int ahora se representa como Integer. Eso es porque un ArrayList no puede declarar tipos primitivos para sus elementos y para ello Java proporciona una clase equivalente para cada primitivo. Estas clases son llamadas wrappers. Es preferible usar tipos primitivos cuando se trata de cómputo de números por razones de desempeño, sin embargo, en ocasiones simplemente no es una opción (Loy 2020).
ArrayList<Integer> listA = new ArrayList <>(); |
Tabla 6. Creación de un objeto tipo ArrayList.
A continuación, puedes ver la clase envoltorio (Wrapper class por su nombre en inglés) equivalente para cada tipo primitivo.
byte -> Byte |
Tabla 7. Clases envoltorio (wrapper) equivalentes para cada tipo primitivo.
Una vez creado el objeto de tipo ArrayList, este se puede ver representado en memoria de la siguiente manera:
Figura 3. Representación en memoria de un ArrayList.
Los elementos de un ArrayList también se encuentran indexados al igual que en el caso de los arreglos. Sin embargo, la manera de interactuar con esta colección es mediante de los métodos disponibles en la clase.
Para agregar elementos al final debes usar el método add(elemento) que agregará un elemento al final de la colección. Es posible también agregar un elemento en una posición especifica indicando el índice donde se hará la inserción add(índice, elemento).
listA.add(2); |
Tabla 8. Adición de elementos a una colección tipo ArrayList.
Después de agregar los elementos se pueden representar de la siguiente manera:
Figura 4. Representación en memoria de una colección tipo ArrayList con elementos.
A diferencia de los arreglos, en un ArrayList si es posible eliminar un elemento y la colección hará lo necesario para que el proceso sea transparente si simplemente queden los elementos deseados. Para lograrlo se usa el método boolean remove(índice) que permite eliminar el elemento indicado por el índice y devuelve un valor boolean, indicando si la eliminación fue exitosa o no.
System.out.println (lista.remove(2)); |
Tabla 9. Eliminación de elementos de un ArrayList.
Cuando la colección sea de valores no numéricos, es posible eliminar elementos de acuerdo con el valor de este. Cuando se opta por esta alternativa, se busca el elemento indicado y se elimina solo si se encuentra; en caso contrario, se devuelve el valor false.
ArrayList<String> listB = new ArrayList<>(); listB.remove(“mundo”); listB.remove(“sistemas”); |
Tabla 10. Ejemplo de adición y eliminación de elementos no numéricos de un ArrayList
Cuando se quiere únicamente reemplazar un valor ya existente sin incrementar el número de elementos se tiene el método set(índice, elemento).
lista.set(0,15); |
Tabla 11. Método de reemplazo de valores en elementos de un array list
Existen otros métodos que facilitan el trabajo con listas, como son los siguientes:
Pilas (Stacks).
Existen además otras maneras más complejas de organizar los datos que facilitan la implementación de técnicas más sofisticadas de procesamiento de datos.
Por un lado, están las pilas o stacks, por su nombre en inglés. Al igual que las colas, tienen métodos específicos para agregar información y para extraerla.
En este caso, se trata de una estructura LIFO (Last In- First Out). Esto significa que el último elemento en ser insertado será el primero en salir.
Hay diferentes situaciones donde una pila puede ser útil como, por ejemplo, para recordar el último cambio de un documento y revertirlo, o para invertir el orden de un conjunto de datos (palabras, por ejemplo).
A continuación, puedes apreciar cómo se comporta esta estructura:
Tabla 12. Comportamiento de una pila durante la inserción y extracción de elementos.
En Java puedes crear una pila mediante la clase LinkedList (que resulta ser muy versátil) en este caso. Para hacer un uso correcto de esta estructura debes limitarte al uso de los siguientes métodos:
Colas (Queues).
Por otro lado, están las colas o queues, por su nombre en inglés. Estas estructuras son especialmente útiles cuando se hace procesamiento de mensajes asíncronos. Por ejemplo, imagina la comunicación con una impresora. En un caso como este es fácil ver cómo la computadora es capaz de enviar la información hacia la impresora con una velocidad mayor a la que la impresora es capaz de imprimir, en este caso se usa el concepto de cola de impresión que no es otra cosa que organizar los documentos a imprimir en una cola y, de esta manera, la impresora puede leer el siguiente elemento a procesar conforme va terminando.
Las colas usan el concepto FIFO (First in – First Out), lo que significa que el primer elemento en entrar a la cola será el primer elemento en salir y se procesarán siguiendo el orden de escritura.
A continuación, puedes apreciar cómo se comporta esta estructura:
Tabla 13. Comportamiento de una cola durante la inserción y extracción de elementos.
Observarás que a diferencia de los arreglos y los ArrayList, en el caso de las colas los espacios de memoria para almacenar los elementos se van reservando conforme estos se van agregando y, cuando un elemento es procesado, este deja de formar parte de la colección.
En Java puedes crear colas mediante el uso de la clase LinkedList que implementa la interfaz Queue (Evans, 2018) y, a diferencia de la clase ArrayList, esta no cuenta con un número de espacios reservados previamente para almacenar los elementos, sino que reserva un espacio de memoria para cada uno al momento de ser agregado. De esta manera, esta colección puede crecer indefinidamente sin mayor problema, pero a cambio el proceso de inserción resulta más lento.
Crear una cola es similar a crear una lista.
LinkedList<Integer> queue = new LinkedList<>(); |
Tabla 14. Creación de una cola usando un LinkedList.
La clase LinkedList cuenta con una cantidad muy grande de métodos, pero los únicos relevantes en este contexto serán los siguientes:
Como puedes ver, las diferentes estructuras de datos de las que dispones te brindan gran versatilidad y puedes sacar provecho de sus fortalezas dependiendo del problema a resolver, ya que cada una ofrece características particulares que las hacen más idóneas en diferentes escenarios.
Aprender a organizar los datos es apenas la base para poder procesarlos y sacarles el mayor provecho; el camino es todavía más largo. Existen otras estructuras más complejas que te permitirán resolver un número cada vez mayor de problemas.
Recuerda que cada estructura que acabas de estudiar forma parte de tu conjunto de herramientas para mejorar tus habilidades de programación, por lo que se te recomienda aplicarlos en diversos escenarios hasta lograr una comprensión plena.
Asegúrate de: