Tipos de datos y estructuras de datos (Java)

José Javier Blanco Rivero

Esta Notebook está hecha para que puedas aprender y/o repasar algunos conceptos y pongas en práctica lo aprendido. Lo primero que debes hacer es hacer click en Remix para clonar la Notebook y comenzar a practicar. Es importante destacar que pueden agregar celdas en cualquier lugar de la Notebook, pueden incluso cambiar su contenido, tomar sus propias notas, etc.

Se sugiere mantener la versión original en otra pestaña, en caso que hayan realizado diferentes cambios y hayan perdido el contenido original.

¿Qué y para qué son los tipos de datos?

Un computador procesa información leyendo y moviendo datos codificados binariamente de una dirección de memoria a otra y/o entre la memoria, los registros y los diferentes niveles de caché.

Sin entrar en detalles demasiado técnicos, es importante destacar que el procesador necesita saber por anticipado la cantidad de bits (binary digit) que van a ocupar en memoria esos datos.

La idea básicamente consiste en codificar la información, tal y como esta se presenta en nuestro mundo social y que nos es inmediatamente comprensible, en una serie de bits que pueda procesar el computador. No es lo mismo trabajar con números enteros que trabajar con números racionales; tampoco es lo mismo trabajar con caracteres como los del alfabeto occidental que trabajar con el alfabeto cirílico -sin mencionar los ideogramas chinos. Por otra parte, a diferencia de los números, las palabras no se puede sumar, restar, multiplicar o dividir. En fin, el computador no sólo necesita saber con qué tipo de dato está trabajando, sino que también necesita saber qué tipo de operaciones se pueden realizar con ellos.

En los inicios de la computación digital se programaba utilizando el lenguaje binario, esto es, unos y ceros. Se podrán imaginar lo difícil y propenso a errores que era esto. Fue así como surgieron lo que se denomina lenguajes de bajo nivel, esencialmente, lenguaje de máquina o Assembly. Se trata de una serie mnemónicos que representan el conjunto de instrucciones del procesador (Instruction Set Architecture). Esto implicaba que el programador debía especificar en cada caso cuántos bytes (cada byte son 8 bits) habrían de utilizarse para almacenar cada pieza de información.

Aunque mucho mejor que programar en código binario, la programación en Assembly también era muy dificultosa, lenta y propensa a errores. Aunado a ello, un programa escrito para un tipo de procesador no era válido en otro, ya que cada modelo o tipo de procesador tiene su ISA.

Entonces surgió Fortran (1957), Formula Translator, el primer lenguaje de alto nivel. Fortran básicamente consiste (aún se usa) en un lenguaje de programación, esto es, un lenguaje formal clausurado (es decir, un conjunto de cadenas de símbolos que utilizan un puñado bien definido de reglas para crear otras cadenas de símbolos que pertenecen al mismo conjunto) y un compilador. Un compilador no es otra cosa que un programa, un software, que traduce en código de máquina un programa escrito en este lenguaje formal del que hablamos. Y fue así como nacieron los lenguajes de alto nivel y, con ellos, una era donde el uso de las computadoras se volvería en algo cotidiano para la realización de casi cualquier tarea.

Con la emergencia de los lenguajes de programación de alto nivel, se logró que los programadores se liberasen del tedioso proceso de tener que ubicar y desplazar los datos en memoria manualmente. El que esto se haya podido lograr depende, entre otras cosas, del desarrollo de la idea de los tipos de datos. Esto es, el compilador se encargaba de definir por nosotros los bytes necesarios para almacenar la información, pero para ello el programador debía especificar en el lenguaje en cuestión, el tipo de dato de las variables que se usaban en cada caso.

Hoy en día, con compiladores más avanzados, los tipos de datos sirven también para verificar la corrección de nuestros programas.

Todo lenguaje de programación cuenta con tres tipos de datos básicos, a saber:

  • Numéricos

  • De texto o alfanuméricos

  • Booleanos (nombre que proviene del álgebra de George Boole y que connota los valores de verdadero y falso)

En el lenguaje de programación llamado Java, por ejemplo, entre los numéricos tenemos:

  • Integer

  • Short

  • Long

  • Float

  • Double

