Herramientas de usuario

Herramientas del sitio


clase:daw:prog:1eval:collections

07 - Arrays, colecciones y maps

Hasta ahora, hemos usado variables simples (números y texto). En Java, existen otro tipo de variables que agrupan datos estructurados: arrays, colecciones y maps.

Arrays

Los Arrays permiten almacenar una colección de objetos o datos del mismo tipo. Son muy útiles y su utilización es muy simple:

  • Declaración del array: Consiste en definir el tipo de datos que contendrá y el nombre: tipo[] nombre. El tipo será un tipo de variable o una clase ya existente, de la cual se quieran almacenar varias unidades.
  • Creación del array: La creación de un array consiste en decir el tamaño que tendrá el array, es decir, el número de elementos que contendrá: nombre=new tipo[dimension], donde dimensión es un número entero positivo que indicará el tamaño del array. Una vez creado el array, éste no podrá cambiar de tamaño.

        int[] numbers;

        numbers = new int[10];

También podemos definir un array poniendo los corchetes al final del nombre:

int numbers[];

Se pueden agrupar las dos instrucciones anteriores en una:

int[] numbers = new int[10];

Una vez hecho esto, ya podemos almacenar valores en cada una de las posiciones del array, usando corchetes e indicando en su interior la posición en la que queremos leer o escribir, teniendo en cuenta que la primera posición es la cero y la última el tamaño del array menos uno. En el ejemplo anterior, la primera posición sería la 0 y la última sería la 9.

numbers[3] = 10;

Para acceder a los datos de un array, lo hacemos de forma similar. Simplemente poniendo el nombre del array y la posición a la cual se quiere acceder entre corchetes:

        int number;

        number = numbers[3];

Los arrays disponen de una propiedad pública muy útil: length, la cual nos permite saber el tamaño de cualquier array. Usando esta propiedad podemos iterar sobre el array accediendo a todos sus elementos:

System.out.println(numbers.length);

10

Usando esta propiedad, podemos incializar un array (rellenar sus valores) utilizando un bucle for, por ejemplo:

        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i + 3;
        }

Otra forma de inicializar un array es definiendo sus valores como una lista separada por comas encerrada entre llaves. Ésto lo debemos hacer a la hora de la declaración del array, y no hace falta indicarle la longitud, ya que se lo indicamos con el número de elementos que le pasemos:

int[] numbers = {1, 3, 312, 15, 69, 7, 8, 9, 0, 1};

Para acceder a los elementos de un array, en Java tenemos otra forma más sencilla: el bucle for-each. Este bucle es muy parecido al for, pero no hace falta calcular el tamaño del array con la propiedad length, ya lo hace Java por nosotros:

        for (int elemento : numbers) {
            System.out.println(elemento);    
        }

Cuando lleguemos el tema (programación funcional) veremos una forma más elegante para recorrer y operar conjuntos de datos (arrays, colecciones, maps…)

Arrays multidimensionales

En los ejemplos anteriores hemos utilizado arrays de una sola dimensión, pero podemos crear arrays de cualquier dimensión. Por ejemplo, podemos crear una matriz (array de dos dimensiones) de 5×5 de enteros aumentando las dimensiones del array:

int[][] m = new int[5][5];

Para acceder a los elementos de un array multidimensional habrá que indicar su posición en las dos dimensiones, teniendo en cuenta que los índices de cada una de las dimensiones empieza a numerarse en 0 y que la última posición es el tamaño de la dimensión en cuestión menos 1.

m[0][3] = 19;

Para inicializar un array multidimensional podemos usar el mismo método que con los arrays unidimensionales, teniendo en cuenta que necesitamos encadenar bucles for, ya que debemos recorrer todas las dimensiones de nuestro array:

        for (int i = 0; i < m.length; i++) {
            for (int j = 0; j < m[i].length; j++) {
                m[i][j] = j + 1;
            }  
        }

Igual que con los arrays unidimensionales, podemos inicializar un array multidimensional a la hora de definirlo:

        int[][] m = {{3, 5, 6}, {2, 4, 9}, {10, 2, 1}};

