zoukankan      html  css  js  c++  java
  • Java集合学习

    java集合源码分析(一)---整体

    不用看了,博主大概就讲了以下:

    父接口 Iterable里面,就一个iterator 方法,返回一个terator 迭代器,然后子接口Collection,还是有这个返回迭代器的方法,然后加了许多其他的方法

    (所以迭代器只有Collection派系的set,list,queue能用吧?然后List类又有ListIterator,这个可以逆向遍历更多方法了)

    Iterator接口
    我们还是先去看下官方的文档

     

    是简单的一个接口,它有三个方法,具体的实现还需要看它的具体实现类

    例如set的遍历:

    public class TraversalSet {
        public static void main(String args[]){
    
            List<String> list = new ArrayList<>(Arrays.asList("tom","cat","Jane","jerry"));//我们一般list.add
            Set<String> set = new HashSet<>();
            set.addAll(list);//我们一般set.add(“a”)
    
    
            //方法1 集合类的通用遍历方式, 从很早的版本就有, 用迭代器迭代
            Iterator it1 = set.iterator();
            while(it1.hasNext()){
                System.out.println(it1.next());
            }
    
    
            //方法2 集合类的通用遍历方式, 从很早的版本就有, 用迭代器迭代
            for(Iterator it2 = set.iterator();it2.hasNext();){
                System.out.println(it2.next());
            }
    
    
            //方法3 增强型for循环遍历
            for(String value: set){
                System.out.println(value);
            }

    for(int i=0; i<集合的大小;i++){ // ... } //手动添加方法4 普通for循环 但是数据大的时候别用这个 好像也用不了 set没有i索引无法输出
    } }

    Collection派系

    其中,ArrayList,HashSet,LinkedList,TreeSet是我们经常会有用到的已实现的集合类

    Map派系

     

    Map实现类用于保存具有映射关系的数据。Map保存的每项数据都是key-value对,也就是由key和value两个值组成。Map里的key是不可重复的,key用户标识集合里的每项数据。

    HashMap,TreeMap是我们经常会用到的集合类

    TreeMap?TreeMap或许不如HashMap那么常用,但存在即合理,它也有自己的应用场景,TreeMap可以实现元素的自动排序

    这点展开以后又全是知识点。。。先存几个链接(TreeMap原理实现及常用方法

    java集合源码分析(三)--ArrayList源码

    这个看博主最后的总结就好。。源码感觉不一样

    成员变量:

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
        //可序列化的版本号
        private static final long serialVersionUID = 8683452581122892189L;
        //默认的数组大小为10 重点
        private static final int DEFAULT_CAPACITY = 10;
        //实例化一个空的数组 当用户指定的ArrayList容量为0的时候 返回这个
        private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
    //一个空数组实例 当用户没有指定 ArrayList 的容量时(即调用无参构造函数),返回的是该数组==>刚创建一个 ArrayList 时,其内数据量为 0。 //当用户第一次添加元素时,该数组将会扩容,变成默认容量为 10(DEFAULT_CAPACITY) 的一个数组===>通过 ensureCapacityInternal() 实现 //它与 EMPTY_ELEMENTDATA 的区别就是:该数组是默认返回的,而后者是在用户指定容量为0返回 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
    //存放List元素的数组 保存了添加到ArrayList中的元素。实际上,elementData是个动态数组 ArrayList基于数组实现,
    用该数组保存数据, ArrayList 的容量就是该数组的长度
    //该值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入 ArrayList 中时,数组将扩容值 DEFAULT_CAPACITY(10) transient Object[] elementData; //List中元素的数量 存放List元素的数组长度可能相等,也可能不相等 private int size; //这个数字就是最大存放的大小emmmmmm private static final int MAX_ARRAY_SIZE = 2147483639;

    构造方法:

    public ArrayList(int var1) {
        if (var1 > 0) {
        //创建一样大的elementData数组
            this.elementData = new Object[var1];
        } else {
            if (var1 != 0) {
            //传入的参数为负数时候 报错
                throw new IllegalArgumentException("Illegal Capacity: " + var1);
            }
            //初始化这个为空的数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    
    }
    //构造方法 无参 数组缓冲区 elementData = {}, 长度为 0
    //当元素第一次被加入时,扩容至默认容量 10
    public ArrayList() {
          //初始化这个为空的数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //构造方法,参数为集合元素 public ArrayList(Collection<? extends E> var1) { //将集合元素转换为数组,然后给elementData数组 this.elementData = var1.toArray(); if ((this.size = this.elementData.length) != 0) { //如果不是object类型的数组,转换成object类型的数组 if (this.elementData.getClass() != Object[].class) { this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class); } } else { this.elementData = EMPTY_ELEMENTDATA; } }

    这个有点晕,我们还是参考这个:通过源码分析ArrayList扩容机制

    总之三个构造方法:

    1.无参默认构造:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。 下面在我们分析 ArrayList 扩容时会讲到这一点内容!

    2.带初始容量参数的构造函数。(用户自己指定容量):(这上面两个链接源码不一样,但表达的意思一样的)

        /**
         * 带初始容量参数的构造函数。(用户自己指定容量)
         */
        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {//初始容量大于0
                //创建initialCapacity大小的数组
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {//初始容量等于0
                //创建空数组
                this.elementData = EMPTY_ELEMENTDATA;
            } else {//初始容量小于0,抛出异常
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }

    就是用户指定容量大于0时创建指定大小的数组;用户指定容量为0 时创建空数组; 这样第一个博主的注释就都理解了

    3.传入集合列表

    -------------------------------------------------------------------------------------------------------------------

    我们还是继续认真看第二个链接的源码分析扩容

    这里以无参构造函数创建的 ArrayList 为例分析

    1. 先来看 add 方法

        /**
         * 将指定的元素追加到此列表的末尾。 
         */
        public boolean add(E e) {
       //添加元素之前,先调用ensureCapacityInternal方法
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //这里看到ArrayList添加元素的实质就相当于为数组赋值
            elementData[size++] = e;
            return true;
        }

    2. 再来看看 ensureCapacityInternal() 方法

    可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1)

       //得到最小扩容量
        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  // 获取默认的容量和传入参数的较大值
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }

    当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10

    3. ensureExplicitCapacity() 方法

    如果调用 ensureCapacityInternal() 判断是否需要扩容方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码!

      //判断是否需要扩容!!
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                //调用grow方法进行扩容,调用此方法代表已经开始扩容了
                grow(minCapacity);
        }

    我们来仔细分析一下:

    • 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10(是由Math.max得来的)。此时,minCapacity - elementData.length > 0 成立,所以会进入 grow(minCapacity) 方法
    • 当add第2个元素时,minCapacity 为2(是因为不再满足if条件不能max了只能为size+1)此时elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。
    • 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。

    直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容

    4. grow() 方法

        /**
         * 要分配的最大数组大小
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
        /**
         * ArrayList扩容的核心方法。
         */
        private void grow(int minCapacity) {
            // oldCapacity为旧容量,newCapacity为新容量
            int oldCapacity = elementData.length;
            //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
            //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
           // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
           //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);//自己为自己的复制 但长度变为新的1.5倍长度了
        }

    int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.

    ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源  

    例:1000(8)  0100(4) 0010(2) 因为是二进制

    我们再来通过例子探究一下grow() 方法

    • 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
    • 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
    • 以此类推······

    ---------------------------------------------------------------------------emmmm似懂非懂。。。

    我们看到这块的代码
    扩容的时候利用位运算把容量扩充到之前的1.5倍
    比如说用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。

    可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…

    将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。

    可以通过调试看到动态增长的数量变化:4->7->11->17->26->…

    公式就是 新的容量 = (旧的容量*3)/2 +1;

    然后还要检测下大小,不能超出最大的范围

    那那那这个Java中ArrayList最大容量是多少啊?
    大约是8G
    ----------------------------------------------------------------------------------------总结 背下来

    问:简单介绍下ArrayList
    答:ArrayList是以数组实现,可以自动扩容的动态数组,当超出限制的时候会增加50%的容量,用system.arraycopy()复制到新的数组,因此最好能先用list.ensureCapacity(N);给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。arrayList的性能很高效,不论是查询和取值很迅速,但是插入和删除性能较差,该集合线程不安全。

    黄线部分主要是这里add:

        /**
         * 在此列表中的指定位置插入指定的元素。 
         *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
         *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
         */
        public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //arraycopy()方法实现数组自己复制自己
            //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;
    index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
    System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }

    问:ArrayList的自动扩容怎么样实现的
    关键字:elementData size ensureCapacityInternal(判断是否需要扩容方法)
    答:每次在add()一个元素时,arraylist都需要对这个list的容量进行一个判断。如果容量够,直接添加,否则需要进行扩容。在1.8 arraylist这个类中,扩容调用的是grow()方法。
    在核心grow方法里面,首先获取数组原来的长度,然后新增加容量为之前的1.5倍。随后,如果新容量还是不满足需求量,直接把新容量改为需求量,然后再进行最大化判断。
    通过grow()方法中调用的Arrays.copyof()方法进行对原数组的复制,再通过调用System.arraycopy()方法进行复制,达到扩容的目的

    问:ArrayList的构造方法过程

    答:ArrayList里面有三种构造方法,第一种:无参的构造方法 先将数组为空,第一次加入的时候 然后扩充默认为10, 第二种是有参的构造方法 ,直接创建这个数组 第三种是传入集合元素,先将集合元素转换为数组,把不是object的数组转化为object数组。

    问:ArrayList可以无限扩大吗?

    答:不能,大于是8G,因为在ArrayList扩容的时候,有个界限判断。 private static final int MAX_ARRAY_SIZE = 2的31次方减一然后减8,-8 是为了减少出错的几率,虚拟机在数组中保留了一些头信息。避免内存溢出。

    问:System.arraycopy和Arrays.copyOf的区别

            //arraycopy()方法实现数组自己复制自己
            //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
            System.arraycopy(elementData, index, elementData, index + 1, size - index);
       /**
         以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 
         */
        public Object[] toArray() {
        //elementData:要复制的数组;size:要复制的长度
            return Arrays.copyOf(elementData, size);
        }

    从两种拷贝方式的定义来看:
    System.arraycopy()使用时必须有原数组和目标数组,Arrays.copyOf()使用时只需要有原数组和数据长度即可。
    从两种拷贝方式的底层实现来看:
    System.arraycopy()是用c或c++实现的,Arrays.copyOf()是在方法中重新创建了一个数组,并调用System.arraycopy()进行拷贝
    两种拷贝方式的效率分析:
    由于Arrays.copyOf()不但创建了新的数组而且最终还是调用System.arraycopy(),所以System.arraycopy()的效率高于Arrays.copyOf()。

    java集合源码分析(四)---LinkedList

    认真看

    //函数会以O(n/2)的性能去获取一个节点
    Node<E> node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    就是判断index是在前半区间还是后半区间,如果在前半区间就从head搜索,而在后半区间就从tail搜索。而不是一直从头到尾的搜索。如此设计,将节点访问的复杂度由O(n)变为O(n/2)。
    --------------------------------------------------------------总结

    问: LinkedList的底层用什么实现的?
    答:LinkedList的底层是用双向链表来实现的

    问:LinkedList的插入删除的时候的高效如何实现?
    答:首先是因为LinkedList的底层是用双向链表实现的,所以插入和删除比较高效,不需要大量的数据移动。插入也是尾部插入。其次,在获取结点的时候,核心的方法是node()方法,采用的思想是折半查找。判断index是在前半区间还是后半区间,如果在前半区间就从head搜索,而在后半区间就从tail搜索。而不是一直从头到尾的搜索,将将节点访问的复杂度由O(n)变为O(n/2)。

    问:LinkedList线程安全吗?为什么?
    答:线程不安全

    问:LinkedList和ArrayList的比较
    答:1 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配,所有只适合频繁查询修改;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象,适合频繁增删。
    2在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
    3当进行频繁的插入删除的操作的时候,LinkedList的性能会更加好一点。

    问:List,LinkedList与ArrayList的不同
    答:第一点:List是接口类,LinkedList和ArrayList是实现类
    第二点:ArrayList是动态数组(顺序表)的数据结构。顺序表的存储地址是连续的,所以在查找比较快,但是在插入和删除时,由于需要把其它的元素顺序向后移动,耗时操作。
    第三点:LinkedList是链表的数据结构。链表的存储地址是不连续的,每个存储地址通过指针指向,在查找时需要进行通过指针遍历元素,所以在查找时比较慢。由于链表插入时不需移动其它元素,所以在插入和删除时比较快。 

    但是在中间插入数据和尾部插入数据都得具体分情况讨论。

    中间插入数据就和插入位置有关,而且还和插入数据量有关(尾部插入数据也是和插入数据量有关)。插入位置有关是因为linkedlist插入之前也需要查找到指定索引位置,越靠近中间值越耗时,因为node方法是两端开始搜索的。

    参考:ArrayList和LinkedList在中间开始插入的快慢比较 的实验结论(但博主把十万当1万了),当插入索引靠前时,arraylist就慢一些,因为它后面的都要移动,而linkedlist很快,因为此时查找很快。当索引靠中间时,linkedlist就吃力了,很慢。。。如果此时把插入数据量弄更大,则linkedlist更不行了。

    emmm???插入方式也有关。。主要看索引是变化的还是不变的。。若索引不变,linkedlist很快。

    String泛型

     ----------------------------------------------------------------------------------------------------------------------------

    Integer泛型  类型影响不大

    插入方式有关 索引位置也有关

     位置的影响比数据量还是大一些

     末尾插入主要是数据量影响:https://blog.csdn.net/qq_28817739/article/details/87740998

    就这样吧绕晕了,还是增删Linkedlist改查Arraylist 虽然增删的话arraylist也不差


    java源码分析(五)---HashMap源码

    哈希 其实是随机存储的一种优化,先进行分类,然后查找时按照这个对象的分类去找。
    哈希通过一次计算大幅度缩小查找范围,自然比从全部数据里查找速度要快

    哈希函数
    如何给别人起外号呢,还不能重复,要突出特色
    哈希函数就是干这个的

    哈希的过程中需要使用哈希函数进行计算。
    哈希函数是一种映射关系,根据数据的关键词 key ,通过一定的函数关系,计算出该元素存储位置的函数

    1. 对key的值首先进行hashCode()运算//通用方法,根据对象的内存地址,返回key的一个特定的哈希码
    2. 二次处理哈希码 //进行无符号右移16位(这是为了取h哈希码的高16位),然后同没有取高16位的自己取异或后返回(这样就更随机松散一些了,因为第3步length太小根本用不到hash的高16位所以只能在第2步这样用一下),求得键对应的真正的hash值
    3. 计算存储到数组的位置 二次处理的哈希码和数组长度减一 进行&运算(这里是为了hash值内存能放下所以对他取余操作,因为除数(length)是2的幂次方的话,hash%length==hash&(length-1),后者效率更高。所以这里也确定了哈希表长度只能是2的幂次方)

    图片步骤总结:这里长度16-1=15 :1111

     最后计算出这个key存在数组下标为5的地方

    如果有hash冲突,用拉链法(即博主说的链地址法,数组+链表)解决。jdk1.8以后在拉链法的基础上增加了链表转为红黑树。

    ps:左移右移无符号左移右移的概念 就是整体移动


    Map接口的源码

    二:5种遍历方法

    方法一: 这是最常见的并且在大多数情况下也是最可取的遍历方式。在键值都需要时使用(get了同时拿到键和值的方法)

    Entry 是 Map 接口中的静态内部接口,表示一个键值对的映射
    通过 Map.entrySet() 方法获得的是一组 Entry 的集合(就是所有键值对的集合吗?),保存在 Set 中,所以 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()); 
    }

    方法二: 在for-each循环中遍历keys或values。(推荐)

    Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 
    //遍历map中的键 
    for (Integer key : map.keySet()) { 
      System.out.println("Key = " + key); 
    } 
    //遍历map中的值 
    for (Integer value : map.values()) { 
      System.out.println("Value = " + value); 
    }

    该方法比entrySet遍历在性能上稍好(快了10%),而且代码更加干净。

    方法三: 使用Iterator遍历

    使用泛型:也是entry的集合的迭代器

    Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 
    Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator(); 
    while (entries.hasNext()) { 
      Map.Entry<Integer, Integer> entry = entries.next(); 
      System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); 
    }

    方法四 :通过键找值遍历(效率低不要用,用方法一) emmmm一直用的这种。。

    Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 
    for (Integer key : map.keySet()) { 
      Integer value = map.get(key); 
      System.out.println("Key = " + key + ", Value = " + value);
      }

    作为方法一的替代,这个代码看上去更加干净;但实际上它相当慢且无效率。因为从键取值是耗时的操作(与方法一相比,在不同的Map实现中该方法慢了20%~200%)。如果你安装了FindBugs,它会做出检查并警告你关于哪些是低效率的遍历。所以尽量避免使用。

    总结:
    如果仅需要键(keys)或值(values)使用方法二。如果你使用的语言版本低于java 5,或是打算在遍历时调用iterator.remove()来删除entries,必须使用方法三。否则使用方法一(键值都要)

    成员变量

    static final float DEFAULT_LOAD_FACTOR = 0.75f;  //加载因子

    加载因子就是表示Hash表中元素的填满程度
    加载因子越大,填充的元素越多,空间利用率就越高,但是冲突就越多
    反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。

     HashMap有一个初始容量大小,默认是16

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  

    为了减少冲突的概率,当hashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,这是一个非常耗时的操作。
    而这个临界值由【加载因子】和当前容器的容量大小来确定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默认情况下是16x0.75=12时,就会触发扩容操作。
    泊松分布统计用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

    put函数

    1. 先对key的hashCode()做hash,然后再计算下标的值
    2. 如果没有碰撞就弄到桶里面
    3. 碰撞了就以链表的形式放在那个桶的链表里面
    4. 如果这个桶的链表里面接了7个结点后,就把链表弄成红黑树
    5. 如果结点已经有了,就替换旧的值,保证key的唯一性
    6. 如果满了就扩容

    get函数

    这个过程就是拿着key找值的

    1. 还是先计算hash值
    2. 先在第一个桶里面找下,看下能否直接命中
    3. 如果用冲突的话,挨个遍历
    4. 计算出桶的位置
    5. 在桶的链表或者树里面挨个找

    前面哈希如果还是产生了频繁的碰撞,会发生什么问题呢?
    作者注释说,他们使用树来处理频繁的碰撞(链表size大于8就转为红黑树

    我们看get函数的时候
    首先,先根据hashCode()做hash,然后确定bucket的index;
    然后,再找到桶之后再接着匹配下key,然后找值

    在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
    因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题

    扩容函数

    在在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
    Capacity就是bucket的大小(就是length吧??默认16),Load factor就是bucket填满程度的最大比例(默认0.75)。如果对迭代性能要求很高的话,不要把capacity设置过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时(即16*0.75=12)就需要调整bucket的大小为当前的2倍。

    我们来看下这块的扩容函数的过程

    当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中

    当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置
    怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:

    可见hash1地址的key没变化,hash2的移动2的2次幂的位置。(这个hash2与新的length-1&运算出1了)

     因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意

    这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。


    总结问题

    问: 什么时候会使用HashMap?他有什么特点?
    答:是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象,把这个当作一个结点,进行存储。

    问:HashMap线程安全吗?为什么?
    答:线程不安全,因为HashMap没有使用sychronized同步关键字,在添加数据put()时,无法做到线程同步,当多个线程在插入数据时,如果发生了哈希碰撞,可能会造成数据的丢失,然后在在扩容resize()时,也无法做到线程同步,当多个线程同时开启扩容,会各自生成新的数组进行拷贝扩容,最终结果只有一个新数组被赋值给table变量,其他的线程均会丢失

    问:如何保证HashMap线程安全
    答:三种方式,Hashtable,ConcurrentHashMap,Synchronized Map
    链接详解 (但是1.8 concurrentHashMap取消了分段锁,改用CAS和synchronized来保证并发安全。具体还不会呢。。。1.7segment实现了ReentrantLock所以是一种可重入锁。。。??也不太懂)

    问:HashMap的工作原理?
    通过hash的方法进行存储结构构建hash表,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

    问:你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
    答:对结点的key进行hash运算操作,计算下标,也就是要放到那个桶里面,如果碰撞的话,就在这个桶的链表或者树里面进行查询结点。

    问:hash的实现方式
    答:在jdk1.8后,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

    问:扩容时候机制
    答:这个分情况讨论这块,因为jdk的版本问题分情况讨论
    JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。
    JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充
    我们看的是jkd1.8这块,当发现现在的桶的占有量已经超过了容量 * 负载因子时候,进行扩容,直接扩容桶的数量为之前的2倍,并且重新调用hash方法,重新计算下标,然后再把之前的结点放到数组里面。

    问:HashMap里面的红黑树
    答:这块,,,自己还在看emmmmm目前回答不上来,当一个桶链表结点数量大于8的时候,就把链表结构转换成红黑树结构,这样的操作查找效率更高,O(n)变成O(logN),这块就是红黑树和链表的区别emmmmm
    ————————————————
    版权声明:本文为CSDN博主「sakurakider」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/sakurakider/article/details/83794770


    Java集合必会14问(精选面试题整理)

  • 相关阅读:
    es index template
    什么是元类
    Normal Data Structure Tricks
    Python 学习笔记
    点分治
    人类智慧贪心
    「JOI 2021 Final」地牢 3
    【美团杯2020】魔塔
    CF917D 的垃圾做法
    【ULR #2】Picks loves segment tree IX
  • 原文地址:https://www.cnblogs.com/gezi1007/p/13155265.html
Copyright © 2011-2022 走看看