HashMap相关(初版-随时更新)

HashMap详解

初始化

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

初始化时调用此方法返回2的次幂 如 cap = 14 ; n = 14 - 1二进制为1101; >>>无符号右移
(1) 1101 |= 1101 >>> 1 (0110) ; 结果 1111
(2) 1111 |= 1101 >>> 2; 结果 1111
其余搞完结果 1111
最后 1111 + 1 = 16
则返回16

当选则指定大小创建hashMap时,初始化值自动转为2的次幂

cap - 1 的意义为 防止数值不对,如16高位不减1 则输出值为32 10000 | 1111 结果 11111

putVal

putVal(hash(key), key, value, false, true);

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

首先会对 传入key 计算hashcode算法位传入对象的hashcode算

为什么要右移16位?

1保证高16位也参与计算, 我们直到int占4字节 32位,16是中位数
2因为大部分情况下,都是低16位参与运算,高16位可以减少hash冲突

代码

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   //声明所需变量,tab要操作的数组 p接点代表 n数组长度 i 数组中的位置
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab变量 n变量赋值
        if ((tab = table) == null || (n = tab.length) == 0)
        //如果hashmap 还未操作过则重新设置数组  //resize 后面讲
            n = (tab = resize()).length;
            //确定要添加元素的位置 
            //为什么要 (n - 1) & hash???  答:合理确定数组中的位置, 更均匀的分配
            //在初始化数组大小时为什么要限制位2的次幂??? 答:因为2的次幂如减一则 后面位置全为1保证跟均匀分配每位都有意义  如 16 二进制 10000 - 1 = 01111 
            //如果不是 2次幂  如 5 101 - 1 = 100  因为 任何一个数&0都是0这样hash冲突会变大
        if ((p = tab[i = (n - 1) & hash]) == null)
        //创建一个node节点
            tab[i] = newNode(hash, key, value, null);
        else {
        //如果不是空,那么此槽位置可能是链表或者红黑树
            Node<K,V> e; K k;
            //如果入参hash在数组中已经存在且是当前节点直接替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            //如果是树节点则进行树操作,树后边再讲
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                //如果节点链表下一个节点是空 则创建一个新节点由当前节点指向新节点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        如果链表长度大于等  7 尝试转换成树,届时里边还会判断数组个数 如个数小于64则resize扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //转树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //结束条件,找到相同hash匹配节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                        //指向下一个节点
                    p = e;
                }
            }
            //不等于空则替换value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                    //后置处理
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //修改次数++  用于快速失败,如并抛出发修改异常
        ++modCount;
        //如果当前数组长度大于临界值
        if (++size > threshold)
        //比较判断扩容
            resize();
            //后置处理
        afterNodeInsertion(evict);
        return null;
    }

resize扩容

原理:老数组长度如大于0,则数组长度 << 1即乘 2 后重新hash计算确定每个值的槽位置

代码

final Node<K,V>[] resize() {
		//必要属性相关初始化
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果已经加过元素
        if (oldCap > 0) {
        	//如果数组已经是最大长度了,设置临界值为最大返回此数组
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } //扩容新容量,并判断容量是否大于默认值16 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //如果大于16 新临界值等于旧临界值×2 
                     //为什么大于 16临界值才可以 <<计算?  答:保证计算精度,如果负载因子特殊,则此	   	计算不一定准确,即 newCap * loadFactor 不一定等于 2 * **(oldCap * loadFactory)** 一直乘2 
                     //例子:
  /*  float f = 0.6797f;
        float l = 1.34f;

        System.out.println(64*f);

        for (int i = 0; i < 5; i++) {
            l=l * 2;
        }
        System.out.println(l);*/
        //上述代码输出 43.500    42.88
        //所以达到一定精度在转换
                newThr = oldThr << 1; // double threshold
        }//已经有初始化的临界
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        //没有值默认初始化为16 临界值12 负载因子0.75
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //创建新的数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //重复指定槽
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果当前节点没构成链表直接赋值
                    if (e.next == null)
                    //同样为了均匀分布
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    //树赋值 -->后边讲树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    //如果是链表且不是树
                    //链表知识复习 链表头:数据内容为第一个元素的结点。
					//头指针:指向头结点元素的指针。
					//头结点:数据内容无效,其指针是头指针。
					//
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                        	//当前节点的下一个节点
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                            	//确定数组头节点 尾节点是空
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                    //链表尾指向头
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                //真是数据头
                                    hiHead = e;
                                else
                                //指向下一个节点
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

