HashMap
Map遍历
map有多种遍历方法,这里是最简单的一种,通过遍历key,来得到key和对应的value
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//遍历map中的key
for (Integer key : map.keySet()) {
System.out.println("Key = " + key+);
//因为hashmap的时间复杂度接近o(1),所以根据key获取value并不消耗性能;
System.out.println("value = " + map.get(key));
}
另一种
遍历Map.Entry
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
equals()和hashcode()方法
Object的hashcode()方法返回对象的hashcode,hashcode由对象的地址转换为整数得来的;
每一个对象的对象头里都存储了一个hashcode;equals()方法比较的是地址;
查找散列集合,先比较key的hashcode,若hashcode相同,则使用使用equals()方法比较value;
java官方对hashCode()方法的说明:
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
测试
只重写equals方法,不重写hashCode方法
public class HashCodeTest {
public static void main(String[] args) {
Collection set = new HashSet();
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
System.out.println(p1.equals(p2)); //因为重写了equels方法,所以输出true
set.add(p1); //p1成功存入集合中
set.add(p2); //p1、p2是2个不同的对象,因次其hashcode也不同,故p1、p2会被认为是2个不同的对象,p2被成功存入集合;
set.add(p1); //之前p1已经存入集合,当再存入p1时,发现存在hashcode相同的元素,因次使用equals方法比较value也相同,因次认为p1已经存在于集合,故舍弃现在的p1;
/*当使用散列集合时,我们想要: 2个对象中的x,y只要相等,就认为相等,在这里只重写equals方法;
,因为默认调用的父类Object的hashCode方法,故只要不是同一个对象,得到的hashCode必不相同;
没有重写hashCode方法,p1,p2这2个对象依旧会被认作为2个不同数据*/
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
@Data
@AllArgsConstructor
class Point {
private int x;
private int y;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
true
x:1,y:1
x:1,y:1
测试内存泄漏
注意,重写hashCode方法,使用不当容易造成内存泄漏;
public class HashCodeTest {
public static void main(String[] args) {
Collection set = new HashSet();
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 2);
set.add(p1);
set.add(p2);
/*注意,这里重写了hashCode方法,返回的hashCode与x,y的值有关
先将p1,p2放入了set中,然后又改变了p2的x,y值,那么此时对应的p2的hashcode也会改变;
当执行set.remode(p2)时,由于此时p2的hashcode已经改变,故时删除不了p2,但是p2依旧在内存中;*/
p2.setX(10);
p2.setY(10);
set.remove(p2);
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
@Data
@AllArgsConstructor
class Point {
private int x;
private int y;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
x:1,y:1
x:10,y:10
如何正确的重写equals() 和 hashCode()
https://blog.youkuaiyun.com/zzg1229059735/article/details/51498310
JDK1.7
JDK1.7 HashMap结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IRQNlAW-1621011692748)(https://note.youdao.com/yws/res/69205/B64E0AD831044FA786862F0700EA53DE)]
HashMap地层是 数组 + 链表 实现的;
数组的每个索引被称作桶
HashMap底层维护一个数组,数组中的每一项都是一个Entry
transient Entry<K,V>[] table;
向 HashMap 中所放置的对象实际上是存储在该数组table当中;
而Map中的key,value则以Entry的形式存放在数组中
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
/** 指向下一个元素的引用 */
Entry<K,V> next;
int hash;
...
}
数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分key(键)、value(值),next(指向下一个Entry)
通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象,则以链表形式存储。
初始化
构造器源码
/** 初始容量,默认16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//即指定初始化容量,又指定负载因子
public HashMap(int initialCapacity, float loadFactor) {
// 判断设置的容量和负载因子合不合理
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//只指定初始化容量,负载因子默认为0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//无参构造器,初始数组(table)容量默认为16,负载因子为0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
无参构造器HashMap<String,String> m = new HashMap<>();
则默认初始化数组容量为16,负载因子0.75 ;
初始化时 指定容量HashMap<String,String> m = new HashMap<>(int initialCapacity);
若 指定的容量不是2的次幂,则返回一个大于且最接近它的2次幂的值;故 hashmap初始化和永远都是2次幂;
例如HashMap<String,String> m = new HashMap<>(31)
则初始化为32
put操作
put源码
public V put(K key, V value) {
// 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
// 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值
int hash = hash(key);
// 搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
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))) {
// 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数+1
modCount++;
// table数组中没有key对应的键值对,就将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
put操作大致流程为:
然后对key判空,如果key==null
,则遍历数组table[0],如果链表中存在key==null
,则替换掉它的value并返回旧value,如果链表中不存在key==null
的entry,则直接添加进去;
如果key!=null,则通过key的hashcode算出table(数组)的下标,然后先判断table对应位置是否为null(为null 表示此处的链表为null 即 还没有任何数据),如果null则遍历查看是否存在相同的key,如果存在则替换value,如果不存在则插入到链表里;
get操作
如果key==null
,则遍历table[0]链表,返key==null
的entry的value,
如果key!=null,则根据key的hashcode计算出table数组(桶)的下标,然后遍历链表并返回key相等的entry的value。
扩容机制
当HashMap中的元素超过了 加载因子 与当前 容量(数组大小) 的乘积(默认16*0.75=12)时,通过调用resize方法重新创建一个原来HashMap大小的两倍的newTable数组,重新计算hash,然后再重新根据hash分配位置。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 根据新传入的newCapacity创建新Entry数组
Entry[] newTable = new Entry[newCapacity];
// 用来将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 再将newTable赋值给table
table = newTable;
// 重新计算临界值,扩容公式在这儿(newCapacity * loadFactor)
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容机制的性能
原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。
所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。
线程安全
HasmMap线程不安全
hashmap里面的方法没有进行同步
扩容 导致死循环
JDK 1.7 hashmap使用的头插法,高并发下 会死循环;
https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng
JDK1.8
JDK1.8中HashMap底层实现为 数组+链表+红黑树 ;桶中的结构可能是链表,也可能是红黑树
JDK1.7中,如果hash冲突的概率高,就会使一个桶中的链表过长,遍历效率低;
JDK1.8中,当数组大于64 且 链表大于8 时 才会转化为红黑树,当红黑树小于6时 就会退化成链表。
JDK 1.8HashMap底层实现:数组+链表+红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDK1Tlym-1621011692750)(https://note.youdao.com/yws/res/79978/08498035AE6640B6866FCB386F3E98AE)]
初始化
无参构造器HashMap<String,String> m = new HashMap<>();
则默认初始化数组容量为16,负载因子0.75 ;
初始化时 指定数组容量HashMap<String,String> m = new HashMap<>(int initialCapacity);
若 指定的数组容量不是2的次幂,则返回一个大于且最接近它的2次幂的值;故 hashmap初始化和永远都是2次幂;
例如HashMap<String,String> m = new HashMap<>(31)
则初始化为32,比如传入6,实际分配8;
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor(initialCapacity) 如果构造函数设置的初始容量不是2的次幂,那么使用以下方法返回一个大于且最靠近它的2的次幂的值
this.threshold = tableSizeFor(initialCapacity);
}
hash表扩容后,如果发现某一个红黑树的长度小于6,则会重新退化为链表
Collections.synchronizedMap(Map map)
HashMap线程不安全,除了使用ConcurrentHashMap 以为,还可以使用Collections.synchronizedMap(Map map)返回一个线程安全的map;
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private final Map<K,V> m; // Backing Map
final Object mutex;
...
public int size() {
synchronized (mutex) {return m.size();}
}
...
}
可以看到SynchronizedMap维护了一个Map 和一个 Object mutex表示锁 , SynchronizedMap中的方法基本上都是对mutex上锁;
常见面试题
HashMap时间复杂度为什么是O(1)
定位到数组的时间复杂度O(1)
链表(长度<8)时间复杂度O(n)
红黑树(长度>8)时间复杂度O(logn)
可以看出,影响hashmap性能的主要因素是 链表长度或红黑树节点数 ,理想情况下 数组中的链表或红黑树节点数 为1,此时时间复杂度也是O(1);
而且,正常情况下,hashmap链表、红黑树节点不会很多,因为当hasdmap中元素的数量 达到 数组(桶)大小 * 0.75(负载因子)时,hashmap就会扩容,hash冲突概率小,也不太可能出现一个桶中 有很多个元素;
因次,HashMap时间复杂度接近O(1)
HashMap扩容(大小)是2的幂次方
https://blog.youkuaiyun.com/miranaibuai/article/details/106674285
https://www.cnblogs.com/jinjian91/p/11917413.html
有2个好处:
- 按位运算,效率高(高16位 异或 运算);
- 为了数据的均匀分布,减少哈希碰撞;
HashMap计算散列位置,如果不按位运算通常的做法是取模,即 先a=n/c,再 x=n-a*c (就结果而言,这里就是取余),这里包含了除法、乘法、减法、运算 效率低;
如果按位运算,就必须让hashmap的容量为2的倍数 才能使数据均匀分布;
hashmap确定落在数组的位置的时候,计算方法是(n - 1) & hash ,奇数n-1为偶数,偶数2进制的结尾都是0,经过&运算末尾都是0,会增加hash冲突。
与运算符(&)
运算规则:
0&0=0;0&1=0;1&0=0;1&1=1
即:两个同时为1,结果为1,否则为0
(n - 1) & hash
例如 hashmap长度为15 --> 1111
hashcode 数组中位置
0 1110 & 0 = 0
1 1110 & 1 0
2 1110 & 10 2
3 1110 & 11 2
4 1110 & 100 4
5 1110 & 101 4
…… …… ……
16 1110 & 10000 0
17 1110 & 10001 0
18 1110 & 10010 2
可以看到如果不是偶数位,按位与 的结果都是分布在偶数位;
hashmap如何解决hash冲突
hashmap用的是 链地址法;
HashMap中hash函数是如何实现
对key的hashCode做hash操作,和 高16位做异或运算;
还有哪些hash函数的实现方式?
还有平方取中法,除留余数法,伪随机数法
为什么链表转化为红黑树的门槛是8
https://blog.youkuaiyun.com/adorechen/article/details/107709726
查看java8 HashMap注释大意是:
在hashcode随机分布时,链表长度和红黑树的出现概率复合泊松分布。在链表长度为8时,红黑树出现概率为百万分子6。
为什么要进行链表转红黑树的优化
如果单纯的使用链表,发生hash碰撞时,链表会不断增长,当对HashMap的相应位置进行查询的时候,时间复杂度位O(n),如果使用红黑树就是O(logn);