一、Map集合类思维导图

java map 对比内容是否相同 java map区别_java map 对比内容是否相同

Map:使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

二、HashMap、Hashtable、LinkedHashMap及TreeMap区别简述

  • Hashmap:散列表,存储Key-Value值,继承于AbstractMap,实现Map、Cloneable、Serializable接口,非线程安全无序
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
  • Hashtable:散列表,存储Key-Value值,继承于Dictionary,实现了Map、Cloneable、Java.io.Serializable接口,采用synchronized实现线程安全的,无序
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable
  • LinkedHashMap:双向链表,继承于HashMap,非线程安全的,允许key,value为null,有序
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
  • TreeMap:红黑树(自平衡二叉查找树),存储Key-Value值,继承于AbstractMap,实现了NavigableMap、Cloneable、Java.io.Serializable接口,非线程安全,有序
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

三、详解及示例说明

  • HashMap

1、存储结构:Entry数组+链表+红黑树

java map 对比内容是否相同 java map区别_System_02

上面左侧是Node[] table哈希桶数组,存放Node链表元素,HashMap最多只允许一条Entry的键位null(多了会覆盖),允许多条Entry的值为null。

HashMap采用哈希表存储,在HashMap中采用拉链法解决哈希冲突;拉链法就是数组+链表,每个数组元素都是一个链表,当Key.hashCode()方法得到hashCode值,然后通过哈希算法(高位运算+取模运算)来定位键值的存储位置,如果Key值相同,则产生哈希碰撞,将数据放在对应下标元素的链表上。

为了让HashMap存储高效,要减少hash碰撞,也就是要hash算法计算结果尽量均匀。如果哈希桶数组很大,较差的hash算法也会分布较均匀,Hash值的范围值是-2147483648~2147483647,前后约40亿的映射空间,基本不会出现碰撞,但是哈希桶数组不可能这么大,内存也放不下;如果hash哈希桶数组很小,较好的hash算法也会很产生较多的hahs碰撞。由此可见,好的Hash算法和好的扩容机制是节省时间&空间和减少hash碰撞的重要因素。

简单看下HashMap如何设计Hash算法,HashMap中hash源码如下

JDK1.7

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8 Hash算法本质:取key的hashCode值、高位运算、取模运算

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

大家都知道位运算效率高于取余运算,所以这里采用位运算,也就决定了HashMap的长度是2的幂次方。

2、多线程操作HashMap导致死循环(JDK1.7)

Hash表如果有新的数据要插入时,会检查容量是否超过设定门限值,如果超过就要对Hash表进行扩容,用新的数组代替原有数组,这里需要对表中所有元素进行rehash计算,所以扩容是一个特别损耗性能的操作,HashMap默认容量是16,我们在初始定义的时候要根据需要确定合适的容量大小,减少不必要的性能损耗。

详细介绍多线程操作导致死锁循环问题

正常的rehash过程如下

java map 对比内容是否相同 java map区别_System_03

首先哈希桶数组初始大小是2,由于hash冲突,三个元素都在table[1] 链表元素里了,Node<k,v>分别为(3,A)、(7,B)和(5,C),哈希桶数组大小不够需要扩容2到4。

下面有Thread1和Thread2两个线程同时操作HashMap进行扩容,Thread1执行到下图代码注释处,Thread2已经扩容完成

void transfer(Entry[] newTable)
    {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;//Thread1挂起
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

java map 对比内容是否相同 java map区别_数组_04

此时,Thread1挂起,e指向3,next指向7,Thread2扩容完成,链表由上图的Thread1链表变成了Thread2的链表,当再次回到Thread1时采用头插法,e.next = newTable[i]//null,即节点3指向空,newTable[i] = e,即节点3插入到table[3] 中第一个位置,此时,next值依然为节点7,执行e=next,e值指向节点7如下图

java map 对比内容是否相同 java map区别_java map 对比内容是否相同_05

此时,由于链表变化,不再是扩容前size=2时的链表顺序,而是Thread2扩容之后的链表顺序,导致next=e.next,即next为节点3而不是节点5,这时候再执行e.next = newTable[i]//节点3,就造成了下图的环形链表,造成死循环。

java map 对比内容是否相同 java map区别_System_06

JDK1.8之后采用尾插法替代了头插法,同时增加了红黑树,极大优化了HashMap的性能,避免了死循环的问题,但还是会造成put元素丢失数据的情况,所以HashMap就是线程不安全的,如果想用线程安全的,JDK建议使用ConcurrentHashMap。

3、HashMap构造方法

  • HashMap():它是默认的构造函数,它创建一个初始容量为16,加载因子为0.75的HashMap实例。
  • HashMap(int initial capacity):它创建一个具有指定初始容量和加载因子0.75的HashMap实例。
  • HashMap(int initial capacity,float loadFactor):它创建一个具有指定初始容量和指定加载因子的HashMap实例。
  • HashMap(Map map):使用与指定映射相同的映射创建HashMap的实例。
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all   other fields defaulted
    }

    // 包含另一个“Map”的构造函数
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);//下面会分析到这个方法
    }

    // 指定“容量大小”的构造函数
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 指定“容量大小”和“加载因子”的构造函数
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

