文章五: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

在这里插入图片描述

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)

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

在这里插入图片描述


0312补充内容

接昨天的15条概述

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

toString()方法的理解

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

11:06 HashMap讲完了(视频215分钟)

在这里插入图片描述

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的一个重要区别

双向链表如下

在这里插入图片描述

LinkedHashMap的遍历比HashMap简单的多

Abstrt
构造方法

在这里插入图片描述

Api
来自HashMap

构造方法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中的对象必须能比较

在这里插入图片描述
8. 存储到的元素,要能比较:

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

思考:如果存储元素实现了自然顺序,TreeMap提供了构造器,按照哪种比较?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lacrimosa&L

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值