Java集合专题

目录

Collection接口的子接口(1)——List接口

List接口的实现类(1)——ArrayList

ArrayList的基本介绍:

ArrayList的底层结构和源码分析(全程截图,手把手带你进行Debug分析)

List接口的实现类(2)——Vector

Vector的基本介绍:

Vector创建和扩容源码分析:

List接口的实现类(3)——LinkedList

LinkedList基本介绍:

LinkedList底层结构和源码分析

Collection接口的子接口(2)——Set接口

Set接口的实现类(1)——HashSet

HashSet的基本介绍:

HashSet扩容机制源码分析

HashSet底层结构和源码分析

HashSet扩容和转成红黑树机制

Map

Map的实现类(1)——HashMap

HashMap底层机制

Map的实现类(2)——Hashtable

Hashtable的基本介绍

Hashtable底层结构

Java中的集合主要分为两大类:单列集合和双列集合

1.Collection接口有两个重要的子接口List和Set,他们的实现子类都是单列集合

2.Map接口的实现子类为双列集合,存放的是键值对 (Key-Value);

两大类的继承图(记住图对后面理解事半功倍)分别如下

Collection

 Set

Collection接口的子接口(1)——List接口

List接口的基本介绍:

1.List集合类中的元素有序,且可重复;

2.List集合中的每一个元素都有其对应的顺序索引,即支持索引;

3.List容器中的元素都对应一个整数型的序号记载在容器的位置,可以根据序号存取容器中的元素;

4.List接口下重要的实现类有ArrayList,Vector,LinkedList(后面会依次详细讲解);

List接口的实现类(1)——ArrayList

ArrayList的基本介绍:

1.ArrayList集合的底层是由数组实现数据存储的,因此其搜索效率高,但是修改效率低;

2.ArrayList集合中可以加入null,并且可以存储多个null;

3.ArrayList基本等同于Vector,但是ArrayList相较于Vector来说是线程不安全的

从源码中我们可以看到

Vector中基本所有方法都使用了synchronized关键字修饰以保证线程安全,而ArrayList且并没有synchronized修饰。

因此ArrayList集合的执行效率更高但是线程不安全,Vector线程安全,但相较于ArrayList来说执行效率较低。

ArrayList的底层结构和源码分析(全程截图,手把手带你进行Debug分析)

1.ArrayList集合中维护了一个Object类型的数组elementData

2.当创建ArrayList对象时,如果使用的时无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需再次扩容,则扩容elementData为1.5倍

3.如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

源码分析:

1.我们先来看一下调用无参构造器创建ArrayList对象是如何完成的

2.进入内部后我们发现,ArrayList的无参构造器其实就是给其成员属性elementData赋值 

 3.为了弄明白elementData的属性是干嘛的,以及这个无参构造函数给elementData赋值了什么内容,我们分别通过Ctrl+鼠标左键单击找到elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA所表示的内容如下:

可以看到 DEFAULTCAPACITY_EMPTY_ELEMENTDATA实际上是一个空的Object数组,

因此可以得出第一个结论:

1.当我们调用ArrayList的无参构造函数创建对象时,其内部实际上是创建了一个空的elementData数组

后面的源码我们暂时不去探究,先点击步出退回到初始状态

接着点击步过往后走,进行for循环查看添加元素时内部如何运行

再次点击步入查看内部源码

这里是先对我们所添加的元素进行装箱处理,我们仍然暂时先不去探索这类知识点,点击步出回到起始位置,然后再次点击步入发现他来到了我们ArrayList中的add方法

下面来研究add方法干了些什么事情

从源码中我们可以看到add方法并没有直接将传入的值加入到elementData集合中,而是先执行了一个ensureCapacityInternal()方法,我们继续点击步入查看ensureCapacityInternal()的内部执行内容

在 ensureCapacityInternal()中又调用了两个方法

我们接着点击步入鼠标左键点击calculateCapacity进入方法内部

