Todo
- 遍历方法
- HashMap的get/put方法
- 为什么hashmap的在链表元素数量超过8时改为红黑树?
map遍历key-value方法
HashMap实现原理
- HashMap源码,知道原理吗?
- 为什么用数组+链表?
- hash冲突的解决办法?
- 用LinkedList代替数组可以吗?
- 既然是可以的为什么不用LinkedList,而选用数组?
- 如何设计一个散列表?
HashMap源码,知道原理吗?
HashMap由数组和链表构成的数据结构,数组每个地方都存储了Key-Value对,在java7中叫Entry,java8中叫Node。Node类实际上是一个单向链表,有next指针,连接下一个Node对象。在java8中,链表的长度大于8的时候,变成红黑树。
transient Node<K,V>[] table;
为什么用数组+链表?
数组具有根据下表随机访问的特性,hashCode(key)计算出hash值,然后对数组长度取模得到下标。
链表是解决hash冲突用的,hash值相同时,在对应下标的位置形成一条链表。
hash冲突的解决办法?
-
开放寻址法 重新探测一个空闲位置,将其插入。线性探测、二次探测、双重散列。
-
链表法 一个槽/桶,会对应一条链表,所有hash相同,key不同的元素,被放到对应下标位置的桶链表中。
-
rehash法
-
公共溢出区域法
用LinkedList代替数组可以吗?
可以
既然是可以的为什么不用LinkedList,而选用数组?
1.HashMap作为一个工业级的散列标,对查询性能要求到极致;
2.数组根据下标随机访问的特性,作为底层实现,查找较快;
3.采用数组这种基本数据结构,可以自己定义扩容机制,即便ArrayList的扩容,是1.5倍扩容,而HashMap是2的次幂。
//resize()扩容
newThr = oldThr << 1; // double threshold
// grow()方法扩容
int newCapacity = oldCapacity + (oldCapacity >> 1); //设置新的存储能力为原来的1.5倍
如何设计一个散列表?
1.散列表初始大小,HashMap默认16;
2.装载因子和动态扩容
3.散列冲突解决方法,拉链过长,性能降低
4.散列函数,简单高效,均匀分布
HashMap在什么条件下扩容?
- HashMap在什么条件下扩容?
- 为什么扩容是2的幂次?
- 为什么要先高16位异或低16位再取模?
HashMap在什么条件下扩容? 如果table的容量大于threshold,就要resize,用一个新数组替代容量小的旧数组,会发生table拷贝。
为什么扩容是2的幂次?
实现数据存储到哪个桶中的算法,就是hash % len,源码中做了优化hash & (len-1),对于2的n次方实际就是1后面n个0,2^n-1,实际就是n个1。
保证容积是2的n次方,是为了保证在做 hash & (len-1) 的时候,每一位都能&1,提高运算效率。
为什么要先高16位异或低16位再取模? 这是扰动函数,降低hash冲突的概率。
HashMap的get/put方法
- put元素的过程?
- get元素的过程?
- String中的hashcode实现?
put元素的过程?
get元素的过程?
String中的hashcode实现?
为什么HashMap的在链表元素数量超过8时改为红黑树?
- 知道jdk1.8中hashmap改了啥么?
- 为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
- 我不用红黑树,用二叉查找树可以么?
- 那为什么阀值是8呢?
- 当链表转为红黑树后,什么时候退化为链表?
HashMap的并发问题?
- HashMap在并发编程环境下有什么问题?
- 在jdk1.8中还有这些问题么?
- 一般怎么解决这些问题的?
参考链接
HashMap在并发编程环境下有什么问题?
-
多线程扩容,引起死循环问题或数据丢失;
-
多线程put的时候导致元素丢失; 如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
-
put非null元素后,get出来的却是null
如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
在jdk1.8中还有这些问题么?
死循环问题已经解决。其他两个问题还是存在。
一般怎么解决这些问题的? 比如ConcurrentHashmap,Hashtable等线程安全等集合类。
你一般用什么作为HashMap的key?
- key可以是null值吗?
- 一般用什么作为HashMap的key?
- 用可变类做HashMap的key有什么问题?
- 实现自定义的class作为HashMap的key该注意什么?
key可以是null值吗?
可以为null
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
一般用什么作为HashMap的key?
用Integer、String这种不可变类当HashMap当key,而且String最为常用。
(1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
(2)因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的覆写了hashCode()以及equals()方法。
用可变类做HashMap的key有什么问题?
hashcode可能发生改变,导致put进去的值,无法get出。
实现自定义的class作为HashMap的key该注意什么?
- 重写hashcode和equals方法注意什么?
- 如何设计一个不变类
针对问题一,记住下面四个原则即可
(1)两个对象相等,hashcode一定相等 (2)两个对象不等,hashcode不一定不等 (3)hashcode相等,两个对象不一定相等 (4)hashcode不等,两个对象一定不等
针对问题二,记住如何写一个不可变类
(1)类添加final修饰符,保证类不被继承。 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
(2)保证所有成员变量必须私有,并且加上final修饰 通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。
(3)不提供改变成员变量的方法,包括setter 避免通过其他接口改变成员变量的值,破坏不可变特性。
(4)通过构造器初始化所有成员,进行深拷贝(deep copy) 如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
this.myArray = array; // wrong
}
}
这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。 为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
(5)在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝 这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。