更新时间: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!