在这里我们可以看到,当elementData为空数组时,calculateCapacity会让我们传入的mincapacity与DEFAULT_CAPACITY(也就是10)进行对比,并返回其中最大的那个数返回。

也就是说calculateCapacity的目的是为了确定这个数组的最小容量至少得有多大,初始扩容为10;

接着点击步过回到 ensureCapacityInternal(),然后点击步入去查看ensureExplicitCapacity()

这里的modCount是用来记录集合被修改次数的参数,目的是为了防止有多个线程同时对其进行修改,了解即可。

接着点击步过来到核心代码

这里的意思是:当我们的实际上需要的最小容量minCapacity比数组elementData的容量要大时,即数组的容量不足以塞下所有元素时,便会执行grow对数组elementData进行扩容。

点击步过再点击步入进入grow方法内部

 这里比较复杂,我们通过代码注释进行分析

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//首先将elementData数组当前的容量赋值给oldCapacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);//通过位运算将oldCapacity的1.5倍赋值给newCapacity
        if (newCapacity - minCapacity < 0)//这里的判断十分巧妙,它使得我们能初始的数组容量并不是直接按照1.5倍进行扩容,而是先直接将我们前面确定的最小容量10又重新赋值给了newCapacity然后在后面进行扩容
            newCapacity = minCapacity;
        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);
    }

我们一直点击步过,直到执行完Arrays.copyOf(),则我们此次的扩容完成。

后面的元素添加源码流程也是如此,大家可以自己试一试

通过对这些流程阅读我们得出第二个结论:

2.当创建ArrayList对象时,如果使用的时无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需再次扩容,则扩容elementData为1.5倍

下面我们再接着来看看ArrayList的有参构造的内部是如何完成的(有参构造除了初始创建时候内部源码不同,其他的基本和无参构造区别不大,快速过一遍)

点击步入

在这里进行初始化,如果有参构造传入参数为0,则和无参构造一模一样,否则就按照传入的参数大小对elementData数组的容量进行初始化,后续扩容机制与无参构造区别不大。

结论:

3.如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

List接口的实现类(2)——Vector

Vector的基本介绍:

1.Vector底层也是一个对象数组,protected Object[] elementData;

2.Vector是线程同步的,即Vector线程安全,因为Vector的操作方法都带有synchronized关键字修饰,开发中如果需亚奥线程同步安全时可以考虑使用Vector

Vector创建和扩容源码分析:

点击步入 

这里通过无参构造中的this方法调用了有参构造方法且有参构造的参数为10。

点击步入来到有参构造

再点击步入进入另一个有参构造 

 在这里实现了对于elementData的容量初始化,且初次容量大小为10

下面步出回到创建Vector的这里,然后点步过来到给数组增加元素这里

 点击步入查看添加元素时的内部操作

又是先进行装箱,不去管他,点击步出回来再点击步入 观察添加细节

步过ensureCapacityHelper方法后步入去观察方法内部代码

我们发现由于我们的数组大小已经扩容至10,而我们现在添加的元素个数还达不到需要数组继续扩容的地步,因此无法观察grow方法内部。

下面我们直接查看扩容的内部源码:

我们给Vector添加10个元素之后再去下断点查看添加第11一个元素会怎样

点击步入

 步过ensureCapacityHelper方法后步入去观察方法内部代码

继续步入到grow方法中

 

可以看到,这里的Vector是对数组容量进行2倍扩容处理,其他的内容和ArrayList源码基本一致。

总结:

实现类底层结构线程安全,效率扩容方式
ArrayList可变数组不安全,效率高

如果是有参构造则按1.5倍扩容

如果是无参构造

则第一次扩容为10,

后面开始按1.5倍扩容

Vector可变数组安全,效率较低

如果是无参,默认10,满后,按照2倍扩容

如果是有参指定大小,则每次都按2倍扩容

