Category: Basics

Java Collections

Otro tema en el que creo falta más expertiz a todos los que trabajamos con código y en Java, es al manejo de la API Collections. Todos sabemos de la interfaz List y ArrayList. Pero, ¿sabemos que es una LinkedList, un Stack o un Queue?

Este post tiene como objetivo ver toda este framework que proporciona una arquitectura para almacenar y manipular grupos de objetos.

La API Collections proporciona las interfaces Set, List, Queue y Deque y las clases ArrayList, Vector, LinkedList, PriorityQueue, HashSet, LinkedHashSet y Treeset.

Jerarquía de la API Collections.

Como podemos ver en la imagen, hay una jerarquía, y todos tienen la interfaz Iterable, esto permite que podamos recorrer estos grupos de colecciones. Además, la Interfaz Collection proporciona muchos métodos en común para todos los demás elementos:

Métodos Interfaz Collection

Esta Interfaz Collection, extiende de la Interfaz Iterable, que es la base de todas las classes de Collection. Sólo contiene un método abstracto:

Interfaz Iterable

Como podemos ver, esta Interfaz sólo contiene un método abstracto Iterator, y finalmente, ese Iterator, es una interfaz que contiene 3 métodos:

Interfaz List

La interfaz list es la interfaz hija de la Interfaz Collection. Como características importantes:

  • Estructura en la que se puede almacenar una colección ordenada de objetos.
  • Puede tener datos duplicados.
  • La interfaz es implementada por las clases: ArrayList, LinkedList, Vector y Stack.

Vamos viendo ahora estas clases una por una.

List <String> stringList= new ArrayList();  
List <String> strList = new LinkedList();  
List <Long> longList = new Vector();  
List <Example> exampleList = new Stack();  

Clase ArrayList

Características principales:

  • Permite almacenar datos duplicados de elementos de distintos data types.
  • Mantiene el orden de inserción y es de tipo non-synchronized.
  • Los elementos de los ArrayList pueden ser accedidos aleatoriamente.
  • Es un Array redimensionable que aumenta su tamaño según se necesite.

Clase LinkedList

Características:

  • Internamente usa una lista doblemente vinculada para almacenar los elementos. Esto es, que cada uno de los elementos tiene un puntero al anterior y al siguiente elemento.
  • Puede almacenar elementos duplicados.
  • Mantiene el orden de inserción y es non-synchronized.
  • Bajo ciertas ocasiones es mejor el rendimiento.

Clase Vector

Utiliza un array dinámico para almacenar los elementos.

  • Es similar a ArrayList, pero es synchronized.
  • Puede ser más lento que los arrays estándar, pero puede ser útil en programas donde se necesite mucha manipulación en los arrays.

Clase Stack

Stack hereda de Vector. Tal como crees implementa una estructura de pila, es decir, Last-In-First-Out.

  • Contiene los mismos métodos que Vector y adiciona otros útiles como: peek(), push(object o).

Cuando hablamos de non-synchronized nos referimos que si dos o más hilos acceden de forma concurrente al mismo no asegura la integridad de la información.

Interfaz Queue

La interfaz Queue mantiene un orden de tipo FIFO. Podemos definirla como una lista ordenada que mantiene elementos que serán procesados.

Las clases que implementan la interfaz Queue: PriorityQueue, Deque, ArrayDeque.

Queue<String> priorityQueue = new PriorityQueue();  
Queue<String> arrayDeque = new ArrayDeque();  

Veamos las clases que implementan esta infertaz.

Clase PriorityQueue

  • Implementa la interfaz Queue.
  • Mantiene los elementos u objetos que serán procesados por su prioridad.
  • No acepta valores nulos a ser almacenados en la cola.

Un ejemplo:

public static void main(String[] args) {
        Queue<String> priorityQueue = new PriorityQueue<>();
        priorityQueue.add("Gonzalo");
        priorityQueue.add("Munoz");
        priorityQueue.add("Mendez");

        //Contiene algunos métodos útiles como element que muestra el elemento que viene
        System.out.println(priorityQueue.element());

        //elimina el elemento que viene
        priorityQueue.remove();

        System.out.println(priorityQueue.poll());
    }

Y la consola muestra:

Interfaz Deque

  • Extiende de Interfaz Queue.
  • La ventaja que tiene es que podemos remover y añadir los elementos desde ambos lados.
  • Su nombre proviene de dounble-ended queue.

Clase ArrayDeque

  • Esta clase implementa la interfaz Deque. Nos facilita utilizar la Deque.
  • A excepción de queue, podemos añadir o borrar elementos desde ambos lados de la cola. Los métodos que ayudan a esto: addFirst(Object o) y addLast(Object o).
  • Es más veloz que un ArrayList y un Stack y no tiene restricciones de capacidad.

Interfaz Set

  • Representa un set de elementos sin ordenar y no nos permite elementos duplicados.
  • Podemos almacenar como máximo un valor nulo.
  • Es implementada por clases HashSet, LinkedHashSet y TreeSet.
Set<String> s1 = new HashSet<>();  
Set<Example> s2 = new LinkedHashSet<>();  
Set<Integer> s3 = new TreeSet<>();  

Clase HashSet

  • Implementa la interfaz Set.
  • Representa una colección que utiliza una tabla hash para el almacenaje.
  • Contiene elementos únicos.
HashSet<String> setExample=new HashSet<String>();  
set.add("1");  
set.add("2");  
set.add("3");  
set.add("1"); 