Entre los alfanuméricos:

  • String

  • Char

Y el booleano:

  • Boolean

Por lo general, a los tipos descritos arriba se les llama tipos primitivos, ya que son los más básicos. Luego tenemos los tipos compuestos, que como su nombre lo indica, son tipos de datos creados a voluntad por el programador que comprenden un conjunto de tipos primitivos. Su nombre varía de lenguaje a lenguaje: objetos, structs, tuplas, etc.

En esta notebook utilizaremos el lenguaje Java, el cual es un lenguaje de tipado fuerte y estático. El tipado fuerte, en oposición al débil, indica que el compilador es muy estricto en cuanto a las posibilidades y condiciones para cambiar una variable con tipo de dato a otro tipo de dato. Y tipado estático significa que el compilador espera que el programa declare con anticipación el tipo de dato de las variables y estructuras de datos que se usan. Existen otros lenguajes con tipado dinámico, y en este caso, esto significa que el compilador se encarga de deducir por sí solo el tipo de dato de las variables y estructuras de datos usadas por el programa en cuestión.

¡Comencemos!

Habrán notado que enumeramos los tipos de datos, pero no los definimos. Dejemos que el mismo lenguaje nos proporcione esa información.

Integer.MAX_VALUE;
4.2s

Acá le hemos pedido a la clase Integer de Java que nos de su máximo valor (para nuestra comodidad nuesta noteboook se encarga de imprimir el resultado en pantalla, pero normalmente tendríamos que agregar una instrucción (e.g. System.out.println() ) para que se imprima el resultado en el stdout (standard output) o línea de comandos).

Ahora averigüemos el mínimo valor de Integer.

Integer.MIN_VALUE;
0.4s

Creemos a continuación una variable de tipo Integer.

int x = 234;
0.2s

Habrán notados dos cosas. Primero: en vez de escribir Integer, escribimos int. La razón de esto nos lleva a adentrarnos un poco en las complejidades del lenguaje que estamos empleando. En Java Integer es una clase envoltorio (wrapper) para el tipo primitivo int. Así que cuando declaramos una variable utilizamos int en vez de la clase envoltorio.

Sin embargo, la clase envoltorio posee un conjunto de métodos estáticos (no se preocupen si no entienden esto ahora, más adelante hablaremos de ello), es decir, métodos que para invocarse no requieren de la previa creación de una instancia, que son de gran utilidad en ciertos casos. Por ejemplo, para conocer el alcance de un entero o integer hemos llamado a dos métodos estáticos de esta clase, a saber, MAX_VALUE y MIN_VALUE, respectivamente.

La segunda cosa que habrán notado es que no se imprimió nada. Y es que sólo hemos declarado la variable y no hemos hecho nada con ella. Pongamos que la quiero imprimir:

System.out.println(x);
0.7s

Ahora sí puedo ver impreso su valor. Lo que hemos hecho es llamar al método println() del campo estático out de la clase System.

Unir estas palabras con puntos no es algo arbitrario, se llama notación de punto y es muy común en lenguajes de la familia C. Se usa de la siguiente manera: primero se escribe la clase, objeto o instancia de una clase y luego se escribe un punto indicando que a continuación vamos a especificar un campo o método propio de esta clase.

Sin embargo, no deben preocuparse si no se entienden bien los conceptos de clase y objeto. Ya tendremos tiempo de profundizar sobre esto. Por ahora sólo basta que sepan que se hace así. Cuando aprendemos un lenguaje no lo aprendemos apelando a razones, sino que imitamos lo que otros hacen. Empecemos así entonces.

Ahora bien, ¿qué ocurre si creamos una variable de tipo int cuyo valor sea mayor o menor a su límite? Probemos.

int z = -21474836489;
0.1s

Pues bien, acá tenemos nuestra primera excepción. Una excepción es un mensaje del compilador que nos dice que algo ha salido mal. En este caso el compilador nos está explicando que la variable z no puede contener a -21474836489 porque sencillamente es demasiado grande. Esto es: no hay suficientes bytes asignados en la variable para contenerlo. Así que cuando intentemos asignarle a una variable un valor que no se corresponde con el tipo declarado, obtendremos una excepción apuntando hacia el error.

