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 unSet
deEntry<K, V>
que son pares clave / valor que representan los elementos del mapakeySet()
: Devuelve unSet
de claves del mapavalues()
: Devuelve unSet
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 Map
y 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 connull
claves y valores. Es una implementación directa deMap
y por lo tanto no garantizan el orden de los elementos ni la seguridad de los hilos.EnumMap
: Una implementación que requiereenum
constantes como las claves del mapa. Por lo tanto, el número de elementos en elMap
están limitadas por el número de constantes de laenum
. Además, la implementación está optimizada para manejar el número generalmente bastante pequeño de elementos comoMap
contendrá.TreeMap
: Como implementación delSortedMap
yNavigableMap
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 unComparator
podemos dar a laTreeMap
constructor.ConcurrentHashMap
: Esta última implementación es probablemente la misma queHashMap
, espere que garantice la seguridad de los subprocesos para las operaciones de actualización, como lo garantiza elConcurrentMap
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
.