文章目录
- 2.1 Map接口
- 2.2 HashMap
- 底层结构
- 概述
- 构造方法
- 0312补充内容
- 2.3 LinkedHashMap
- 2.4 补充
- 2.5 TreeMap
- 概述
文章概述:
顶级接口Map
子实现HashMap(最为重要)
子实现LinkedHashMap(在HashMap的基础上,额外维护了一个双向链表)
子实现TreeMap(Map的红黑树实现)
2.1 Map接口
概述
1、Map集合体系的顶级接口
2、Map的实现子类,不允许存储重复元素(不可以重复的key)
3、有些子实现允许存储null(指key):HashMap、LinkedHashMap;有些子实现不允许存储null:TreeMap
4、有些子实现是有序的(指key):LinkedHashMap、TreeMap(指的是大小有序),有些子实现是无序的(指key):HashMap
5、只能添加key - value数据,添加的数据是成对的
Api
2.2 HashMap
底层结构
HashMap底层结构: 数组 + 链表 + 红黑树
Jdk 1.8 之前:没有红黑树
Jdk 1.8 之后:补充了红黑树(在这一版做了改进)
红黑树:是由链表实现的
概述
- Map的一个子类
- 底层结构:数组 + 链表 + 红黑树
- 数组的初始容量(默认的初始容量是16),数组扩容(默认2倍)【4.23视频】【12个元素后扩容,】
- 无序(计算的位置不确定)
- key可以是null
- 不允许存储重复key(对于HashMap什么叫重复元素?)
- 线程不安全
- 默认的加载因子是0.75
什么是加载因子?(饱和度)
存储元素(key-value)个数 > 加载因子 * 数组长度 ---> 要扩容
假如: 数组长度128, 加载因子0.75 , 最多能存储 96个元素
尽量在使用hashmap的时候, 把它的加载因子设置到 0.5 ~ 1之间
- HashMap中存储元素的过程(需要好好理解!)
记住存储元素的过程,理解上面(1到8)就没有问题
9.1: 先把要存储的key-value数据中的key拿出来, 计算hash值
int hash = (h = key.hashCode()) ^ (h >>> 16) 为什么异或运算? 充分散列
9.2: 把key经过计算的hash值拿出来, 和底层数组长度取余, 得到一个数组下标,
那么也就意味着, 这个下标就是这份key-value数据在数组存储的位置
9.3: 有可能经过key获得他的hash, 又取余, 得到下标, 发现这个下标所对应存储位置, 没有存储元素,
可以直接存储(存储一个Node类型 的结点: Node包含四个参数, hash, key, value, next)
9.4: 有可能经过key获得他的hash, 又取余, 得到下标, 发现这个下标所对应存储位置, 已经存储了元素,
比较是否重复: 重复, 新value覆盖旧value
(注:这时并没有结束,有可能这个位置存储的是链表或者红黑树,所以要接着往下判断)
比较不重复: 判断是红黑树还是链表, 如果是链表按照链表的比较方法,如果红黑树按照红黑树的比较方式
如果重复, 新value覆盖旧value,
如果不重复, 添加
对于上面过程的一些说明(个人添加)
首先获得key的hash值,hash值是通过hashcode值计算的
9.3的代码分析见下面
9.3的说明:next的作用:一会相同位置再次添加元素,可以拼接在next的下面
某一个位置存单个元素的概率还是比较大的
某一个位置存链表的概率有,但是相对没那么大
长度为16的数组存红黑树的概率为zero 0
128位长度的数组存红黑树的概率:千万分之几,概率非常非常小(虽然有红黑树这个东西,但是概率还是非常低的)
9.4发现这个下标所对应存储位置, 已经存储了元素
这个存储位置有两种可能:存储的是链表、存储的是红黑树(对于两种情况,操作不同)
Lesson3分别分析链表和红黑树的情况
9.3的代码分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/*
hash(Object key)
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
if ((tab = table) == null || (n = tab.length) == 0)
// n = 新数组的长度, 16(扩容完的数组,长度为16)
n = (tab = resize()).length;
// n-1 = 15 -> 1111
// (n - 1) & hash ---> 相当于取余
// i = 经过key计算的hash值,又和数组取余之后的下标
// (p = tab[i] ) == null : 如果为真, 代表着散列的位置, 没有存储其他内容, 可以直接添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
9.4的代码分析
HashMap的源码分析1(初始化情况、扩容的情况)
我是红色
- 对于hashmap来讲, 给定初始容量, 它构建的底层数组是一个大于等于给定值的2的最小的幂值长度
给定初始容量,是指第二个构造方法
HashMap ( int initialCapacity ) 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
HashMap<String, Integer> map = new HashMap<>(14);
- 对于hashmap来说, 什么是重复元素?
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
先检测, 存储位置的元素p, 它的hash值是否和要存储元素的hash值一样,
P的key 和 存储的key是否直接相等, 或者, 是否相equals
zs
ls
两个参数的hash值有可能相同吗?
有,因为异或运算
hashcode值可能不同,但是异或运算后完全可能得到相同的hash值
仅仅使用hash值来判断是否相同是不保险的
所以添加了:相equals或者直接相等
DemoHashMap4:User对象,两个zs1只能存一个
key相同了,不用判断key.equals(k)了
见下面的第12点
- 如果key重复了, 那么, 新的value值会覆盖旧的value值
不是存储的第2个
DemoHashMap4
如果希望存储的是同一个元素:即zs1和zs2只存储一个(主观感受zs且18是同一个对象),应该如何改造?
仅仅重写equals()可以吗?
并不一定完全没用:
因为zs1和zs2计算的hashcode值即使不同但是经过异或运算后可能hash值相同
即使hash值不同,但是取余后有可能散列到同一个位置,如下:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这时,满足hash值相同(即使hashcode值不同,但是判断的是hash值)且满足相equals,所以判断为同一个元素,但是,
这种几率非常非常小
因为并不保险,所以再重写hashcode()方法(直接连hashcode都重写了)
都是zs和18,计算的hashcode值一样,必定hash值一样,hash值一样,必定散列到同一个位置,且又重写了equals方法,
必定被认为是同一个元素,新的value覆盖旧value
HashMap<User, Integer> map = new HashMap<>();
User zs1 = new User("zs", 18);
User zs2 = new User("zs", 18);
map.put(zs1, 1);
map.put(zs2, 2);
System.out.println(map);
class User{
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
以上,主体逻辑讲完了!
- 链表,什么时候转化为红黑树?
超过8达到9的时候, 转化为红黑树
链表长度,超过8达到9的时候, 转化为红黑树.
添加一个元素,链表中元素达到9个的时候,树化操作
static final int TREEIFY_THRESHOLD = 8;
// 说明, 这个位置存储的是个链表(单独拿出来研究)
// 已经比较了, p是链表的头结点
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;
p = e; //不重复,下移
}
- 红黑树, 什么时候转化为链表?
Root !=null root.left != null root.right != null root.left.left != null
删除元素后,元素很少的时候,需要转化为链表
删除和6没有关系,扩容才有关系
6转化为链表, 错误 (不完善)
- 链表超过8达到9的时候, 链表不一定转化为红黑树
如果底层数组长度小于64, 是优先选择扩容, 而非转化为红黑树
前提: 底层数组长度, 大于64, (这个时候才转化为红黑树)
如果底层数组长度小于64, 是优先选择扩容, 而非转化为红黑树
(这种文字块好看吗!)
/*
概述第15点
*/
static final int MIN_TREEIFY_CAPACITY = 64;
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();
//大于等于64的时候,做树化操作,转化为红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
构造方法
前两个构造方法的源码分析见上面!!
后两个自己看!!!
0312补充内容
接昨天的15条概述
- 如果数组扩容(2倍), 一个元素扩容需要重新散列, 原本在下标 x位置 要么还在x位置 要么在oldLength + x 位置
toString()方法的理解
toString()方法的理解:next为null就一直往后,否则遍历,遇到链表和红黑树是一样的道理,详细见下面的源码分析。
11:06 HashMap讲完了(视频2第15分钟)
toString()方法的理解:
class HashMap{
/*
第1个地方:entrySet()方法:返回EntrySet()对象
*/
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
/*
上面返回的EntrySet对象
*/
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
/*
EntrySet对象的iterator()方法
*/
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
//调用这里的next()方法,底层调用nextNode()方法
public final Map.Entry<K,V> next() { return nextNode(); }
}
class HashIterator{
Node next; // 标记下一次遍历的位置(全局的 )
//Entry<K,V> e = i.next();核心是依赖下面的nextNode()方法
//nextNode()方法核心一句话是(下面的if...do...while...)
//
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
/*
核心一句话:AM 11:00的时候讲的,无法截图
对于我们,主要判断:(next = (current = e).next),对于后面的条件,我们toString的时候一般不为null
简单概述: 为null就向后走,否则遍历
遇到链表,同上
遇到红黑树:next还是对的(源码中:TreeNode、继承了LinkedHashMap.Entry<K,V>有before和after、继承了HashMap.Node有Next)(所以对于红黑树,还是可以next)
*/
// current 当前遍历的结点
// index: 遍历的位置
if ((next = (current = e).next) == null && (t = table) != null) {
do {
} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
}
}
//toString不在HashMap中,在父类AbstractMap中
class AbstractMap{
public String toString() {
//调用的方法和对应的对象放在上面
Iterator<Entry<K,V>> i = entrySet().iterator();
if (! i.hasNext())
return "{}";
StringBuilder sb = new StringBuilder();
sb.append('{');
for (;;) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
sb.append(key == this ? "(this Map)" : key);
sb.append('=');
sb.append(value == this ? "(this Map)" : value);
if (! i.hasNext())
return sb.append('}').toString();
sb.append(',').append(' ');
}
}
}
Day 10
2.3 LinkedHashMap
概述
- LinkedHashMap 是HashMap的一个子类,
- LinkedHashMap 特点基本上遵从于HashMap 的特点
- LinkedHashMap在HashMap结构的基础上, 额外维护了一个双向链表, 以用来保存存储顺序
- LinkedHashMap 是有序的
3是LinkedHashMap和HashMap的一个重要区别
双向链表如下
LinkedHashMap的遍历比HashMap简单的多
Abstrt
构造方法
Api
构造方法400行,代码700行。基本都是复用的Hashmap
2.4 补充
一旦把数据存储到map中,不应该通过引用来修改存储的元素
如果实在想修改, 可以先删除, 再添加
2.5 TreeMap
概述
- TreeMap是Map的红黑树实现
- TreeMap底层是红黑树()
- TreeMap 里面元素存储的时候需要比较
- 有序(大小有序)
- 不允许存储null key (因为null没有办法比较大小)
- 不允许存储重复元素 (比较大小不能重复-自然顺序)
- 线程不安全(没有任何和锁相关的)
String实现类compareable接口有compare方法,可以比较大小
有序:依次存储157
重复:两个5只能存一个
改为User类型
实现接口,只存了zs 2
换成ls,只存了zs 2(因为return 0)
自己重写compareTo方法
存储到TreeMap中的对象必须能比较
8. 存储到的元素,要能比较:
第一种方式:实现自然排序
第二种方式:提供一个比较器:匿名对象,就是一个比较器
思考:如果存储元素实现了自然顺序,TreeMap提供了构造器,按照哪种比较?