Probemos ahora con Short.

Short.MAX_VALUE;
0.2s
Short.MIN_VALUE
0.2s

Bien, es hora de que intenten tirar sus primeras líneas de código. Declaren una variable de tipo short.

//Inserte su código aquí

En el caso de long tenemos que su tamaño está entre los siguientes valores:

Long.MAX_VALUE;
0.3s
Long.MIN_VALUE;
0.3s

Como vemos, una variable de tipo long puede almacenar números considerablemente más grandes.

Pero hasta ahora sólo hemos visto números enteros. Pasemos ahora a los números reales. En informática tenemos dos clases, los de precisión sencilla o Float y los de precisión doble, Double.

Double.MAX_VALUE;
0.3s
Double.MIN_VALUE;
0.2s
Float.MAX_VALUE;
0.2s
Float.MIN_VALUE;
0.2s

Hagamos un pequeño ejercicio. Declaren tres variables, una long, otra double y otra float. Y súmenlas. No olviden el punto y coma ( ; ) después de declarar cada variable. Esto le indica al compilador cuándo termina una línea.

// Escriba aquí su código

Acá probablemente verán otra excepción. Nos dice algo así como 'incompatible types: possible lossy conversion from double to float'. ¿Qué ocurre aquí? Pues bien, resulta que no se puede convertir un número de doble precisión a uno de precisión simple. Probablemente un lenguaje de tipado débil te lo permitiría, pero no Java. Cuando el compilador de Java (javac) suma números de distinto tipo realiza una operación que se llama casteo, que consiste en este caso en cambiar el tipo de una de las variables para poder realizar la operación. Por ejemplo, ¿qué ocurre si sumamos un entero y un double?

int num1 = 1213;
double num2 = 343.34545342;
num1+num2;
0.3s

Como podemos ver el resultado es del tipo double, por lo que el compilador promueve la variable int a double para realizar la operación. Sin embargo, cuando se trata de una operación aritmética entre double y float esto no es posible. Aunque ambos son números reales, cada cual está implementado de forma distinta, de modo que no es posible promover un número de precisión simple a doble precisión o viceversa.

Pasemos ahora a los Strings. Java tiene la particularidad de que los Strings no son un tipo de dato primitivo, sino que se trata de una clase como lo es Integer o Float. No obstante, a diferencia de estos casos, no existe un primitivo para los Strings, sino que usamos la clase directamente. Para declarar una variable tipo String se puede hacer de una de las siguientes maneras:

String s = "Esto es un string";
System.out.println(s);  
0.7s
String cadena = new String("Esta es una cadena de caracteres");
System.out.println(cadena)
0.6s

Un String sencillamente es una cadena de caracteres, de allí que en otros lenguajes un String no sea otra cosa que una lista (ya veremos las listas en estructuras de datos) de caracteres, por lo que podríamos acceder a cada letra por índice, tal como se accede a los elementos de un vector o lista.

Y bueno, en Java también podemos hacerlo, por supuesto.

cadena.indexOf('E')
0.2s

Acá hemos preguntado en qué índice se encuentra la letra 'E'.

Y si queremos saber qué caracter se encuentra en el índice 5, por ejemplo, no tenemos más que llamar al siguiente método de la clase String y pasarle como argumento al número 5.

cadena.charAt(5);
0.2s

Como vemos, en el índice 5 se encuentra la letra 'e'. En informática, y con esto me adelanto un poco a lo que veremos en estructuras de datos, los índices empiezan a contarse desde 0.

Ahora declaremos una variable de tipo char.

char letra = 'a';
System.out.println(letra)
0.7s

Por último, veamos los booleanos. Un booleano, dicho escuetamente, expresa una alternativa entre lo verdadero y lo falso. Las variables booleanas se emplean frecuentemente en estructuras condicionales.

Para declarar un booleano lo hacemos de la siguiente manera:

boolean f = false;
boolean v = true;
0.2s

Existen un conjunto de operadores que nos permiten aplicar la lógica booleana, pero sobre este tema volveremos en otra lección.

Estructuras de datos

