jdk1.8.0_144
Map 是 Java 三种集合中的一种位于 java.util 包中, Map 作为一个接口存在定义了这种数据结构的一些基础操作, 它的最终实现类有很多: HashMapTreeMapSortedMap 等等, 这些最终的子类大多有一个共同的抽象父类 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 是如何排序的:
- /**
- * Sort a Map by Keys.JDK7
- * @param map To be sorted Map.
- * @return Sorted Map.
- */
- public Map<String, Integer> sortedByKeys(Map<String, Integer> map) {
- List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet());
- Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
- @Override
- public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
- return o1.getKey().compareTo(o2.getKey());
- }
- });
- Map<String, Integer> linkedMap = new LinkedHashMap<>();
- Iterator<Map.Entry<Strin g, Integer>> iterator = list.iterator();
- while (iterator.hasNext()) {
- Map.Entry<String, Integer> entry = iterator.next();
- linkedMap.put(entry.getKey(), entry.getValue());
- }
- return linkedMap;
- }
- View Code
从 JDK7 版本对 Map 排序的代码可以看到, 首先需要定义泛型参数为 Map.Entry 类型的 List, 利用 Collections.sort 对集合 List 进行排序, 再定义一个 LinkedHashMap, 遍历集合 List 中的元素放到 LinkedHashMap 中, 也就是说并没有一个类似 Collections.sort(Map, Comparator)的方法对 Map 集合类型进行直接排序 JDK8 对此作了改进, 通过 Stream 类对 Map 进行排序
- /**
- * Sort a Map by Keys.JDK8
- * @param map To be sorted Map.
- * @return Sorted Map.
- */
- public Map<String, Integer> sortedByKeys(Map<String, Integer> map) {
- Map<String, Integer> result = new LinkedHashMap<>();
- map.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(x -> result.put(x.getKey(), x.getValue()));
- return result;
- }
可见代码量大大减少, 简而言之, 这四个方法是 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 之前存在以下代码:
- HashMap hashMap = new HashMap();
- ArrayList arrayList = new ArrayList();
- hashMap.put(arrayList, "this is list");
- System.out.println(hashMap.get(arrayList));
- LinkedList linkedList = new LinkedList();
- System.out.println(hashMap.get(linkedList));
这段代码在不使用泛型的时候能运行的很好, 如果此时 get 方法中的参数变成了泛型, 而不是 Object, 那么此时 hashMap.get(linkedList)这句话将会在编译时出错, 因为它不是 ArrayList 类型
2. 无法确定 Key 的类型这里有一个例子:
- public class HashMapTest {
- public static void main(String[] args) {
- HashMap < SubFoo,
- String > hashMap = new HashMap < >();
- //SubFoo 是 Foo 类的子类
- test(hashMap); // 编译时出错
- }
- public static void test(HashMap < Foo, String > hashMap) { // 参数为 HashMap,key 值是 Foo 类, 但是不能接收它的子类
- System.out.println(hashMap.get(new Foo()));
- }
- }
上面这种情况把 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 对象引用相关的一些知识
- Map<String, Integer> map = new HashMap<String, Integer>();
- map.put("a", 1);
- map.put("b", 2);
- System.out.println(map.keySet()); //output: [a, b]
- Set<String> sets = map.keySet();
- sets.remove("a");
- System.out.println(map.keySet()); //output: [b]
- sets.add("c"); //output: throws UnsupportedOperationException
- 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 表达式作为参数, 例如:
- map.replaceAll((k, v) -> 10); // 将 Map 中的所有值替换为 10
- map.replaceAll((k, v) -> { // 如果 Map 中的 key 值等于 a, 其 value 则替换为 10
- if (k.equals("a")) {
- return 10;
- }
- return v;
- });
- default V putIfAbsent(K key, V value)
在 ConcurrentHashMap 中也有一个 putIfAbsent 方法, 那个方法指的 key 值不存在就插入, 存在则不插入 JDK8 中在 Map 中直接也新增了这个方法, 这个方法 ConcurrentHashMap#putIfAbsent 含义相同, 这个方法等同于:
- if (!map.containsKey(key, value)) {
- map.put(key, value);
- } else {
- map.get(key);
- }
在之前提到了一个方法和这个类似 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 实现本地缓存中随处可见, 这个方法类似于下列代码:
- if (map.get(key) == null) {
- value = func(key); // 计算 value 值
- map.put(key, value);
- }
- 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
来源: https://www.cnblogs.com/yulinfeng/p/8476573.html