1、Map接口简介

集合根据数据存储的不同分为两种形式:单值集合、二元偶对象集合,在之前所使用的Collection都属于单值集合。
现在所学习的Map属于二元偶对象集合,所谓的二元偶对象指的是存储的数据为“key=value”结构对,在使用的时候可以根据key查询出相应的value的内容。

所以Collection和Map存储数据的目的分别为:Collection是为了数据的输出而存储,而Map是为了数据的查询而存储。

java.util.Map是进行二元偶对象数据存储的最大父接口,在里面所有存放的内容会按照“key=value”的形式进行保存,所以在数据存放的时候就需要保存有两个内容,Map接口的常用方法如下:

V put(K key, V value) 
          将指定的值与此映射中的指定键关联(向集合中保存数据,如果key存在则发生替换,
          同时返回旧的内容,如果key不存在返回的内容为空) (非常重要)
 V get(Object key) 
          返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 (非常重要)
 V remove(Object key) 
          如果存在一个键的映射关系,则将其从此映射中移除 
 int size() 
          返回此映射中的键-值映射关系数(获取集合长度) 
Collection<V> values() 
          返回此映射中包含的值的 Collection 视图  (返回所有内容)
 Set<K> keySet() 
          返回此映射中包含的键的 Set 视图  (获取所有的key,key不可重复)
          (key一旦重复,会使用新的内容去替换旧的内容)
Set<Map.Entry<K,V>> entrySet() 
          返回此映射中包含的映射关系的 Set 视图  (将所有的内容以Map.Entry集合的形式返回) (非常重要)

十六、Java集合框架之Map集合_数据
在JDK1.9之后在Map接口之中提供有许多的of)方法,利用此方法可以方便的创建一个key不重复的Map集合

**例:**创建Map集合

public class Demo05 {

    public static void main(String[] args) {
        Map<String,Integer> map =Map.of("one",1,"two",2,"three",3);
        System.out.println(map);

    }

}

程序执行结果:{two=2, three=3, one=1}

通过结果可以发现,此时的程序会按照“key=value”的形式进行保存,同时此时的存储是无序的(Map的功能是进行查询,是否有序意义不大),但是使用此种方式创建的Map集合,如果出现有key重复的问题,那么程序就会出现如下的异常:
Exception in thread"main"java.lang.Illegal ArgumentException:kluplicate key:

因为key作为Map操作的核心控制点,所以这个内容的重复实际上对于整个的Map而言就需要更新,如果想要正确的使用Map接口,那么就必须使用它的几个子类,常见的子类:HashMap、LinkedHashMap、TreeMap、HashTable。

2、HashMap

HashMap是Map接口中最为常见的一个子类,也是主要使用的一个子类,此类通过名称就可以发现,采用Hash的方式进行存储,所以其存储的时候都是无序的,此类的定义结构如下:

public class HashMap<K,V> extends  AbstractMap<K,V>
           implements Map<K,V>, Cloneable, Serializable

十六、Java集合框架之Map集合_数据_02
:使用HashMap进行数据存储

public class Demo05 {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        System.out.println("【未发生替换】"+map.put("hello","you"));
        System.out.println("【已发生替换】"+map.put("hello","guys"));
        System.out.println("【未发生替换】"+map.put("wow","gays"));

        map.put("empty",null);
        map.put(null,"empty");

        System.out.println(map);
    }
}

程序执行结果:
【未发生替换】null (由于没有相同的key,所以返回内容是null)
【已发生替换】you (返回旧的数据)
【未发生替换】null (由于没有相同的key,所以返回内容是null)
{null=empty, hello=guys, wow=gays, empty=null}
通过以上的存储可以发现,Map中的key绝对是唯一的标记,不可能重复,同时在Map集合里面也可以实现null的内容存储。 HashMap允许key 和 value 同时为空
使用Map集合的意义在于需要根据key进行内容的查找。

**例:**根据key查找数据