Guardar o almacenar datos en nuestra computadora representa sólo una parte de las actividades que realizamos en nuestro computador; la otra parte consiste en leer o recuperar esos datos. No en balde, la operaciones básicas de un computador son la lectura y la escritura de datos. Cuando creamos una variable (o un objeto, cosa que veremos más adelante) estamos almacenando un valor en memoria. Y al indicar el tipo de datos estamos especificando cómo se codifican y qué cantidad de bytes se destinan para almacenar esos datos.

Hasta acá todo bien. Sin embargo, a medida que almacenamos más y más datos en nuestra computadora, surge un problema, a saber, el acceso rápido y eficiente a esos datos. En este orden de ideas, el estudio de las estructuras de datos es un área de mucho interés para las Ciencias de la Computación.

Por supuesto, como en muchos otros casos, no existe una regla general. Todo depende qué tipo de dato se quieren almacenar y con qué propósito. Por ejemplo, ¿me interesa almacenar los datos en cierto orden?, ¿cuál? Y ¿cómo pienso acceder a esos datos? ¿Por índice? ¿Por llave?

Imaginemos que queremos programar una agenda que almacene los datos de todos nuestros contactos personales. Necesitamos las siguientes variables:

  • Nombre

  • Apellido

  • Número telefónico (local y celular)

  • Correo electrónico

  • Dirección

Primero pensemos en qué tipo de dato necesitamos para cada variable.

String nombre;
String apellido;
String direccion;
String email;
0.3s

Y ¿qué nos falta? El número telefónico. ¿Qué tipo de dato le asignarían?

Si están pensando en algún tipo de dato numérico, se equivocan. Aunque obviamente hablamos de números, un número telefónico responde a convenciones sociopolíticas y no tiene ninguna propiedad numérica inherente. Vemos que son números naturales, pero no los podemos sumar, ni multiplicar...No tiene sentido. Además, en ocasiones solemos organizar los números telefónicos en grupos, es decir, un prefijo que podría representar el código de área, por ejemplo, y posteriormente sigue el cuerpo. En este orden de ideas, lo mejor es representar a un número telefónico como un String o cadena de caracteres.

String numero_telefonico;
0.2s

Bien, pero habíamos dicho que necesitamos saber tanto el número telefónico local como el celular. Una posibilidad es crear dos variables, digamos telef_local y telef_celular. Eso estaría bastante bien, pero nos interesa aprender sobre estructuras de datos, así que utilicemos alguna.

Si nos damos cuenta el número telefónico es un género y local y celular serían especies de este género. Cuando buscamos un número telefónico podríamos estar interesados en sólo el celular o ambos. Pero si sólo queremos el local, por decir algo, deberíamos poder acceder sólo a esa información sin traernos el resto.

Mapas

Para esta tarea el mejor candidato es el mapa (Map) o dictionary como lo llaman en Python (en R lo más parecido sería una lista). Un mapa relaciona llaves y valores. Creemos un mapa en Java:

Map<String,String> num_telef = new HashMap<>();
0.2s

Acá hemos declarado un mapa cuya llave es de tipo String y cuyo valor también lo es. Ahora agregémosle algunos datos:

num_telef.put("local", "3203433");
num_telef.put("celular", "11-23457890");
0.3s

Bien, ahora digamos que quiero consultar los datos que he guardado en mi mapa. Tengamos en cuenta que un mapa recupera los datos a través de su llave, así que llamaremos al método get() y le pasaremos como parámetro una de las llaves que deseemos.

num_telef.get("local");
0.2s

Digamos que no recordamos las llaves. El método keySet() nos las traerá.

num_telef.keySet()
0.2s

Si volvemos a nuestra agenda, bien podríamos utilizar también un mapa. Veamos: crearemos un mapa cuya llave será el nombre o alias de nuestro contacto y cuyos valores estarán representados por otro mapa que contendrá los datos que describimos arriba.

Map<String,Map<String,String>> agenda = new HashMap<>();
4.2s

Pues bien, acá hemos declarado un mapa que contiene otro mapa. Nuestra declaración indica que la llave de nuestro primer mapa es un String y que su valor es Map<String, String>, es decir, otro mapa cuya llave es un String y su valor respectivo es otro String.

