Java集合框架之Map接口(上)

Map接口主要借助了hash的思想,以hash表键值对的形式存储,键用于hash定位,具有极高的效率。其接口主要实现类如下:

Map
├Hashtable(基本同hashMap,默认为11,只不过hashtable为线程安全的,不允许有null值,put, get 都加锁)
├HashMap(Entry链表+数组,默认容量为16,负载因子为0.75;长度大于n*16*0.75则容量增大一倍)
└LinkedHashMap(底层为hashMap的Entry双向链表,继承自hashMap)
└TreeMap(底层为红黑树实现,继承自AbstractMap,而AbstractMap又实现了Map接口)
└ LinkedHashMap(底层为hash表和双链表表)
├concurrentHashMap(采用锁分离 来保证大并发的效率,Segment数组结构和HashEntry数组结构组成,table[]--hashTable  ,segments[]--table;put加锁,get不加锁)

一、Map接口常见实现类介绍

    Map底层数据结构为哈希表,用Entry数组表示,Entry数据结构如下:

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

protected Entry(int hash, K key, V value, Entry<K,V> next) {
   this.hash = hash;
   this.key = key;
   this.value = value;
   this.next = next;
}

...

}

Entry数组存储示意图如下:



        在存储entry时,根据key的hash值定位到Entry数组的相应位置,如果该位置没有元素则直接存入,若该位置有元素则将entry链接至以该位置元素为表头的链表(JDK1.8中此处做了优化,当链表长度超过一定长度时会转变为红黑树存储)。在Entry同一个位置的entry具有相同的key哈希值。在查找相应的元素时,首先计算该entry对象key属性的哈希值,然后根据其hash值定位到Entry数组相应的位置,然后遍历链表并比较entry对象,直到找到相等值。

       Entry数组(哈希表、散列表)的容量是可变的,在初始化时有初始化大小initialCapacity 和负载因子load factor。当数组容量达到Entry.leng*load factor时,会重新分配一个容量为原来两倍的Entry数组,并将原来Entry数组中的元素重新hash至新的Entry数组。负载因子是时间和空间上的一种折中,负载因子越大表示散列表填充程度越大,反之越小。散列表填充程度越大,发生元素碰撞的概率越大,链表长度也就越长,查找元素时也就越慢。增大负载因子可以减少散列表所占空间,但会增加查询数据的时间开销(put/get均会用到查询);减小负载因子会提高数据查询的性能,但会增加散列表所占用的存储空间。

     一般情况下load factor默认为0.75,可以根据 实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。 


    1、Hashtable继承自抽象类Dictionary,并实现Map接口

   默认大小为11、线程安全(对Entry数组的操作加锁synchronized)、key-value不允许为null、扩容为2*n+1、散列方法是(hash & 0x7FFFFFFF) % tab.length

  a、put操作

     先根据entry的key哈希值定位到散列表的相应位置,如果该位置具有相同key的元素直接覆盖,如果散列表达到容量极限需要扩容并重新哈希原来的散列表,最后把待插入的entry放入的到相应的位置。

    其源码如下:

   public synchronized V put(K key, V value) {
// Make sure the value is not null
      if (value == null) {
      throw new NullPointerException();
  }
     // Makes sure the key is not already in the hashtable.
     Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;//根据key值哈希定位
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
         if ((e.hash == hash) && e.key.equals(key)) {//key已经存在则直接覆盖
             V old = e.value;
            e.value = value;
           return old;
     }
 }
     modCount++;
    if (count >= threshold) {//容量达到阈值则扩容极限重新哈希
       // Rehash the table if the threshold is exceeded
          rehash();
              tab = table;
             index = (hash & 0x7FFFFFFF) % tab.length;
}
      // Creates the new entry.
      Entry<K,V> e = tab[index];//取出表头元素
      tab[index] = new Entry<K,V>(hash, key, value, e);//将元素插入作为新的表头
      count++;
      return null;
      }

   hastable扩容:

   protected void rehash() {
       int oldCapacity = table.length;
      Entry[] oldMap = table;//老的散列表
      int newCapacity = oldCapacity * 2 + 1;//为保证散列效果,表长度为奇数
      Entry[] newMap = new Entry[newCapacity];.//新的散列表,容量为原来的两倍
     modCount++;
     threshold = (int)(newCapacity * loadFactor);//扩容阈值
     table = newMap;//原来的table引用指向新的散列表
      //重新hash老的散列表,并将其插入到新的散列表中
     for (int i = oldCapacity ; i-- > 0 ;) {
         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
              Entry<K,V> e = old;
             old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = newMap[index];
           newMap[index] = e;
        }
    }
      }

   原来的散列表仍然保存,能够保证在扩容时,其他线程正常访问散列表。


    b、get操作

    先根据key定位到相应的列表,然后遍历列表,找不到返回null

    public synchronized V get(Object key) {
            Entry tab[] = table;
           int hash = key.hashCode();
           int index = (hash & 0x7FFFFFFF) % tab.length;//根据key的哈希值定位
           for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
              if ((e.hash == hash) && e.key.equals(key)) {//遍历列表并比较
              return e.value;
              }
          }
           return null;
    }

    2、HashMap继承自抽象类AbstractMap,并实现Map接口

      默认大小为16、线程非安全、允许key-value为null、扩容为2^n、二次散列hash&(length-1)

     初始化大小为第一个大于给定值并且为2^n的整数,如果给定大小为20,那么初始化大小为32。初始化源码如下:

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

        // Find a power of 2 >= initialCapacity找到第一个大于给定值并且为2^n的整数,为了便于散列,且在定位时低位跟1做位与
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init(); //回调函数,子类实现
    }


    a、put操作

      跟hashtable操作基本类似,散列方式不同、扩容方式不同。根据key的hash值找到散列表中的索引后,会循环遍历table[i]所在链表,若找到已存在key值则直接覆盖,如不存在则通过addEntry添加新对象至链表头部。

  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());//二次散列使得散列更加均匀
        int i = indexFor(hash, table.length);//根据散列定位

     //若i处索引不为null,通过循环不断遍历e的下一个元素
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;

         //有相同key则覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);//回调函数,子类实现
                return oldValue;
            }
        }
      //能执行到此处,说明两点1、i处索引为空,2、遍历完链表没有找到与key相同的值
        modCount++;
        addEntry(hash, key, value, i);//将key、value 添加到索引i处

        return null;
    }

   二次散列:

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

    根据哈希值找到索引:

   static int indexFor(int h, int length) {
        return h & (length-1);//h每一位跟1做与操作,极快
    }

    添加元素,先添加再扩容

    void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);//扩容为原来的两倍
     }

   扩容: 

  void resize(int newCapacity) {
         Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
      }

    重新hash:

    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;//原来的散列表直接赋值null
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
      }

    如果在resize过程中,有其他线程视图调用get遍历数据会在成错误。


