Tema 5. Tipo Map y SortedMap Autor: José C. Riquelme 1. El tipo Map 1.1 Definición Java proporciona en el paquete java.util un tipo denominado Map que podría traducirse como aplicación (aunque vulgarmente y por simplificar es común también el nombre de “mapa”). Un tipo Map es la forma que tiene Java de guardar un conjunto de pares relacionados al igual que se hace en teoría de conjuntos cuando se define una aplicación entre dos conjuntos, de manera que a cada elemento del conjunto inicial le corresponde una y solo una imagen en el conjunto final. De esta manera Map se define sobre dos tipos de datos, los que están en el conjunto inicial, llamados claves (keys) y los que están en el conjunto final, denominados valores (values). Un Map entonces está definido por un conjunto (Set) de claves, una colección (Collection) de valores y un conjunto de pares clave-valor. Como sucede en el concepto matemático de aplicación sobreyectiva cada clave es única, de ahí que el conjunto inicial se modele con un Set, para que no haya claves repetidas, pero el conjunto de valores puede tener elementos “repetidos” ya que dos claves distintas pueden tener el mismo valor. Por ejemplo si a una cadena de caracteres le hacemos corresponder su valor numérico, tendríamos una aplicación como: “uno” 1 “1” “I” “1.0” “2” 2 “II” “1+1” Hay que señalar que el tipo Map no extiende a Collection y que no es Iterable, por tanto, sus elementos no pueden ser recorridos con el for extendido. Ejemplos cotidianos de tipos Map son los listines telefónicos (clave: nombre contacto, valor: número de teléfono), listas de clase (clave: nombre del alumno, valor: calificaciones), ventas de un producto (clave: código de producto, valor: total de euros de recaudación), índices de palabras por páginas en un libro (clave: palabra, valor: lista de números de página donde aparece), la red de un Metro (clave: 2 Introducción a la Programación nombre de la estación, valor: conjunto de líneas que pasan por esa estación), o al revés (clave: número de línea, valor: lista de estaciones de esa línea), etc. La clase que implementa la interfaz Map es HashMap y de nuevo obliga a definir de forma correcta el método hashCode del tipo de las claves para evitar claves repetidas. Asimismo al igual que pasa en el tipo Set, los objetos mutables que se introducen en un Map son “vistas” o referencias a objetos y por tanto, si éstos cambian, el Map puede dejar de ser coherente, esto es, podría tener claves repetidas. Un Map cuyas claves sean String y cuyos valores sean Integer se definiría e inicializaría: Map<String, Integer> m = new HashMap<String, Integer>(); 1.2 Métodos Los métodos que proporciona Java para el tipo Map son los siguientes. Como siempre más información en http://docs.oracle.com/javase/7/docs/api/java/util/Map.html void clear() Removes all of the mappings from this map. boolean containsKey(Object key) Returns true if this map contains a mapping for the specified key. boolean containsValue(Object value) Returns true if this map maps one or more keys to the specified value. V get(Object key) Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. boolean isEmpty() Returns true if this map contains no key-value mappings. Set<K> keySet() Returns a Set view of the keys contained in this map. V put(K key, V value) Associates the specified value with the specified key in this map. V remove(Object key) Removes the mapping for a key from this map if it is present. int size() Returns the number of key-value mappings in this map. Collection<V> values() Returns a Collection view of the values contained in this map. 5. Tipos Map y SortedMap .Algunas consideraciones sobre los métodos son las siguientes: put(key, value) añade a la función la clave key, a la que le asocia el valor value. Devuelve el valor que previamente tuviera asociada la clave, si ya estaba en el Map o null en caso contrario. get(key) devuelve el valor asociado con la clave key, si ésta existe en el Map o null si no existe. remove(key) elimina la clave key si estaba en el Map, en cuyo caso devuelve el valor que tuviera asociado. Si no existía esa clave, no hace nada y devuelve null. keySet() devuelve el conjunto de las claves que forman parte del Map. Como es un conjunto SÍ es iterable. values() devuelve una Collection con los valores que contiene el Map. Puede haber elementos duplicados, de ahí que sea una Collection y no un Set. También es iterable. Los objetos devueltos por estas operaciones son vistas del Map original, por lo que las modificaciones que se hagan sobre el Map original repercutirán en las vistas devueltas y viceversa. 1.3 Inicialización de un objeto de tipo Map La inicialización de un objeto de tipo Map siempre sigue la misma estructura, según el esquema siguiente a. Creación del objeto (HashMap) b. Para cada clave mediante un recorrido c. Si la clave ya está (containsKey) d. Obtener valor asociado (get) e. Actualizar valor u obtener nuevovalor a partir de valor f. Si es necesario añadir al map clave y nuevovalor (put) g. Si la clave no está h. Inicializar valor i. Añadir clave y valor (put) En el paso a. se construye el objeto tipo Map invocando al constructor de la clase HashMap. El paso b. normalmente incluye un recorrido sobre la colección de elementos que se vayan a insertar como claves. Para cada clave se calculará el valor correspondiente, normalmente mediante algún tipo de cálculo u operación de acceso a una colección de datos a partir de la clave. Una vez tenemos el par clave-valor a insertar en el Map, nos preguntamos en el paso c. mediante el método containsKey si la clave ya estaba anteriormente en el Map. Si la clave ya estaba debemos obtener el valor asociado a esa clave en el Map mediante el método get. Dependiendo de si la actualización consiste en sustituir un nuevo valor por el anterior o actualizar el ya existente se ejecutan los pasos e. y f. Por ejemplo, si el Map es asociar a cada palabra una lista con los números de páginas de un libro donde aparece, la actualización será añadir a la vista de la lista que conforma el valor un nuevo elemento y por tanto no hace falta invocar al método put. Sin embargo, si el Map fuera para actualizar las ventas de un producto, al invocar al método get obtendríamos el 3 4 Introducción a la Programación valor de las ventas pasadas, a este dato se le debería sumar las nuevas ventas y ahora sí actualizaríamos mediante el método put las ventas realizadas del producto clave. Finalmente si la clave no estaba en el Map, se calcula el valor inicial en el paso h. para añadir el par clave-valor mediante el método put en el paso i. 1.4 Ejemplo 1 Supongamos que se quiere calcular la frecuencia absoluta de aparición de los caracteres de un String. Para ello se define un Map cuyas claves son de tipo Character y los valores de tipo Integer. El método recibirá un String y devolverá un objeto de tipo Map<Character, Integer>. El código del método sería: public static Map<Character, Integer> contador_carac(String frase){ Map<Character,Integer> contador = new HashMap<Character,Integer>(); frase=frase.toUpperCase(); // todos los caracteres en mayúsculas for(int i=0;i<frase.length();i++) { Character caracter = frase.charAt(i); if (contador.containsKey(caracter)) { Integer valor=contador.get(caracter); valor++; contador.put(caracter, valor); } else contador.put(caracter, 1); } return contador; } Nótese como el método sigue fielmente el esquema antes indicado. Se recorren los caracteres del String de entrada mediante un for clásico. Para cada carácter se pregunta si ya está, de forma que si está, se obtiene el valor correspondiente al número de veces que ha aparecido anteriormente, se incrementa en uno y se vuelva a actualizar. La llamada al método put es obligatoria ya que valor es de tipo Integer y por tanto inmutable. En el caso de que el carácter no estuviera se inicia la frecuencia a 1. 1.5 Ejemplo 2 Supongamos que dado una lista de String que denominamos libro se quiere obtener un índice de las posiciones que ocupan las cadenas de libro mediante una lista de enteros. El argumento de entrada será por tanto de tipo List<String> y el argumento de salida un Map<String, List<Integer>>. El método tendría el siguiente código: public static Map<String, List<Integer>> indice_pal(List<String>libro){ Map<String,List<Integer>> indice=new HashMap<String,List<Integer>>(); int pos=0; for(String palabra: libro) { if (indice.containsKey(palabra)) { indice.get(palabra).add(pos); 5. Tipos Map y SortedMap } else{ List<Integer> lis = new ArrayList<Integer>(); lis.add(pos); indice.put(palabra, lis); } pos++; } return indice; } Nótese que la principal diferencia con el ejercicio anterior es que no es necesario invocar al método put en el caso de que la palabra ya esté en el índice. Observe la sentencia indice.get(palabra).add(pos); equivalente a List<Integer> lis = indice.get(palabra); lis.add(pos); Igualmente debe observar que incluso en este caso tampoco es necesario invocar a put ya que lis es una vista de la lista correspondiente y por tanto al añadir un elemento a lis, ya lo estamos haciendo a la lista guardada en el conjunto imagen del Map. Este problema se puede restringir también a algunas palabras claves, no a todas las de la lista libro. Supongamos que tenemos las palabras claves en un Set de String, ¿Cómo modificaría el código del método anterior para que en el Map sólo estuvieran las palabras claves? 2. El tipo SortedMap 2.1 Definición El tipo SortedMap es un Map en el que el conjunto de las claves está ordenado. La clase que implementa SortedMap es TreeMap necesita que el tipo de las claves o bine implemente Comparable o se proporcione un orden externo mediante un Comparator. Por tanto, inicializaciones de SortedMap como: SortedMap<String,List<Integer>> indice = new TreeMap<String,List<Integer>>(); Crea un SortedMap donde las claves de tipo String están ordenadas por el orden natural de String. Para crear un SortedMap por un orden no natural debemos invocar al constructor pasándole como argumento un comparator: Comparator<Persona> cm_ed = new ComparatorPersonaEdad(); SortedMap<Persona,List<Integer>>mp=new TreeMap<Persona,List<Integer>>(cm_ed); 5 6 Introducción a la Programación Las reflexiones que se hicieron para el tipo SortedSet respecto del orden definido por un Comparator siguen siendo válidas para el SortedMap. Esto es, dos claves c1 y c2 son iguales para un SortedMap si compare(c1,c2)==0. Por tanto, si el Comparator no rompe los empates por el orden natural, en el SortedMap anterior sólo habría una Persona clave por cada edad. 2.2 Métodos SortedMap hereda de Map, por tanto, todos los métodos de Map son válidos también para un objeto SortedMap. Además Java proporciona algunos métodos para manejar el orden en el conjunto de las claves: http://docs.oracle.com/javase/7/docs/api/java/util/SortedMap.html K firstKey() Returns the first (lowest) key currently in this map. SortedMap<K,V> headMap(K toKey) Returns a view of the portion of this map whose keys are strictly less than toKey. Set<K> keySet() Returns a Set view of the keys contained in this map. K lastKey() Returns the last (highest) key currently in this map. SortedMap<K,V> subMap(K fromKey, K toKey) Returns a view of the portion of this map whose keys range from fromKey, inclusive, to toKey, exclusive. SortedMap<K,V> tailMap(K fromKey) Returns a view of the portion of this map whose keys are greater than or equal to fromKey. Hay que tener en cuenta que si no vamos a usar estos métodos y sólo queremos que la visualización del Map salga ordenada se puede declarar un Map mediante la clase TreeMap. Por ejemplo, pruebe a definir en el Ejemplo 2: Map<String,List<Integer>> indice=new TreeMap<String,List<Integer>>(); Y verá como al imprimir el Map en la consola las palabras salen ordenadas alfabéticamente. 2.3 Ejemplo 3 Dado el Map construido en el ejemplo 1 que relaciona cada carácter con su frecuencia absoluta de aparición en un texto, se quiere construir un Map inverso, en el que las claves sean de tipo Integer y los valores asociados a cada clave sean de tipo List<Character> con los caracteres cuya frecuencia absoluta es la clave del Map. Además se desea mostrar los caracteres ordenados por frecuencia de mayor a menor, por tanto se necesitará crear un SortedMap. Por tanto, el método recibirá un Map<Character, Integer> y devolverá un SortedMap<Integer,List<Character>>. Su código es: 5. Tipos Map y SortedMap public static SortedMap<Integer, List<Character>> invierteMap (Map<Character, Integer> m) { SortedMap<Integer, List<Character>> res = new TreeMap<Integer,List<Character>>(new ComparadorIntegerInverso()); Set<Character> sp = m.keySet(); for(Character caracter: sp){ Integer num=m.get(caracter); if (res.containsKey(num)) { res.get(num).add(caracter); } else { List<Character> lista = new LinkedList<Character>(); lista.add(caracter); res.put(num, lista); } } return res; } Nótese que para que el orden del SortedMap sea de mayor a menor ha debido crearse un Comparator para Integer inverso. Su código es muy simple: public class ComparadorIntegerInverso implements Comparator<Integer> { public int compare(Integer arg0, Integer arg1) { return arg1.compareTo(arg0); } } 3. Ejercicios propuestos 1. ¿Cómo modificaría el código del ejemplo 2 para que en el Map sólo estuvieran las palabras claves dadas por un Set de String? 2. Modifique los constructores de los ejemplos 1 y 2 para que las claves estén ordenadas. 3. Modifique el ejemplo 3 para que el método reciba un entero F y devuelva un Map con sólo aquellas palabras cuya frecuencia absoluta sea mayor que F. 4. Cambie el código de los ejemplos 1 y 3 para que en vez de frecuencias absolutas trabajen con frecuencias relativas. ¿Cuáles son los 3 caracteres de mayor frecuencia relativa en inglés y en español? 5. A partir del tipo Gen definido en los boletines de problemas y dado un conjunto (Set) de genes de distintas especies, y un nucleótido mediante un carácter, implemente un método estático en la clase Genes que devuelva el número de veces que dicho nucleótido aparece en los genes de cada especie. Esto es, dados los genes: Gen g1 = new GenImpl("BDH1", 5, "Homo Sapiens", "ATGCTGGCCAT"); Gen g2 = new GenImpl("LGALS8", 5, "Homo Sapiens","TGCTCCGAATGG"); Gen g3 = new GenImpl("PAOX", 5, "Homo Sapiens","GGCCTTAATGC"); 7 8 Introducción a la Programación Gen g4 = new GenImpl("ZNF324B", 1, "Homo Sapiens","GCTTAGCCA"); Gen g5 = new GenImpl("LYZL4", 0, "Homo Sapiens","GTTCCATGACATT"); Gen Gen Gen Gen Gen g6 = new GenImpl("BIRC5", 9, "Canis Lupus", "TGCCATGACC"); g7 = new GenImpl("MYL4", 9, "Canis Lupus","TTTGGACAGT"); g8 = new GenImpl("PRKCA", 9, "Canis Lupus","GGCCATACTGA"); g9 = new GenImpl("PYY", 9, "Canis Lupus","CCAGTATGCA"); g10 = new GenImpl("KRT10", 9, "Canis Lupus","GTACCTAGTAC"); Queremos obtener esta salida si se invoca al método con cada uno de los nucleótidos : Nucleótido Nucleótido Nucleótido Nucleótido A: C: G: T: {Homo {Homo {Homo {Homo Sapiens=11, Sapiens=15, Sapiens=14, Sapiens=16, Canis Canis Canis Canis Lupus=13} Lupus=14} Lupus=12} Lupus=13}