List接口的实现类(3)——LinkedList

LinkedList基本介绍:

1.LinkedList底层实现了双向链表和双端队列特点;

2.可以添加任意元素(元素可重复),包括null;

3.线程不安全,没有实现同步;

4.LinkedList的添加和删除是通过链表来完成的,效率会比数组更高

LinkedList底层结构和源码分析

 我们先来通过无参构造创建一个LinkedList对象来查看其创建过程

点击步入

我们发现LinkedList的无参构造中没有任何操作,单纯创建了一个LinkedList对象

因此我们再点击几次步过就回到了初始代码部分。并且此时就创建好了一个LinkedList对象

 下面我们给LinkedList添加元素来查看其添加元素时的源码

点击步入

这时我们发现系统调用了LinkedList的add方法来完成元素的添加操作

继续步入查看linkLast()内部源码 

这里的last和frist均为LinkedList里面的成员属性,且类型为Node<E>类型

Node<E>类中有如下元素,next指向下一个节点,prev指向前一个节点(双向链表)

当我们添加第一个元素时,由于LinkedList中此前并没有元素,因此他的frist指针和last指针实际上都指向null;

所以final Node<E> l = last;表示l也指向了last指向的null

然后执行final Node<E> newNode = new Node<>(l, e, null);

我们步入进去

可以看到就是将创建了一个值为e,prev节点为l ,next节点为null的新Node节点

然后将执行last = newNode;

表示last指针指向新创建的这个newNode节点

继续往下执行,由于此时的l确实为null,因此first也指向了newNode这个新节点

此时的数据结构是一个这样的

全都指向了新添加的这个节点,这样此次添加过程就结束了

我们再来看在LinkedList中有数据的情况下再添加元素是怎样操作的

我们来到linkedList.add(2);这里点击步入

这里先让节点 l 指向last所指向的节点

然后新创建一个节点newNode,这个新节点的prev指向的是之前已经创建好的那个节点

next指向为空

然后让last指向这个新节点

然后因为 l 此时并不指向空,而是指向着以前创建好的节点,因此让 l 的next指向这个新节点,完成连接

此时的结构是这样的

 这就完成了添加新节点的操作

后面的LinkedList的remove操作都是链表操作,大家学好数据结构即可看懂源码

Collection接口的子接口(2)——Set接口

Set接口的基本介绍:

1.Set接口的实现类无序(即添加和取出的顺序不一致),没有索引

2.Set接口的实现类中不允许有重复元素,所以最多包好一个null

3.Set接口的重要实现类有:HashSet和TreeSet

注意:Set接口的遍历方式中,由于其没有索引,因此只能通过迭代器和增强for循环来遍历获取元素,无法通过索引遍历获取

Set接口的实现类(1)——HashSet

HashSet的基本介绍:

1.HashSet实际上是一个HashMap,源码如下

2.HashSet可以存放null值,但是只能有一个null

3.HashSet不保证元素是有序的,取决于hash后,在确定索引的结果

4.不能有重复元素/对象

HashSet扩容机制源码分析

1.HashSet底层是HashMap

2.添加一个元素时,先得到hash值,然后转换成索引值

3.找到存储数据表table,看索引位置是否已经存放有元素

4.如果没有则直接加入

5.如果索引位置已经有元素,则调用equals比较,如果相同,就放弃添加,如果不同,则将元素添加到最后

6.在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),就会进行树化(红黑树)

HashSet底层结构和源码分析

我们步入构造器中发现其无参构造实际上是构建了一个HashMap

继续步入HashMap中,我们能发现在HashMap的构造器中构造了一个具有默认初始容量(16)和默认负载因子(0.75)的空HashMap

 然后跟着顺序在步过几步回到原点往下走,去观察其添加元素时的源码

可以看到在add方法中调用了HashMap的一个put方法,我们步入进去看一看