Para acceder a los elementos de un array multidimensional, lo podemos hacer de forma similar a los arrays unidimensionales:

        for (int[] elemento_fila : m) {
            for (int elemento_columna : elemento_fila) {
                System.out.println(elemento_columna);
            }       
        }

El array anterior es un ejemplo de array multidimensional regular, ya que es un array que contiene, a su vez, arrays de números del mismo tamaño. Podemos crear arrays multidimensionales irregulares si hacemos que en las dimensiones posteriores contengan arrays de distinto tamaño entre sí:

        int[][] irregular = new int[3][];

        irregular[0] = new int[5];
        irregular[1] = new int[7];
        irregular[2] = new int[8];

Paso de arrays como parámetro de una función

Fíjate en el siguiente código:

class PasoArrayParametro {

    static void inicilizaArray(int[] array) {
        for (int i = 0; i < array.length; i++) {
            array[i] = i + 5;
        }
    }
    public static void main(String[] args) {
        int[] numbers = new int[3];

        for (int i : numbers) {
            System.out.print(i + " ");
        }
        System.out.println();
        System.out.println("----");
        inicilizaArray(numbers);
        for (int i : numbers) {
            System.out.print(i + " ");
        }
        System.out.println("");
    }

}

Si lo ejecutamos, vemos la siguiente salida:

0 0 0 
----
5 6 7 

Cuando vimos el paso de parámetros a funciones, dijimos que en Java todos los parámetros se pasan por valor. Entonces, ¿por qué se ha modificado el contenido del array? Si el paso de parámetros es por valor, se debería habar pasado una copia del array y el array original no se debería haber modificado.

Cuando pasamos un array a un método, en lugar de copiar todos los elementos desde la zona de memoria del programa principal a la zona de memoria del método en cuestión, lo que se copia únicamente es la referencia o la dirección de memoria donde comienza el array. Por tanto, es como si el parámetro se pasara por referencia, al modificar los elementos del parámetro en el método, automáticamente quedarán modificados en la zona de memoria del programa principal. Es decir, estaremos modificando los datos originales.

El hecho de que los arrays se pasen por referencia tiene sentido, puesto que, si el número de elementos de un array es muy grande, la acción de realizar la copia de todos los elementos a la zona de memoria del método, tendría un coste espacial y temporal muy alto. Es decir, tendríamos que utilizar memoria y tiempo extra para realizar esta copia.

Ésto no solo ocurre con los arrays. En realidad, casi cualquier dato estructurado (variables que no sean simples como números y texto) funciona como lo descrito anteriormente cuando lo pasamos como parámetro a una función

Los arrays almacenan un número de elementos del mismo tipo que definimos cuando lo declaramos. ¿Qué pasa si queremos aumentar el tamaño de un array después de definirlo? Para hacerlo, deberíamos crear otro array con una dimensión mayor y copiar los elementos del primero en el segundo. Aunque esto funcionaría, tenemos otro tipo de datos estructurados que nos simplifican mucho esa (y otra) tarea: collections y maps.

Colecciones

Las colecciones (collections) en Java representan un grupo de objetos. Algunas colecciones permiten duplicados y otras no, igual que algunas están ordenadas y otras no.

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.

Entenderás lo que es una interfaz cuando lleguemos a la POO. Por ahora, lo único que tienes que tener en cuenta es que una interfaz define una serie de métodos (operaciones) que tienen todas las clases que deriven de ella (implementen la interfaz).

La interfaz inicial, a través de la cual se han construido el resto de colecciones, es la interfaz 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:

  • int size(): retorna el número de elementos de la colección.
  • boolean isEmpty(): retorna verdadero si la colección está vacía.
  • boolean contains (Object element): retorna verdadero si la colección tiene el elemento pasado como parámetro.
  • boolean add(E element): añade elementos a la colección.
  • boolean remove (Object element): elimina elementos de la colección.
  • Object[] toArray(): pasa la colección a un array de objetos tipo Object.
  • void clear(): vacía la colección.