public class Demo05 {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("hello","you");
        map.put("q","you");
        map.put("w","you");
        map.put("e","you gays");

        System.out.println(map.get("e"));
        System.out.println(map.get("everyone"));
    }

}

程序执行结果:
you gays (设置的key为e 可以正常查询)
null (由于key不存在返回的结果就是null)

如果只是进行方法的使用研究那么对于HashMap而言意义不大,因为必须要关注HashMap的源代码实现机制。(JDK1.8之后HashMap算法进行了重大的变更)。

分析HashMap源码实现机制

1、无参构造

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;    // all other fields defaulted
    }
    
 static final float DEFAULT_LOAD_FACTOR = 0.75f;  (默认的扩充阀值为“75%”)
 final float loadFactor;

2、put()方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

3、putVal()方法 实现了节点的相关创建以及扩容的调用(resize())

4、resize()

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 (默认容量大小为16个)
 transient Node<K,V>[] table;  (默认根据数组形式存储)
 static final int MAXIMUM_CAPACITY = 1 << 30; (最大存储30位)
 oldCap<<1(每次扩容1倍)

5、性能保证 static final int TREEIFY_THRESHOLD=8; (树状阈值)

if (binCount>=TREEIFY THRESHOLD-1)
treeifyBin(tab,hash);  //进行树状结构转换
 break;

static final int MIN_TREEIFY_CAPACITY = 64;

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //这里还有一个限制条件,当table的长度小于MIN_TREEIFY_CAPACITY(64)时,只是进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)   
            resize();                                                                                                   //而扩容的时候,链表的长度有可能会变短
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {        
                //将链表中的结点转换为树结点,形成一个新链表
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);

            if ((tab[index] = hd) != null)   //将新的树结点链表赋给第index个桶
                hd.treeify(tab);            //执行 TreeNode中的treeify()方法
        }
}

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
}

对于HashMap来讲,如果要进行扩容,则表示当前的存储容量达到“75%”的时候才会选择扩容。在JDK1.8之后如果),链表长度大于8而且整个map中的键值对大于等于MIN_TREEIFY_CAPACITY (64)时,才进行链表到红黑树的转换为了保证数据的查询性能,HashMap会将原始的链表存放结构转为红黑树结构进行保存,利用红黑树中自旋的处理实现树的平衡修复。
先看一下数组链表和哈希表的区别
再参考这里HashMap更详细的源码讲解
最后再看HashMap数据结构和存储过程详解看这里

3、LinkedHashMap

HashMap之中进行数据存储的时候并不会进行顺序的定义,所以如果现在要想实现顺序的存储,就可以利用LinkedHashMap子类来完成,这个类的定义如下:

public class LinkedHashMap <K,V> extends HashMap <K,V> 
                                        implements Map<K,V>

十六、Java集合框架之Map集合_红黑树_03
**例:**观察LinkedHashMap实现

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("hello","you");
        map.put("q","you");
        map.put("w","you");
        map.put("e","you gays");

        System.out.println(map);
    }

}

程序执行结果:
{hello=you, q=you, w=you, e=you gays}

4、TreeMap

java.util.TreeMap实现的是一个排序的树结构,可以依据key的自然顺序实现排序的处理,此类的定义如下:

public class TreeMap<K,V>extends AbstractMap<K,V>
                implements NavigableMap<K,V>, Cloneable, Serializable

十六、Java集合框架之Map集合_map集合_04
:使用TreeMap进行排序

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> map = new TreeMap<>();
        map.put("hello","you");
        map.put("q","you");
        map.put("empty",null);
//        map.put(null,"empty");

        System.out.println(map);
    }

}

程序执行结果:
{empty=null, hello=you, q=you}

既然此时是要进行key数据的排序,那么在使用的过程之中key的数据内容就绝对不能为null

5、Hashtable

类集中有三老(出现时间很古老,让人聊起来觉得很显老,使用起来觉得资格很老),Vectory、Enumeration、Hashtable常见的三老,Hashtable是在JDK1.0的时候提出的集合操作最早偶对象存储。
Hashtable定义如下:

public class Hashtable<K,V>extends Dictionary<K,V>
                   implements Map<K,V>, Cloneable, Serializable

Dictionary是最早实现字典存储结构的父类。

十六、Java集合框架之Map集合_红黑树_05
例:使用Hashtable

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> map = new Hashtable<>();
        map.put("hello","you");
        map.put("q","you");
//        map.put("empty",null);
//        map.put(null,null);  //key value 都为空

        System.out.println(map);
    }

}

程序执行结果:{hello=you, q=you}
Hashtable在进行数据存储的时候是不允许存放null数据的,不管是key还是value,如果发现有null,那么最终都会出现“NullPointerException”。
而相比较HashMap不管是在key还是在value上都没有关于null的限制。

面试题:请解释HashMap与Hashtable的区别?

  • HashMap在进行存储的时候默认的大小为16,在桶的容量达到8位之后为了保证数据的查询性能使用红黑树进行存储;HashMap中的全部方法都使用异步处理,属于非线程的安全操作; HashMap存储的key和value都允许为null。
  • Hashtable进行存储时默认的大小为11;Hashtable中的方法使用同步处理,属于线程安全的操作。Hashtable存储的key和value都不允许为null。

关于红黑树 推荐看这篇博文 漫画讲解红黑树

6、Map.Entry

通过一系列的分析之后实际上就可以得出整个Map接口的基本使用情况,但是对于数据的存储必须进行详细说明,Map集合与Collection集合的最大不同在于,它所存储的数据是二元偶对象。

Collection:
十六、Java集合框架之Map集合_map集合_06
Map:
十六、Java集合框架之Map集合_子类_07
在Map里面由于需要存放有两个内容,很明显为了可以进行整体的处理方便,就在Map接口里面定义有一个Map.Entry的内部接口,此接口主要是进行key和value封装的,而且在Map接口里面也可以发现Map.Entry的子类

static class Node<K,V> implements Map. Entry<K,V>{
final int hash;
 final K key;
 V value; 
 Node<K,V>next;
 }

Map.Entry 里面可以包装key和value,那么也可以通过Map.Entry获取对应的key和value此接口定义如下:

public static interface Map.Entry<K,V>

关键方法

 K getKey() 
          返回与此项对应的键。 
 V getValue() 
          返回与此项对应的值。 

:创建Map.Entry对象
·JDK1.9之后增加的方法:static <K,V> Map.Entry <K,V> entry(K k,V v)

public class Demo06 {

    public static void main(String[] args) {
        Map.Entry<String,String> entry = Map.entry("hello","guys");
        System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());

    }

}

程序执行结果:
key:hello value:guys

Map.Entry实质上定义了一个Map偶对象的存储标准,所有的Map接口的子类都依据此标准实现相应的节点数据的存储,可以直接利用此实例实现key与value的分离。

7、Iterator输出Map集合

面对于集合数据的输出,肯定要考虑使用Iterator接口完成,但是在Map接口中并没有任何一个方法可以直接获取到Iterator接口实例,所以此时就必须经过一系列的转换得来。

之所以在Map接口中没有提供有直接获取Iterator接口实例的方法,原因就在于它的存储不是一个普通的数据,而是一个偶对象,而Iterator每一次可以输出的全部都是单个实例为此基本的输出流程如下:

  • 1、通过Map接口中的entrySet()方法,将Map实例转为Set接口实例;
Set<Map.Entry<K,V>> entrySet() 
          返回此映射中包含的映射关系的 Set 视图 
  • 2、获取了Set集合实例之后就可以调用iterator()方法获取Iterator接口实例,泛型类型为“Map.Entry<K,V>”;

  • 3、通过Iterator进行迭代操作,获取每一组的“Map.Entry<K,V>”实例,进行key与value的分离。

