顺序存储与链式存储的集合-HashMap、HashTable

HashMap,日常最常用的数据结构之一。它是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。下面将通过源码分析存储结构、初始化、插入、查询、移除等来深入分析Hashmap的实现原理。

0.equal hashcode ==的区别

为了分析HashMap,我们首先应该理解hashCode及equal的区别,如下:
== 内存地址比较
equal Object默认内存地址比较,一般需要复写
hashcode 主要用于集合的散列表,Object默认为内存地址,一般不用设置,除非作用于散列集合。

(1)hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。当equals方法被重写时,通常有必要重写 hashCode 方法。
(2)但hashCode相等,不一定equals()

1.存储结构

HashMapde的存储结构是采用顺序存储结构及链式存储结构。顺序存储结构存储着每个链表的头结点。每个Key根据计算bucketindex来确定数组下标。 bucketindex=hash&(length-1)。当 bucketindex相同时,插入链表头部。
顺序存储与链式存储的集合-HashMap、HashTable_第1张图片
  // Entry是单向链表。
  // 它是 “HashMap链式存储法”对应的链表。
  // 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
  static class Entry implements Map.Entry {
  final K key;
  V value;
  // 指向下一个节点
  Entry next;
  final int hash;

  // 构造函数。
  // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
  Entry(int h, K k, V v, Entry n) {
  value = v;
  next = n;
  key = k;
  hash = h;
  }

  public final K getKey() {
  return key;
  }

  public final V getValue() {
  return value;
  }

  public final V setValue(V newValue) {
  V oldValue = value;
  value = newValue;
  return oldValue;
  }

  // 判断两个Entry是否相等
  // 若两个Entry的“key”和“value”都相等,则返回true。
  // 否则,返回false
  public final boolean equals(Object o) {
  if (!(o instanceof Map.Entry))
  return false;
  Map.Entry e = (Map.Entry)o;
  Object k1 = getKey();
  Object k2 = e.getKey();
  if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  Object v1 = getValue();
  Object v2 = e.getValue();
  if (v1 == v2 || (v1 != null && v1.equals(v2)))
  return true;
  }
  return false;
  }

  // 实现hashCode()
  public final int hashCode() {
  return (key==null ? 0 : key.hashCode()) ^
  (value==null ? 0 : value.hashCode());
  }

  public final String toString() {
  return getKey() + "=" + getValue();
  }

  // 当向HashMap中添加元素时,绘调用recordAccess()。
  // 这里不做任何处理
  void recordAccess(HashMap m) {
  }

  // 当从HashMap中删除元素时,绘调用recordRemoval()。
  // 这里不做任何处理
  void recordRemoval(HashMap m) {
  }
  }

2.初始化(加载因子)

 HashMap有两个参数影响其性能:初始容量加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,会调用方法将容量翻倍。所以这是时间和空间的矛盾,最后根据自己的业务来设定。
// 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。    
    static final int DEFAULT_INITIAL_CAPACITY = 16;    
   
    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)    
    static final int MAXIMUM_CAPACITY = 1 << 30;    
   
    // 默认加载因子为0.75   
    static final float DEFAULT_LOAD_FACTOR = 0.75f;    
   // 指定“容量大小”和“加载因子”的构造函数    
    public HashMap(int initialCapacity, float loadFactor) {    
        if (initialCapacity < 0)    
            throw new IllegalArgumentException("Illegal initial capacity: " +    
                                               initialCapacity);    
        // HashMap的最大容量只能是MAXIMUM_CAPACITY    
        if (initialCapacity > MAXIMUM_CAPACITY)    
            initialCapacity = MAXIMUM_CAPACITY;    
        //加载因此不能小于0  
        if (loadFactor <= 0 || Float.isNaN(loadFactor))    
            throw new IllegalArgumentException("Illegal load factor: " +    
                                               loadFactor);    
   
        // 找出“大于initialCapacity”的最小的2的幂    
        int capacity = 1;    
        while (capacity < initialCapacity)    
            capacity <<= 1;    
   
        // 设置“加载因子”    
        this.loadFactor = loadFactor;    
        // 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。    
        threshold = (int)(capacity * loadFactor);    
        // 创建Entry数组,用来保存数据    
        table = new Entry[capacity];    
        init();    
    }    

