文章目录


文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_红黑树


文章概述:

顶级接口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

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_红黑树_02

2.2 HashMap

底层结构

HashMap底层结构:  数组 + 链表 + 红黑树

Jdk 1.8 之前:没有红黑树

Jdk 1.8 之后:补充了红黑树(在这一版做了改进)

红黑树:是由链表实现的

概述


  1. Map的一个子类
  2. 底层结构:数组 + 链表 + 红黑树
  3. 数组的初始容量(默认的初始容量是16)数组扩容(默认2倍)【4.23视频】【12个元素后扩容,】
  4. 无序(计算的位置不确定)
  5. key可以是null
  6. 不允许存储重复key(对于HashMap什么叫重复元素?)
  7. 线程不安全
  8. 默认的加载因子是0.75

什么是加载因子?(饱和度)
存储元素(key-value)个数 > 加载因子 * 数组长度 ---> 要扩容

假如: 数组长度128, 加载因子0.75 , 最多能存储 96个元素
尽量在使用hashmap的时候, 把它的加载因子设置到 0.5 ~ 1之间
  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(初始化情况、扩容的情况)​

我是红色

  1. 对于hashmap来讲, 给定初始容量, 它构建的底层数组是一个大于等于给定值的2的最小的幂值长度
给定初始容量,是指第二个构造方法
HashMap ( int initialCapacity ) 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
HashMap<String, Integer> map = new HashMap<>(14);
  1. 对于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点
  1. 如果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);
}
}
以上,主体逻辑讲完了!
  1. 链表,什么时候转化为红黑树?

超过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; //不重复,下移
}
  1. 红黑树, 什么时候转化为链表?

Root !=null root.left != null root.right != null root.left.left != null

删除元素后,元素很少的时候,需要转化为链表
删除和6没有关系,扩容才有关系
6转化为链表, 错误 (不完善)

  1. 链表超过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);
}
}

​Hashmap源码分析2(构造方法2)​

构造方法

前两个构造方法的源码分析见上面!!
后两个自己看!!!

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_红黑树_03


0312补充内容

接昨天的15条概述

  1. 如果数组扩容(2倍), 一个元素扩容需要重新散列, 原本在下标 x位置 要么还在x位置 要么在oldLength + x 位置

toString()方法的理解

toString()方法的理解:next为null就一直往后,否则遍历,遇到链表和红黑树是一样的道理,详细见下面的源码分析。

11:06 HashMap讲完了(视频2第15分钟)

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_hashmap_04

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

概述


  1. LinkedHashMap 是HashMap的一个子类,
  2. LinkedHashMap 特点基本上遵从于HashMap 的特点
  3. LinkedHashMap在HashMap结构的基础上, 额外维护了一个双向链表, 以用来保存存储顺序
  4. LinkedHashMap 是有序的

3是LinkedHashMap和HashMap的一个重要区别

双向链表如下

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_数组_05

LinkedHashMap的遍历比HashMap简单的多

Abstrt

构造方法

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_红黑树_06

Api



构造方法400行,代码700行。基本都是复用的Hashmap

2.4 补充

一旦把数据存储到map中,不应该通过引用来修改存储的元素

如果实在想修改, 可以先删除, 再添加

2.5 TreeMap

概述


  1. TreeMap是Map的红黑树实现
  2. TreeMap底层是红黑树()
  3. TreeMap 里面元素存储的时候需要比较
  4. 有序(大小有序)
  5. 不允许存储null key (因为null没有办法比较大小)
  6. 不允许存储重复元素 (比较大小不能重复-自然顺序)
  7. 线程不安全(没有任何和锁相关的)

String实现类compareable接口有compare方法,可以比较大小

有序:依次存储157
重复:两个5只能存一个

改为User类型
实现接口,只存了zs 2
换成ls,只存了zs 2(因为return 0)
自己重写compareTo方法

存储到TreeMap中的对象必须能比较

文章五:Map接口与其子实现(HashMap、LinkedHashMap、TreeMap)(0311、0312)_链表_07

8. 存储到的元素,要能比较:

第一种方式:实现自然排序
第二种方式:提供一个比较器:匿名对象,就是一个比较器

思考:如果存储元素实现了自然顺序,TreeMap提供了构造器,按照哪种比较?