HashMap:基于jdk7

一.基础

1.概述

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

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

2.构造函数

(1)HashMap()

构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的HashMap。

(2)HashMap(int initialCapacity)

构造一个带指定初始容量为initialCapacity和默认加载因子 (0.75) 的HashMap。

(3)HashMap(int initialCapacity, float loadFactor)

构造一个带指定初始容量为initialCapacity和加载因子为loadFactor的 HashMap。

初始容量、加载因子 这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

3.结构

这里写图片描述

新建一个HashMap时,都会初始化一个长度为initialCapacity的table数组。table数组i位置的元素为Entry节点,即table[i] = entry;

public HashMap(int initialCapacity, float loadFactor) {
      //初始容量不能 小于 0
      if (initialCapacity < 0)
          throw new IllegalArgumentException("Illegal initial capacity: "
                  + initialCapacity);
      //初始容量不能 大于 最大容量值,HashMap的最大容量值为2^30
      if (initialCapacity > MAXIMUM_CAPACITY)
          initialCapacity = MAXIMUM_CAPACITY;
      //负载因子不能 小于 0
      if (loadFactor <= 0 || Float.isNaN(loadFactor))
          throw new IllegalArgumentException("Illegal load factor: "
                  + loadFactor);

      // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
      int capacity = 1;
      while (capacity < initialCapacity)
          capacity <<= 1;
      
      this.loadFactor = loadFactor;
      //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
      threshold = (int) (capacity * loadFactor);
      //初始化table数组
      table = new Entry[capacity];
      init();
  }

Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,即:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
     V value;
     Entry<K,V> next;
     final int hash;
     //构造函数
     Entry(int h, K k, V v, Entry<K,V> n) {
         value = v;
         next = n;
         key = k;
         hash = h;
     }
     .......
 }

二.存储

(1)put(key,vlaue)

public V put(K key, V value) {
      //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
      if (key == null)
          return putForNullKey(value);
      //计算key的hash值
      int hash = hash(key.hashCode());                  ------(1)
      //计算key hash 值在 table 数组中的位置
      int i = indexFor(hash, table.length);             ------(2)
      //从i出开始迭代 e,找到 key 保存的位置
      for (Entry<K, V> e = table[i]; e != null; e = e.next) {
          Object k;
          //判断该条链上是否有hash值相同的(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;     //返回旧值
          }
      }
      //修改次数增加1
      modCount++;
      //将key、value添加至i位置处
      addEntry(hash, key, value, i);
      return null;
  }

当key为空时,将Entry<K,V>存放在buketIndex为0的位置,即table[0]。
当key不为空时,执行key.hashCode()方法计算出hash值找到buketIndex,即找到存放的位置,存在三种情况:
①buketIndex位置为空,没有元素,此时直接将Entry<K,V>存入table[buketIndex];
②buketIndex位置不为空且对象相同(key值相等或key.equals),则替换掉原有的值V;
③buketIndex位置不为空且对象不同,则hashCode发生碰撞,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。单链表在Java中的实现就是对象的引用(复合)。

(2)链的产生

这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

(3)hashCode与hash

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的, 在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。

static int indexFor(int h, int length) {
    return h & (length-1);
}

HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,速度比直接取模快得多,而且取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。 这是HashMap的一个优化。

假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

h & (table.length-1)hashtable.length-1
8 & (15-1)0100 & 11100100
9 & (15-1)0101 & 11100100
8 & (16-1)0100 & 11110100
9 & (16-1)0101 & 11110101

当hash码8和9和和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是 这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方 时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

三.key的hashcode与equals方法改写

hashcode与equals方法是对应map找到对应元素是两个关键方法。

Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。

在改写equals方法的时候,需要满足以下三点:
(1) 自反性:就是说a.equals(a)必须为true。
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
(3) 传递性:就是说a.equals(b)=true,并且b.equals©=true的话,a.equals©也必须为true。
通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。

四.hashmap的resize(扩容问题)

随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

当数组内元素数量(所有entry的数量entryset.size)达到initialCapacity*loadFactor(默认0.75)的大小,为了减少碰撞,对map进行扩增,为原来两倍。
假如扩容前table数组大小未达到最大2^30,即将数组增大一倍,否则返回

void resize(int newCapacity) {   //传入新的容量  
  Entry[] oldTable = table;    //引用扩容前的Entry数组  
   int oldCapacity = oldTable.length;  
   if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了  
       threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了  
       return;  
   }  
 
   Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组  
   transfer(newTable);                         //!!将数据转移到新的Entry数组里  
   table = newTable;                           //HashMap的table属性引用新的Entry数组  
   threshold = (int) (newCapacity * loadFactor);//修改阈值  
} 

使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了旧的Entry数组  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
        if (e != null) {  
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
                e.next = newTable[i]; //标记[1]  
                newTable[i] = e;      //将元素放在数组上  
                e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  
} 

五.读取get(key)

通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

