FechaVersiónDescripción
15/10/20211.0.0Versión inicial
01/12/20211.0.1Corrección de errores
03/12/20211.0.2Cierre de tema
03/12/20211.0.3Se añade ListIterator
17/12/20242.0.0Se añaden nuevas estructuras
18/12/20242.0.1Nuevo tratamiento de fechas y horas. Mejora de expresiones regulares.

Unidad 5 - Estructura de datos dinámicas

1. Introducción

Cuando el volumen de datos a manejar por una aplicación es elevado, no basta con utilizar variables. Manejar los datos de un único pedido en una aplicación puede ser relativamente sencillo, pues un pedido está compuesto por una serie de datos y eso simplemente se traduce en varias variables. Pero, ¿qué ocurre cuando en una aplicación tenemos que gestionar varios pedidos a la vez?

Lo mismo ocurre en otros casos. Para poder realizar ciertas aplicaciones se necesita poder manejar datos que van más allá de meros datos simples (números y letras). A veces, los datos que tiene que manejar la aplicación son datos compuestos, es decir, datos que están compuestos a su vez de varios datos más simples. Por ejemplo, un pedido está compuesto por varios datos, los datos podrían ser el cliente que hace el pedido, la dirección de entrega, la fecha requerida de entrega y los artículos del pedido.

Ya hemos trabajo con arrays, pero, a veces, los datos tienen estructuras aún más complejas, y son necesarias soluciones adicionales.

En esta UD aprenderemos esas soluciones adicionales que consisten básicamente en la capacidad de poder manejar varios datos del mismo o diferente tipo de forma dinámica y flexible. Aunque hablaremos de diferentes estructuras de datos dinámicas, nos centraremos en manejar las listas y en concretos los ArrayLists.

 

2. Colecciones

El manejo de las estructuras de datos dinámicas es una tarea muy importante en el desarrollo de software. Sin embargo, su manejo, creando y manipulando directamente sus elementos y las referencias a ellos, podría considerarse un trabajo de bajo nivel.

Java incluye un conjunto de interfaces y clases genéricas, conocido como el Java Collection Framework (marco de trabajo de colecciones de Java), el cuál contiene estructuras de datos, interfaces y algoritmos pre empaquetados para manipular estructuras de datos tales como listas, pilas, colas, conjuntos y mapas clave – valor. Podríamos considerarlo como la librería de las estructuras dinámicas.

Las colecciones definen un conjunto de interfaces, clases genéricas y algoritmos que permiten manejar grupos de objetos, todo ello enfocado a potenciar la reusabilidad del software y facilitar las tareas de programación. Parecerá increíble el tiempo que se ahorra empleando colecciones y cómo se reduce la complejidad del software usándolas adecuadamente. Las colecciones permiten almacenar y manipular grupos de objetos que, a priori, están relacionados entre sí (aunque no es obligatorio que estén relacionados, lo lógico es que si se almacenan juntos es porque tienen alguna relación entre sí), pudiendo trabajar con cualquier tipo de objeto.

Collection es la interfaz raíz en la jerarquía de colecciones. Es decir, define los métodos básicos que permitirán manejar todos los tipos de colecciones. A partir de Collection se derivan otras estructuras de datos como:

De estas a su vez se derivan otras pero como se ha dicho todas ellas compartirán los métodos definidos en la estructura Collection que explican a continuación.

 

2.1. Interfaz básica

Las colecciones en Java parten de una serie de interfaces básicas. Cada interfaz define un modelo de colección y las operaciones que se pueden llevar a cabo sobre los datos almacenados, por lo que es necesario conocerlas.

La interfaz inicial, a través de la cual se han construido el resto de colecciones, es la interfaz java.util.Collection, que define las operaciones comunes a todas las colecciones derivadas.

A continuación se muestran las operaciones más importantes definidas por esta interfaz. Ten en cuenta que Collection es una interfaz genérica donde la letra E se utiliza para representar cualquier clase y al utilizarse se deberá sustituir por una clase concreta.

 

MétodoDescripción
int size()Devuelve el número de elementos de la colección.
boolean isEmpty()Devuelve true si la colección está vacía.
boolean contains(Object objeto)Devuelve true si la colección tiene el elemento pasado como parámetro.
boolean add(E elemento)Permitirá añadir elementos a la colección. Devuelve true si se añade correctamente.
boolean remove(Object objeto)Permitirá eliminar elementos de la colección. Devuelve true si se borra correctamente.
Iterator iterator()Permitirá crear un iterador para recorrer los elementos de la colección. Esto se ve más adelante, no te preocupes.
Object[] toArray()Permite pasar la colección a un array de objetos tipo Object
void clear()Vacía la colección

En todos los tipos de colecciones en Java dispondremos de estos métodos comunes más otros particulares dependiendo de sus funcionalidades. Más adelante veremos como se usan estos métodos.

 

2.2. Elegir una colección

En esta parte nos centraremos en trabajar con la colección ArrayList.

Sin embargo, cuando vayamos a desarrollar una nueva aplicación es importante tener en cuenta los siguientes puntos:

En función de las respuestas que demos, existirán colecciones que debido a su estructura y funcionamiento interno, serán más eficientes que otras y deberemos tenerlo en cuenta.

3. Listas

Las listas son una estructura de datos que nos recuerdan a los arrays pero que proporcionan mayor flexibilidad ya que podemos añadir y eliminar elementos sin preocuparnos por el tamaño de la lista. La lista crece según añadimos elementos y se reduce cuando los eliminarnos sin que nosotros tengamos que hacer nada al respecto. De echo, las listas son una de las estructuras de datos fundamentales que te vas ha encontrar en programación.

Sus características son las siguientes:

Para ello, además de los métodos heredados de Collection, añade métodos que permiten esas funcionalidades.

Dentro de las listas podemos encontrar ArrayList y LinkedList. Las 2 son muy parecidas de manejar estando su diferencia en la estructura y funcionamiento interno. Cuando la lista vaya a cambiar frecuentemente, es decir cuando tengamos que introducir elementos nuevos y borrar otros de forma habitual, las LinkedList serán más eficientes. Para la mayoría de las soluciones sin embargo, los ArrayLists son suficientes y por ello vamos a centrar esta UD en su manejo.

 

3.1. Métodos