¡Genial! Ahora agregemos nuestro primer contacto. Como vimos arriba, para agregar un elemento a nuestro mapa utilizamos el método put(). Primero generemos los datos de contacto para después agregarlos a la agenda, es decir, primero generemos el mapa anidado para después agregarlo al mapa que lo contiene.

Map<String,String> contacto1 = new HashMap<>();
contacto1.put("Nombre", "Juan");
contacto1.put("Apellido", "Biaggio");
contacto1.put("Email", "juanbiaggio@gmail.com");
contacto1.put("Telef_local", "23458990"); //optemos por esta modalidad en vez de generar otro mapa
contacto1.put("Telef_celular", "911 33439087");  
contacto1.put("Direccion", "Viamonte 2341");
0.8s

Ahora agregemos nuestro contacto1 a la agenda:

agenda.put("Juancho", contacto1);
0.3s

Probemos. Vamos a consultar nuestro único contacto.

agenda.get("Juancho");
0.3s

Perfecto. Acá podemos ver todos los datos de nuestro contacto.

Pero digamos que sólo quiero saber la dirección de Juancho.

agenda.get("Juancho").get("Direccion");
0.4s

¡Bien! Pero, ¿por qué funcionó esto? Pues bien, ocurre que el llamado al primer get() devuelve un mapa, y todo mapa admite este método, por lo que podemos volver a llamarlo. Si el tipo de dato de retorno hubiese sido distinto, habríamos obtenido una excepción.

Ahora anímense a generar un par de nuevos contactos por ustedes mismos.

// Utilice esta celda para ingresar los contactos
//Utilice esta celda para realizar consultas sobre los datos que acaba de ingresar.

Arrays o vectores

El array o vector es una estructura de datos muy utilizada. Podemos imaginar un array como un conjunto de celdas (como en Excel), donde cada celda contendrá un solo dato y el acceso a los datos así almacenados se realiza a través de su índice, empezando desde 0 hasta alcanzar el tamaño del array.

En Java los arrays tienen la particularidad de que tienen un tamaño fijo que se determina cuando se les declara. Si necesitamos algo parecido a un array pero con tamaño variable, debería usar una lista (List). También Java tiene la restricción de que los elementos de un array deben ser todos del mismo tipo.

Existen varias formas de declarar un array.

int [] vector1 = new int [10]
0.2s

Arriba hemos declarado un vector de tamaño 10 y de tipo entero. Para agregarle elementos hacemos lo siguiente:

vector1[0]= 1;
vector1[1]=122;
vector1[3]=32;
0.4s

A continuación complete el vector hasta alcanzar su tamaño límite. Tenga en cuenta que si se excede obtendrá una excepción del tipo ArrayOutOfBoundsException, que por más intimidante que parezca no significa más que eso, que hemos excedido el tamaño permitido.

// Escriba aquí su código

Para obtener un elemento, como mencionamos arriba, debemos conocer su índice. Esta es la sintaxis:

vector1[3];
0.2s

También podemos unificar los pasos de la declaración y la asignación de valores de la siguiente manera:

String nombres [] = {"Juliana", "Marcos", "Miguel", "Pierina", "Angelo", "Bianca"};
0.1s
nombres[1]; 
0.2s

Y ¿qué ocurre si cambiamos de opinión o cambiaron nuestras necesidades y deseamos aumentar o disminuir el tamaño del array?

¡Podemos convertirlo a una lista!

List<String> lista_nombres = Arrays.asList(nombres); 
0.1s
lista_nombres.forEach(System.out::println);
0.4s

Para poder almacenar el resultado que vamos a obtener necesitamos crear la variable adecuada. En este caso sabemos que estamos trabajando con un array de tipo String. Entonces creamos una variable de tipo List<String> o una lista de Strings. A continuación, le pedimos al computador que nos imprima todos los nombres al stdout.

La clase Arrays contiene un conjunto de métodos que nos facilitan trabajar con esta estructura de datos. Por ejemplo, supongamos que deseamos ordenar los elementos de nuestro vector.

Arrays.sort(nombres);
System.out.println(Arrays.toString(nombres)); 
0.6s