**例:**通过entrySet()方法实现Iterator输出

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> all = new HashMap<>();
        all.put("hello", "你好");
        all.put("gyus", "伙计们");
        all.put("happay", "非常开心见到你们");

        Set<Map.Entry<String, String>> set = all.entrySet();
        Iterator<Map.Entry<String,String>> it = set.iterator();
        while (it.hasNext()){
            Map.Entry<String,String> entry = it.next();
            System.out.println("key="+entry.getKey()+"  "+"value="+entry.getValue());
        }
    }

}

程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好

十六、Java集合框架之Map集合_红黑树_08
从JDK1.5之后Map集合也可以使用foreach进行输出,因为其内部实现了Iterator接口:
源代码:

final class EntryIterator extends HashIterator implements Iterator<Map. Entry<K,V>>{
public final Map. Entry<K,V> next(){ 
return nextNode();
}
}

:使用foreach输出

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> all = new HashMap<>();
        all.put("hello", "你好");
        all.put("gyus", "伙计们");
        all.put("happay", "非常开心见到你们");

        for(Map.Entry<String,String> entry:all.entrySet()){
            System.out.println("key="+entry.getKey()+"  "+"value="+entry.getValue());
        }
    }

}

程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好

**例:**使用keySet()方法实现Iterator输出

public class Demo06 {

    public static void main(String[] args) {
        Map<String, String> all = new HashMap<>();
        all.put("hello", "你好");
        all.put("gyus", "伙计们");
        all.put("happay", "非常开心见到你们");

        Set<String> set = all.keySet();
        Iterator<String> it = set.iterator();
        while (it.hasNext()){
            String key = it.next();
            String value = all.get(key);
            System.out.println("key="+key+"  "+"value="+value);

        }
    }

}

程序执行结果:
key=gyus value=伙计们
key=happay value=非常开心见到你们
key=hello value=你好

不管是何种集合最终的输出的归宿只有一点就是通过Iterator,但是需要注意的是,Map一般很少直接输出,因为其功能主要是进行数据查询。

8、自定义key类型

对于此时的Map集合可以发现,设置的K和V两个泛型类型只要是引用数据类型就可以了,这也就包括了自定义的类,即自定义的类也可以成为Map中的key类型。

但是此时作为key类型所在的类一定要覆写hashCode()与equals()两个方法,因为牵扯到对象比较问题

Map集合根据key获取数据时的流程:
1、利用hashCode()方法生成结果进行比较,因为这只是一个数字,它的比较速度会更加快;
2、如果发现哈希码相同的时候才会进行内容的比较。

class Member{
    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "姓名:"+this.name+"  "+"年龄:"+this.age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Member member = (Member) o;
        return age == member.age &&
                Objects.equals(name, member.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }

}


public class Demo07 {

    public static void main(String[] args) {
        Map<Member,String> map = new HashMap<>();
        map.put(new Member("张三",10),new String("张三"));

        System.out.println(map.get(new Member("张三",10)));
    }

}

程序执行结果:
张三

在以后实际编写的代码过程之中,如果遇见了Map集合,则一般对于Map集合中的Key类型,最为常见的是String,其次是Long或者Integer。

哈希冲突(了解)

结论:
哈希码实际上是进行对象比较的关键所在,而在进行Map存储的时候实际上也是依靠哈希码得到的一个存储空间,但是在很多情况下依然有可能会出现哈希冲突的问题。
那么这个时候的解决方案(四种解决方案):

  • 1、开放定址法
  • 2、链地址法
  • 3、再哈希法
  • 4、建立公共溢出区
    而Java是利用链地址法的形式解决的。把所有重复的内容放在一个链表之中进行保存。
    十六、Java集合框架之Map集合_子类_09

总结

1、只要碰见单值集合优先考虑使用List,List接口优先考虑的是ArrayList;

2、只要进行key的内容查找操作,就属于Map接口,Map接口优先考虑HashMap子类

3、集合的输出全部使用Iterator接口完成。