loadFactor加载因子用来控制数组存放数据疏密程度,太大会导致数组利用率低,太小导致存放数据分散,数组利用率低,官方默认值0.75f是一个比较合适的临界值。上面提到的扩容门限值threshold=capacity*loadFactor,当size>=threshold就要扩容。负载因子的值在0和1之间变化。初始容量&负载因数的设定极大程度上决定了HashMap的使用性能

4、HashMap常用方法代码示例

package Map;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Hashmapdemo1 {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        //key不能重复,value值可以,key重复会被覆盖
        map.put(1, "sss");
        map.put(1, "Java");
        map.put(2, "is");
        map.put(3, "beautiful");
        System.out.println("------直接输出hashmap:  -------");
        System.out.println(map);
        //遍历输出hashmap
        //1、通过keySet()遍历
        System.out.println("------遍历keySet()遍历hashmap:  -------");
        for (int key : map.keySet()) {
            System.out.println("key: " + key + "; value: " + map.get(key));
        }
        //2、通过entrySet()遍历
        System.out.println("------遍历entrySet()遍历hashmap:  -------");
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println("key: " + entry.getKey() + "; value: " + entry.getValue());
        }
        //3、通过values遍历值
        System.out.println("------遍历values遍历hashmap值:  -------");
        for (String value : map.values()) {
            System.out.println("values: " + value);
        }
        //4、通过迭代器遍历entrySet()
        System.out.println("------遍历entrySet().iterator遍历hashmap:  -------");
        Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, String> entryit = it.next();
            System.out.println("key: " + entryit.getKey() + "; value: " + entryit.getValue());
        }

        System.out.println("map大小:" + map.size());
        System.out.println("map是否为空:" + map.isEmpty());
        System.out.println("map移除key为2的值:" + map.remove(2));
        System.out.println("map移除key为2之后:" + map);
        System.out.println("map取key为1的值:" + map.get(1));
        System.out.println("map是否包含key为2的元素:" + map.containsKey(2));
        System.out.println("map是否包含value为Java的元素:" + map.containsValue("Java"));
        System.out.println("map替换key为1的值为 C++:" + map.replace(1, "C++"));
        System.out.println("map替换key为1的值为 C++之后map:" + map);
    }
}

 注:使用entrySet()遍历方式比keySet()快,entrySet是直接从Entry对象中获得,时间复杂度o(1),keySet遍历要从Map中重新获取,时间复杂度为o(n),keySet比entrySet多遍历了一次table。

  • Hashtable

Hashtable与HashMap类似,不同的地方在于Hashtable不允许键值为null;同时Hashtable是线程安全的,采用synchronized修饰,但是由于synchronized的修饰的是全表,导致同步性能非常差,总体上速度比HashMap慢,现在Hashtable基本没怎么用,这里就不深入探索了。

  • ConcurrentHashMap

底层采用分段的数组+链表/红黑树,JDK1.7时,ConcurrentHashMap对整个哈希桶数组进行分割分段(Segment),每一把锁只锁其中一部分数据,提高并发访问效率,JDK1.8时,ConcurrentHashMap没有Segment概念,通过synchronized+CAS来实现线程安全。对比Hashtable对整个存储结构用同一把锁,效率提高很多,所以涉及线程安全的map建议使用concurrentHashMap。

  • LinkedHashMap

HashMap是集合Map中最为常用的,但是HashMap是无序的,LinkedHashMap为我们解决了这个问题,作为HashMap的子类,虽然增加了时间&空间的开销,但是它通过额外的双向链表保证了迭代顺序迭代顺序可以是插入时的顺序,也可以是访问的顺序。LinkedHashMap结构图如下

java map 对比内容是否相同 java map区别_ci_07

这些Node<k,v>中,出去橙色绿色的线,就是上面HashMap的结构,单独的橙色绿色的线就是一个双向链表,由此可见LinkedHashMap结构其实就是HashMap&LinkedeList融合体。

 默认的是按照插入顺序迭代输出的,示例代码:

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class MapDemo {

    public static void main(String[] args) {
        HashMap<Character, String> map = new HashMap<>();
        map.put('a', "Java1");
        map.put('z', "Java2");
        map.put('b', "Java5");
        map.put('h', "Java3");
        map.put('s', "Java4");
        Iterator<Map.Entry<Character, String>> it1 = map.entrySet().iterator();
        while (it1.hasNext()) {
            // 按照hash顺序输出(对比插入顺序是“无序”的)
            Map.Entry<Character, String> entry = it1.next();
            System.out.println(entry.getKey()+" "+entry.getValue());
        }
        System.out.println();
        LinkedHashMap<Character, String> linkedMap = new LinkedHashMap<>();
        linkedMap.put('a', "Java1");
        linkedMap.put('z', "Java2");
        linkedMap.put('b', "Java5");
        linkedMap.put('h', "Java3");
        linkedMap.put('s', "Java4");
        Iterator<Map.Entry<Character, String>> it2 = linkedMap.entrySet().iterator();
        while (it2.hasNext()) {
            // 按照插入顺序输出结果
            Map.Entry<Character, String> entry = it2.next();
            System.out.println(entry.getKey()+" "+entry.getValue());
        }
    }

}

这里需要注意的是:LinkedHashMap遍历速度与实际数据有关和容量无关,因为它通过双向链表遍历,而HashMap遍历速度和其容量有关。

  •  TreeMap

TreeMap是一个有序的key-value集合,通过红黑树实现的,能够把它保存的记录根据键排序,默认按键值升序排序,或者根据Comparator进行排序,取决于使用的构造方法,非线程安全

默认自然排序示例代码如下

import java.util.*;

public class MapDemo {

    public static void main(String[] args) {
        HashMap<Character, String> map = new HashMap<>();
        map.put('a', "Java1");
        map.put('z', "Java2");
        map.put('b', "Java5");
        map.put('h', "Java3");
        map.put('s', "Java4");
        Iterator<Map.Entry<Character, String>> it1 = map.entrySet().iterator();
        while (it1.hasNext()) {
            // 按照hash顺序输出(对比插入顺序是“无序”的)
            Map.Entry<Character, String> entry = it1.next();
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
        System.out.println();
        TreeMap<Character, String> treeMap = new TreeMap<>();
        treeMap.put('a', "Java1");
        treeMap.put('z', "Java2");
        treeMap.put('b', "Java5");
        treeMap.put('h', "Java3");
        treeMap.put('s', "Java4");
        Iterator<Map.Entry<Character, String>> it2 = treeMap.entrySet().iterator();
        while (it2.hasNext()) {
            // 按照key值排序
            Map.Entry<Character, String> entry = it2.next();
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }

}

这里key为什么能够自然排序,其实是Integer类继承了Comparable接口

public final class Integer extends Number implements Comparable<Integer>

 如果要排序的key值类型没有实现排序,那么程序就会运行报错,如下图会报错

package Map;

import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo {
    static class bird {
        int age;

        public bird(int age) {
            this.age = age;
        }
    }

    public static void main(String[] args) {
        TreeMap<bird, Integer> map = new TreeMap<>();
        map.put(new bird(1), 1);
        map.put(new bird(2), 2);
        map.put(new bird(66), 66);
        map.put(new bird(8), 8);
        Iterator<Map.Entry<bird, Integer>> iterator = map.entrySet().iterator();
        //这里key值是bird类,本身是无序的,虽然编译没错,但是TreeMap是有序的
        //所以这样运行时会报错的bird cannot be cast to java.lang.Comparable
        while (iterator.hasNext()) {
            Map.Entry<bird, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

 Comparable接口排序示例代码如下

package Map;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo {
    static class bird implements Comparable<bird> {
        int age;

        public bird(int age) {
            this.age = age;
        }

        @Override
        public int compareTo(bird b) {
            if (this.age > b.age)
                return 1;
            else if (this.age < b.age)
                return -1;
            return age;
        }
    }

    public static void main(String[] args) {
        TreeMap<bird, Integer> map = new TreeMap<>();
        map.put(new bird(1), 1);
        map.put(new bird(2), 2);
        map.put(new bird(66), 66);
        map.put(new bird(8), 8);
        Iterator<Map.Entry<bird, Integer>> iterator = map.entrySet().iterator();
        //这里key值是bird类,本身是无序的,但实现了Comparable的compareTo方法
        while (iterator.hasNext()) {
            Map.Entry<bird, Integer> entry = iterator.next();
            System.out.println(entry.getValue());
        }
    }
}

未完待续......

本博客是个人学习总结,参考学习了很多知识(网络&源码&书),如果内容有误或者有资料参考未列举出来,欢迎交流~

---------------------------------------------------------------------------------------------------------------------------------------------------------------------

2019.12.14 更新

// 如果map里没有key为“name”的value值,就test
String name = map.getOrDefault("name", "test")
// 如果map里没有key为“name”的value值,就test 再加test
map.put(name,map.getOrDefault(name,"test")+"test");