En Java, para las listas se dispone de una interfaz llamada java.util.List, y dos implementaciones básicas, java.util.LinkedList y java.util.ArrayList, con diferencias significativas entre ellas.

Los métodos de la interfaz List, que obviamente estarán en todas las implementaciones, y que permiten las operaciones anteriores son:

MétodoDescripción
E get(int index)Permite obtener un elemento partiendo de su posición (index).
E set(int index, E element)Permite cambiar el elemento almacenado en una posición de la lista (index), por otro (element).
void add(int index, E element)Otra versión del método add. Inserta un elemento (element) en la lista en una posición concreta (index), desplazando los elementos siguientes.
E remove(int index)Otra versión del método remove. Elimina un elemento indicando su posición (index) en la lista.
boolean add(E element)Añade un elmento al final de la lista
void clear()Elimina todos los elementos de la lista
int size()Devuelve el número de elementos de una lista
String toString()Devuelve los elementos de una lista formateados como los arrays: ["hola", "kaixo", "agur", "adios"]
Object[] toArray()Devuelve un array con los elementos de la lista en el mismo orden.

Fíjate que las listas conservan los métodos de las colecciones (add, clear, size...) y de la clase Object (toString) y añade otras más para posibilitar las funcionalidades descritas.

Al igual que los arrays, los elementos de una lista empiezan a numerarse por 0. Es decir, que el primer elemento de la lista es el 0.

Recuerda también que List es una interfaz genérica, podemos crear listas con elementos de cualquier clase, por lo que <E> se corresponderá con la clase usada para crear esa lista.

Hay otros métodos que para funcionar correctamente necesitan encontrar un elemento en la lista. Funcionan con los tipos básicos, enteros, double, String.. pero no con el resto de objetos:

MétodoDescripción
E remove(Object o)Elimina un elemento indicando de la lista.
int indexOf(Object o)Permite conocer la primera aparición (índice) de un elemento. Si dicho elemento no está en la lista retornará -1.
boolean contains(Object o)Devuelve true si el objeto indicado está en la lista, false en caso contrario
int lastIndexOf(Object o)Permite conocer la última aparición (índice) de un elemento. Si dicho elemento no está en la lista retornará -1.

 

3.2. Uso de listas

Pues para usar una lista haremos uso de su implementación ArrayList. El siguiente ejemplo muestra como usar un ArrayList pero valdría también para LinkedList.

No olvides importar las clases java.util.LinkedList y java.util.ArrayList según sea necesario para poder utilizar estas clases.

En este ejemplo se usan los métodos de acceso posicional a la lista:

La lista ArrayList representa una familia de listas que se diferencian en la clase dede elemento que almacenan. Usaremos ArrayList para almacenar una lista de cadenas de caracteres. ArrayList guardará diferentes elementos todos ellos de la clase Punto y ArrayList será una lista de Clientes.

Fíjate que nunca podemos declarar algo de la clase ArrayList. Siempre tendremos que sustituir la E por la clase concreta que queremos utilizar.

En el ejemplo anterior, se realizan muchas operaciones, ¿cuál será el contenido de la lista al final? Pues será "Adios" y "Agur".

 

3.3. Otros tipos de listas

¿Y en qué se diferencia una LinkedList de una ArrayList? Los LinkedList utilizan listas doblemente enlazadas.

Las listas enlazadas sus elementos se encapsulan en los llamados nodos. Los nodos van enlazados unos a otros para no perder el orden y no limitar el tamaño de almacenamiento. Cuando queremos añadir un elemento al final solo tenemos que enlazarlo al último elemento. Para eliminar un elemento de una lista, solo hay que "puentearlo". Es decir, hay que cambiar el enlace del elemento anterior para que conecte directamente con el siguiente, dejando el elemento a borrar fuera de la lista.

Tener un doble enlace significa que en cada nodo se almacena la información de cuál es el siguiente nodo y también, información de cuál es el nodo anterior. Si un nodo no tiene nodo siguiente o nodo anterior, se almacena null para ambos casos.

 

Lista_doblemente_enlazada

 

En el caso de los ArrayList, éstos se implementan utilizando arrays que se van redimensionando conforme se necesita más espacio o menos. La redimensión es transparente a nosotros, no nos enteramos cuando se produce, pero eso redunda en una diferencia de rendimiento notable dependiendo del uso.

Los ArrayList son más rápidos en cuanto a acceso a los elementos. Acceder a un elemento según su posición es más rápido en un array que en una lista doblemente enlazada que exige recorrer la lista. En cambio, eliminar un elemento implica muchas más operaciones en un array que en una lista enlazada de cualquier tipo.

¿Y esto que quiere decir? Que si se van a realizar muchas operaciones de eliminación de elementos sobre la lista, conviene usar una lista enlazada (LinkedList), pero si no se van a realizar muchas eliminaciones, sino que solamente se van a insertar y consultar elementos por posición, conviene usar una lista basada en arrays redimensionados (ArrayList).

LinkedList tiene otras ventajas que nos puede llevar a su uso. Implementa las interfaces java.util.Queue y java.util.Deque. Dichas interfaces permiten hacer uso de las listas como si fueran una cola de prioridad o una pila, respectivamente.

Las colas, también conocidas como colas de prioridad, son una lista pero que aportan métodos para trabajar de forma diferente. ¿Recordáis una cola para que te atiendan en una ventanilla? Pues igual. Se trata de que el que primero llega es el primero en ser atendido (FIFO). Simplemente se aportan tres métodos nuevos:

Cola

Las pilas, mucho menos usadas, son todo lo contrario a las listas. Una pila es igual que una montaña de hojas en blanco, para añadir hojas nuevas se ponen encima del resto, y para retirar una se coge la primera que hay, encima de todas. En las pilas el último en llegar es el primero en ser atendido. Para ello se proveen de tres métodos:

 

Pila

 

ArrayList vs. LinkedList

La clase LinkedList es una colección que puede contener muchos objetos del mismo tipo, al igual que ArrayList.

La clase LinkedList tiene todos los mismos métodos que la clase ArrayList porque ambas implementan la interfaz List. Esto significa que puede agregar elementos, cambiar elementos, eliminar elementos y limpiar la lista de la misma manera.

Sin embargo, si bien la clase ArrayList y la clase LinkedList se pueden usar de la misma manera, se construyen de manera muy diferente.