Esto nos retorna todos los elementos únicos.

Clase LinkedHashSet

  • Representa la implementación LinkedList para la interfaz Set.
  • Es similar a HashSet, la diferencia es que usa una lista doblemente enlazada para almacenar data y retener el orden de los elementos.

Interfaz SortedSet

  • Es una interfaz optativa a Set que proporciona una ordenación total de los elementos.
  • Los elementos en SortedSet están ordenados en orden ascendente.
  • Proporciona los métodos adicionales que inhiben el orden natural de los elementos.

Clase TreeSet

  • Es la clase que usa la interfaz SortedSet.
  • Contiene elementos únicos.
  • Están almacenados en orden ascendente.
SortedSet<String> ts
                = new TreeSet<>();

        ts.add("Gonzalo");
        ts.add("Zon");
        ts.add("Alberto");
        Iterator<String> itr = ts.iterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
        }

Esto nos ordena los elementos y el resultado es:

Java Generics

Los genéricos en Java están a partir del SDK 5.0 del lenguaje, y a pesar de llevar tanto tiempo con nosotros, aún hay muchos creadores de código que no saben utilizarlos correctamente.

Un pequeño ejemplo de genéricos

Es muy común que tengamos la necesidad de almacenar objetos en listas para su uso.

Por ejemplo, tomemos un listado con nombres:

List names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

Ahora, sabemos que esta lista tiene Strings, porque nosotros la declaramos con sólo nombres, verdad? Entonces hagamos alguna iteración e imprimamos los resultados, esto debería hacerse así:

for(String name: names){
    System.out.println("Name " + name);
}

o quizás:

names.forEach((String name) -> System.out.println(name));

Verdad? Bueno, la verdad es que el compilador nos va a lanzar un error en estos casos. No hay un contrato que nos asegure que cada elemento de esa lista es un String, por lo que en nuestros for y forEach lo que tenemos no es un String, sino que un Object que podemos castear para obtener nuestro esperado String:

for(Object name: names){
    String obtainedName = (String) name;
    System.out.println("Name " + obtainedName);
}

names.forEach(name -> System.out.println((String) name));

Este cast tiene varios problemas, en primer lugar, es molesto tener que hacer casting cada vez que necesitamos una variable, y por otro lado, si por equivocación metemos un valor numérico u otro objeto a la lista y hacemos un cast, nos encontraremos con una excepción.

Este problema se soluciona fácilmente si declaramos entre <> el tipo que queremos tenga nuestra lista.

List<String> names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

for(String name: names){
    System.out.println("Name " + name);
}

names.forEach(name -> System.out.println( name));

Bienvenido a los genéricos!

Clases genéricas

Veamos una clase Java

public class GenericClassTest<T> {

    T object;


    GenericClassTest(T object){
        this.object = object;
    }

    public T getObject(){
        return this.object;
    }

    public static void main(String[] args) {
        GenericClassTest<Long> longGenericClassTest = new GenericClassTest<>(29L);
        System.out.println("Long Generic Test Class: " + longGenericClassTest.getObject());

        GenericClassTest<List<String>> listGenericClassTest = new GenericClassTest<>(Arrays.asList("Gonzalo", "Munoz"));
        listGenericClassTest.getObject().forEach(System.out::println);
    }

}

Analicemos el código, acá en la declaración de la clase estamos agregándole el uso de genéricos. Esto hará que acepte cualquier tipo de objeto.

En los atributos de clase definimos un object de tipo T, y creamos un constructor que inicializará esta variable.

Finalmente estamos haciendo un método getter para obtener este objeto.

Más abajo, en la misma clase he declarado un método main donde hacemos una instancia de nuestra clase, primero con un Long, y luego con un listado de String, e imprimiendo estos valores por consola para ver que nuestra clase genérica funciona de las mil maravillas.

También podemos agregar más genéricos a una clase:

public class GenericClassTwoParameters<T,U> {

    T obj1;
    U obj2;

    GenericClassTwoParameters(T obj1, U obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    /**
     * Simple method to retrieve the generic data
     */
    public void getParameters(){
        System.out.println("Obj1: "+obj1);
        System.out.println("Obj2: "+obj2);
    }

    public static void main(String[] args) {
        GenericClassTwoParameters<String, Integer> genericClassTwoParameters = new GenericClassTwoParameters<>("Manzanas", 20);
        
        genericClassTwoParameters.getParameters();
    }
}

Funciones Genéricas

También podemos escribir funciones genéricas que pueden ser llamadas con diferentes tipos de argumentos a partir de lo que le pasemos al método genérico.

 static <T> void genericMethod(T element){
        System.out.println(element);
    }

    public static void main(String[] args) {
        //Veamos como se comporta este método:
        genericMethod("Hello World!");

        genericMethod(2000L);

        genericMethod(2.0);
    }

Acá podemos apreciar que nuestro método genérico le estamos pasando un String, luego un Long y finalmente un Double, y la salida por consola:

Algunas características de los métodos genéricos:

  • Antes del tipo de retorno tienen un type parameter.
  • El type parameter puede ser acotado: Esto es cuando ponemos por ejemplo que los tipos aceptados deban ser numéricos:
public <T extends Number> void generic(T element){...}
// Y esto también se acepta
public <T extends Number & Comparable> void generic {...}
  • Los métodos genéricos también pueden tener distintos type parameters:
public static <T, U> List<T> fromArrayToList(T[] a) {...}