HashMap使用
HashMap是基于哈希表的Map接口的非同步实现,继承自AbstractMap,AbstractMap是部分实现Map接口的抽象类
- 哈希表也称散列表,根据关键码值key进行访问的数据结构,也就是说,能够将关键码映射到表中一个位置我们就可以区访问value,加快查找的速度,这个映射函数叫做散列函数,存放记录的属组叫做散列表。我们之前使用的两种数据结构,数组寻址容易(时间复杂度为O(1)),插入和删除困难(复杂度为O(N)),而链表寻址困难(复杂度为O(N)),插入和删除容易(复杂度为O(1))。综合两者特性,哈希表变成了一个寻址容易,插入删除也容易的数据结构。
- Map接口,Map,图,是一种存储键值对映射的容易,在Map中键可以是任意类型的对象,但是键是不允许重复的,每一个键对应一个值。例如:name(man)-age(19)
HashMap使用
//1.声明HashMap对象
Map<String, Integer> map = new HashMap<>();
//2.添加数据 put
map.put("树", 90);
map.put("兽", 79);
map.put("龙", 80);
//3.根据键获取值 get
System.out.println("根据键获取值:"+map.get("树"));
//4.获取Map中键值对的个数
System.out.println("map当中键值对的个数:"+map.size());
//5.判断Map集合是否包含键为key的键值对
System.out.println(map.containsKey("妖"));
System.out.println(map.containsKey("树"));
//6.判断Map集合是否包含值为value的键值对
System.out.println(map.containsValue(90));
System.out.println(map.containsValue(100));
//7、根据键删除Map中的健值对 remove
System.out.println(map.remove("树"));
System.out.println(map.remove("妖"));//这里删除了一个不存在的键
//8、获取HashMap中的所有键值对
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String, Integer> next = iterator.next();
System.out.println(next.getKey()+":: "+next.getValue());
}
运行结果
HsahMap底层结构
jdk1.8之前:
在jdk1.8之前,哈希表的结构是以数组和链表使用链地址法构成的,表中有一个散列函数f,每一key值对应一个value的存储位置,当不同的key值对应同一个位置点时,就出现了哈希冲突。
此时,会在此节点后创建链表,并将newNode尾插法插入当前链表之后。
HsahMap的一次插入过程:
HashMap结构:
jdk1.8之后:
jdk1.8之后对HashMap进行了改进,使用了数组加链表加红黑树的结构,当链表长度超过阀值8时,将链表转换为红黑树。大大地减少了查找时间 。
- 红黑树:红黑树是一种自平衡二叉查找树 它在O(log2 N)的时间内做查找、添加、删除。红黑树和AVL树是二叉查找树的变体,红黑树的性能要好于AVL树
- 红黑树特点:
1)每个节点是红色或黑色
2)根节点一定是黑色
3)每个叶子节点是黑色
4)如果一个节点是红色,则叶子节点必须是黑色
5)每个节点到叶子节点所经过的黑节点的个数必须是一样
结构:
HashMap源码分析
- 类的继承关系:
HashMap继承了抽象Map类,并且实现了Cloneable和Serializable接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap允许空值和空键
HashMap是非线程安全
HashMap元素是无序 LinkedHashMap TreeMap
(HashTable不允许为空 线程安全)
- 类的属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 16 默认初始容量 用来给table初始化
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容机制
static final int TREEIFY_THRESHOLD = 8; //链表转为红黑树的节点个数
static final int UNTREEIFY_THRESHOLD = 6;//红黑树转为链表的节点个数
static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V>
transient Node<K,V>[] table; //哈希表中的桶
transient Set<Map.Entry<K,V>> entrySet; //迭代器遍历的时候
transient int size;
int threshold;
final float loadFactor;
- 类中重要方法
put方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//resize() 初始化(和扩容)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//当前位置不存在节点,创建一个新节点直接放到该位置
else{
//当前位置存在节点 判断key是否重复
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断第一个节点的key是否与所要插入的key相等
//hashCode 表示将对象的地址转为一个32位的整型返回 不同对象的hashCode有可能相等
//比较hash相比于使用equals更加高效
else if (p instanceof TreeNode)
//判断当前节点是否是红黑树节点
//是的话,则按照红黑树插入逻辑实现
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//判断e是否是key重复的节点
p = e;
}
}
}
Hsah算法
resize时机
1)table==null
2) table需要扩容的时候
过程
1)table进行扩容
2)table原先节点进行重哈希
a.HashMap的扩容指的是数组的扩容,因为数组的空间是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来
b.在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组
c.在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树
中的与阿安素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中
d.最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收
常见面试题
- 1 jdk1.7与jdk1.8HashMap有什么联系?
1)头插与尾插:
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
2)扩容后数据存储位置的计算方式
- 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)。
2.而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
3)数据结构:
JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN),提高了效率)。
- 2 用过HashMap没有?说说HashMap的结构(底层数据结构+put方法的描述)
hashmap的底层数据结构是数组加链表,它继承了数组的线性查找和链表的寻址修改提高效率,在java1.8中加入了红黑树。
使用put操作时,会先计算key的 hash值,再调用putval方法。如果哈希表是空的就进行初始化resize操作,如果没有产生碰撞,可以 简单的理解为 哈希表中没有相等的hash ,就直接存入hash桶中,如果产生了碰撞,并且hash和key都相等,为了保证key的唯一性,就直接覆盖原节点。如果没有相等的key,但是属于红黑树节点,就新增一个红黑树节点。如果没有相等的节点key,也不是红黑树节点,就转为链表。
- 3 说说HashMap的扩容过程
HashMap的扩容指的是数组的扩容,因为数组的空间是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来。在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组。在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树中的与阿安素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中。最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收。
- 4 HashMap中可以使用自定义类型作为其key和value吗
HashMap中可以使用自定义类型作为其key和vallue,但需要重写hashCode()和equals方法。
- 5 HashMap中table.length为什么需要是2的幂次方
HashMap存储数据时要避免位置碰撞且数据分配均匀,于是采用位移运算的算法计算存储链表的位置,假设HashMap的长度不为2的幂次方则有可能产生碰撞,例如:
长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
- 6 HashMap与HashTable的联系
1)时间
HashTable比HashMap出现的早一个版本,HashTable在1.1,HashMap在1.2
2)父类和接口
相同:都实现了Map、Cloneable、Serializable接口
不同:HashTable继承的是Dictionary抽象类,HashMap继承的是AbstractMap抽象类
3)null处理
HashTable在存储null键时,进行hash计算时会抛出空指针异常;存储null值时会抛出空指针异常。
HashMap在存储null键时,将null的hashCode值定为0,将其存储在哈希表的第0个bucket中;存储null值时正常,可以有多个 null值。
4)数据结构
相同:都创建了一个继承自Map.Entry的私有内部类Entry,同时创建一个Entry类型的引用数组,用来表示哈希表,数组的长度,即使哈希桶的数量,对于映射到同一个哈希桶的键值对使用Entry链表来存储。
不同:HashTable的初始长度是11,扩容算法为n<<1 +1;HashMap的初始长度是16,扩容算法是2*n。
5)线程安全
HashTable是线程安全的,使用了synchronized对方法进行同步。
HashMap是线程不安全的,在多线程场景中可以使用ConcurrentHashMap替代。
- 7 HashMap、LinkedHashMap、TreeMap之间的区别和联系
Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 ConcurrentHashMap。
LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
一般情况下,我们用的最多的是HashMap,在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.
- 8 HashMap与WeakHashMap的区别和联系
weakHashMap
特性:
1.weakHashMap是基于Key-Value的散列表(数组+链表),采用拉链法实现的。一般用于单线程当中,非线程安全,weakHashMap中的键是"弱键"。
备注:当"弱键"被GC会收时,它对应的键值也会从weakHashMap中删除。
2.继承于抽象类AbstractMap,并且实现Map接口。
3.默认容量大小是16,加载因子是0.75。
4.最多只允许一条key为Null,允许多条value为Null。
HashMap
特性:
1.HashMap是基于Key-Value的散列表(JDK7:数组+链表,JDK8:数组+链表+红黑树),采用拉链法实现的。一般用于单线程当中,非线程安全,HashMap的键是"强键"。
2.继承于抽象类AbstractMap,并且实现Map接口。遍历时,取得的数据完全是随机的。
3.默认容量大小是16,加载因子是0.75。
4.最多只允许一条key为Null,允许多条value为Null。
5.HashMap实现了Cloneable和Serializable接口,而WeakHashMap没有。
1).HashMap实现Cloneable,说明它能通过clone()克隆自己。
2).HashMap实现Serializable,说明它支持序列化,能通过序列化去传输。
6.添加、删除操作时间复杂度都是O(1)。
- 9 WeakHashMap中涉及到的强弱软虚四种引用
强引用
A a = new A(); //a是强引用
只要是强引用,GC就不会回收被引用的对象
软引用SoftReference
一般用户实现Java对象的缓存,缓存可以有可以没有,一般将有用但是非必须的对象用软引用关联只要是软引用关联的对象,在Java发生内存溢出异常之前,会将这些对象列入要回收的范围,如果回收之后发现内存还是不够,才会抛出OOM异常map -》 SoftReference -》 SoftReference.get()
弱引用 WeakReference
弱引用是用来一些非必须的对象,比软引用更弱一些只要是被弱引用关联的对象,只能够生存到下一次垃圾回收之前,一旦发生垃圾回收,无论当前内存是否够用,都会回收掉被弱饮用关联的对象
虚引用 PhantomReference
别名幽灵引用 最弱的引用关系,一个对象是否具有虚引用的存在,完全是不会对其生命周期产生影响,也无法通过虚引用获取一个对象的实例,它存在的唯一目的就是在对象被垃圾回收之后收到一个系统通知
- 10 HashMap是线程安全的吗
hashmap不是线程安全的,在不保证线程安全的环境下速度较快。在jdk 1.8中的注释有写到:
- <p><strong>Note that this implementation is not synchronized.</strong>
- If multiple threads access a hash map concurrently, and at least one of
- the threads modifies the map structurally, it <i>must</i> be
- synchronized externally. (A structural modification is any operation
- that adds or deletes one or more mappings; merely changing the value
- associated with a key that an instance already contains is not a
- structural modification.) This is typically accomplished by
- synchronizing on some object that naturally encapsulates the map.