Cómo funciona ArrayList

La clase ArrayList tiene una matriz regular dentro de ella. Cuando se agrega un elemento, se coloca en la matriz. Si la matriz no es lo suficientemente grande, se crea una matriz nueva, más grande, para reemplazar la anterior y se elimina la anterior.

Cómo funciona LinkedList

La LinkedList almacena sus elementos en "contenedores". La lista tiene un vínculo al primer contenedor y cada contenedor tiene un vínculo al siguiente contenedor en la lista. Para agregar un elemento a la lista, el elemento se coloca en un nuevo contenedor y ese contenedor se vincula a uno de los otros contenedores de la lista.

Cuándo se usa Use una ArrayList para almacenar y acceder a los datos, y LinkedList para manipularlos.

Métodos de LinkedList

En muchos casos, ArrayList es más eficiente, ya que es común necesitar acceso a elementos aleatorios en la lista, pero LinkedList proporciona varios métodos para realizar ciertas operaciones de manera más eficiente:

MétodoDescripción
addFirst()Agrega un elemento al principio de la lista.
addLast()Agregar un elemento al final de la lista
removeFirst()Eliminar un elemento del principio de la lista
removeLast()Eliminar un elemento del final de la lista
getFirst()Obtener el elemento al principio de la lista
getLast()Obtener el elemento al final de la lista
remove(index)Elimina el elemento de la posición indicada

Existen más métodos con los cuales trabajaremos poco a poco

 

 

3.4 Java List Sorting

Otra clase útil en el paquete java.util es la clase Collections, que incluye el método sort() para ordenar listas alfabéticamente o numéricamente. Sirven tanto para ArrayList como para LinkedList

Por defecto la ordenación es ascendente.

Veamos un ejemplo con los coches y otro con números.

 

4. Java HashMap

En el apartado de ArrayList, aprendiste que los Arrays almacenan elementos como una colección ordenada y que debes acceder a ellos con un número de índice (tipo int). Sin embargo, un HashMap almacena elementos en pares "clave/valor" y puedes acceder a ellos mediante un índice de otro tipo (por ejemplo, una cadena).

Un objeto se usa como clave (índice) para otro objeto (valor). Puede almacenar diferentes tipos: claves de tipo String y valores de tipo int, o el mismo tipo, como: claves de tipo String y valores de tipo String:

 

4.1 Recorrer un HashMap en bucle

Recorrer los elementos de un HashMap con un bucle for-each.

Nota: utilice el método keySet() si solo desea las claves y el método values() si solo desea los valores:

4.2 Otros tipos

Las claves y los valores de un HashMap son en realidad objetos. En los ejemplos anteriores, utilizamos objetos de tipo "String". Recuerda que un String en Java es un objeto (no un tipo primitivo). Para utilizar otros tipos, como int, debe especificar una clase contenedora equivalente: Integer. Para otros tipos primitivos, utilice: Boolean para boolean, Character para char, Double para double, etc.

Veamos un ejemplo:

5. Java HashSet

Un HashSet es una colección de elementos donde cada elemento es único y se encuentra en el paquete java.util:

Veamos una serie de ejemplos con la colección de coches utilizada con los ArrayList y los LinkedList:

Todo esto también lo podemos realizar con otras clases envolventes, Wrapper.

 

6. Trabajando con colecciones

Cuando trabajemos con colecciones hay una serie de aspectos que es importante tener en cuenta:

  1. ¿Cómo crear colecciones de datos de los tipos primitivos (int, double, char o boolean)?

  2. ¿Cómo recorrer una colección para trabajar con sus elementos?

  3. ¿Qué posibilidades ofrecen los métodos estáticos de las diferentes colecciones?

  4. Diferencias ente objetos mutables e inmutables

A continuación, profundizaremos en estos aspectos.

 

6.1. Clases Wrapper o envoltorio.

6.1.1. Introducción

¿Habéis probado a crear un ArrayList de números enteros? ¿Ha sido posible?

Seguramente al compilar se ha producido un error "unexpected type". Es decir, que el tipo int no era uno de los tipos esperados.

Si repasáis lo que hemos visto sobre las colecciones, veréis que son estructuras de datos que pueden almacenar elementos de cualquier tipo de clase. No nos dice nada de los tipos primitivos pero ya vemos que no los admite. Las colecciones son clase genéricas y pueden almacenar cualquier objeto o tipo referenciado (como las clases, arrays...). Los tipos primitivos (int, double, char o boolean) no se pueden usar como tipo de dato en las colecciones.

Entonces ¿qué hacemos si necesitamos almacenar números pero en una colección no podemos almacenar tipos primitivos? La respuestas son las denominadas clases Wrapper o envoltorio.

Wrapper o envoltorio es el calificativo que se da a unas clases especiales cuyo único objetivo es almacenar los tipo primitivos como clases. Es decir, son clases que tendrán un único atributo que coincidirá con el tipo y el valor del tipo primitivo. De esta manera cuando necesitemos trabajar con objetos podremos seguir manejando números, letras y boleanos.

Además, se les ha añadido una serie de métodos que pueden resultar especialmente útiles.

Las clases que necesitaremos para los tipos primitivos seras:

 

6.1.2. Métodos más usados.

Como el resto de clases tendrán sus constructores propios:

Además, las clases Wrapper proporcionan los siguientes métodos interesantes:

Métodos de instancia para extraer el dato numérico del envoltorio. xxxValue(). Permiten pasar de un objeto a un tipo primitivo. Se habla de "Unboxing".

Métodos estáticos de clase para crear números a partir de cadenas de caracteres. Xxx.parseXxx(String). Permiten leer texto por teclado o de un fichero y luego convertirlo a su tipo primitivo.

Las clases envoltorio y en especial los métodos parseXxx, son muy utilizados para leer datos tanto de ficheros como desde el teclado. Permiten leer todos los datos como texto con next, comprobar que cumplen un patrón concreto y después convertirlos al tipo adecuado.

Métodos estáticos de clase para crear envoltorios de números a partir de cadenas de caracteres. Xxx.valueOf(String). Pasamos de un texto a un objeto de una de las clases envoltorio. Se habla de "Boxing".

 

6.1.3. Manejando las colecciones.

Ya conocemos las clases envoltorio. ¿Cómo las usamos para crear colecciones?