这里的key就是我们添加进集合的元素,value是我们在上一步调用put方法时传入的PREESENT,这个PRESENT其实是一个静态的Object对象,主要是是为了起到占位的作用,让单列集合Hashset使用到双列HashMap

我们先去看hash(key)这个方法做了什么,Ctrl+左键点击hash进入

可以看到 hash(key)方法是为了得到key对应的hash值,但是hash(key)得到的hash值并不与key的hashCode相同,因为它还做了一个位运算的处理以避免hashCode的碰撞

下面我们接着步入去观察最核心的部分,即putVal()方法干了什么事情

代码太长截图不完整,直接复制下来

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                Node<K,V>[] tab; Node<K,V> p; int n, i; //定义了辅助变量
                //table 就是 HashMap 的一个数组,类型是 Node[]
                //if 语句表示如果当前table 是null, 或者 大小=0
                //就是第一次扩容,到16个空间.
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;

                //(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
                //并把这个位置的对象,赋给 p
                //(2)判断p 是否为null
                //(2.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node (key="java",value=PRESENT)
                //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)

                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                    //一个开发技巧提示: 在需要局部变量(辅助变量)时候,在创建
                    Node<K,V> e; K k; //
                    //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
                    //并且满足 下面两个条件之一:
                    //(1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象
                    //(2)  p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同
                    //就不能加入
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    //再判断 p 是不是一颗红黑树,
                    //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else {//如果table对应索引位置,已经是一个链表, 就使用for循环比较
                          //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                          //    注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
                          //    , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                          //    注意,在转成红黑树时,要进行判断, 判断条件
                          //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                          //            resize();
                          //    如果上面条件成立,先table扩容.
                          //    只有上面条件不成立时,才进行转成红黑树
                          //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break

                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) {
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
                                    treeifyBin(tab, hash);
                                break;
                            }
                            if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break;
                            p = e;
                        }
                    }
                    if (e != null) { // existing mapping for key
                        V oldValue = e.value;
                        if (!onlyIfAbsent || oldValue == null)
                            e.value = value;
                        afterNodeAccess(e);
                        return oldValue;
                    }
                }
                ++modCount;
                //size 就是我们每加入一个结点Node(k,v,h,next), size++
                if (++size > threshold)
                    resize();//扩容
                afterNodeInsertion(evict);
                return null;
            }

由于我们添加第一个元素时table还为空,因此执行如下语句

 可以看到,系统执行了resize方法并将其赋值给了tab后求了长度再赋值给了n

我们点进resize看看resize做了什么

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;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
    }

我们往下执行,会发现系统最后会执行这部分代码

在这里,系统将新集合的容量扩容为了16,并且紧接着就定义了一个临界值,并且赋值为

DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY

这样做的意义是为了保证在集合容量达到0.75倍时,就要开始扩容,以避免当突然有大量线程来添加时,发生阻塞

然后执行完这三段代码后,就完成了加载因子的初始化和数组table的扩容

我们的resize方法就结束了,即resize主要的作用就是扩容我们的集合容量

接着往下走,执行下面这个if语句

这段代码的目的如下:

(1)根据key,得到hash去计算该key应该存放到table表的哪个索引位置并把这个位置的对象,赋给 p
(2)判断p 是否为null
(2.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node (key="java",value=PRESENT)就放在该位置 tab[i] = newNode(hash, key, value, null)

(2.2)如果不为空就执行else语句所包含的代码,后面讲

继续向下执行

如果++过后的size大于12,则进入resize方法继续扩容

最后返回null

说明:

(1)这里的afterNodeInsertion(evict);方法其实运用的是模板方法思想,是HashMap为了留给子类实现的用来增加功能的

(2)这里返回null才能保证最后我们在这里操作成功,即返回null就说明添加成功了

 接着看添加第二个元素时的源码,前面部分基本相同,不同的在于putVal方法执行的代码不一样

这里代码比较简单就不做分析,我们接着看当添加相同元素时的源码

步入进去会发现,系统执行这部分源码

                    Node<K,V> e; K k; //
                    //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
                    //并且满足 下面两个条件之一:
                    //(1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象
                    //(2)  p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同
                    //就不能加入
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    //再判断 p 是不是一颗红黑树,
                    //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else {//如果table对应索引位置,已经是一个链表, 就使用for循环比较
                          //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                          //    注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
                          //    , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                          //    注意,在转成红黑树时,要进行判断, 判断条件
                          //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                          //            resize();
                          //    如果上面条件成立,先table扩容.
                          //    只有上面条件不成立时,才进行转成红黑树
                          //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) {
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
                                    treeifyBin(tab, hash);
                                break;
                            }
                            if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break;
                            p = e;
                        }
                    }

 e指针和p指针的运动轨迹如下

HashSet扩容和转成红黑树机制

 1.HashSet底层是HashMap, 第一次添加时,table 数组扩容到 16, 临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12

2.如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32, 新的临界值就是 32*0.75 = 24, 依次类推

3.在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ), 并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树), 否则仍然采用数组扩容机制

Map

Map接口实现类的特点一(JDK8中的Map接口特点)

1.Map和Collection并列存在,用于保存具有映射关系的数据:Key-Value

2.Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中

3.Map中的key不允许重复,原因和HashSet一样

4.Map中的value可以重复

5.Map的key可以为null,value也可以为null,注意key只能有一个为null,value可以多个为null

6.常用String类作为Map的key

7.key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value

8.Map存放数据的每对key-value是放在一个HashMAp$Node中的,又因为Node实现了Entry接口,因此也可以说一对key-value就是一个Entry

Map接口实现类的特点二(JDK8中的Map接口特点)

1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)

2. k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合 ,该集合存放的元素的类型 Entry, 而一个Entry对象就有k,v EntrySet<Entry<K,V>> 即: transient Set<Map.Entry<K,V>> entrySet;

3. entrySet 中, 定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap$Node,这是因为 static class Node<K,V> 实现了 Map.Entry<K,V>接口

4. 当把 HashMap$Node 对象 存放到 entrySet (entrySet指向Node中的key-value)就方便我们的遍历, 因为 Map.Entry 提供了重要方法 K getKey(); V getValue();

Map的实现类(1)——HashMap

HashMap底层机制

1.HashMap底层维护了Node类型的数组table,默认为null

2.当创建对象时,将加载因子初始化为0.75

3.当添加key-value时,通过key的hash值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相比较是否相等,如果相等,则直接替换value;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容

4.第1次添加时,需要扩容table容量为16,临界值为12

5.以后再扩容,则需要扩容table容量为原来的2倍,临界值也为原来的2倍,依此类推

6.在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认时8),就会进行树化(红黑树)

1.HashMap是Map接口使用频率最高的实现类

2.HashMap是以key-value对的方式来存储数据

3.key不能重复,但是值可以重复,允许使用null键和null值

4.如果添加相同的key,则会覆盖原来的key-value,等同于修改(只会修改value)

5.与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。

6.HashMap没有实现同步,因此线程不安全

Map的实现类(2)——Hashtable

Hashtable的基本介绍

1.Hashtable存放的也是键值对

2.Hashtable的键和值都不能为null,否则会抛出空指针异常

3.Hashtable使用方法基本上和HashMap一样

4.Hashtable是线程安全的,HashMap是线程不安全的

Hashtable底层结构

1.底层有数组 Hashtable$Entry[] 初始化大小为 11

2. 临界值 threshold 8 = 11 * 0.75

3. 扩容: 按照自己的扩容机制来进行即可

4. 执行 方法 addEntry(hash, key, value, index); 添加K-V 封装到Entry 

5. 当 if (count >= threshold) 满足时,就进行扩容(执行rehash()方法)

6. 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小小西瓜呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值