Colecciones Java: la interfaz del mapa

    Introducción

    El Java Collections Framework es un marco fundamental y esencial que cualquier desarrollador de Java fuerte debe conocer como la palma de su mano.

    Una colección en Java se define como un grupo o colección de objetos individuales que actúan como un solo objeto.

    Hay muchas clases de colección en Java y todas ellas extienden el java.util.Collection y java.util.Map interfaces. En su mayoría, estas clases ofrecen diferentes formas de formular una colección de objetos dentro de un solo objeto.

    Colecciones de Java es un marco que proporciona numerosas operaciones sobre una colección: búsqueda, clasificación, inserción, manipulación, eliminación, etc.

    Esta es la tercera parte de una serie de artículos de las colecciones de Java:

    • La interfaz de lista
    • La interfaz de conjunto
    • La interfaz del mapa (estás aquí)
    • Las interfaces Queue y Deque

    Limitaciones de listas y conjuntos

    En primer lugar, analicemos las limitaciones de List y Set. Proporcionan muchas funciones para agregar, eliminar y verificar la presencia de elementos, así como mecanismos de iteración. Pero cuando se trata de recuperar elementos específicos, no son muy útiles.

    los Set La interfaz no proporciona ningún medio para recuperar un objeto específico, ya que no está ordenado. Y el List La interfaz simplemente brinda la posibilidad de recuperar elementos por su índice.

    Desafortunadamente, los índices no siempre hablan por sí mismos y, por lo tanto, tienen poco significado.

    Mapas

    Ahí es donde el java.util.Map aparece la interfaz. UN Map asocia elementos a claves, lo que nos permite recuperar elementos mediante esas claves. Estas asociaciones tienen mucho más sentido que asociar un índice a un artículo.

    Map es una interfaz genérica con dos tipos, uno para las claves y otro para los valores. Por tanto, si quisiéramos declarar un Map almacenando el recuento de palabras en un texto, escribiríamos:

    Map<String, Integer> wordsCount;
    

    Tal Map usa un String como su clave y un Integer como su valor.

    Agregar elementos

    Ahora vamos a sumergirnos en el Map operaciones, comenzando con la adición de elementos. Hay algunas formas de agregar elementos a una Map, el más común es el put() método:

    Map<String, Integer> wordsCount = new HashMap<>();
    wordsCount.put("the", 153);
    

    Nota: Además de asociar un valor a una clave, el put() El método también devuelve el valor asociado anteriormente, si lo hubiera, y null de otra manera.

    Pero, ¿qué pasa si solo queremos agregar un elemento solo si no hay nada asociado a su clave? Luego tenemos algunas posibilidades, la primera es probar la presencia clave con el containsKey() método:

    if (!wordsCount.containsKey("the")) {
        wordsCount.put("the", 150);
    }
    

    Gracias a containsKey() método, podemos comprobar si un elemento ya está asociado a la clave the y solo agregue un valor si no.

    Sin embargo, eso es un poco detallado, especialmente considerando que hay otras dos opciones. En primer lugar, veamos el más antiguo, el putIfAbsent() método:

    wordsCount.putIfAbsent("the", 150);
    

    Esta llamada al método logra el mismo resultado que la anterior, pero usando solo una línea.

    Ahora veamos la segunda opción. Desde Java 8, otro método, similar a putIfAbsent(), existe – computeIfAbsent().

    Funciona aproximadamente de la misma manera que el anterior, pero toma una función Lambda en lugar de un valor directo, lo que nos da la posibilidad de crear una instancia del valor solo si no hay nada adjunto a la clave todavía.

    El argumento de la función es la clave, en caso de que la instanciación del valor dependa de él. Entonces, para lograr el mismo resultado que con los métodos precedentes, tendríamos que hacer:

    wordsCount.computeIfAbsent("the", key -> 3 + 150);
    

    Proporcionará el mismo resultado que antes, solo que no calculará el valor 153 si ya hay otro valor asociado a la clave the.

    Nota: Este método es particularmente útil cuando el valor es pesado para instanciar o si el método se llama con frecuencia y queremos evitar crear demasiados objetos.

    Recuperando elementos

    Hasta ahora, aprendimos a poner elementos en un Map, pero ¿qué tal recuperarlos?

    Para lograr eso, usamos el get() método:

    wordsCount.get("the");
    

    Ese código devolverá el recuento de palabras de la palabra the.

    Si ningún valor coincide con la clave dada, entonces get() devoluciones null. Sin embargo, podemos evitarlo utilizando el getOrDefault() método:

    wordsCount.getOrDefault("duck", 0);
    

    Nota: Aquí, si no hay nada asociado a la clave, obtendremos 0 atrás en lugar de null.

    Ahora, eso es para recuperar un elemento a la vez usando su clave. Veamos cómo recuperar todos los elementos. los Map La interfaz ofrece tres métodos para lograr esto:

    • entrySet(): Devuelve un Set de Entry<K, V> que son pares clave / valor que representan los elementos del mapa
    • keySet(): Devuelve un Set de claves del mapa
    • values(): Devuelve un Set de valores del mapa

    Eliminar elementos

    Ahora que sabemos cómo colocar y recuperar elementos de un mapa, ¡veamos cómo eliminar algunos!

    Primero, veamos cómo eliminar un elemento por su clave. Con este fin, usaremos el remove() método, que toma una clave como parámetro:

    wordsCount.remove("the");
    

    El método eliminará el elemento y devolverá el valor asociado, si lo hay; de lo contrario, no hace nada y devuelve null.

    los remove() El método tiene una versión sobrecargada que también toma un valor. Su objetivo es eliminar una entrada solo si tiene la misma clave y valor que los especificados en los parámetros:

    wordsCount.remove("the", 153);
    

    Esta llamada eliminará la entrada asociada a la palabra the solo si el valor correspondiente es 153, de lo contrario no hace nada.

    Este método no devuelve un Object, sino que devuelve un boolean decir si un elemento ha sido eliminado o no.

    Iterando sobre elementos

    No podemos hablar de una colección de Java sin explicar cómo iterar sobre ella. Veremos dos formas de iterar sobre los elementos de un Map.

    El primero es el for-each bucle, que podemos usar en el entrySet() método:

    for (Entry<String, Integer> wordCount: wordsCount.entrySet()) {
        System.out.println(wordCount.getKey() + " appears " + wordCount.getValue() + " times");
    }
    

    Antes de Java 8, esta era la forma estándar de iterar a través de un Map. Afortunadamente para nosotros, se ha introducido una forma menos detallada en Java 8: forEach() método que toma un BiConsumer<K, V>:

    wordsCount.forEach((word, count) -> System.out.println(word + " appears " + count + " times"));
    

    Dado que es posible que algunos no estén familiarizados con la interfaz funcional, BiConsumer – acepta dos argumentos y no devuelve ningún valor. En nuestro caso, pasamos un word y es count, que luego se imprimen mediante una expresión Lambda.

    Este código es muy conciso y más fácil de leer que el anterior.

    Comprobación de la presencia de un elemento

    Aunque ya teníamos una descripción general de cómo verificar la presencia de un elemento en un Map, hablemos de las posibles formas de lograrlo.

    En primer lugar, está el containsKey() método, que ya usamos y que devuelve un boolean valor que nos dice si un elemento coincide con la clave dada o no. Pero, también está el containsValue() método que comprueba la presencia de un determinado valor.

    Imaginemos un Map representando los puntajes de los jugadores para un juego y el primero en alcanzar 150 victorias, entonces podríamos usar el containsValue() método para saber si un jugador gana el juego o no:

    Map<String, Integer> playersScores = new HashMap<>();
    playersScores.put("James", 0);
    playersScores.put("John", 0);
    
    while (!playersScores.containsValue(150)) {
        // Game taking place
    }
    
    System.out.println("We have a winner!");
    

    Recuperar tamaño y comprobar el vacío

    Ahora, en cuanto a List y Set, existen operaciones para contar el número de elementos.

    Esas operaciones son size(), que devuelve el número de elementos del Mapy isEmpty(), que devuelve un boolean decir si el Map contiene o no ningún elemento:

    Map<String, Integer> map = new HashMap<>();
    map.put("One", 1);
    map.put("Two", 2);
    
    System.out.println(map.size());
    System.out.println(map.isEmpty());
    

    La salida es:

    2
    false
    

    SortedMap

    Ahora hemos cubierto las principales operaciones que podemos realizar en Map mediante el HashMap implementación. Pero hay otras interfaces de mapa que heredan de él que ofrecen nuevas funciones y hacen que los contratos sean más estrictos.

    El primero que aprenderemos es el SortedMap interfaz, que asegura que las entradas del mapa mantendrán un cierto orden basado en sus claves.

    Además, esta interfaz ofrece características que aprovechan el orden mantenido, como el firstKey() y lastKey() métodos.

    Reutilicemos nuestro primer ejemplo, pero usando un SortedMap esta vez:

    SortedMap<String, Integer> wordsCount = new TreeMap<>();
    wordsCount.put("the", 150);
    wordsCount.put("ball", 2);
    wordsCount.put("duck", 4);
    
    System.out.println(wordsCount.firstKey());
    System.out.println(wordsCount.lastKey());
    

    Debido a que el orden predeterminado es el natural, esto producirá el siguiente resultado:

    ball
    the
    

    Si desea personalizar los criterios de pedido, puede definir un Comparator en el TreeMap constructor.

    Al definir un Comparator, podemos comparar claves (no entradas de mapa completo) y ordenarlas en función de ellas, en lugar de valores:

    SortedMap<String, Integer> wordsCount =
        new TreeMap<String, Integer>(new Comparator<String>() {
            @Override
            public int compare(String e1, String e2) {
                return e2.compareTo(e1);
            }
        });
    
    wordsCount.put("the", 150);
    wordsCount.put("ball", 2);
    wordsCount.put("duck", 4);
    
    System.out.println(wordsCount.firstKey());
    System.out.println(wordsCount.lastKey());
    

    A medida que se invierte el orden, la salida ahora es:

    the
    ball
    

    los NavigableMap interfaz es una extensión de la SortedMap interfaz y agrega métodos que permiten navegar por el mapa más fácilmente al encontrar las entradas más bajas o más altas que una determinada tecla.

    Por ejemplo, el lowerEntry() El método devuelve la entrada con la clave más grande que es estrictamente menor que la clave dada:

    Tomando el mapa del ejemplo anterior:

    SortedMap<String, Integer> wordsCount = new TreeMap<>();
    wordsCount.put("the", 150);
    wordsCount.put("ball", 2);
    wordsCount.put("duck", 4);
    
    System.out.println(wordsCount.lowerEntry("duck"));
    

    La salida sería:

    ball
    

    ConcurrentMap

    Finalmente, el último Map La extensión que cubriremos es la ConcurrentMap, que hacen que el contrato de la Map interfaz más estricta al garantizar que sea segura para subprocesos, que se pueda usar en un contexto de subprocesos múltiples sin temer que el contenido del mapa sea inconsistente.

    Esto se logra realizando las operaciones de actualización, como put() y remove(), sincronizado.

    Implementaciones

    Ahora, echemos un vistazo a las implementaciones de los diferentes Map interfaces. No los cubriremos todos, solo los principales:

    • HashMap: Esta es la implementación que más utilizamos desde el principio y es la más sencilla, ya que ofrece una asignación simple de clave / valor, incluso con null claves y valores. Es una implementación directa de Map y por lo tanto no garantizan el orden de los elementos ni la seguridad de los hilos.
    • EnumMap: Una implementación que requiere enum constantes como las claves del mapa. Por lo tanto, el número de elementos en el Map están limitadas por el número de constantes de la enum. Además, la implementación está optimizada para manejar el número generalmente bastante pequeño de elementos como Map contendrá.
    • TreeMap: Como implementación del SortedMap y NavigableMap interfaces, TreeMap asegura que los elementos que se le agreguen observarán un orden determinado (según la clave). Este orden será el orden natural de las claves o el impuesto por un Comparator podemos dar a la TreeMap constructor.
    • ConcurrentHashMap: Esta última implementación es probablemente la misma que HashMap, espere que garantice la seguridad de los subprocesos para las operaciones de actualización, como lo garantiza el ConcurrentMap interfaz.

    Conclusión

    El marco de las Colecciones de Java es un marco fundamental que todo desarrollador de Java debería saber utilizar.

    En este artículo, hemos hablado de la Map interfaz. Cubrimos las principales operaciones a través de un HashMap así como algunas extensiones interesantes como SortedMap o ConcurrentMap.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *