JAVA_map总结

map,键值对的集合,由于和pojo的结构和map类似,经常相互转换,或者作为带有特定标识的数据的集合存储方式二使用。

还是先放结论:

类型数据结构特点描述
HashMap散列表(拉链法)最常用,无序,线程不安全
Hashtable散列表(拉链法)无序,线程安全
LinkedHashMap双向链表+散列表(拉链法)有序(插入顺),线程不安全
WeakHashMap散列表(拉链法)无序,线程不安全,弱引用键,保存对象被回收时,自动删除
TreeMap红黑树有序(自动排序),线程不安全

基本概念,何为哈希?

先扔个概念摘自百度百科,哈希:

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

关于散列表:

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

拉链法:

拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1

嗯,看了觉得很迷糊对吧,那么来一步步的说。

  1. 我们的map,是键值对,key-value的形式,每次存取都需要判别一次key,但是要是每次比较,每次遍历,那样实在是太慢了
  2. 所以呢,就变了一个函数hash(key),取得的结果是一个int,用这个int作为数组的下标,数组内容存储对应的value。这种使用哈希算法作为下标值,在一个数组上进行存储对象的方法就是散列表
  3. 但是呢,有可能hash(key)的结果是一样的,但是key却不一样。为了避免这种冲突问题,数组存储的内容不是直接存储值,而是进行了一定变变换,或者链表或者序列,总之使得key是同一哈希值数据,在数组的同一位置上存储。这就是拉链法。

嗯,干巴巴的说果然还是很难理解,那么,现在有以下数据,要用散列表(拉链法)的方式进行存储,结果会是这么样的呢?

  1. 数据:

    {"aa","1"},{"ab","2"},{"bA","3"},{"bB","4"},{"bC","5"}

    顺便,在jdk1.8.0_121中,对String对象的哈希算法如下:

     public int hashCode() {
         int h = hash;
         if (h == 0 && value.length > 0) {
             char val[] = value;
    
             for (int i = 0; i < value.length; i++) {
                 h = 31 * h + val[i];
             }
             hash = h;
         }
         return h;
     }
    
  2. 则计算以上数据key值得哈希值分别为: 3104,3105,3103,3104,3105

    于是使用数组存储的话,大体是这么个印象

     数组[3103] = {"bA","3"}  
     数组[3104] = {"aa","1"},{"bB","4"}  
     数组[3105] = {"ab","2"},{"bC","5"}
    
  3. 由于存在哈希值相同的情况,所以需要使用拉链法进行。由于要在数组同一位置存多个数据,采用单链的方式的话,需要记录链表下一个,next的值,所以需要包装一下。结果的话,大体是这种效果:

     数组(3103) -> {"bA","3"}  
     数组(3104) -> {"aa","1"} -> {"bB","4"}  
     数组(3105) -> {"ab","2"} -> {"bC","5"}
    

HashMap

经过以上说明,基本已经对hash有了概念的话,接下来就方便了。

还是上源码(jdk1.8.0_121,java.util.HashMap):

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;    // 键
    V value;        // 值
    Node<K,V> next; // 单链搜索用对象引用

    Node(int hash, K key, V value, Node<K,V> next) {......}
    public final K getKey()        {......}
    public final V getValue()      {......}
    public final String toString() {......}
    public final int hashCode() {......}
    public final V setValue(V newValue) {......}
    public final boolean equals(Object o) {......}
}

这是HashMap的关键,hashmap本质是散列表,是Node<K,V>[] table。由于需要保存key-value的键值对,还使用了单链的拉链法,所以封装了一个内部类class来进行存储,包含用于存储键的key,存储值的value ,哈希相同时,存储下一个对象的引用用的next

另外,HashMap的方法是不带synchronized的,所以非线程安全。

示例:

HashMap<String,String>  hashMap = new HashMap<String,String>();
hashMap.put("aa","1");
hashMap.put("ab","2");
hashMap.put("bA","3");
hashMap.put("bB","4");
hashMap.put("bC","5");
Iterator iter = hashMap.entrySet().iterator();
while (iter.hasNext()){
    Map.Entry entry = (Map.Entry)iter.next();
    System.out.println("key:"+entry.getKey()+",value:"+entry.getValue());
}
// 结果
// key:aa,value:1
// key:bB,value:4
// key:ab,value:2
// key:bC,value:5
// key:bA,value:3

Hashtable

由于HashMap和Hashtable非常相似,所以仅仅说一说Hashtable的区别。

  1. 继承对象不同:

    HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。

    Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。

    因此,Dictionary是类似字典的枚举类,相对于AbstractMap,对键值对的支持没有那么全

  2. Hashtable的方法有synchronized,是线程安全的。

    注,有synchronized安全,Hashtable因为效率低,并不是HashMap的线程安全替代品,推荐使用java.util.concurrent.ConcurrentHashMap对象或者Collections.synchronizedMap(Map<K,V> m)的方法。

  3. 对于null:

    HashMap的key、value都可以为null。

    Hashtable的key、value都不可以为null。