Para crear una colección, las usaremos igual que lo hemos hecho con cualquier otra clase en Java:

 

A la hora de añadir y leer elementos podemos utilizar los métodos vistos en el apartado anterior:

La buena noticia es que a partir de la version 5 de Java este proceso lo realiza Java automáticamente y podemos escribir:

De esta manera, solo tendremos que usar la clase envoltorio para crear la colección. En el resto de operaciones podemos trabajar con los tipos primitivos directamente y Java se encargará de realizar las conversiones necesarias.

 

6.1.4.- Leer datos según un patrón

Hemos visto que las clases envoltorio permiten convertir un texto a int o double mediante los métodos parseInt y parseDouble. Pero, ¿podríamos comprobar si esa cadena de caracteres es realmente un número entero antes de convertirla para evitar que se produzca una excepción?

Para ello, podríamos utilizar las expresiones regulares o regex de Java.

Vamos a ver las características que tiene los números int. Son números de 32-bit que van del -231 al 231-1. Es decir toman valores comprendidos entre el -2147483648 y el 2147483647. Identificar todos estos valores con una expresión regular es difícil pero si los límitamos a valores entre -999999999 y +999999999 la cosa se simplifica. Estaríamos descartando algunos números enteros pero evitaríamos excepciones.

¿Cómo sería la expresión regular para ese rango de valores?

  1. Puede tener signo o no. Si lo tiene siempre será -. La expresión sería: -? que significa que el signo - puede aparecer o no.

  2. Todos los dígitos pueden tener valores entre 0 y 9. La expresión sería: [0-9] o \d que significa que los caracteres que pueden aparecer en la cadena son los dígitos del 0 al 9.

  3. Siempre debe aparecer al menos un dígito y como máximo 9. La expresión sería: {1,9} que significa que un carácter puede aparecer entre 1 y 9 veces.

Si las juntamos, la expresión completa será:

 

Es importante no dejar ningún espacio en blanco, ya que producirá un error al ajecutarse.

La forma de aplicar esta expresión a una cadena de caracteres será:

 

Las expresiones regulares las podemos usar también para comprobar que el texto introducido es un DNI válido, un correo electrónico, un télefono o una fecha. Conviene consultar si existe la expresión que queremos usar antes de empezar a diseñar una. Hay muchos ejemplos en Internet .

 

6.2.- Recorrer una colección

Para recorrer un array hemos usado el siguiente código basado en un bucle for:

 

Para ello, es indispensable que los elementos de la estructura de datos se referencien mediante un índice.

Una estructura parecida nos puede servir también para las listas pero no para el resto de colecciones.

Por ello, vamos a ver otras 2 maneras de recorrer colecciones expresamente diseñadas para ellas. Estas son:

  1. El bucle for-each

  2. La clase Iterator

 

6.2.1.- Bucle for-each

El bucle "for-each" o bucle "para cada", se parece mucho a un bucle for con la diferencia de que no hace falta una variable i de inicialización.

Existe a partir de Java 5 y en principio puede resultar más cómoda y compacta que el uso de la clase Iterator. Sin embargo, como veremos, tendrá sus limitaciones y en algunos casos deberemos recurrir obligatoriamente a los iteradores.

En el siguiente código se usa un bucle for-each, en el que texto va tomando los valores de todos los elementos almacenados en el conjunto hasta que llega al último. En este caso, no se necesita ningún índice para recorrer la estructura de datos. La sentencia for-each se encarga de pasar por cada uno de los elementos y guardarlo en texto. Fíjate que se llama for-each pero solo se escribe for:

 

La estructura for-each es muy sencilla: la palabra for seguida de "(tipoDatos nombre : estructura)" y el cuerpo del bucle.

Los bucles for-each se pueden usar para todas las colecciones y también para los arrays pero no permiten modificar la colección dentro del bucle. Es decir, obtenemos el valor de cada elemento, podemos trabajar con él pero no podríamos borrarlo. Para ello, habría que recurrir a la clase Iterator.

 

6.2.2.- Iteradores.

¿Qué son los iteradores? Son un mecanismo que nos permite recorrer todos los elementos de una colección de forma sencilla, de forma secuencial, y de forma segura.

Cuando queremos modificar una colección mientras la estamos recorriendo, en concreto cuando queremos borrar el último elemento que hemos procesado, necesitaremos utilizar iteradores. Además, los podemos encontrar en programas de versiones antiguas de Java, anteriores a la aparición del bucle for-each.

Ahora la pregunta es, ¿cómo se crea un iterador? Pues creando un objeto de la clase Iterator a partir de la colección que queremos recorrer. Es decir, invocando el método "iterator()" de cualquier colección.

Veamos un ejemplo en el que t es una colección cualquiera:

 

Fijate que se ha especificado un parámetro para el tipo de dato genérico en el iterador (poniendo "<String>" después de Iterator). Esto es porque los iteradores son también clases genéricas (podemos tener iteradores de cualquier clase), y es necesario especificar el tipo base que contendrá el iterador. Sino se especifica el tipo base del iterador, igualmente nos permitiría recorrer la colección, pero retornará objetos tipo Object (clase de la que derivan todas las clases), con lo que nos veremos obligados a forzar la conversión de tipo.

Para recorrer y gestionar la colección, el iterador ofrece tres métodos básicos:

¿Cómo recorreríamos una colección con estos métodos? Pues de una forma muy parecida a como leemos datos por teclado y un fichero. Un bucle mientras (while) con la condición hasNext() nos permite hacerlo:

 

¿Qué elementos contendría la lista después de ejecutar el bucle? Efectivamente, todas las palabras menos las que coinciden con "borrar".

Tenemos que pensar que las listas permiten acceso posicional a través de los métodos get y set, y acceso secuencial a través de iteradores, ¿cuál es para tí la forma más cómoda de recorrer todos los elementos? ¿Un acceso posicional a través un bucle "for (int i = 0; i < lista.size(); i++)" o un acceso secuencial usando un bucle "while (iterador.hasNext())"?

¿Qué inconvenientes tiene usar los iteradores sin especificar el tipo de objeto? En el siguiente ejemplo, se genera una lista con los números del 0 al 10. De la lista, se eliminan aquellos que son pares y solo se dejan los impares. En el primer ejemplo se especifica el tipo de objeto del iterador y en el segundo ejemplo no, observa el uso de la conversión de tipos en la línea 6.

 