Por defecto, Java ordena de modo natural, como le llaman, es decir, los números los ordenará de forma ascendente y los strings en orden alfabético. Es posible cambiar el orden en que queremos los elementos, pero ese tema lo abordaremos en otra oportunidad.

Listas

Las listas son un tipo mucho más moderno y versátil de estructura de datos que los arrays y poseen una API (Application Programming Interface) que nos permite hacer muchísimas cosas con nuestros datos. Una lista no difiere mucho de un array en el sentido que almacena elementos de un mismo tipo y el acceso a éstos se realiza a través de un índice.

Existen distintos tipos de listas, por ejemplo, LinkedList y ArrayList. Podríamos discurrir con lujo de detalles en las características y propiedades de cada una, pero lo fundamental es saber cuándo necesitamos de la una o de la otra. Deberías escoger una lista de tipo LinkedList si la operación más frecuente que vas a realizar es la inserción o remoción de datos. En cambio, si la operación más frecuente que vas a realizar es la consulta, tu mejor opción es el ArrayList.

¿Qué les parece si realizamos una lista de mercado?

Usemos un ArrayList, ya que la lista de mercado la confeccionamos una vez y luego la consultamos (a menos que sean tan indecisos como para agregar y quitar productos hasta último momento).

List<String> lista_mercado = new ArrayList<>();
lista_mercado.add("Detergente");
lista_mercado.add("Desinfectante");
lista_mercado.add("Arroz");
lista_mercado.add("Pollo");
lista_mercado.add("Panceta");
lista_mercado.add("Vino"); // ¡No puede faltar!
0.9s
lista_mercado.stream().forEach(System.out::println);
1.0s

Alternativamente, podemos agregar datos al tiempo que declaramos la lista:

List<String> lista_mercado2 = List.of("Pasta", "Harina 0000", "Café", "Mate", "Asado");
0.2s
lista_mercado2.stream().forEach(System.out::println);
0.7s

Pongamos que deseo buscar algo en mi lista, pero no recuerdo qué era. Sólo sé que tenía más de 6 letras.

lista_mercado.stream().filter(a -> a.length() > 6).forEach(System.out::println);
0.6s

No, no, lo que busco no se encuentra en esta lista.

lista_mercado2.stream().filter(a -> a.length() > 6).forEach(System.out::println);
0.5s

¡Sí! Era la harina. Ahora necesito buscar algo cuyo nombre empieza por 'V'.

lista_mercado.stream().filter(a -> a.startsWith("V")).forEach(System.out::println);
0.5s

Y ¡sí! ¡¿Cómo me pude olvidar?!

Si se preguntan qué significa la expresión a -> a.[método], se trata de una función lambda o función anónima. La a puede sustituirse por cualquier otra letra; sencillamente, lo que estamos diciendo es que la función anónima: i) recibe un sólo parámetro, la a, y después de la flecha ( -> ) ii) estamos indicando que utilizaremos un método de la clase a la que a pertenece. Como a pertenece a la clase String es posible usar un método como startsWith(). El dato de retorno que arroje este método será lo que nuestra función lambda devolverá.

En el caso concreto de arriba, startsWith() devuelve un booleano, y justamente, el método filter() sólo admite métodos booleanos, ya que utilizará los valores de true o false para filtrar elementos mientras recorre la colección.

En todo caso, no es momento de preocuparse con las funciones lambdas, ya que tendremos ocasión de verlas con detenimiento en otra lección.

Ahora les toca agregar algunos elementos a la lista y buscarlos según las letras en que terminen. (Pista 1: exploren los métodos de la clase String; Pista 2: si después del punto (.) presionan Tab, van a ver desplegada una lista de los métodos que pueden usar a continuación)

// Escribe aquí tu código
// Escribe aquí tus consultas

Colas o filas (Queues) y pilas (Stacks)

Una cola o fila (Queue) es una estructura de datos que organiza sus elementos en el mismo orden en que se agregan. Podríamos decir, en consonancia con la metáfora de la fila, que los organiza por orden de llegada. Por esta razón, en ciencias de la computación, se describe la conducta de esta estructura de datos como FIFO (first-in-first out), es decir, el primero que llega es el primero que sale.