其他的还有api不同,遍历种类不同,遍历顺序不同容量初值不同,hash算法不同等,姑且知道即可


LinkedHashMap

首先,LinkedHashMap继承于HashMap,所以HashMap的相关功能都具备。然后,观察以下源码:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

private static final long serialVersionUID = 3801124242820219131L;

transient LinkedHashMap.Entry<K,V> head;

transient LinkedHashMap.Entry<K,V> tail;

一个头,一个尾,扩展节点指前指后,对,双向链表。

LinkedHashMap比HashMap多维护的这个双向链表保存了map内容的插入顺序,并重写了相关的方法,使得对LinkedHashMap遍历时的,可以根据插入顺序,有序的遍历。

示例代码:

LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<String,String>();
linkedHashMap.put("aa","1");
linkedHashMap.put("ab","2");
linkedHashMap.put("bA","3");
linkedHashMap.put("bB","4");
linkedHashMap.put("bC","5");
Iterator iter3 = linkedHashMap.entrySet().iterator();
while (iter3.hasNext()){
    Map.Entry entry = (Map.Entry)iter3.next();
    System.out.println("key:"+entry.getKey()+",value:"+entry.getValue());
}
// 结果
// key:aa,value:1
// key:ab,value:2
// key:bA,value:3
// key:bB,value:4
// key:bC,value:5

WeakHashMap

直降上例子:

Map<String, String> weak = new WeakHashMap<String, String>();
weak.put(new String("1"), "1");
weak.put(new String("2"), "2");
weak.put(new String("3"), "3");
weak.put(new String("4"), "4");
weak.put(new String("5"), "5");
weak.put(new String("6"), "6");
System.out.println(weak.size());  // 结果:6
System.gc();  //手动触发 Full GC
try {
    Thread.sleep(50); //我的测试中发现必须sleep一下才能看到不一样的结果
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(weak.size());  // 结果:0

WeakHashMap的key对象,不被其他地方引用时,会在gc执行后自动被删除。这就是WeakHashMap最重要的特点:弱引用

总之,记住这一点即可,除非是用于做监控程序什么的,不然WeakHashMap很少被使用。


TreeMap

还是先上例子:

TreeMap<String,String> treeMap = new TreeMap<String,String>();
treeMap.put("bB","4");
treeMap.put("bC","5");
treeMap.put("aa","1");
treeMap.put("ab","2");
treeMap.put("bA","3");
Iterator iter4 = treeMap.entrySet().iterator();
while (iter4.hasNext()){
    Map.Entry entry = (Map.Entry)iter4.next();
    System.out.println("key:"+entry.getKey()+",value:"+entry.getValue());
}
// 结果
// key:aa,value:1
// key:ab,value:2
// key:bA,value:3
// key:bB,value:4
// key:bC,value:5

可以发现,遍历的输出结果,自动的进行了排序(默认按照键,可在构造中设置Comparator)。

另外,TreeMap的本质是红黑树,适用于有序的整理遍历。关键代码如下:

private transient Entry<K,V> root;

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;                // 键 
    V value;              // 值
    Entry<K,V> left;      // 左节点
    Entry<K,V> right;     // 右节点
    Entry<K,V> parent;    // 父节点
    boolean color = BLACK;// 自身颜色

    Entry(K key, V value, Entry<K,V> parent) {......}
    public K getKey() {......}
    public V getValue() {......}
    public V setValue(V value) {......}
    public boolean equals(Object o) {......}
    public int hashCode() {......}
    public String toString() {......}
}

红黑树比较麻烦,之后再介绍一些数据在说一说,这里对TreeMap,记住本质是红黑树,自动排序即可。


PS:set集合

map大体就是以上说明,趁着这个机会,也把set稍微总结下吧。

  1. 首先,set继承于Collection,所以类似于List,使用add(),remove()的方法。
  2. 但是,由于set要求使用和map的key一样的,对key进行判断,不允许重复的特性,所以效果上和map的key非常相似。
  3. 因此,set的底层,甚至是直接使用了map对应的对象,直接利用map的key执行实现其不允许重复的特性

|类型|实现使用map对象|数据结构|特点描述|

|---|---|---------|

|HashSet|HashMap|散列表(拉链法)|最常用,无序,线程不安全|

|LinkedHashSet|LinkedHashMap|双向链表+散列表(拉链法)|有序(插入顺),线程不安全|

|TreeMap|TreeMap|红黑树|有序(自动排序),线程不安全|