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 }
View Code

  从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而不是泛型。在这个地方似乎是使用泛型更加合适。思考以下场景:

  1. 最开始我写了一段代码,定义HashMap<String, String>,定义HashMap<String, String>,此时我put("a", "a"),同时我通过get("a")获取值。
  2. 写着写着,我发现我应该定义为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-objecthttp://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的公众号 

posted @ 2018-02-26 22:54  OKevin  阅读(1924)  评论(0编辑  收藏  举报