常见的hash算法

Hash算法的特点

输入敏感:原始输入信息发生任何变化,新的Hash值都应该出现很大变化。
不可逆性:给定明文和Hash算法,在有限时间和有限资源内能计算得到Hash值。但是给定Hash值,在有限时间内很难逆推出明文。
冲突避免:很难找到两段内容不同的明文,使得它们的Hash值一致。

常用的构造散列函数的方法

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:

1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)

2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

3. 平方取中法:取关键字平方后的中间几位作为散列地址。

4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。

5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

处理冲突的方法
  1. 开放寻址法;Hi=(H(key) + di) MOD m, i=1,2,…, k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:

(1) di=1,2,3,…, m-1,称线性探测再散列;

(2)di=1^2, (-1)^2, 22,(-2)2, (3)^2, …, ±(k)^2,(k<=m/2)称二次探测再散列;

(3)di=伪随机数序列,称伪随机探测再散列。 ==

2. 再散列法:Hi=RHi(key), i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。

3. 链地址法(拉链法)

4. 建立一个公共溢出区

TreeMap和TreeSet的深入理解

旋转
树的左旋和右旋的过程用一个图来表示比较简单直观:
在这里插入图片描述
从图中可以看到,我们的左旋和右旋主要是通过交换两个节点的位置,同时将一个节点的子节点转变为另外一个节点的子节点。具体以左旋为例,在旋转前,x是y的父节点。旋转之后,y成为x的父节点,同时y的左子节点成为x的右子节点。x原来的父节点成为后面y的父节点。这么一通折腾过程就成为左旋了。同理,我们也可以得到右旋的过程。

红黑树的官方定义如下:

红黑树是一种二叉树,同时它还满足下列5个特性:

  1. 每个节点是红色或者黑色的。

  2. 根节点是黑色的。

  3. 每个叶节点是黑色的。(这里将叶节点的左右空子节点作为一个特殊的节点对待,设定他们必须是黑色的。)

  4. 如果一个节点是红色的,则它的左右子节点都必须是黑色的。

  5. 对任意一个节点来说,从它到叶节点的所有路径必须包含相同数目的黑色节点。

    这部分的定义看得让人有点不知所云,我们先看一个红黑树的示例

    在这里插入图片描述

treeSet怎么排序

A:自然排序:要在自定义类中实现Comparerable接口 ,并且重写compareTo方法

B:比较器排序:在自定义类中实现Comparetor接口,重写compare方法

2.自然排序
自然排序要进行一下操作:

1.Student类中实现 Comparable接口
2.重写Comparable接口中的Compareto方法

int compareTo(T o)
比较此对象与指定对象的顺序。

故Student类为: 特别注意在重写Compareto方法时,注意排序

如果比较引用类型的需要实现 Comparable接口,重写Compareto方法

3、比较器排序

比较器排序步骤:
1.单独创建一个比较类,这里以MyComparator为例,并且要让其继承Comparator接口
2.重写Comparator接口中的Compare方法
int compare(T o1,T o2)
比较用来排序的两个参数。

3.在主类中使用下面的 构造方法

TreeSet(Comparator<? superE> comparator)构造一个新的空 TreeSet,它根据指定比较器进行排序。

版权声明:本文为CSDN博主「晓锋残月」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaofei__/article/details/53138681

四.数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用 Array 而不是

ArrayList?
答:不同点:定义上:Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类
型。容量上:Array 大小固定,ArrayList 的大小是动态变化的。操作上:ArrayList 提供更多
的方法和特性,如:addAll(),removeAll(),iterator()等等。使用基本数据类型或者知道数
据元素数量的时候可以考虑 Array;ArrayList 处理固定数量的基本类型数据类型时会自动装
箱来减少编码工作量,但是相对较慢。

2.15 快速失败(fail-fast)和安全失败(fail-safe) 一:快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增
加、删除、修改),则会抛出 Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCo
unt 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使
用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmod
Count 值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如
果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会
抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检
测并发修改的 bug。
场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过
程中被修改)。

二:安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原
有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改
并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,
迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,
在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并
发修改。
快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另
外一个线程在修改这个集合,就会抛出 ConcurrentModification 异常,java.util 下都是快速
失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影
响下层。在 java.util.concurrent 下都是安全失败

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值