Utilizaremos un Queue cuando sea importante para nosotros procesar los datos en el mismo orden en que nos van llegando o cuando nuestra lógica de negocio (así se le llama a las condiciones y validaciones que nuestro modelo del problema que queremos resolver le impone a los datos) implique algún tipo de lista de espera.

En Java no podemos directamente crear un objeto del tipo Queue, ya que es una interface. Más adelante explicaremos qué es una interfaz o interface y por qué no la podemos instanciar. Así que en Java, por lo general, usamos alguna de las más usadas implementaciones de Queue, como lo son ArrayDeque y Deque (pronunciado deck).

Las referidas implementaciones de Queue permiten no sólo crear filas sino que también podemos usarlas para crear un pila.

Una pila es una forma de agrupar un conjunto de objetos uno sobre otro; ahora, no es que literalmente lo estén en memoria, sino que lo fundamental es que esta estructura de datos se comporta siguiendo el formato LIFO (last-in, first-out), esto es, el último que llega es el primero que sale. En este orden de ideas, tanto ArrayDeque como Deque son double ended queues.

Imaginemos que al programar la página web de una tienda de electrodomésticos, necesitamos procesar las órdenes de compra que se realizan a través de esta plataforma. Hagamos abstracción de lo que se hace con las órdenes y enfoquémonos exclusivamente en la recepción de las órdenes antes de ser procesadas. De la misma forma que la fila en la caja en una tienda física, queremos darle prioridad a quien primero emitió la orden.

ArrayDeque<Integer> ordenes_de_compra = new ArrayDeque<>();
0.2s

Como es usual, las órdenes de compra se gestionan a través de un número que las identifica, por eso hemos creado un ArrayDeque de tipo Integer.

Para agregar elementos a nuestra cola o fila podemos utilizar alguno de los siguientes métodos, add() u offer(). Con estos métodos tenemos garantizado que los elementos se agregarán siempre al final.

ordenes_de_compra.add(124390);
ordenes_de_compra.offer(23321);
0.2s

Como podemos observar, estos métodos arrojan true, es decir, el dato de retorno es un booleano que nos dice si fue exitosa la operación (true) o no (false).

Agreguen un par de elementos más a la fila:

// Escriba aquí su código

Ahora tomemos el primer elemento de la fila. Para eso usaremos el método poll(), el cual nos devolverá el primer método y lo sacará de la fila.

ordenes_de_compra.poll();
0.2s

Veamos qué quedó en la fila. Para esto tenemos que recorrer la colección con un método que ya hemos empleado antes, el método forEach().

ordenes_de_compra.stream().forEach(System.out::println);
0.8s

Como ejercicio tratemos de crear una pila (pueden usar la misma clase ArrayDeque). Téngase en cuenta que en un stack insertamos los elementos al final y retiramos el último elemento (LIFO). Ya conocemos los métodos que realizan la primera función, mientas que para retirar un elemento del final de la fila deberán usar el método removeLast().

//Utilice esta celda para declarar su fila
0.6s
//Utilice esta celda para consultar insertar elementos
0.6s
//Utilice esta celda para sacar elementos. Recuerde que para ver los resultados deberás imprimirlos en pantalla recorriendo la colección con forEach y ejecutando la impresión con println

Cojuntos (Sets)

Como tendremos ocasión de conversar, la teoría de conjuntos tiene una gran importancia en las ciencias de la computación y muchas de sus propiedades informan los principios con los que se diseñan algoritmos e inclusive enteros lenguajes de programación.

Un conjunto como estructura de datos se diferencia del resto en que sus elementos no admiten repetición. Si se agrega x al conjunto A dos o más veces, al recorrer la colección sencillamente vamos a obtener x (de hecho, en Java, al querer agregar un elemento repetido vamos a obtener un false como retorno del método add()).

La ventaja de usar un conjunto como estructura de datos consiste en poder usar las operaciones más conocidas en teoría de conjuntos, tales como unión, diferencia, e intersección. Por lo general, cuando utilizamos un conjunto no estamos interesados en el orden de los elementos.

