Map 接口
Map:双列数据,存储key-value对的数据
-
HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
-
LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
-
-
TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序底层使用红黑树
-
Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型
Map结构的理解
-
Map与Collection并列存在。用于保存具有 映射关系的数据:key-value
-
Map 中的 key 和 value 都可以是任何引用类型的数据
-
Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
-
常用String类作为Map的“键”
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
-
Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中, HashMap是 Map 接口使用频率最高的实现类
Map中定义的方法
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
HashMap
面试题:
-
HashMap的底层实现原理?
-
HashMap 和 Hashtable的异同?
HashMap的结构
-
所有的key构成的集合是Set:无序的、不可重复的 —> key所在的类要重写equals()和hashCode()
-
所有的value构成的集合是Collection:无序的、可重复的 —>value所在的类要重写equals()
-
一个键值对:key-value构成了一个Entry对象。
-
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
-
所有的entry构成的集合是Set:无序的、不可重复的
-
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
-
HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap的底层实现原理
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
jdk7底层分析:
初始化
jdk7调用构造函数初始化,底层创建了长度是16的一维数组Entry[] table。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iqCdwz9-1609142920120)(C:\Users\Bryce\Desktop\博文\images\hashmap.png)]
HashMap有四个构造方法,但是最终都会调用第一个HashMap(int,float),下面我们分析源码
public HashMap(int initialCapacity, float loadFactor) {
//初始化数组大小小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始化数组大小最大为默认最大值,最大值是1>>30=1073741824
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子要在0到1之间
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//这下面三行代码是容量初始化,返回的是大于输入参数且最接近的2的整数次幂的数,默认为16,
//如果是自己输入容量,例如15,经计算是2^4=16>15>2^3=8,初始容量还是16
//如果是17,就是2^5=32>17>2^4=16,初始容量就是32
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//临界值,影响扩容
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建底层数组
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
添加数据
static int indexFor(int h, int length) {
//位与运算获取数组存放位置,比取模运算效率快
return h & (length-1);
}
public V put(K key, V value) {
//存放null值
if (key == null)
return putForNullKey(value);
//计算hash值
int hash = hash(key);
//计算数组中存放位置
int i = indexFor(hash, table.length);
//当e=null跳出循环,情况1
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//当e.hash != hash,情况2
//当!key.equals(k),情况3
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
//替换值
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加数据
addEntry(hash, key, value, i);
return null;
}
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
-
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
-
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
-
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
-
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
-
如果equals()返回false:此时key1-value1添加成功。----情况3
-
如果equals()返回true:使用value1替换value2。
-
-
补充:关于情况2和情况3:此时key1-value1和原来的数据在同一位置以链表的方式存储。
底层扩容
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容至两倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加元素
createEntry(hash, key, value, bucketIndex);
}
插入数据
在数组中某一个位置形成链表时,把旧的元素放到next中,新的元素替换旧元素之前的位置并指向旧的元素,简称头部插入
void createEntry(int hash, K key, V value, int bucketIndex) {
//取出旧元素
Entry<K,V> e = table[bucketIndex];
//新元素替换旧元素
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//元素的构造方法
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
jdk8底层分析
初始化
jdk8初始化,并不创建数组,当有
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
// 初始化数组大小小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始化数组大小最大为默认最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 加载因子要在0到1之间
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// threshold是根据当前的初始化大小和加载因子算出来的边界大小,
// 当桶中的键值对超过这个大小就进行扩容
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
添加数据和插入数据
put()方法
public V put(K key, V value) {
//先计算key的hash值,然后再调用putVal
return putVal(hash(key), key, value, false, true);
}
调用putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//当table为null,进入resize()初始化创建函数
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算元素在数组中存放位置,如果位置上为null,则直接添加,情况1
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果此位置不为空,新元素和旧元素hash相等,k值相同,直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树结点的话,进行红黑树插入
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);
//如果链表长度大于8时,将链表转红黑树
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;
//更新p节点
p = e;
}
}
// 如果存在这个映射就覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判断是否允许覆盖,并且value是否为空
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回调以允许LinkedHashMap后置操作
afterNodeAccess(e);
return oldValue;
}
}
// 更改操作次数
++modCount;
// 大于临界值,进行扩容至两倍,并将原先的数组中的元素放到新数组中
if (++size > threshold)
resize();
// 回调以允许LinkedHashMap后置操作
afterNodeInsertion(evict);
return null;
}
底层扩容
第一次put进行resize()会创建数组,再次进入则是扩容,扩容操作十分消耗性能
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//进行初始化数组大小和临界值,如果都没指定直接进入else使用默认值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建好数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//旧tab的值转移到新tab,重新计算下标
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//进入split方法,当数节点小于等于6时,树结构转换为链表结构
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
转换红黑树节点
将链表节点转为红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//数组为空或长度小于64,继续扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将链表节点转红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//tl为空是第一次遍历,将头结点赋值给hd
if (tl == null)
hd = p;
else {
//如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
p.prev = tl;
tl.next = p;
}
//用于在下一次循环中作为上一个节点进行一些链表的关键操作p.prev = tl
tl = p;
} while ((e = e.next) != null);
//将table该索引位置赋值给新转的TreeNode的头结点
if ((tab[index] = hd) != null)
//如果该节点不为空,则以以头结点hd为根节点,构建红黑树
hd.treeify(tab);
}
}
总结
jdk1.7和jdk1.8中HashMap的区别
- 调用HashMap()构造函数时: 1.7 创建数组;1.8 首次调用put()方法时,创建数组
- 数据结构的差异: 1.7 数组 + 链表 ;1.8 数组 + 链表或红黑树
- 底层的数组是: 1.7 Entry[];jdk 8 Node[],
- 链表的插入方式优化: 1.7 头插法; 1.8 尾插法
- 扩容的改进: 1.7 全部 rehash ;1.8 简单判断(判断新增位是 0 还是 1),要么原位置,要么【原位置 + 旧容量】的位置
- 插入数据的差别: 1.7 先判断是否要扩容再插入; 1.8 先插入,插入完成再判断是否需要扩容
链表和红黑树的互相转换
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
- 当数组的某一个索引位置上的元素以红黑树形式存在的数据个数 <=6时,此时此索引位置上的所数据改为使用链表
常用方法:
- 添加:put(Object key,Object value)
- 删除:remove(Object key)
- 修改:put(Object key,Object value)
- 查询:get(Object key)
- 长度:size()
- 遍历:keySet() / values() / entrySet()
LinkedHashMap的底层实现原理
-
LinkedHashMap 是 HashMap 的子类
-
在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
-
与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
源码中:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;//能够记录添加的元素的先后顺序
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
TreeMap
-
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
-
TreeSet底层使用红黑树结构存储数据
-
TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
-
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
public class User implements Comparable{
private String name;
private int age;
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
@Override
public String toString() {
return "User{ name='" + name + '\'' +
", age=" + age + '}';
}
@Override
public boolean equals(Object o) {
System.out.println("User equals()....");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() { //return name.hashCode() + age;
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
// return -this.name.compareTo(user.name);
int compare = -this.name.compareTo(user.name);
if(compare != 0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
public class TreeMapTest {
//向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
//自然排序
@Test
public void test1(){
TreeMap map = new TreeMap();
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);
map.put(u1,98);
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
//定制排序
@Test
public void test2(){
TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}
throw new RuntimeException("输入的类型不匹配!");
}
});
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);
map.put(u1,98);
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
}
Hashtable
-
Hashtable是个古老的 Map 实现类,JDK1.0就提供了。
-
与HashMap的相同
- 与Hashtable实现原理和HashMap相同,功能相同。
- 底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
-
与HashMap的不同
- Hashtable 不允许使用 null 作为 key 和 value
- 不同于HashMap,Hashtable是线程安全的。
Properties
-
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
-
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
-
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
问题总结:
- HashMap的⼯作原理(简单描述下回答思路)
- 利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标
- 存储时,如果出现 hash 值相同的 key,此时有两种情况。(1) 如果 key 相同,则覆盖原始值;(2) 如果 key 不同(出现冲突),则将当前的 key-value 放入链表中
- 获取时,直接找到 hash 值对应的下标,在进一步判断 key 是否相同,从而找到对应值。
- 理解了以上过程就不难明白 HashMap 是如何解决 hash 冲突的问题,核心就是使用了数组的存储方式,然后将冲突的 key 的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
- 底层数据结构
jdk7 Entry数组+链表
jdk8 Node数组+链表+红黑树
- 初始化核⼼参数
jdk7 创建数组和临界值
jdk8 无参构造只设置加载因子,有参构造
还会用位运算计算出大于输入参数且最接近他的2次幂的数作为数组大小
- hash算法
jdk7 用了向右移位的方式将高低位二进制特征混合起来,来解决低位变化不大的难点
jdk8 hashcode右移 16,再与原 hashcode 做异或运算,可以将高低位二进制特征混合起来
hash算法的运用也是数组大小要用2次幂的原因,也让kv值在数组中更均匀的分布
- 寻址算法
长度减一和hash进行位与运算,位运算比取模快
- hash冲突
HashMap 中哈希碰撞(冲突)的条件是指不同 key 被映射到同一个桶。 当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了。
链地址法:
将所有哈希地址为 i 的元素构成一个成为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因为查找、插入、删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。HashMap 中使用的就是链地址法。
- 扩容机制
jdk7 当个数超过容量大小,或者超出临界值(且要存放的位置位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
jdk8 当个数大于临界值,进行扩容至两倍,并将原先的数组中的元素放到新数组中,重新计算位置,扩容发生在存放后
- put⽅法,get⽅法执⾏流程
get
1. 通过 hash 值获取 key 映射到的桶
2. 根据该桶的存储结构决定是遍历红黑树还是遍历链表
3. table [i] 的首个元素是否和 key 一样,如果相同则返回该 value
4. 如果不同,先判断首元素是否是红黑树节点,如果是,则去红黑树中遍历查找,反之去链表中遍历查找。
put
1. 对 key 的 hashCode () 做 hash 运算,计算 index
2. 查看 table [index] 是否存在数据,没有,则构造一个 Node 节点存放在其中;
3. 存在数据,说明发生了 hash 冲突,继续判断 key 是否相等,相等,用新的 value 替换原数据;
4. 若不相等,判断当前节点类型是不是树形节点,如果是树形节点,创造树形节点插入红黑树中;(如果当前节点是树形节点证明当前已经是红黑树了)
5. 若不为树形节点,创建普通 Node 加入链表中;判断链表长度是否大于 8 并且数组长度大于 64,则链表转换为红黑树;
6. 插入后,判断当前节点数是否大于阈值,如果大于,则扩容为原数组的两倍
- hashMap是否是线程安全
HashMap 的实现里没有锁的机制,因此它是线程不安全的。
不安全情况1
A线程:map.put(1,2);
B线程:map.get(1);
B 线程获取的值本来应该是 2,但是如果 B 线程在刚到达获取的动作还没执行的时候,
线程执行的机会又跳到线程 A,此时线程 A 又对 map 赋值 如:map.put (“1”,“3”);
然后线程虚拟机又执行线程 B,B 取到的值为 3,这样 map 中第一个存放的值 就会丢失。。。。。
不安全情况2
假如两个线程进行kv值插入,hash相同,当一个线程比较了hash值后被挂起,另一个线程正常插入,前一个线程继续执行,因为已经比较过hash了,会直接进行插入,导致值得覆盖
不安全情况3
map可以设置或者默认加载因子,默认为 0.75,那么阀值就是 12,所以在往 HashMap 中 put 的值到达 12 时,它将自动扩容两倍,如果两个线程同时遇到 HashMap 的大小达到 12 的倍数时,就很有可能会出现在将 oldTable 转移到 newTable 的过程中遇到问题,从而导致最终的 HashMap 的值存储异常。
1.7中进行扩容时候因为头插法容易形成环形链和数据丢失 - HashMap 和 Hashtable的异同?
1、Hashtable 是早期提供的接口,HashMap 是新版 JDK 提供的接口。
2、Hashtable 继承 Dictionary 类,HashMap 继承了AbstractMap类,都实现 Map 接口。
3、Hashtable 线程安全,HashMap 线程非安全。
4、Hashtable 不允许 null 值,HashMap 允许 null 值。
5、Hashtable 默认容量为11,,扩容时,将容量变为原来的 2 倍加 1,而 HashMap默认容量16, 扩容时,将容量变为原来的 2 倍。