Un iterador es seguro porque esta pensado para no sobrepasar los límites de la colección, ocultando operaciones más complicadas que pueden repercutir en errores de software. Pero realmente se convierte en inseguro cuando es necesario hacer la operación de conversión de tipos. Si la colección no contiene los objetos esperados, al intentar hacer la conversión, saltará una incomoda excepción. Usar genéricos aporta grandes ventajas, pero usándolos adecuadamente.

Si al final usas iteradores, y piensas eliminar elementos de la colección (e incluso de un mapa), debes usar el método remove del iterador y no el de la colección. Si eliminas los elementos utilizando el método remove de la colección, mientras estás dentro de un bucle de iteración, o dentro de un bucle for-each, los fallos que pueden producirse en tu programa son impredecibles. ¿Logras adivinar porqué se pueden producir dichos problemas?

Los problemas son debidos a que el método remove del iterador elimina el elemento de dos sitios: de la colección y del iterador en sí (que mantiene interiormente información del orden de los elementos). Si usas el método remove de la colección, la información solo se elimina de un lugar, de la colección.

 

6.2.3.- ListIterator vs Iterator.

Iterator y ListIterator son los dos de los tres cursores de Java. Tanto Iterator como ListIterator están definidos por Collection Framework en el paquete Java.Util . ListIterator es la interfaz secundaria de la interfaz Iterator. La principal diferencia entre Iterator y ListIterator es que Iterator puede atravesar los elementos de la colección solo en dirección hacia adelante, mientras que ListIterator puede atravesar los elementos en una colección tanto en dirección hacia adelante como hacia atrás .

Algunas diferencias más entre Iterator y ListIterator con la ayuda del cuadro de comparación que se muestra a continuación.

Bases para la comparaciónIteradorListIterator
BASICEl iterador puede atravesar los elementos en una colección solo en dirección hacia adelante.ListIterator puede atravesar los elementos de una colección tanto hacia delante como hacia atrás.
AñadirIterator no puede agregar elementos a una colección.ListIterator puede agregar elementos a una colección.
ModificarEl iterador no puede modificar los elementos de una colección.ListIterator puede modificar los elementos de una colección usando set ().
atravesarEl iterador puede atravesar Mapa, Lista y Conjunto.ListIterator solo puede atravesar objetos de lista.
ÍndiceIterator no tiene un método para obtener un índice del elemento en una colección.Usando ListIterator, puede obtener un índice del elemento en una colección.

 

ListIterator es una interfaz en un marco de Colección y extiende la interfaz Iterator . Usando ListIterator, puede recorrer los elementos de la colección en ambas direcciones hacia adelante y hacia atrás . También puede agregar, eliminar o modificar cualquier elemento de la colección. En resumen, podemos decir que elimina los inconvenientes del iterador.

Los métodos de ListIterator son los siguientes:

 

MétodoDescripción
hasNext ()si devuelve true, se confirma que hay más elementos en una colección.
next ()Devuelve los siguientes elementos de la lista.
nextIndex ()devuelve el índice de los siguientes elementos de la lista.
hasPrevious ()devuelve true si hay elementos en la dirección inversa en una colección.
previous ()Devuelve el elemento anterior en una colección.
previousIndex ()devuelve el índice del elemento anterior en una colección.
remove ()elimina el elemento de una colección.
set ()modifica el elemento en una colección.
add ()agrega el nuevo elemento en una colección.

Diferencias clave entre el iterador y el listador :

  1. La diferencia básica entre Iterator y ListIterator es que, al ser el cursor, Iterator puede atravesar elementos en una colección solo en dirección hacia adelante. Por otro lado, el ListIterator puede atravesar en ambas direcciones hacia adelante y hacia atrás.

  2. Usando iterador no puedes agregar ningún elemento a una colección. Pero, al usar ListIterator puedes agregar elementos a una colección.

  3. Usando Iterator, no puede eliminar un elemento de una colección donde, como Puede eliminar un elemento de una colección usando ListIterator.

  4. Usando Iterator puedes recorrer todas las colecciones como Mapa, Lista, Conjunto. Pero, mediante ListIteror, puede atravesar la lista de objetos implementados solamente.

  5. Puede recuperar un índice de un elemento utilizando Iterator. Pero como la Lista es secuencial y está basada en índices, puede recuperar un índice de un elemento utilizando ListIterator.

Por lo que podemos concluir que se puede usar ListIterator cuando tiene que atravesar particularmente un objeto List en dirección tanto hacia adelante como hacia atrás. De lo contrario, puede utilizar Iterator ya que admite todos los objetos de colección de tipos.

 

Anexo I.- Introducción a las excepciones

En Java los errores en tiempo de ejecución (cuando se esta ejecutando el programa) se denominan excepciones, y esto ocurre cuando se produce un error en alguna de las instrucciones de nuestro programa, como por ejemplo cuando se hace una división entre cero, cuando un objeto es 'null' y no puede serlo, cuando no se abre correctamente un fichero, etc. Cuando se produce una excepción se muestra en la pantalla un mensaje de error y finaliza la ejecución del programa.

En Java (al igual que en otros lenguajes de programación), existen mucho tipos de excepciones y enumerar cada uno de ellos seria casi una labor infinita. En lo referente a las excepciones hay que decir que se aprenden a base experiencia, de encontrarte con ellas y de saber solucionarlas.

Cuando en Java se produce una excepción se crear un objeto de una determina clase (dependiendo del tipo de error que se haya producido), que mantendrá la información sobre el error producido y nos proporcionará los métodos necesarios para obtener dicha información. Estas clases tienen como clase padre la clase Throwable, por tanto se mantiene una jerarquía en las excepciones. A continuación mostramos algunas de las clases para que nos hagamos una idea de la jerarquía que siguen las excepciones, pero existen muchísimas más excepciones que las que mostramos:

A continuación vamos a mostrar un ejemplo de como al hacer una división entre cero, se produce una excepción. Veamos la siguiente imagen en el que podemos ver un fragmento de código y el resultado de la ejecución del código:

 