En el enlace anterior tienes todas las operaciones (métodos) que tiene definida la interfaz java.util.Collection.

Conjuntos (set)

Los conjuntos son un tipo de colección que NO admite duplicados, derivados del concepto matemático de conjunto. La interfaz Set define cómo deben ser los conjuntos, y extiende la interfaz Collection , aunque no añade ninguna operación nueva. Las implementaciones (clases genéricas que implementan la interfaz Set) más usadas son las siguientes:

  • HashSet: Conjunto que almacena los objetos usando tablas hash, lo cual acelera enormemente el acceso a los objetos almacenado. Inconvenientes: necesitan bastante memoria y no almacenan los objetos de forma ordenada (al contrario, pueden aparecer completamente desordenados).
  • LinkedHashSet: Conjunto que almacena objetos combinando tablas hash, para un acceso rápido a los datos, y listas enlazadas para conservar el orden. El orden de almacenamiento es el de inserción, por lo que se puede decir que es una estructura ordenada a medias. Inconvenientes: necesitan bastante memoria y es algo más lenta que HashSet.
  • TreeSet: Conjunto que almacena los objetos usando unas estructuras conocidas como árboles rojo-negro. Son más lentas que los dos tipos anteriores. pero tienen una gran ventaja: los datos almacenados se ordenan por valor. Es decir, aunque se inserten los elementos de forma desordenada, internamente se ordenan dependiendo del valor de cada uno.
Una tabla hash es una estructura de datos que asocia llaves o claves con valores. La operación principal que soporta de manera eficiente es la búsqueda: permite el acceso a los elementos (teléfono y dirección, por ejemplo) almacenados a partir de una clave generada (usando el nombre o número de cuenta, por ejemplo). Funciona transformando la clave con una función hash en un hash, un número que identifica la posición donde la tabla hash localiza el valor deseado.

Veamos un ejemplo de uso básico de la estructura HashSet. Para crear un conjunto, simplemente creamos el HashSet indicando el tipo de objeto que va a almacenar (no olvides hacer la importación de java.util.HashSet primero):

HashSet<Integer> conjunto = new HashSet<Integer>();

Una práctica habitual es definir el tipo de datos conjunto como la interfaz genérica (Set), así, si queremos cambiar el tipo de Set sólo tendríamos que cambiarlo en la definición:

Set<Integer> conjunto = new HashSet<>();

Existen varias formas de inicializar un conjunto. Por ejemplo, podemos ir añadiendo elementos uno a uno con el método add():

        conjunto.add(2);
        conjunto.add(10);
        conjunto.add(3);
        conjunto.add(23);
        conjunto.add(99);
}

Otra forma más cómoda de inicializar un conjunto (válido a partir de Java 9) es utilizar el método of():

conjunto = Set.of(2, 10, 3, 23, 99);

Si inicializamos un conjunto con el método of(), el conjunto que se crea es inmutable, es decir, no podemos cambiarlo, con lo que no podremos añadir, eliminar ni modificar elementos.

Para recorrer un HashSet podemos utilizar, igual que hicimos con los arrays un bucle for-each:

        for (Integer number : conjunto) {
            System.out.println(number);
        }

2
3
99
23
10

¿Qué pasa si intentamos añadir un elemento repetido? Vamos a probar:

        conjunto.add(2);
        conjunto.add(10);
        conjunto.add(3);
        conjunto.add(23);
        conjunto.add(99);
        conjunto.add(10);


        for (Integer number : conjunto) {
            System.out.println(number);
        }

2
3
99
23
10

Como ves, el dato repetido no se añade al HashSet y el compilador no lanza ningún error. Si te fijas en el método add, éste devuelve un boolean. Devolverá true si el elemento se ha añadido o false si no se ha añadido:

        conjunto.add(2);
        conjunto.add(10);
        conjunto.add(3);
        conjunto.add(23);
        conjunto.add(99);
        
        if(!conjunto.add(10)) {
            System.out.println("El número ya está en la lista");
        }


        for (Integer number : conjunto) {
            System.out.println(number);
        }

