更新时间:2021年03月18日 08时58分23秒 来源:黑马程序员
一、HashMap的内部存储结构
Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:
从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。接下来我会从初始化阶段详细的讲解HashMap的内部结构。
1、初始化
首先来看三个常量:
static final int DEFAULT_INITIAL_CAPACITY = 16; 初始容量:16
static final int MAXIMUM_CAPACITY = 1
<< 30; 最大容量:2的30次方:1073741824
static final float DEFAULT_LOAD_FACTOR = 0.75f;
装载因子,后面再说它的作用
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
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 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
就是说传入参数的构造方法,我们把重点放在
while (capacity < initialCapacity) capacity <<= 1;
上面,该代码的意思是,实际的开辟的空间要大于传入的第一个参数的值。举个例子:
new HashMap(7,0.8),loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是4,2 << 2的结果为8)。所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组,所以,这种方法最红取决于capacity的大小。
2、get(Object key)操作
get(Object key)操作时根据键来获取值,如果了解了put操作,get操作容易理解,先来看看源码的实现:
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k)))//-------------------1---------------- return e.value; } return null; }
意思就是:
1、当key为null时,调用getForNullKey(),源码如下:
private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
2、当key不为null时,先根据hash函数得到hash值,在更具indexFor()得到i的值,循环遍历链表,如果有:key值等于已存在的key值,则返回其value。如上述get()代码1处判断。
总结下HashMap新增put和获取get操作:
//存储时: int hash = key.hashCode(); int i = hash % Entry[].length; Entry[i] = value; //取值时: int hash = key.hashCode(); int i = hash % Entry[].length; return Entry[i];
理解了就比较简单。
此处附一个简单的HashMap小算法应用:
<font style="color:rgb(77, 77, 77)"><font face="""><font style="font-size:16px">package com.xtfggef.hashmap; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * 打印在数组中出现n/2以上的元素 * 利用一个HashMap来存放数组元素及出现的次数 * @author erqing * */ public class HashMapTest { public static void main(String[] args) { int [] a = {2,3,2,2,1,4,2,2,2,7,9,6,2,2,3,1,0}; Map<Integer, Integer> map = new HashMap<Integer,Integer>(); for(int i=0; i<a.length; i++){ if(map.containsKey(a[i])){ int tmp = map.get(a[i]); tmp+=1; map.put(a[i], tmp); }else{ map.put(a[i], 1); } } Set<Integer> set = map.keySet();//------------1------------ for (Integer s : set) { if(map.get(s)>=a.length/2){ System.out.println(s); } }//--------------2--------------- }</font></font></font>
此处注意两个地方,map.containsKey(),还有就是上述1-2处的代码。理解了HashMap的上面的操作,其它的大多数方法都很容易理解了。搞清楚它的内部存储机制,一切OK!