Java集合中的Map接口
Posted CoderBuff
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合中的Map接口相关的知识,希望对你有一定的参考价值。
jdk1.8.0_144
Map是Java三种集合中的一种位于java.util包中,Map作为一个接口存在定义了这种数据结构的一些基础操作,它的最终实现类有很多:HashMap、TreeMap、SortedMap等等,这些最终的子类大多有一个共同的抽象父类AbstractMap。在AbstractMap中实现了大多数Map实现公共的方法。本文介绍Map接口定义了哪些方法,同时JDK8又新增了哪些。
Map翻译为“映射”,它如同字典一样,给定一个key值,就能直接定位value值,它的存储结构为“key : value"形式,核心数据结构在Map内部定义了一个接口——Entry,这个数据结构包含了一个key和它对应的value。首先来窥探Map.Entry接口定义了哪些方法。
interface Map.Entry<K, V>
K getKey()
获取key值。
V getValue()
获取value值。
V setValue(V value)
存储value值。
boolean equals(Object o)
int hashCode()
这两个方法我在《万类之父——Object》中提到过,这是Object类中的方法,这两个方法通常是同时出现,也就是说要重写equals方法时为了保证不出现问题往往需要重写intCode方法。而重写equals则需要满足5个规则(自反性、对称性、传递性、一致性、非空性)。当然具体是如何重写的,此处作为接口并不做解释而是交由它的子类完成。
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey()
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue()
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp)
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp)
这四个方法放到一起是因为这都是JDK8针对Map更为简单的排序新增加的泛型方法,这里的泛型方法看似比较复杂,我们针对第一个方法先来简单回顾一下泛型方法。
一个泛型方法的基本格式就是泛型参数列表需要定义在返回值前。这个方法的返回值返回的是Comparator<Map.Entry<K, V>>,也就是说它的泛型参数列表是“<K extends Comparable<? super K>, V>”,有两个泛型参数K和V。参数K需要实现Comparable接口。
既然这是JDK8为Map排序新增的方法,那它是如何使用的呢? 不妨回忆下JDK8以前对Map是如何排序的:
1 /** 2 * Sort a Map by Keys.——JDK7 3 * @param map To be sorted Map. 4 * @return Sorted Map. 5 */ 6 public Map<String, Integer> sortedByKeys(Map<String, Integer> map) { 7 List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet()); 8 Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { 9 @Override 10 public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { 11 return o1.getKey().compareTo(o2.getKey()); 12 } 13 }); 14 Map<String, Integer> linkedMap = new LinkedHashMap<>(); 15 Iterator<Map.Entry<Strin g, Integer>> iterator = list.iterator(); 16 while (iterator.hasNext()) { 17 Map.Entry<String, Integer> entry = iterator.next(); 18 linkedMap.put(entry.getKey(), entry.getValue()); 19 } 20 21 return linkedMap; 22 }
从JDK7版本对Map排序的代码可以看到,首先需要定义泛型参数为Map.Entry类型的List,利用Collections.sort对集合List进行排序,再定义一个LinkedHashMap,遍历集合List中的元素放到LinkedHashMap中,也就是说并没有一个类似Collections.sort(Map, Comparator)的方法对Map集合类型进行直接排序。JDK8对此作了改进,通过Stream类对Map进行排序。
1 /** 2 * Sort a Map by Keys.——JDK8 3 * @param map To be sorted Map. 4 * @return Sorted Map. 5 */ 6 public Map<String, Integer> sortedByKeys(Map<String, Integer> map) { 7 Map<String, Integer> result = new LinkedHashMap<>(); 8 map.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(x -> result.put(x.getKey(), x.getValue())); 9 return result; 10 }
可见代码量大大减少,简而言之,这四个方法是JDK8利用Stream类和Lambda表达式弥补Map所缺少的排序方法。
comparingByKey() //利用key值进行排序,但要求key值类型需要实现Comparable接口。
comparingByValue() //利用value值进行排序,但要求key值类型需要实现Comparable接口。
comparingByKey(Comparator) //利用key值进行排序,但key值并没有实现Comparable接口,需要传入一个Comparator比较器。
comparingByValue(Comparator) //利用value值进行排序,但value值并没有实现Comparable接口,需要传入一个Comparator比较器。
再多说一句,Comparator采用的是策略模式,即不修改原有对象,而是引入一个新的对象对原有对象进行改变,此处即如果key(或value)并没有实现Comparable接口,此时可在不修改原有代码的情况下传入一个Comparator比较器进行排序,对原有代码进行修改是一件糟糕的事情。
参考链接:《JDK8的新特性——Lambda表达式》《似懂非懂的Comparable与Comparator》
Map.Entry接口中定义的方法到此结束,下面是Map接口中锁定义的方法。
int size()
返回Map中key-value键值对的数量,最大值是Integer.MAX_VALUE(2^31-1)。
boolean isEmpty()
Map是否为空,可以猜测如果size() = 0,Map就为空。
boolean containsKey(Object key)
Map是否包含key键值。
boolean containsValue(Object value)
Map是否包含value值。
V get(Object key)
通过key值获取对应的value值。如果Map中不包含key值则返回null,也有可能该key值对应的value值本身就是null,此时要加以区别的话可以先使用containsKey方法判断是否包含key值。
V put(K key, V value)
向Map中存入key-value键值对,并返回插入的value值。
Map从JDK5过后就改为了泛型类,get方法的参数不是泛型K,而是一个Object对象呢?包括上面的containsKey(Object)和containsValue(Object)参数也是Object而不是泛型。在这个地方似乎是使用泛型更加合适。思考以下场景:
- 最开始我写了一段代码,定义HashMap<String, String>,定义HashMap<String, String>,此时我put("a", "a"),同时我通过get("a")获取值。
- 写着写着,我发现我应该定义为HashMap<Integer, String>,此时IDE 会自动的在put("a", "a")方法报错,因为Map的泛型参数类型key修改为了Integer,我能很好的发现它并改正。但是,我的get("a")并不会有任何提示,因为它的参数是Object能接收任意类型的值,假如我get方法同样使用了泛型此时IDE就会提醒我这个地方参数类型不对,应该是Integer类型。那么为什么会出现get方法是使用Object类型,而不是泛型呢?难道JDK的作者没有想到这一点吗?明明能在编译时就能发现的问题,为什么要在运行时再去判断?
这个问题在StackOverflow上也有讨论,链接:https://stackoverflow. com/questions/1926285/why-does-hashmapcontainskey-take-an-parameter-of-type-object,http://smallwig.blogspot.com/2007/12/why-does-setcontains-take-object-not-e.html 我大致翻译了一下这可能有以下几个方面的原因:
1.这是为了保证兼容性 泛型是在JDK1.5才出现的,而HashMap则是在JDK1.2才出现,在泛型出现的时候伴随着不少兼容性问题,为了保证其兼容性不得不做了一些处理,例如泛型类型的擦除等等。假设在JDK1.5之前存在以下代码:
1 HashMap hashMap = new HashMap(); 2 ArrayList arrayList = new ArrayList(); 3 hashMap.put(arrayList, "this is list"); 4 System.out.println(hashMap.get(arrayList)); 5 LinkedList linkedList = new LinkedList(); 6 System.out.println(hashMap.get(linkedList));
这段代码在不使用泛型的时候能运行的很好,如果此时get方法中的参数变成了泛型,而不是Object,那么此时hashMap.get(linkedList)这句话将会在编译时出错,因为它不是ArrayList类型。
2.无法确定Key的类型。这里有一个例子:
1 public class HashMapTest { 2 public static void main(String[] args) { 3 HashMap<SubFoo, String> hashMap = new HashMap<>(); 4 //SubFoo是Foo类的子类 5 test(hashMap); //编译时出错 6 } 7 8 public static void test(HashMap<Foo, String> hashMap) { //参数为HashMap,key值是Foo类,但是不能接收它的子类 9 System.out.println(hashMap.get(new Foo())); 10 } 11 }
上面这种情况把test方法中的参数类型修改为HashMap<? extends Foo, String>即可。但是这是在get方法的参数类型是Object情况下才正确,如果get方法的参数类型是泛型,那它对于“? extends Foo”是一无所知的,换句话说,编译器不知道它应该接收Foo类型还是SubFoo类型,甚至是SubSubFoo类型。对于第二个假设,不少网友指出,get方法的参数类型可以是“<T extends E>”,这就能避免第二个问题了。
在国外网友的讨论中,我还是比较倾向于第一种兼容性问题,毕竟泛型相对来说较晚出现,对于作者John也说过,他们尝试把它泛型化,但泛型化过后产生了一系列的问题,这不得不使得他们放弃将其泛型化。其实在源码的get方法注释中能看到put以前也是Object类型,在泛型出现过后,put方法能成功的改造成泛型,而get由于要考虑兼容性问题不得不放弃将它泛型化。
V remove(Object key)
删除Map中的key-value键值对。
void putAll(Map<? extends K, ? extends V> m)
这个方法的参数是一个Map,将传入的Map全部放入此Map中,当然对参数Map有要求,“? extends K”意味着传入的Map其key值需要是此Map的key或者是子类,value同理。
void clear()
移除Map中所有的key-value键值对。
Set<K> keyset()
返回key的set集合,注意set是无序且不可存储重复的值,当然Map中也不可能存在重复的key值,也没有有序无序一说。其实这个方法的运用还是有点意思的,这会涉及到Java对象引用相关的一些知识。
1 Map<String, Integer> map = new HashMap<String, Integer>(); 2 map.put("a", 1); 3 map.put("b", 2); 4 System.out.println(map.keySet()); //output: [a, b] 5 Set<String> sets = map.keySet(); 6 sets.remove("a"); 7 System.out.println(map.keySet()); //output: [b] 8 sets.add("c"); //output: throws UnsupportedOperationException 9 System.out.println(map.keySet());
第4行的输出的是Map中key的set集合,即“[a,b]” 。
接着创建一个set对象指向map.keySet()方法返回set的集合,并且通过这个set对象删除其中的“a”元素。此时再来通过map.keySet()方法打印key的集合,会发现此时打印“[b]”。这是因为我们在虚拟机栈上定义的sets对象其指针指向的是map.keySet()返回的对象,也就是说这两者指向的是同一个地址,那么只要任一一个对其改变都会影响这个对象本身,这也是Map接口对这个方法的定义,同时Map接口对该方法还做了另外一个限制,不能通过keySet()返回的Set对象对其进行add操作,此时将会抛出UnsupportedOperationException异常,原因很简单如果给Set对象add了一个元素,相对应的Map的key有了,那么它对应的value值呢?
Collection<V> values()
返回value值的Collection集合。这个集合就直接上升到了集合的顶级父接口——Collection。为什么不是Set对象了呢?原因也很简单,key值不能重复返回Set对象很合理,但是value值肯定可以重复,返回Set对象显然不合适,如果仅仅返回List对象,那也不合适,索性返回顶级父接口——Collection。
Set<Map.Entry<K, V>> entrySet()
返回Map.Entry的Set集合。
boolean equals(Object o)
int hashCode()
equals在Object类中只是用“==”简单的实现,对于比较两个Map是否值相等显然需要重写equals方法,重写equals方法通常需要重写hashCode方法。重写equals方法需要遵守5个原则:自反性、对称性、传递性、一致性、非空性。在满足了这个几个原则后还需要满足:两个对象equals比较相等,它们的hashCode散列值也一定相等;但hashCode散列值相等,两个对象equals比较不一定相等。
default V getOrDefault(Object key, V defaultValue)
这个方法是JDK8才出现的,并且使用了JDK8的一个新特性,在接口中实现一个方法,叫做default方法,和抽象类类似,default方法是一个具体的方法。这个方法主要是弥补在编码过程中遇到的这样场景:如果一个Map不存在某个key值,则存入一个value值。以前是会写一个判断使用contanisKey方法,现在则只需要一句话就可以搞定map.put("a", map.getOrDefault("a", 2)); 它的实现也很简单,就是判断key值在Map中是否存在,不存在则存入getOrDefault中的defaultValue参数,存在则再存入一次以前的value参数。 (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue;
default void forEach(BiConsumer<? super K, ? super V> action)
这个方法也是JDK8新增的,为了更方便的遍历,这个方法几乎新增在JDK8的集合中,使用这个新的API能方便的遍历集合中的元素,这个方法的使用需要结合Lambda表达式:map.forEach((k, v) -> System.out.println("key=" + k + ", value=" + v))
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
替换Map中的value值,Lambda表达式作为参数,例如:
1 map.replaceAll((k, v) -> 10); //将Map中的所有值替换为10 2 map.replaceAll((k, v) -> { //如果Map中的key值等于a,其value则替换为10 3 if (k.equals("a")) { 4 return 10; 5 } 6 return v; 7 });
default V putIfAbsent(K key, V value)
在ConcurrentHashMap中也有一个putIfAbsent方法,那个方法指的key值不存在就插入,存在则不插入。JDK8中在Map中直接也新增了这个方法,这个方法ConcurrentHashMap#putIfAbsent含义相同,这个方法等同于:
1 if (!map.containsKey(key, value)) { 2 map.put(key, value); 3 } else { 4 map.get(key); 5 }
在之前提到了一个方法和这个类似——getOrDefault。注意不要搞混了,调用putIfAbsent会直接插入,而getOrDefault不会直接插入到Map中。
default boolean remove(Object key, Object value)
原来的remove方法是直接传递一个key从Map中移除对应的key-value键值对。新增的方法需要同时满足key和value同时在Map有对应键值对时才删除
default boolean replace(K key, V oldValue, V newValue)
和replaceAll类似,当参数中的key-oldValue键值对在Map存在时,则使用newValue替换oldValue。
default V replace(K key, V value)
这个方法是上面方法的重载,不会判断key值对应的value值,而是直接使用value替换key值原来对应的值。
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
如果Map中不存在key值,则调用Lambda表达式中的函数主体计算value值,再放入Map中,下次再获取的时候直接从Map中获取。这其实在Map实现本地缓存中随处可见,这个方法类似于下列代码:
1 if (map.get(key) == null) { 2 value = func(key); //计算value值 3 map.put(key, value); 4 } 5 return map.get(key);
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
这个方法给定一个key值,通过Lambda表达式可计算自定义key和value产生的新value值,如果新value值为null,则删除Map中对应的key值,如果不为空则用新的替换旧的值。
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
这个方法是上面两个方法的结合,有同时使用到上面两个的地方可使用这个方法代替,其中Lambda表达式的函数主体使用三木运算符。
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
“合并”,意味着旧值和新值都会参与计算并复制。给定key和value值参数,如果key值在Map中存在,则将旧value和给定的value一起计算出新value值作为key的值,如果新value为null,那么则从Map中删除key。如果key不存在,则将给定的value值直接作为key的值。
Map映射集合类型作为Java中最重要以及最常用的数据结构之一,Map接口是它们的基类,在这个接口中定义了许多基础方法,而具体的实习则由它的子类完成。JDK8在Map接口中新值了许多default方法,这也为我们在实际编码中提供了很大的便利,如果是使用JDK8作为开发环境不妨多多学习使用新的API。
这是一个能给程序员加buff的公众号
以上是关于Java集合中的Map接口的主要内容,如果未能解决你的问题,请参考以下文章