HashMap
java.util
Class HashMap<K,V>
java.lang.Object
java.util.AbstractMap<K,V>
java.util.HashMap<K,V>
参数类型
K - 由该Map维护的键的类型
V - 映射值的类型
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
面试题一:
HashMap的key存对象时应该注意什么?
答:如果自定义对象作为Map的键,那么必须重写hashCode和equals.
面试题二:
HashMap中hashCode作用?
答:1、返回对象的哈希吗值(散列嘛),用来支持哈希表,比如HashMap
2、可以提高哈希表的性能
hasing(哈希法)的概念:
散列法(Hashing)是一种将字符组成的字符串转换为固定长度(一般是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法。由于通过更短的哈希值比用原始值进行数据库搜索更快,这种方法一般用来在数据库中建立索引并进行搜索,同时还用在各种解密算法中。
对HashMap的理解?
HashMap是基于哈希表来实现的Map接口,此实现提供了所有可选的Map操作,并允许null的值和null的键(HashMap类大致相当于Hashtable,除了它是不同步的,并允许null值).HashMap不能保证map存入对象的顺序,并且它不能保证顺序在一段时间内不变.
HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子.初始容量是创建哈希表时的容量,负载因子是在容量自动增加之前允许哈希表得到满足的度量,当在散列表中的条目数超过了初始容量和负载因子的乘积,那么哈希表会被重新散列(即内部结构被重建),使哈希表扩大两倍.因此如果迭代性能很重要,不要将初始容量设置的太高(或负载因子太低),因为哈希表被重建很耗性能.
默认的初始容量为16,默认的负载因子是0.75.
1 public class HashMapDemo {
2 public static void main(String[] args) {
3 //构造一个空的HashMap
4 Map map = new HashMap();
5 //默认的初始容量为16,默认的负载因子是0.75
6 Map map1 = new HashMap(16, (float) 0.75);
7 }
8 }
map对应的详细方法:
1 public class HashMapDemo {
2 public static void main(String[] args) {
3 //构造一个空的HashMap
4 Map map = new HashMap();
5 //默认的初始容量为16,默认的负载因子是0.75
6 Map map1 = new HashMap(16, (float) 0.75);
7 map.put("1","a");
8 map.put("2","b");
9 map.put("3","c");
10 map.put("4","d");
11 //方法详细信息
12 //1.public int size() 返回此地图中键值映射的数量
13 System.out.println("数量:"+map.size());
14 //2.public boolean isEmpty() 如果此map不包含键值映射,则返回 true 。
15 System.out.println("空?"+map.isEmpty());
16 //3.public V get(Object key) 返回到指定键所映射的值
17 System.out.println("映射值:"+map.get("1"));
18 //4.public boolean containsKey(Object key) 如果此映射包含指定键的映射,则返回 true 。
19 System.out.println("是否包含key 1的映射:"+map.containsKey("1"));
20 //5.public V put(K key,V value) 新加映射关系。 如果地图先前包含了该键的映射,则替换旧值。
21 map.put("5","e");
22 //6.public void putAll(Map<? extends K,? extends V> m)
23 // 将指定地图的所有映射复制到此地图,这些映射将替换此映射对当前在指定映射中的任何键的任何映射。
24 map1.put("1","A");
25 map1.put("2","B");
26 map1.put("3","C");
27 map.putAll(map1);
28 System.out.println("新映射"+map.get("1"));//A
29 //7.public V remove(Object key) 从该map中删除指定键的映射(如果存在)。
30 map.remove("5");
31 System.out.println("删除5后:"+map.get("5"));
32 //8.public void clear() 从map中删除所有的映射。
33 //map.clear();
34 //9.public boolean containsValue(Object value)
35 System.out.println("是否有value对应的映射key"+map.containsValue("C"));
36 //10.public Set<K> keySet() 返回此map中包含的键的Set视图。
37 Set mapSet = map.keySet();
38 Iterator iterator = mapSet.iterator();
39 while (iterator.hasNext()){
40 System.out.println("key 的 set值"+iterator.next());
41 }
42 //11.public Collection<V> values() 返回此地图中包含的值的Collection视图。
43 Collection collection=map.values();
44 Iterator valuesIterator = collection.iterator();
45 while (valuesIterator.hasNext()){
46 System.out.println("values的值:"+valuesIterator.next());
47 }
48 //12.public V getOrDefault(Object key,V defaultValue) 返回到指定键所映射的值,如果没有显示默认值
49 System.out.println("如果没有key对应的value:"+map.getOrDefault("8","aa"));
50 //13.public boolean remove(Object key,Object value) 删除指定key和 value的对应关系,如果关系不对应,则不改变
51 System.out.println(map.remove("4","D"));
52 //14.public boolean replace(K key,V oldValue,V newValue) 替换指定key值对应的value值
53 map.replace("1","A","aaa");
54 //jdk8的forEach的用法
55 map.forEach((key,value)->{
56 System.out.println("key:"+key+" value:"+value);
57 });
58 }
59 }
HashMap和HashTable的区别:
HashMap和HashTable都实现了Map接口,他们的主要区别有:线程安全性,同步(synchronization),以及速度.
1.HashMap几乎可以等价于HashTable,除了HashMap是非synchronized的,并可以接受null值(HashMap可以接受为null的键值,而HashTable则不可以).
2.HashMap是非synchronized,而HashTable是synchronized,这意味着HashTable是线程安全的,多个线程可以共享一个HashTable,而如果没有正确同步的话,多个线程不能共享一个HashMap,Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable扩展性更好.
3.另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
4.由于HashTable是线程安全的也是synchronized,所以在单线程环境下比HashMap更慢,如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过HashTable.
5.HashMap不能保证随着时间的推移,map中的元素顺序是不变的.
synchronized的作用:
synchronized意味着在一次仅有一个线程能够更改HashTable,就是说任何线程要更新HashTable就要首先获得同步锁,其他线程要等到同步锁释放后才能再次获得同步锁对HashTable进行更新.
如何让HashMap实现同步功能:
HashMap可以通过下面的语句实现同步功能:
Map m = Collections.synchronizedMap(hashMap);
HashMap和HashSet的区别:
HashSet实现了set接口,它不允许集合中有重复的值,当我们提到HashSet时第一件事想到的就是在将对象存储在HashSet之前,要先确保对象重写equals()和hasCode()方法,这样才能比较对象的值是否相等,以确保set中没有存储相等的对象,如果我们没有重写这两个方法,将会使用这个方法的默认实现.
HashMap | HashSet |
HashMap实现了Map接口 | HashSet实现了Set接口 |
HashMap存储键值对 | HashSet仅仅存储对象 |
使用put()方法存储元素 | 使用add()方法存储元素 |
HashMap使用键对象来计算hasCode的值 | HashSet使用成员对象来计算hashCode的值 |
HashMap比较快,因为是使用唯一的键来获取对象 | HashSet比HashMap慢 |
HashMap存储键值原理,以及扩容机制:
put(K key,V value);
在使用默认构造器初始化一个HashMap对象的时候,首次put键值的时候会先计算对应key的hash值,通过hash值来确定存放的地址;
1 static final int hash(Object key){
2 int h;
3 return (key == null) ? 0 : (h=key.hashCode())^(h>>>16);
4 }
紧接着调用了putVal方法,在刚刚初始化之后的table值为null因此程序会进入到resize()方法中,而resize()方法就是用来扩容的,扩容后得到了一个table的节点(Node数组),接着根据传入的hash值去获得一个对应的节点p,并去判断是否为空,是的话就存入一个新的节点(Node),反之如果当前存放的位置已经有值了就会进入到else中,接着根据前面得到的节点p的hash值以及key跟传入的hash值以及参数进行比较,如果一样则覆盖,如果存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在链表的头部),如果链表的长度大于8那么就会以红黑树的形式进行保存.
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
2 boolean evict) {
3 Node<K,V>[] tab; Node<K,V> p; int n, i;
4 if ((tab = table) == null || (n = tab.length) == 0) //首次初始化的时候table为null
5 n = (tab = resize()).length; //对HashMap进行扩容
6 if ((p = tab[i = (n - 1) & hash]) == null) //根据hash值来确认存放的位置。如果当前位置是空直接添加到table中
7 tab[i] = newNode(hash, key, value, null);
8 else {
9 //如果存放的位置已经有值
10 Node<K,V> e; K k;
11 if (p.hash == hash &&
12 ((k = p.key) == key || (key != null && key.equals(k))))
13 e = p; //确认当前table中存放键值对的Key是否跟要传入的键值对key一致
14 else if (p instanceof TreeNode) //确认是否为红黑树
15 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
16 else {//如果hashCode一样的两个不同Key就会以链表的形式保存
17 for (int binCount = 0; ; ++binCount) {
18 if ((e = p.next) == null) {
19 p.next = newNode(hash, key, value, null);
20 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 判断链表长度是否大于8
21 treeifyBin(tab, hash);
22 break;
23 }
24 if (e.hash == hash &&
25 ((k = e.key) == key || (key != null && key.equals(k))))
26 break;
27 p = e;
28 }
29 }
30 if (e != null) { // existing mapping for key
31 V oldValue = e.value;
32 if (!onlyIfAbsent || oldValue == null)
33 e.value = value; //替换新的value并返回旧的value
34 afterNodeAccess(e);
35 return oldValue;
36 }
37 }
38 ++modCount;
39 if (++size > threshold)
40 resize();//如果当前HashMap的容量超过threshold则进行扩容
41 afterNodeInsertion(evict);
42 return null;
43 }
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
get(K key);
先前HashMap通过hashCode来存放数据,那么get方法一样要通过hashCode来获取数据,可以看到如果当前table没有数据的话,直接返回null值,反之通过传进来的hash值找到对应节点(Node)first,如果first的hash值以及key跟传入的参数匹配就返回对应的value,反之判断是否是红黑树,如果是红黑树则从根节点开始进行匹配,如果有对应的数据则返回结果,如果没有则返回null,如果是链表的话就会循环查询链表,如果当前节点不匹配的话就会从当前节点获取下一个节点来进行循环匹配,如果有对应的数据则返回结果,如果没有返回null.
1 public V get(Object key){
2 Node<K,V> e;
3 return (e = getNode(hash(key),key)) == null ? null : e.value;
4 }
1 final Node<K,V> getNode(int hash, Object key) {
2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
3 //如果当前table没有数据的话返回Null
4 if ((tab = table) != null && (n = tab.length) > 0 &&
5 (first = tab[(n - 1) & hash]) != null) {
6 //根据当前传入的hash值以及参数key获取一个节点即为first,如果匹配的话返回对应的value值
7 if (first.hash == hash && // always check first node
8 ((k = first.key) == key || (key != null && key.equals(k))))
9 return first;
10 //如果参数与first的值不匹配的话
11 if ((e = first.next) != null) {
12 //判断是否是红黑树,如果是红黑树的话先判断first是否还有父节点,然后从根节点循环查询是否有对应的值
13 if (first instanceof TreeNode)
14 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
15 do {
16 //如果是链表的话循环拿出数据
17 if (e.hash == hash &&
18 ((k = e.key) == key || (key != null && key.equals(k))))
19 return e;
20 } while ((e = e.next) != null);
21 }
22 }
23 return null;
24 }
HashMap面试一连问:
面试官:在项目中使用过HashMap吗?在什么情况下使用它的?说说你对HashMap的理解?
答:在项目中经常使用HashMap,HashMap是用来存储键值对的,可以接受null的键和null值,是非synchronized的,如果使用HashMap要存储自己的对象作为键值,要注意重写对象的hashCode()和equals()方法.
面试官:那HashMap的工作原理是什么?HashMap的get()方法的原理是什么?
答:HashMap是基于hashing的原理,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象,当我们使用put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来存储Entry对象,HashMap是在bucket中存储键对象和值对象,作为Map.Entry.
面试官:当两个对象的hashCode相同会发生什么?
答:因为hashCode相同,所以他们的bucket位置相同,"碰撞"会发生,因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中.
面试官:当两个键的hashCode相同,存储在一个bucket上时,你是如何获得对象的?
答:找到bucket的位置后,会调用key.equals()方法找到链表中正确的节点,最终找到值对象.
面试官:如何减少碰撞的发生?你在开发中都应该怎么避免碰撞?
答:在开发中尽量使用不可变的,声明为final的对象作为键值,并且采用合适的equals()和hashCode()方法来减少碰撞的发生,提高效率,使用String,Integer这样的wrapper类做为键是非常好的选择.
面试官:HashMap如果超出了容量怎么办?
答:HashMap默认的负载因子是0.75,也就是说,当一个map填满了75%的bucket时候,和其他集合类(如ArrayList)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket中,这个过程叫做rehashing,因为它调用hash方法找到新的bucket位置,这个过程会严重降低效率,所以我们如果觉得map中存储数量比较多,可以初始化的时候,设置HashMap的初始容量和负载因子,负载因子根据map对象查询频率做一些改变.
面试官:重新调整HashMap的大小时候,在多线程下会不会存在条件竞争?
答:当重新调整HashMap的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要调整大小了,它们会试着调整大小,在调整大小的过程中,存储在链表中的元素的次序反过来,因为移动到新的bucket位置时,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历.如果竞争条件发生了,那么就会发生死循环,不过在多线程环境下,我们不应该使用HashMap,而可以使用ConcurrentHashMap来避免多线程不一致的发生.
面试官:为什么String,Integer这样的wrapper类更适合作为HashMap的键值?
答:因为String是不可变的,也是final的,而且已经重写了hashCode()和equals()方法,其他的wrapper类也具有这些特点.