3.bucketindex

HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。所以可以采用%的方式,既哈希值%容量=bucketIndex。而源码的实现采用 h & (length-1),具有更高的效率。这里注意,为什么HashMap的默认容量要求2N次方。
当容量一定是2^n时,h & (length - 1) == h % length

4.put

     HashMap添加元素主要先根据key的hash计算出bucketindex,如果该buckeindex下标的链表存在,则遍历进行替换,否则往数组添加新的链表。
   
 // 将“key-value”添加到HashMap中    
    public V put(K key, V value) {    
        // 若“key为null”,则将该键值对添加到table[0]中。    
        if (key == null)    
            return putForNullKey(value);    
        // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。    
        int hash = hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        for (Entry e = table[i]; e != null; e = e.next) {    
            Object k;    
            // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!    
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
                V oldValue = e.value;    
                e.value = value;    
                e.recordAccess(this);    
                return oldValue;    
            }    
        }    
   
        // 若“该key”对应的键值对不存在,则将“key-value”添加到table中    
        modCount++;  
        //将key-value添加到table[i]处  
        addEntry(hash, key, value, i);    
        return null;    
    }    
 
 // 返回h在数组中的索引值,这里用&代替取模,旨在提升效率   
    // h & (length-1)保证返回值的小于length    
    static int indexFor(int h, int length) {    
        return h & (length-1);    
    }    

  // 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。    
    void addEntry(int hash, K key, V value, int bucketIndex) {    
        // 保存“bucketIndex”位置的值到“e”中    
        Entry e = table[bucketIndex];    
        // 设置“bucketIndex”位置的元素为“新Entry”,    
        // 设置“e”为“新Entry的下一个节点”    
        table[bucketIndex] = new Entry(hash, key, value, e);    
        // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小    
        if (size++ >= threshold)    
            resize(2 * table.length);    
    }    

5.get

HashMap根据key获取元素主要就是通过bucketindex找到链表,进行查询。
   
 // 获取key对应的value    
    public V get(Object key) {    
        if (key == null)    
            return getForNullKey();    
        // 获取key的hash值    
        int hash = hash(key.hashCode());    
        // 在“该hash值对应的链表”上查找“键值等于key”的元素    
        for (Entry e = table[indexFor(hash, table.length)];    
             e != null;    
             e = e.next) {    
            Object k;    
            //判断key是否相同  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))    
                return e.value;    
        }  
        //没找到则返回null  
        return null;    
    }    

6.remove

  // 删除“键为key”的元素    
    final Entry removeEntryForKey(Object key) {    
        // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算    
        int hash = (key == null) ? 0 : hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        Entry prev = table[i];    
        Entry e = prev;    
   
        // 删除链表中“键为key”的元素    
        // 本质是“删除单向链表中的节点”    
        while (e != null) {    
            Entry next = e.next;    
            Object k;    
            if (e.hash == hash &&    
                ((k = e.key) == key || (key != null && key.equals(k)))) {    
                modCount++;    
                size--;    
                if (prev == e)    
                    table[i] = next;    
                else   
                    prev.next = next;    
                e.recordRemoval(this);    
                return e;    
            }    
            prev = e;    
            e = next;    
        }    
   
        return e;    
    }    

7.HashTable

(1)HashTable的实现原理基本与HashMap一致,除了细微的方法实现不一致外,所以不进行原理分析。
(2)HashTable为线程安全,HashMap为非线程安全
(3)HashMap可以接受为null的key和value,而Hashtable则不行
(4)HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常
(5)Java 5提供了ConcurrentHashMap(局部锁机制),它是HashTable的替代,比HashTable的扩展性、性能更好

8.总结

(1)HashMap是顺序结构及链表结构的组合
(2)最后根据业务设置容量及加载因子可以提高插入及查找效率,同时可以避免增容带来的效率问题
(3)HashMap为非线程安全
(4)HashMap的精髓为bucketindex(使元链表均匀分布在数组),可以提供空间的利用率及元素的插入及查找效率。
(5)由于hash相等,equal不一定相等。有可能导致撞库的问题,由于HashMap具有链表的功能。可以避免撞库问题。

你可能感兴趣的:(Java,数据结构与算法,数据结构与算法)