Como vemos en nuestro programa tenemos 3 instrucciones. La primera debe de imprimir por pantalla el mensaje "ANTES DE HACER LA DIVISIÓN", la segunda debe de hacer la división y la última debe de imprimir por pantalla el mensaje "DESPUES DE HACER LA DIVISIÓN". La primera instrucción la ejecuta perfectamente, pero al llegar a la segunda se produce una "ArithmeticException" (excepción de la clase ArithmeticException) y se detiene la ejecución del programa ya que estamos dividiendo un número entre '0'.

Por suerte Java nos permite hacer un control de las excepciones para que nuestro programa no se pare inesperadamente y aunque se produzca una excepción, nuestro programa siga su ejecución. Para ello tenemos la estructura "try – catch – finally" que la mostramos a continuación:

 

Respecto a la estructura "try – catch – finally", se ha de decir que primero se ejecuta el bloque "try", si se produce una excepción se ejecuta el bloque "catch" y por último el bloque "finally". En esta estructura se puede omitir el bloque "catch" o el bloque "finally", pero no ambos.

Sabiendo esta estructura, podemos reescribir nuestro programa para que se ejecuten las tres instrucciones aunque se produzca una excepción. Previamente debemos de saber cual va a ser la clase de la excepción que puede aparecer que seria la "ArithmeticException" para definirla en la parte del "catch". Nuestro programa quedaría de la siguiente forma y se ejecutaría sin problema obteniendo también la información de la excepción:

 

Como vemos capturamos la excepción en un objeto "ex" de la clase "ArithmeticException" y podemos obtener el mensaje de error que nos da la excepción. Vemos también que el programa termina su ejecución aunque se haya producido una excepción.

Dentro de una misma estructura podemos definir todas las excepciones que queramos. En el caso anterior hemos definido solo la excepción "ArithmeticException"; pero por ejemplo, podemos definir también la excepción "NullPointerException", por si nos viene un valor a 'null' al hacer la división:

 

En resumen, hemos puesto en esta entrada un ejemplo muy sencillo para controlar un par de excepciones bastante obvias como la división entre '0' y un 'null', que perfectamente lo podríamos haber controlado con una sentencia de control "if" mirando el contenido de los atributos, pero la finalidad de esta entrada era ver como controlar las excepciones con la estructura "try – catch – finally", que si lo sabemos utilizar nuestro programa deberá seguir funcionando aunque se produzcan excepciones. Decir también que es casi imposible aprenderse todas las excepciones que hay en Java, ya que estas las iréis aprendiendo según os las vayáis encontrando en vuestros desarrollos. Estas que os hemos mostrados son bastante comunes al igual que las que os podéis encontrar con el tratamiento de ficheros, de arrays, etc.

 

Anexo II.- Manejo de fechas

Para trabajar con fechas vamos a utilizar la clase LocalDateTime para lo que necesitamos la librería java.time.LocalDateTime.

Lo primero que vamos a hacer es crear la fecha y hora actual y mostrarla por consola:

 

Lo que veremos es:

Cómo se puede observar nos va a mostrar año, mes y día así como las horas, minutos, segundos y milisegundos.

 

 

 

En ocasiones no nos va a interesar la hora, sólo la fecha. Para ello haremos uso de la clase LocalDate que se encuentra en el paquete java.time.LocalDate.

y muestra lo siguiente:

Año, mes y día.

 

 

Mostrar la hora actual

Para mostrar la hora actual (hora, minuto, segundo y nanosegundos), importe la clase java.time.LocalTime y utilice su método now():

Crear un tipo de datos de tipo Fecha

Formato de fecha y hora

La "T" en el ejemplo anterior se utiliza para separar la fecha de la hora. Puede utilizar la clase DateTimeFormatter con el método ofPattern() en el mismo paquete para formatear o analizar objetos de fecha y hora. El siguiente ejemplo eliminará tanto la "T" como los nanosegundos de la fecha y hora:

El método ofPattern() acepta todo tipo de valores, si desea mostrar la fecha y la hora en un formato diferente. Por ejemplo:

ValorResultado
yyyy-MM-dd1988-09-29
dd/MM/yyyy29/09/1988
dd-MMM-yyyy29-Sep-1988
E, MMM dd yyyyThu, Sep 29 1988

 

Comparar objetos Date: compareTo.

Uno de los métodos más útiles para comparar fechas es el método compareTo.

Es un método de la clase Date que devuelve un entero.

Por ejemplo:

 

En este caso fecha, 01-01-2020, es más antigua que fecha, 20-03-2024, y el resultado será negativo.

Lo que veremos es:

 

Formato de fecha y hora

La "T" en el ejemplo anterior se utiliza para separar la fecha de la hora. Puede utilizar la clase DateTimeFormatter con el método ofPattern() en el mismo paquete para formatear o analizar objetos de fecha y hora. El siguiente ejemplo eliminará tanto la "T" como los nanosegundos de la fecha y hora:

 

Resultado es:

El método ofPattern() acepta todo tipo de valores, si desea mostrar la fecha y la hora en un formato diferente. Por ejemplo:

 

ValorEjemplo
yyyy-MM-dd"1988-09-29"
dd/MM/yyyy"29/09/1988"
dd-MMM-yyyy"29-Sep-1988"
E, MMM dd yyyy"Thu, Sep 29 1988"

Como se observa, para incluir texto dentro del formato hay que usar las comillas simples.

Las letras principales para definir el formato se muestran a continuación y dependiendo del número de veces que aparezca cambiará el valor que se muestre:

 

FormatoDescripción
yAño. Por ejemplo: yyyy --> 2018
MMes. Por ejemplo: MM --> 01 o MMMM --> enero
dDía. Por ejemplo: dd --> 24
EDía de la semana. Por ejemplo: EEEE --> jueves
hHora AM/PM (1-12). Por ejemplo: hh --> 04
mMinutos. Por ejemplo: mm --> 34
sSegundos. Por ejemplo: ss --> 25
HHora 0-23. Por ejemplo: hh --> 16
aAM/PM
zZona horaria. Por ejemplo: zzz --> CET (Central European Time)

Prestad especial atención a cómo queremos formatear las horas. No es lo mismo hh:mm:ss que HH:mm:ss.

Si usamos la "h" mínuscula, las horas se mostrarán de la 1 a la 12 mientras que si usamos la "H" mayúscula, las horas se mostrarán de la 0 a la 23.

 

Anexo III: Formato decimal en Java