El número ya está en la lista
2
3
99
23
10

Listas (list)

Las listas son elementos de programación un poco más avanzados que los conjuntos. Las principales diferencias con respecto a las colecciones son:

  • Las listas SI pueden almacenar duplicados. Si no queremos duplicados, hay que verificar manualmente que el elemento no esté en la lista antes de su inserción.
  • Acceso posicional: Podemos acceder a un elemento indicando su posición en la lista.
  • Búsqueda. Es posible buscar elementos en la lista y obtener su posición. En los conjuntos, al ser colecciones sin ordenar, solo se podía comprobar si contenía o no un elemento de la lista, retornando true o false.
  • Extracción de sublistas: Es posible obtener una lista que contenga solo una parte de los elementos de forma muy sencilla.

En Java, para las listas se dispone de una interfaz llamada List , y varias implementaciones, entre las que destacan LinkedList y ArrayList.

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

  • E get(int index): Obtiene un elemento partiendo de su posición (index).
  • E set(int index, E element): Cambia el elemento almacenado en una posición de la lista (index), por otro (element).
  • void add(int index, E element): Inserta un elemento (element) en la lista en una posición concreta (index), desplazando los existentes. Si le pasamos solo el elemento (element) la inserción la hará al final de la lista.
  • E remove(int index): Elimina un elemento indicando su posición (index) en la lista.
  • boolean addAll(int index, Collection<? extends E> c): Inserta una colección pasada por parámetro en una posición de la lista, desplazando el resto de elementos.
  • int indexOf(Object o): Devuelve la posición (índice) de un elemento en la lista o -1 si el elemento no está en la lista.
  • int lastIndexOf(Object o): Devuelve la última ocurrencia del objeto en la lista (dado que la lista si puede almacenar duplicados) o -1 si el elemento no está en la lista.
  • List<E> subList(int from, int to): Genera una sublista (una vista parcial de la lista) con los elementos comprendidos entre la posición inicial (from, incluida) y la posición final (to, no incluida).

Ten en cuenta que los elementos de una lista empiezan a numerarse por 0. Es decir, que el primer elemento de la lista es el 0. Ten en cuenta también que List es una interfaz genérica, por lo que <E> corresponde con el tipo base usado como parámetro genérico al crear la lista.

Las listas se utilizan de forma muy parecida a los conjuntos. Veamos un ejemplo de creación y utilización de un ArrayList:

        List<Integer> lista = new ArrayList<>();


        lista.add(1); // Añade un elemento al final de la lista.
        lista.add(3); // Añade otro elemento al final de la lista.
        lista.add(1,2); // Añade en la posición 1 el elemento 2.
        lista.add(lista.get(1)+lista.get(2)); // Suma los valores contenidos en la posición 1 y 2, y lo agrega al final.
        lista.remove(0); // Elimina el primer elementos de la lista.
        for (Integer elemento: lista)
            System.out.println("Elemento:" + elemento); // Muestra la lista.
        }    
        

Elemento:2
Elemento:3
Elemento:5

Igual que con los conjuntos, podemos usar el método of() para crear listas inmutables:

        lista = List.of(1, 3, 5, 67);

Vamos a ver otro ejemplo de como utilizar indexOf para encontrar el primer elemento en una lista y sustituirlo:

        lista.clear();

        lista.add(1); 
        lista.add(2); 
        lista.add(3); 
        lista.add(2);
        lista.set(lista.indexOf(2), 20);
        for (Integer elemento: lista) {
            System.out.println("Elemento:" + elemento); // Muestra la lista.
        }

Elemento:1
Elemento:20
Elemento:3
Elemento:2

¿En qué se diferencia un LinkedList de un ArrayList? Los LinkedList utilizan listas doblemente enlazadas. Los elementos de la lista 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. Tener un doble enlace significa que en cada nodo se almacena la información de cuál es el siguiente nodo y además, de cuál es el nodo anterior. Si un nodo no tiene nodo siguiente o nodo anterior, se almacena null (o nulo) para ambos casos.