En Java Set es también una interface, así que no la podemos instanciar directamente sino que empleamos alguna de sus implementaciones. Una de las más comunes es HashSet, pero si dado el caso queremos mantener el orden de los elementos de un conjunto podemos utilizar un TreeSet (aunque si nos interesa la eficiencia del acceso a los elementos, TreeSet no es la mejor opción para almacenar elementos ordenados).

Esta vez probemos con un ejercicio sociológico. Digamos que estamos trabajando con teoría de roles y queremos analizar una base de datos que contiene información familiar y profesional de una cantidad determinada de individuos, así como los hobbies o deportes que practica.

Como tendrán oportunidad de estudiar en teoría sociológica, un individuo asume simultáneamente distintos roles (padre, estudiante, deportista, actor, representante, ciudadano, etc.) y va asumiendo nuevos roles conforme avanza con la edad (por ejemplo, abuelo/a, jubilada/o, etc.). Podemos usar un conjunto para definir un rol y después analizar qué individuos pertenecen a un mismo conjunto, qué individuos coinciden en más de un grupo, etc.

Creemos unos cuantos roles, digamos:

  • Empleados formales

  • Empleados informales (monotributistas)

  • Miembros de asociaciones o clubes deportivos

  • Miembros o militantes de partidos políticos

Y cualquier otra cosa que se les ocurra...

De momento para simplificar usemos un String para representar el nombre del individuo. Así que crearemos un conjunto de Strings.

Set<String> empleados_formales = new HashSet<>();
Set<String> monotributistas = new HashSet<>();
Set<String> deportistas_amateur = new HashSet<>();
Set<String> miembros_pp = new HashSet<>();
4.5s

Ahora agregemos algunos individuos a cada conjunto.

empleados_formales.add("Silvina");
empleados_formales.add("Mario");
empleados_formales.add("Jimena");
monotributistas.add("Pedro");
monotributistas.add("Lola");
monotributistas.add("Juliana");
deportistas_amateur.add("Piero");
deportistas_amateur.add("Silvina");
deportistas_amateur.add("Lola");
deportistas_amateur.add("Martin");
miembros_pp.add("Martin");
miembros_pp.add("Silvina");
miembros_pp.add("Juliana");
1.3s

Verifiquemos el contenido de cada rol recorriendo la colección.

empleados_formales.stream().forEach(System.out::println);
0.9s
monotributistas.stream().forEach(System.out::println);
0.7s
deportistas_amateur.stream().forEach(System.out::println);
0.8s
miembros_pp.stream().forEach(System.out::println);
0.7s

Bien. Dado que los monotributistas y los empleados formales forman parte de un mismo conjunto más general como el de los trabajadores, podemos unir ambos grupos.

Set<String> trabajadores = new HashSet<>(empleados_formales);
trabajadores.addAll(monotributistas);
trabajadores.stream().forEach(System.out::println);
0.8s

Ahora digamos que quiero saber quiénes son trabajadores y también deportistas.

Set<String> trabajadores_y_deportistas = new HashSet<>(trabajadores);
trabajadores_y_deportistas.retainAll(deportistas_amateur);
trabajadores_y_deportistas.stream().forEach(System.out::println);
0.7s

Perfecto, Silvina y Lola son deportistas y también trabajadoras.

Su tarea ahora es buscar quiénes son trabajadores, pero no deportistas (Pista: utilizar el método removeAll() )

// Escriba aquí su código

Hemos llegado al final de esta lección. Al final les dejamos un par de celdas para que pongan en práctica lo aprendido.

// celda de práctica 1
// celda de práctica 2

Para seguir aprendiendo

Cuando trabajamos con determinado lenguaje de programación nuestra fuente principal debe ser la documentación oficial. Para Java, véase https://docs.oracle.com/en/java/javase/17/

Allí podremos encontrar también tutoriales y la información más reciente sobre el lenguaje.

Pero muchas veces necesitamos ver ejemplos prácticos. Existen páginas como https://www.geeksforgeeks.org/java/?ref=shm o https://www.javatpoint.com/java-tutorial que nos serán de mucha ayuda.

También existen canales de YouTube muy útiles, tales como píldoras informáticas.

Runtimes (1)
Runtime Languages (1)