接口 Map<K,V>

  • 类型参数: K - 此映射所维护的键的类型 V - 映射值的类型
    因为Map中的Entry键值对、Key键都是有Set存放的,这也就确保了键、键值对是不可重复的(Value可重复)

public interface Map<K,V>
将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。

JAVA映射空格 java映射表_链表


常用方法

  1. void clear()
    从此映射中移除所有映射关系(可选操作)。
  2. boolean containsKey(Object key)
    如果此映射包含指定键的映射关系,则返回 true。
  3. boolean containsValue(Object value)
    如果此映射将一个或多个键映射到指定值,则返回 true。
  4. Set<Map.Entry<K,V>> entrySet()
    返回此映射中包含的映射关系的 Set 视图。
  5. boolean equals(Object o)
    比较指定的对象与此映射是否相等。
  6. V get(Object key)
    返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
  7. boolean isEmpty()
    如果此映射未包含键-值映射关系,则返回 true。
  8. Set keySet()
    返回此映射中包含的键的 Set 视图。
  9. V put(K key, V value)
    将指定的值与此映射中的指定键关联(可选操作)。
  10. void putAll(Map<? extends K,? extends V> m)
    从指定映射中将所有映射关系复制到此映射中(可选操作)。
  11. V remove(Object key)
    如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
  12. int size()
    返回此映射中的键-值映射关系数。
  13. Collection values()
    返回此映射中包含的值的 Collection 视图。

HashMap

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

构造方法摘要

  1. HashMap()
    构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  2. HashMap(int initialCapacity)
    构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
  3. HashMap(int initialCapacity, float loadFactor)
    构造一个带指定初始容量和加载因子的空 HashMap。
  4. HashMap(Map<? extends K,? extends V> m)
    构造一个映射关系与指定 Map 相同的新 HashMap。

HashMap的底层实现结构——链表数组(Node<K,V>[] table)

JAVA映射空格 java映射表_键值对_02


如上图;Java中的HashMap的基本结构就如上图所示,竖着看是一个数组,横着看就是多个链表。当新建一个HashMap的时候,就初始化了一个数组。

存储方式:HashMap在存储时存储方式与HashSet基本一致:首先它在put方法执行时,通过Key计算出其哈希码,并由此确定该键值对存放位置,倘若该位置为空则直接存入,否则使用key的Equals方法与该位置元素对比,若为同一个key那么替换该键值对,否则遍历该键值对对应链表寻找相同key直至表尾直接存放该新键值对!

思考:那么我们在添加的过程中如果空间较小,很容易出现不同key对应同一位置需要放置于链尾,
这就造成一个问题,链表可能很长!——我们在使用get方法获取键值对的时候需要的复杂度会非常高!那么就要让链表部分尽量短,也就是空间要相对大,如何实现呢?

  • 通过对table数组部分扩容以减少产生长链的可能,并且在扩容的时候对所有键值对重新计算位置以保持整个HashMap中链部分相对较少,但是扩容时需要重新计算Hashcode又使得计算量增大,那么我们何时选择扩容呢?显然等到数组满了才扩容是非常愚蠢的,因为等待数组满了,链表早已经长的夸张了!于是我们规定了加载因子loadFactor(默认0.75),当数组空间被占用75%时就自动成倍扩容空间。

下面是Jdk1.8中的put方法实现

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//首次存储位置为空则直接存放即可
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//碰到了相同key的键值对
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//遍历链表直至链表尾
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//链表中碰到了相同key的键值对
                        break;
                    p = e;
                }
            }
            if (e != null) { // 说明有相同key的键值对,执行替换操作
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;//返还原value
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Hashtable、ConcurrentHashMap——HashMap的安全版!

  • Hashtable

Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。也就是说,他俩的区别主要就在于两个 1.Hashtable内部方法加入了synchronized同步化使得线程更为安全!2.Hashtable绝对不允许null的键和值,会以NullPointerException回应!

  • ConcurrentHashMap
    首先它内部同样不允许null的存在!
//本例说明HashMap在迭代器迭代过程只能通过迭代器删除键值对,否则将抛出异常!
		Map<String,Integer> map = new HashMap<>();
		Iterator<String> it = map.keySet().iterator();
		while(it.hasNext()) {
			String key = it.next();
			System.out.println(key+"="+map.get(key));
			map.remove(key);	
			//java.util.ConcurrentModificationException抛出!
		}

在HashMap中:当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。简而言之,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map,我们可以说:ConcurrentHashMap是Hashtable的升级替代版!

那么我们能否让HashMap线程同步安全化?

HashMap可以通过下面的语句进行同步:

Map m = Collections.synchronizeMap(hashMap);

结论
Hashtable和HashMap有几个主要的不同:线程安全以及速度。如果你使用Java 5或以上的话,推荐使用ConcurrentHashMap,而仅在你需要完全的线程安全的时候使用Hashtable。

LinkedHashMap——HashMap的改造线性子类

关 注 点

结 论

LinkedHashMap是否允许空

Key和Value都允许空

LinkedHashMap是否允许重复数据

只要还是Map,Key重复会覆盖、Value允许重复

LinkedHashMap是否有序

有序

LinkedHashMap是否线程安全

非线程安全

验证其有序化:

public static void main(String[] args) {  
	
      Map<String, String> map = new HashMap<String, String>(); 

      map.put("a3", "aa");

      map.put("a2", "bb"); 

      map.put("b1", "cc");

      for (Iterator iterator = map.values().iterator(); iterator.hasNext();)     {

            String name = (String) iterator.next(); 

            System.out.println(name);   

     }  /**输出:
			bb
			cc
			aa*
			*/

  }
public static void main(String[] args) {   

     Map<String, String> map = new LinkedHashMap<String, String>();

     map.put("a3", "aa");       

     map.put("a2", "bb"); 

     map.put("b1", "cc"); 

     for (Iterator iterator = map.values().iterator(); iterator.hasNext();) {           

             String name = (String) iterator.next(); 

             System.out.println(name);     

     }/**输出:
			aa
			bb
			cc*
			*/

}

LinkedHashMap如何实现有序化的?

该类在构造过程中会若无专门初始化,默认设置boolean accessOrder这一属性为false,accessOrder为true时,按访问顺序排序,false时,按插入顺序排序。

LinkedHashMap的底层是双向链表,它重新定义了数组中保存的元素Entry(继承于HashMap.Entry),该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。仍然保留next属性,所以既可像HashMap一样快速查找,用next获取该链表下一个Entry,也可以通过双向链接,通过after完成所有数据的有序迭代。

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);
        }
    }

测试accessOrder 不同下的迭代结果:

public static void insertOrder() {

        // 默认是插入顺序
        LinkedHashMap<Integer,String>  insertOrder = new LinkedHashMap();

        String value = "Hello";
        int i = 0;

        insertOrder.put(i++, value);
        insertOrder.put(i++, value);
        insertOrder.put(i++, value);
        insertOrder.put(i++, value);
        insertOrder.put(i++, value);

        //遍历
        Set<Integer> set = insertOrder.keySet();
        for (Integer s : set) {
            String mapValue = insertOrder.get(s);
            System.out.println(s + "---" + mapValue);
        }/**输出:
         0---Hello 
         1---Hello 
         2---Hello 
         3---Hello  
         4---Hello
         **/  
    }
@Test
	public  void accessOrder() {
        // 设置为访问顺序的方式
        LinkedHashMap<Integer,String> accessOrder = new LinkedHashMap<>(16, 0.75f, true);

        String value = "Hello";
        int i = 0;
        accessOrder.put(i++, value);
        accessOrder.put(i++, value);
        accessOrder.put(i++, value);
        accessOrder.put(i++, value);
        accessOrder.put(i++, value);

        // 访问一下key为3的元素再进行遍历
        accessOrder.get(3);
        accessOrder.get(1);
        // 遍历
        Set<Integer> sets = accessOrder.keySet();
        for (Integer key : sets) {
            System.out.println(key );
        }
        /*
         * 0
		   2
		   4
		   3
		   1
         */

    }

倘若在访问顺序下使用get方法同样会引起java.util.ConcurrentModificationException异常,由于迭代过程中使用了get访问,修改了迭代顺序!

Set<Integer> sets = accessOrder.keySet();
        for (Integer key : sets) {
            System.out.println(accessOrder.get(key) );
            //java.util.ConcurrentModificationException
        }

TreeMap——可实现按照key的排序方式排序所有键值对

  • TreeMap()
    使用键的自然顺序构造一个新的、空的树映射。
  • TreeMap(Comparator<? super K> comparator)
    构造一个新的、空的树映射,该映射根据给定比较器进行排序。
  • TreeMap(Map<? extends K,? extends V> m)
    构造一个与给定映射具有相同映射关系的新的树映射,该映射根据其键的自然顺序 进行排序。
    通过构造器我们就能发现,我们可以通过传入规定比较器对其键值对中键进行排序,也可以采用K类内部CompareTo方法(自然顺序)进行排序
Map<String,Integer> users = new TreeMap<String,Integer>();
        users.put("jack",19);
        users.put("lily",20);
        users.put("susan",22);
        users.put("robin",27);
        // 对users遍历
        for (Map.Entry<String,Integer> entry: users.entrySet()){
            System.out.println(entry.getKey()+":"+entry.getValue());
        	//jack:19	lily:20		robin:27	susan:22
        }

对于TreeMap的存储过程同样不难理解:
类似于TreeSet的二叉查找树模型,它是通过key类的compareTo或是传入比较器,

  • 若根节点为空,插入,
  • 小于等于根节点,递归在左子树找位置,
  • 大于根节点,递归在右子树找位置;
  • 当找到非空的位置则说明二者compareTo返回0相等,实施替换!