hashMap是轻量级的hashTable,主要在hash值和散列定位方面做了优化

hashTable 默认大小为何是11?
hashtable默认大小是11是因为除(近似)质数求余的分散效果好:
Hashtable的扩容是这样做的:
        int oldCapacity = table.length;
        int newCapacity = oldCapacity * 2 + 1;
虽然不保证capacity是一个质数,但至少保证它是一个奇数。

Hashtable的寻址是这样做的:
Entrytab[]=table;inthash=key.hashCode();intindex=(hash&0x7FFFFFFF)%tab.length;
直接用key的hashCode(),不像HashMap里为了增强hash的分散效果而要做二次hash

hashMap 与hashtable区别:    父类不同,线程安全性、hash值、默认长度,扩容大小,null
1、二者继承自不同的类,hashMap继承自AbstractMap ,hashTable继承自Dictionary,但二者都实现了Map接口
2、hashtable是线程安全的
3、二者的散列表长度取法不一样。hashMap默认是16,长度是2^n。hashTable 默认长度为11,且长度是自定义的init*2增长
4、二者的在散列表中的定位不同,hashMap是自定义hash值之后hash&(length-1), hashTable是直接取hashcode然后(hashcode&0X7FFFFFFF)%length
5、hashtable不允许key-Value为null
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值