public V get(Object key) {
  // 若为null,调用getForNullKey方法返回相对应的value
    if (key == null)
        return getForNullKey();
    // 根据该 key 的 hashCode 值计算它的 hash 码  
    int hash = hash(key.hashCode());
    // 取出 table 数组中指定索引处的值
    for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
        Object k;
        //若搜索的key与查找的key相同,则返回相对应的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

六. EntrySet、KeySet实现原理

1.实现原理

HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。所以HashMap是一种结合了数组和链表的结构。因此,我们有3种对数据的观测方式:keySet,values,entrySet。

keySet是从key的值角度出发的结果。它里面包含了这个键值对表里面的所有键的值的集合,因为HashMap明确规定一个键只能对应一个值,所以不会有重复的key存在,这也就是为什么可以用集合来装key。

values则是从键值对的值的角度看这个映射表,因为可以有多个key对应一个值,所以可能有多个相同的values。

entrySet是从键值对的角度思考这个问题,它返回一个键值对的集合。(键值对相等当且仅当键和值都相等)。

2.代码解析

keySet.iterator()

HashMap中的keySet源码:

    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }

   private final class KeySet extends AbstractSet<K> {
      public Iterator<K> iterator() {
          return newKeyIterator();
      }
      public int size() {
          return size;
      }
      public boolean contains(Object o) {
          return containsKey(o);
      }
      public boolean remove(Object o) {
          return HashMap.this.removeEntryForKey(o) != null;
      }
      public void clear() {
          HashMap.this.clear();
      }
  }

由以上源码可知,当调用hashmap.keySet()方法时,若keySet为空,则返回一个新建的内部类KeySet(); 但是我们发现它有个iterator()方法,我们在敲代码中经常使用这个迭代器迭代取出HashMap对象中的key和value,再深入看:

Iterator newKeyIterator() {
    return new KeyIterator();
}

HashMap内部类KeyIterator :

private final class KeyIterator extends HashIterator {
	public Object next() {
		return nextEntry().getKey();
	}	
	final HashMap this$0;	
	private KeyIterator() {
		this$0 = HashMap.this;
		super();
	}
}

HashIterator :

private abstract class HashIterator implements Iterator {

	public final boolean hasNext() {
		return next != null;
	}

	final HashMap.Entry nextEntry() {
		if (modCount != expectedModCount)
			throw new ConcurrentModificationException();
		HashMap.Entry entry = next;
		if (entry == null)
			throw new NoSuchElementException();
		if ((next = entry.next) == null) {
			for (HashMap.Entry aentry[] = table; index < aentry.length
					&& (next = aentry[index++]) == null;)
				;
		}
		current = entry;
		return entry;
	}

	public void remove() {
		if (current == null)
			throw new IllegalStateException();
		if (modCount != expectedModCount) {
			throw new ConcurrentModificationException();
		} else {
			Object obj = current.key;
			current = null;
			removeEntryForKey(obj);
			expectedModCount = modCount;
			return;
		}
	}

	HashMap.Entry next;
	int expectedModCount;
	int index;
	HashMap.Entry current;
	final HashMap this$0;

	HashIterator() {
		this$0 = HashMap.this;
		super();
		expectedModCount = modCount;
		if (size > 0) {
			for (HashMap.Entry aentry[] = table; index < aentry.length
					&& (next = aentry[index++]) == null;)
				;
		}
	}
}

由此可以Iterator是继承了这个HashIterator 这个内部类,而HashIterator 又访问了外部HashmMap的一些属性得以访问到整个对象的table数组的所有Entry的。

3.输出hashmap.keySet();

写个测试的案例:

@Test
public void testPrint(){
	Map<String, Object> map = new HashMap<String, Object>();
	map.put("now", "u");
	map.put("see", "me");
	System.out.println(map.keySet());
}

输出结果为:

[see, now]

当我们调用map.keySet时候,为什么会打印出keySet的这个结果呢?原理是什么?仿照HashMap写一个自定义的Iterator

package MyMap.Demo1;

import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;

public class MyTestIterator {
    public static void main(String[] args) {
	TestIterator t = new TestIterator();
        Set<Integer> set = t.keySet();
        System.out.println(set);
    }	
	
}


class TestIterator {
    public Set<Integer> keySet() {

        final ArrayList<Integer> result = new ArrayList<Integer>();
        result.add(1);
        result.add(2);
        result.add(3);

        Set<Integer> keySet = new AbstractSet<Integer>() {
        	
            public Iterator<Integer> iterator() {
                return new Iterator<Integer>() {
                    private Iterator<Integer> i = result.iterator();

                    @Override
                    public boolean hasNext() {
                    	System.out.println("===========hasNext");
                        return i.hasNext();
                    }

                    @Override
                    public Integer next() {
                    	System.out.println("===========next");
                        return i.next();
                    }

                    @Override
                    public void remove() {
                    	System.out.println("===========remove");
                        i.remove();
                    }
                };
            }

            @Override
            public int size() {
                return 0;
            }
        };
        return keySet;
    }
}

打印结果为:

===========hasNext
===========hasNext
===========next
===========hasNext
===========next
===========hasNext
===========next
===========hasNext
[1, 2, 3]

是在哪里调用了keySet的iterator方法呢?在所有的输出语句打个断点,原来当我们使用
System.out.println( t.keySet()); 输出Set<Integer>对象时,由于AbstractSet类继承了AbstractCollection类,而AbstractCollection又重写了Object类的toString()方法,因此在输出时调用了AbstractCollection.java类中的toString方法,重写的toString()方法又调用了iterator,从而迭代取出结果拼接成字符串。

public String toString() {
       Iterator<E> it = iterator();
       if (! it.hasNext())
           return "[]";

       StringBuilder sb = new StringBuilder();
       sb.append('[');
       for (;;) {
           E e = it.next();
           sb.append(e == this ? "(this Collection)" : e);
           if (! it.hasNext())
               return sb.append(']').toString();
           sb.append(',').append(' ');
       }
   }

六.参考

http://www.cnblogs.com/dsj2016/p/5551059.html
http://www.cnblogs.com/chenssy/p/3521565.html
http://www.genshuixue.com/i-cxy/p/8040323

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值