La clase DecimalFormat de java nos permite mostrar los números en pantalla con el formato que queramos, por ejemplo, con dos decimales, con una coma para separar los decimales, etc. DecimalFormat también es útil para presentar un número o recoger y reconstruir el número. Veamos unos ejemplos.

 

Redondear decimales, por ejemplo, dos decimales

Antes de nada, dejar claro que por ejemplo, usar dos decimales en java, no es algo que se haga con las variables o el código. En código se usa double o float con todos sus decimales y lo único que se hace es, a la hora de presentarlo en pantalla, imprimirlo o mostrarlo en alguna ventana de nuestra aplicación, darle el formato que queramos, el formato de dos decimales, con separador de miles y coma decimal, o el que queramos. Para ello, la clase DecimalFormat de java es la clase que debemos usar.

Un uso simple de DecimalFormat puede ser este

 

Hemos cogido un número con muchos decimales y lo hemos redondeado a dos decimales para sacarlo por pantalla. Para ello, sólo hemos tenido que indicar la máscara ####.## en el constructor de la clase DecimalFormat. Las # representan una cifra, así que hemos puesto que el número se ponga en pantalla con cuatro cifras, un punto decimal y dos decimales.

En la API de DecimalFormat podemos ver todos los posibles caracteres que admite la máscara.

Si usamos ceros en vez de #, los huecos se rellenarán con ceros por delante.

 

Al poner 0 en vez de #, las cifras que falten por delante o por detrás, se pondrán con ceros. Es una forma, por ejemplo, de obligar a presentar con dos decimales aunque el número sólo tenga uno o ninguno.

Por supuesto, podemos poner # en la parte entera y 0 en la parte decimal, para que salgan dos decimales aunque sean ceros, pero no se rellenen por delante los ceros de la parte entera.

 

Mostrar porcentajes

Una característica curiosa, es que si usamos en la máscara el signo de porcentaje %, el número se multiplicará automáticamente por 100 al presentarlo en pantalla.

 

Redondeo al fijar el número de decimales

Como hemos visto al principio, con DecimalFormat podemos indicar cuántos decimales queremos mostrar en la salida. Un detalle a tener en cuenta es que la clase DecimalFormat es lo suficientemente lista como para redondear, es decir, incrementar en uno el último decimal visible si es necesario. Por ejemplo, si queremos dos decimales, el número 1.2345 se verá como 1.23, pero si la cifra del tercer decimal es 5 o más, como por ejemplo en 1.2356, entonces el resultado será 1.24. El siguiente código nos lo muestra:

 

Puntos decimales y separador de miles : DecimalFormatSymbols

La clase DecimalFormat usa por defecto el formato para el lenguaje que tengamos instalado en el ordenador. Es decir, si nuestro sistema operativo está en español, se usará la coma para los decimales y el punto para los separadores de miles. Si estamos en inglés, se usará el punto decimal.

Una opción para cambiar esto, es crear una clase DecimalFormatSymbols que vendrá rellena con lo del idioma por defecto, y cambiar en ella el símbolo que nos interese. Por ejemplo, si estamos en español y queremos usar el punto decimal en vez de la coma, podemos hacer esto

 

En la API de DecimalFormatSymbols puedes ver qué más símbolos se pueden cambiar.

También es posible coger el DecimalFormaSymbols de alguna localización concreta que nos interese y modificar o no lo que haga falta. Por ejemplo, si nos interesa que la coma decimal sea un punto en vez de una coma, podríamos coger el DecimalFormatSymbols de Inglaterra

 

Aunque, ojo, esto cambia todo, también cosas como la moneda (libras esterlinas o euros), etc.

 

Reconstruir el número

Si suponemos que un usuario escribe un número, podemos leerlo y reconstruirlo con DecimalFormat:

 

Anexo IV.- Expresiones regulares

¿Qué es una expresión regular?

Una expresión regular es una secuencia de caracteres que forma un patrón de búsqueda. Cuando busca datos en un texto, puede utilizar este patrón de búsqueda para describir lo que está buscando.

Una expresión regular puede ser un solo carácter o un patrón más complicado.

Las expresiones regulares se pueden utilizar para realizar todo tipo de operaciones de búsqueda y reemplazo de texto.

Java no tiene una clase de expresión regular incorporada, pero podemos importar el paquete java.util.regex para trabajar con expresiones regulares. El paquete incluye las siguientes clases:

 

 

¿Tienen algo en común todos los números de DNI y de NIE? ¿Podrías hacer un programa que verificara si un DNI o un NIE es correcto? Seguro que sí. Si te fijas, los números de DNI y los de NIE tienen una estructura fija: X1234567Z (en el caso del NIE) y 1234567Z (en el caso del DNI). Ambos siguen un patrón que podría describirse como: una letra inicial opcional (solo presente en los NIE), seguida de una secuencia numérica y finalizando con otra letra. ¿Fácil no?

Pues esta es la función de las expresiones regulares: permitir comprobar si una cadena sigue o no un patrón preestablecido. Las expresiones regulares son un mecanismo para describir esos patrones, y se construyen de una forma relativamente sencilla. Existen muchas librerías diferentes para trabajar con expresiones regulares, y casi todas siguen, más o menos, una sintaxis similar, con ligeras variaciones. Dicha sintaxis nos permite indicar el patrón de forma cómoda, como si de una cadena de texto se tratase, en la que determinados símbolos tienen un significado especial. Por ejemplo "[01]+" es una expresión regular que permite comprobar si una cadena conforma un número binario.

Veamos cuáles son las reglas generales para construir una expresión regular:

Con las reglas anteriores podemos indicar el conjunto de símbolos que admite el patrón y el orden que deben tener. Si una cadena no contiene los símbolos especificados en el patrón, en el mismo orden, entonces la cadena no encajará con el patrón. Veamos ahora como indicar repeticiones:

 

Veamos una serie de ejemplos:

Ejemplo explicado En este ejemplo, se busca la secuencia de palabras "tres tristes tigres" en una texto.

Primero, se crea el patrón utilizando el método Pattern.compile(). El primer parámetro indica qué patrón se está buscando y el segundo parámetro tiene un indicador que indica que la búsqueda no debe distinguir entre mayúsculas y minúsculas. El segundo parámetro es opcional.

El método matcher() se utiliza para buscar el patrón en una cadena. Devuelve un objeto Matcher que contiene información sobre la búsqueda realizada.