Los ArrayList 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, ya que en esta última hay que 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).

Maps

Los maps son un tipo de array especial (llamado asociativo) que permite almacenar pares de valores conocidos como clave y valor. La clave se utiliza para acceder al valor, como una entrada de un diccionario permite acceder a su definición.

En Java existe la interfaz Map, que define los métodos que deben tener los maps, y existen tres implementaciones principales de dicha interfaz:

  • HashMap: No existe orden entre las claves y permite un sola clave nula.
  • TreeMap: Mantiene ordenados los datos según la clave y no permite claves nulas.
  • LinkedHashMap: Mantiene ordenados los datos según orden de inserción y permite una sola clave nula.

Los maps utilizan clases genéricas para dar extensibilidad y flexibilidad, y permiten definir un tipo base para la clave, y otro tipo diferente para el valor. Veamos un ejemplo de cómo crear un HashMap, que es extensible los otros dos tipos de mapas:

Map<String, Integer> diccionario = new HashMap<>();

El map anterior permite usar cadenas como llaves y almacenar de forma asociada a cada llave un número entero. Veamos los métodos principales de la interfaz Map , disponibles en todas las implementaciones. En los ejemplos, V es el tipo base usado para el valor y K el tipo base usado para la llave:

  • V put(K key, V value): Asocia el valor (value) con la clave (key) en el map. Si la clave no existe en el map crea un nuevo par clave-valor. Si ya existe, reemplazará el valor.
  • V get(Object key): Obtiene el valor asociado a una clave (key) ya almacenada en el mapa. Si no existe la clave, retornará null.
  • V remove(Object key): Elimina la clave (key) y el valor (value) asociado. Retorna el valor asociado a la clave, por si lo queremos utilizar para algo, o null, si la clave no existe.
  • boolean containsKey(Object key): Devuelve true si el map tiene almacenada la clave (key). En caso contrario devolverá false.
  • boolean containsValue(Object value): Devuelve true si el map tiene almacenada el valor (value). En caso contrario devolverá false.
  • int size(): Devuelve el número de pares clave-valor almacenado en el map.
  • boolean isEmpty(): Devuelve true si el map está vacío, false en cualquier otro caso.
  • void clear(): Vacía el map.
  • Set<K> keySet(): Devuelve el conjunto de claves contenidas en el map.

Veamos un ejemplo de la utilización de algunos de los métodos anteriores:

        Map<String, Integer> diccionario = new HashMap<String, Integer>();
        int valor;

        diccionario.put("edad", 18); //añadimos el par clave = "edad" / valor = 18 
        diccionario.put("año", 2022); //añadimos el par clave = "año" / valor = 2022
        diccionario.put("edad", 34); //Sustituimo el valor de la clave "edad"

        //Recorremos el HashMap y mostramos las claves y los valores
        for (String clave : diccionario.keySet()) { 
            valor = diccionario.get(clave);
            System.out.println(clave + ": " + valor);
        }

edad: 34
año: 2022

Igual que con los conjuntos y las listas, también podemos crear maps inmutables con el método Map.of(). En este caso, tenemos que ir pasándole pares de clave-valor, según lo establecido cuando creamos el Map:

        diccionario = Map.of("edad", 18, "año", 2022);

Ejercicios

Ejercicio 1.a Crea un array de enteros con los siguientes números:

{1, 2, 3, 5, 8, 13, 21, 34, 55}

Haz que se muestre por pantalla el cuadrado de cada número del array utilizando un bucle for sencillo.

Ejercicio 1.b

Utiliza un bucle for-each para mostrar el cuadrado de los números del ejercicio anterior.

Ejercicio 1.c

Haz que el array del ejercicio 1.a se modifique con el cuadrado de cada número y muéstralo por pantalla.

Ejercicio 2.a

Crea un array de 5 enteros. Haz que la aplicación pida al usuario cada elemento del array y, al acabar, muéstralo por pantalla.

