1、特点
(1)、插入无序
(2)、 以键值对<k,v>的形式储存
(3)、键不能重复,如果重复,新的值会覆盖旧的值;
如果是自定义元素,必须重写hashcode方法和equals方法
(4)、键可以为有一个为null,值可以多个为null;
特定:键为null的元素放在数组0号位置;
(5)、底层数组的容量为2的指数级
2、 数据结构:
数组+链表
核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
3、底层源码分析
(1)构造函数:
//指定大小和加载因子
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity) {
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)
(2)基本属性:
Node<K,V>[] table; //hashMap底层是数组实现
Set<Map.Entry<K,V>> entrySet;
int size; //元素的个数
static final int DEFAULT_INITIAL_CAPACITY = 16; //默认容量
float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子
int MAXIMUM_CAPACITY = 1 << 30; //哈希表中数组容量的最大值
int TREEIFY_THRESHOLD = 8; // 扩容阈(yu 2)值 ,
//如果capacity*loadfactor >= threshld, 引起扩容(resize);
内部类:Entry<K,V> -------------------------------------《1.7中是Entry,1.8中是Node》
final K key;
V value;
Entry<K,V> next;
final int hash; //key的hash值
(3)默认值: 默认初始容量16,默认负载因子0.75;
(4)扩容方式: 二倍扩容(仍满足2的指数级关系)
(1) 扩容临界: 当前size >= 扩容阈值(当前容量 * 负载因子)
(2)然后重新计算已存入map的元素的hash值并且重新存放;
/*<jdk:1.7>
* 会在元素个数超过阈值时(当前容量*负载因子)二倍扩容数组,然后重新计算已
* 存入map的元素的hash值并且使之均匀存储;
*/
Entry<K,V>[] resize() {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable); //重新计算已存入元素的hash值,重新存储,
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
(5)增删改查方法:
A、添加元素:***********************************
首先判断数组是否是null?是则初始化数组,则下一步
然后判断是不是key==null ?是则放置到数组下标为0的位置;否则下一步
利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,此时有两种情况(用equals比较)。
(1)如果key相同,则覆盖原始值;
(2)如果key不同(出现冲突),则将当前的key-value放入链表头部;
如果数组当前下标为空,直接新建Entry放在此处;
null没有哈希值;但是map中可以储存null;
<jdk:1.7>
//put方法源码分析:
public V put(K key, V value) {
//第一次插入元素,需要对数组进行初始化:注意:数组大小为2的指数关系
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
//key不为null
//通过key来哈希找到该数据所存在的索引位置:key相关的哈希值
int hash = hash(key); //根据key的keyCode重新计算hash值。
int i = indexFor(hash, table.length); //找到位置索引
//遍历该位置i下面的链表:(判断key是否存在,是则替换旧value)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 如果发现已有该键值,则存储新的值,并返回原始值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
B、获取元素:*****************************
先判断 key == null? 是则调用getForNullKey()在数组0号下标查找
获取时,直接找到hash值对应的下标,再遍历链表判断key是否相同,从而找到对应值。
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)))
return e.value;
}
return null;
}
C、删除元素:******************************
首先检查k == null ? 是则使其 hash值 为0;
获取其hash值,在数组指定下标处开始遍历(key==nulltable[0]处遍历查找);
遍历判断 入参 key equals (entry.key)? 是则返回此元素,然后返回其value。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> 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;
}
(6)继承关系
class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
4、方法探究
V put(K key,V val)添加键值对;返回的类型是值的类型的数据(还不知道返回的是什么)
V replace(K key, V val);替换key所对应的值为val,并且返回那个被替换的值;
V remove(K key);删除入参键对应的映射,(如果存在)
boolean remove(K key, V val); 如果map中有此映射而且值不为null时,删除并返回true;
boolean replace (K key, V oldVal , V newVal ); 只有当这对键值对存在时,才能替换旧的值;
5、遍历map
1、遍历键值对 entrySet();返回的是一个键值对的 set
<jdk:1.8>
Iterator <Map.Entry<Integer,String >>iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<Integer,String> e = iter.next();
Integer k = e.getKey();
String v = e.getValue();
System.out.println(k+"-->"+v);
}
2、遍历键, KeySet();返回的是一个键的 set 集合
Iterator<Integer> iter2 = map.keySet().iterator();
while (iter2.hasNext()) {
Integer k = iter2.next();
System.out.println(k);
}
3、遍历值,Values();返回的是一个value的Collection对象;
Iterator <String> iter3 = map.Values().iterator();
while(iter3.hasNext())) {
String str = iter3.next();
System.out.println(str);
}
6、思考:
(1)如何使HashMap实现线程安全:
使用集合工具类Collections使hashMap边的安全,同理,也可以让List、Set等具有线程安全性
Map m = Collections.synchronizedMap(new LinkedHashMap(...));
(2)hashMap在jdk1.7和jdk1.8中的区别:
jdk1.8中
数组+链表+红黑树
(3)究竟是如何将map中元素的key放到set中去的:
首先使用KeySet()方法返回的只是一个set引用,
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
然后使用iteator方法得到一个迭代器,
public final Iterator<K> iterator() { return new KeyIterator(); }
但是返回的这个迭代器是KeyIteator,此类继承的是hashIteator抽象类,里面包括hasnext,remove和一个构造器,但是重写了Next(方法,返回的nextNode.key;
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key;}
}
然后我们在迭代过程中,调用此next方法,就不返回一个node的key,所以keySet并不是把map里面的值存到set中去了,而是在我们调用next方法的时候,把元素的key返回给了我们而已,同理可得,entrySet()方法和Values()方法,都是使用了同样的道理,在我们调用set的其他方法的时候,不是从set里面取出来返回的,而是直接指向了map里面的元素的值或者调用了返回调用map里面方法的结果而已。