El método find() devuelve verdadero si se encontró el patrón en la cadena y falso si no se encontró.

Flags Los flags del método compile() cambian la forma en que se realiza la búsqueda. A continuación, se muestran algunas de ellas:

Patrones de expresiones regulares

El primer parámetro del método Pattern.compile() es el patrón. Describe lo que se busca.

Los corchetes se utilizan para buscar un rango de caracteres:

ExpresiónDescripción
[abc]Encuentra un carácter de las opciones entre corchetes.
[^abc]Encuentra un caracter que NO esté entre corchetes
[0-9]Encuentra un carácter del rango 0 a 9

Metacaracteres

Los metacaracteres son caracteres con un significado especial:

MetacarácterDescripción
|Encuentra una coincidencia para cualquiera de los patrones separados por
.Encuentra solo una instancia de cualquier carácter
^Encuentra una coincidencia como el comienzo de una cadena como en: ^Hola
$Encuentra una coincidencia al final de la cadena como en: World$
\dEncuentra un dígito
\sEncontrar un carácter de espacio en blanco
\bBusque una coincidencia al principio de una palabra como esta: \bPALABRA, o al final de una palabra como esta: PALABRA\b
\uxxxxBusque el carácter Unicode especificado por el número hexadecimal xxxx

Cuantificadores

Los cuantificadores definen cantidades:

CuantificadorDescripción
n+Coincide con cualquier cadena que contenga al menos un n
n*Coincide con cualquier cadena que contenga cero o más ocurrencias de n
n?Coincide con cualquier cadena que contenga cero o una ocurrencia de n
n{x}Coincide con cualquier cadena que contenga una secuencia de X n's
n{x,y}Coincide con cualquier cadena que contenga una secuencia de X a Y n's
n{x,}Coincide con cualquier cadena que contenga una secuencia de al menos X n's

 

Uso de expresiones regulares I

¿Y cómo uso las expresiones regulares en un programa? Pues de una forma sencilla. Para su uso, Java ofrece las clases Pattern y Matcher contenidas en el paquete java.util.regex.*. La clase Pattern se utiliza para procesar la expresión regular y "compilarla", lo cual significa verificar que es correcta y dejarla lista para su utilización. La clase Matcher sirve para comprobar si una cadena cualquiera sigue o no un patrón. Veamoslo con un ejemplo:

 

En el ejemplo, el método estático compile de la clase Pattern permite crear un patrón, dicho método compila la expresión regular pasada por parámetro y genera una instancia de Pattern (p en el ejemplo). El patrón p podrá ser usado múltiples veces para verificar si una cadena coincide o no con el patrón, dicha comprobación se hace invocando el método matcher, el cual combina el patrón con la cadena de entrada y genera una instancia de la clase Matcher (m en el ejemplo). La clase Matcher contiene el resultado de la comprobación y ofrece varios métodos para analizar la forma en la que la cadena ha encajado con un patrón:

Veamos algunas construcciones adicionales que pueden ayudarnos a especificar expresiones regulares más complejas:

 

Uso de expresiones regulares II

¿Te resultan difíciles las expresiones regulares? Al principio siempre lo son, pero no te preocupes. Hasta ahora has visto como las expresiones regulares permiten verificar datos de entrada, permitiendo comprobar si un dato indicado sigue el formato esperado: que un DNI tenga el formato esperado, que un email sea un email y no otra cosa, etc. Pero ahora vamos a dar una vuelta de tuerca adicional.

Los paréntesis, de los cuales no hemos hablado hasta ahora, tienen un significado especial, permiten indicar repeticiones para un conjunto de símbolos, por ejemplo: "(#[01]){2,3}". En el ejemplo anterior, la expresión "#[01]" admitiría cadenas como "#0" o "#1", pero al ponerlo entre paréntesis e indicar los contadores de repetición, lo que estamos diciendo es que la misma secuencia se tiene que repetir entre dos y tres veces, con lo que las cadenas que admitiría serían del estilo a: "#0#1" o "#0#1#0".

Pero los paréntesis tienen una función adicional, y es la de permitir definir grupos. Un grupo comienza cuando se abre un paréntesis y termina cuando se cierra el paréntesis. Los grupos permiten acceder de forma cómoda a las diferentes partes de una cadena cuando esta coincide con una expresión regular. Lo mejor es verlo con un ejemplo (seguro que te resultará familiar):

 

Usando los grupos, podemos obtener por separado el texto contenido en cada uno de los grupos. En el ejemplo anterior, en el patrón hay tres grupos: uno para la letra inicial (grupo 1), otro para el número del DNI o NIE (grupo 2), y otro para la letra final o letra NIF (grupo 3). Al ponerlo en grupos, usando el método group(), podemos extraer la información de cada grupo y usarla a nuestra conveniencia.

Ten en cuenta que el primer grupo es el 1, y no el 0. Si pones m.group(0) obtendrás una cadena con toda la ocurrencia o coincidencia del patrón en la cadena, es decir, obtendrás la secuencia entera de símbolos que coincide con el patrón.

En el ejemplo anterior se usa el método find, éste buscará una a una, cada una de las ocurrencias del patrón en la cadena. Cada vez que se invoca, busca la siguiente ocurrencia del patrón y devolverá true si ha encontrado una ocurrencia. Si no encuentra en una iteración ninguna ocurrencia es porque no existen más, y retornará false, saliendo del bucle. Esta construcción while es muy típica para este tipo de métodos y para las iteraciones, que veremos más adelante.

Lo último importante de las expresiones regulares que debes conocer son las secuencias de escape. Cuando en una expresión regular necesitamos especificar que lo que tiene que haber en la cadena es un paréntesis, una llave, o un corchete, tenemos que usar una secuencia de escape, dado que esos símbolos tienen un significado especial en los patrones. Para ello, simplemente antepondremos "\\" al símbolo. Por ejemplo, "\\(" significará que debe haber un paréntesis en la cadena y se omitirá el significado especial del paréntesis. Lo mismo ocurre con "\\[", "\\]", "\\)", etc. Lo mismo para el significado especial del punto, éste, tiene un significado especial (¿Lo recuerdas del apartado anterior?) salvo que se ponga "\\.", que pasará a significar "un punto" en vez de "cualquier carácter". La excepción son las comillas, que se pondrían con una sola barra: "\"".