Ejercicio 2.b

Modifica el ejercicio anterior para que la aplicación para que el usuario pueda elegir el tamaño del array.

Ejercicio 2.c

Haz que la inicialización del array y mostrar los elementos del ejercicio anterior sean dos funciones separadas.

Ejercicio 3.a

Crea una matriz de 5×8 de booleans con los siguientes valores:

{true, true, true, true, true},
{true, false, false, false, true},
{true, false, false, false, true},
{true, false, false, false, true},
{true, false, false, false, true},
{true, false, false, false, true},
{true, false, false, false, true},
{true, true, true, true, true} 

Haz que se muestre por pantalla la matriz según la siguiente regla:

  • Si el valor es true, se mostrará un 0 (cero)
  • Si el valor es false se mostrará un espacio en blanco.

Ejemplo de salida:

OOOOO
O   O
O   O
O   O
O   O
O   O
O   O
OOOOO

Ejercicio 3.b

Crea otra matriz a partir del ejercicio anterior con los siguientes valores:

{false, false, false, false, true},
{false, false, false, true, true},
{false, false,  true, false, true},
{false, true, false, false, true},
{true, false, false, false, true},
{false, false, false, false, true},
{false, false, false, false, true},
{false, false, false, false, true}       

Muestra las dos matrices por pantalla según las reglas anteriores.

Ejercicio 3.c

Haz una aplicación que pida un número binario al usuario. El programa mostrará los bits (empezando por el de la derecha) utilizando las matrices anteriores. La aplicación mostrará la frase “No se puede representar el bit” si éste no es un 1 o un 0.

Ejercicio 3.d

Modifica la aplicación anterior para que el usuario introduzca por pantalla un número decimal y la aplicación muestre el número en binario (utilizando los métodos anteriores).

Ejercicio 4.a

Crea un conjunto de números enteros. Pide por pantalla el tamaño del conjunto, y haz que el usuario vaya metiendo números al conjunto hasta alcanzar el tamaño introducido. Muestra el conjunto final por pantalla.

Ejercicio 4.b

Modifica la aplicación anterior para que el tamaño del conjunto sea variable. El usuario introducirá números en el conjunto hasta que escriba un 0 (cero). El 0 no debe formar parte del conjunto final.

Ejercicio 4.c

Haz que la aplicación muestre la frase “El número está repetido y no se añadirá al conjunto” si el usuario introduce un número que ya existe en el conjunto.

Ejercicio 4.d

Separa los números en dos conjuntos: pares e impares. Muestra los dos conjuntos resultantes por pantalla.

Ejercicio 5.a

Haz un programa que contenga una lista de alumnos con los siguientes valores:

("Ana", "Pedro", "Antonio", "Amparo", "Luis", "María")

Por cada alumno, el programa pedirá la nota y mostrará la frase “El alumno nombre_alumno está aprobado con nota_alumno” si la nota es mayor o igual a 5.

Ejercicio 5.b

Modifica el programa anterior para que los alumnos aprobados se añadan a otra lista. Muestra la lista de aprobados cuando se hayan introducido todas las notas (no hace falta mostrar la frase anterior por cada alumno aprobado).

Ejercicio 5.c

Modifica el ejercicio anterior para que la aplicación muestre un menú con las opciones “Introducir alumno” y “Salir”. Cuando se introduce un alumno, el programa pedirá el nombre del alumno y su nota. Si la nota es mayor o igual a 5, se añadirá a una lista de alumnos aprobados. En caso contrario, se añadirá a la lista de suspendidos. Cuando el usuario elija la opción “Salir”, el programa mostrará ambas listas (aprobados y suspendidos).

Ejercicio 6

Crea un Map con los nombres de los alumnos y sus notas. El programa deberá pedir la nota de un listados predefinido de alumnos (puedes utilizar los anteriores) y a continuación mostrará el Map por pantalla.

clase/daw/prog/1eval/collections.txt · Última modificación: 2022/10/28 12:48 por cesguiro