集合基础增强原理分析

List集合基础增强底层源码分析

集合分为三个系列,分别为:List、set、map

List系列

特点:元素有序可重复

有序指的是元素的添加顺序,也就是说,元素被第一个存进去的时候,它就在第一位,这就是list集合的元素添加顺序;

通常情况下我们所说的有序有两个概念,第一个是添加顺序,第二个是大小顺序(实际上就是元素值的大小)

List下面重点关注两个实现类分别是:

ArrayList LinkedList

ArrayList 

ArrayList底层实现是数组,既然是数组,那它就必然有数组的特点:查找快,增删慢;

这里简单解释一下:

首先我们建立一个数组,一旦建立一个数组,那么程序就会在内存中开辟一个连续的内存存储空间,并且它是有下标的,从0开始,一旦定义长度就无法发生改变

现在,假设我们往数组里面存值,当然是你定义什么类型,你就存什么类型的值进去,如果现在想获取,我们直接通过下标就可以进行获取了,但是我删除元素的时候,是怎么删除的呢?

数组删除过程

假设我定义一个数组如下图:

红框内,代表我存的值,黑线上方则是他们值对应的下标

假设我现在想删除c这个元素,这个时候d就会向前移动e也会移到d的位置f也会移到e的位置上,结果后面会空出来的那个就被删掉了,结果就成了?下面这张图B-1:

这个就是数组的删除过程。

数组添加过程

刚讲了删除过程,现在我们就来看看数组的添加过程:

添加元素首先会给即将进来的这个元素分配一个空间在如图B-1f后面,如果你想添加到如图B-1下标为1的空间里,那么它就会开始依次向后移,b占领d的位置d占领e的位置f占领后面新开辟空间的位置上

这就是它的增加与删除过程,所以它增删慢,查询快;

那既然我刚讲了,数组一旦定义,长度不能发生改变,那么在咱们List集合底层是数组实现的,你List集合定义的时候你给过长度吗?很显然并没有给长度,那就可以无限添加元素,你填100个也行,你添10k也无所谓,既然数组长度不变,你说它底层怎么搞的呢?接下来我们就定义一个数组并且查看它的底层代码。

List集合底层源码详解

我用的是IDEA,按两下Shift键输入ArrayList回车即可查看源码,首先刚进来我们看到的是版本号:

private static final long serialVersionUID = 8683452581122892189L;//版本号

/**
 * Default initial capacity.(默认初始容量,也就说,当我们定义一个List集合没有给他指定长度的时候,默认长度就是10)
 */
private static final int DEFAULT_CAPACITY = 10;
那如果我添加11元素的时候怎么办?

答:扩容,下面我会详讲扩容

刚开始建这个集合的时候,默认长度是10
/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;//你现在数组的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度 = oldCapacity + (oldCapacity>> 1)其中 >>符号是右移符,也就是/2的意思,算出来的结果是1.5,所以它扩容的时候是1.5倍1.5倍的扩的
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)(按住 Ctrl键 点进去 MAX_ARRAY_SIZE 就可以查看数组的最大容量,下面我有详讲
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
上面这个源码,就是数组初始化与扩容的方法,也就是说ArrayList里面它数组初始化是在你添加第一个元素的时候,当它扩完容后接下来就是数组的拷贝

你在创建List集合的时候,你也可以给它传一个初始容量,如何操作请继续向下阅读。

在ArrayList集合底层源码里有一个有参构造跟无参构造,它们分别在什么时候使用呢?

首先先看一下有参构造:
/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param  initialCapacity  the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
什么时候用有参构造:当你向集合中添加元素数量比较多的时候,你最好给它指定初始容量,因为这样就可以减少很多次的扩容和元素拷贝。
原理:如果你10000个元素,你让它10个以上扩一次容,扩完容后你再让它连续扩容必然会造成元素拷贝,元素拷贝它的性能就高不了,这样如此反复的话必然会拉低它的性能,如果你元素过多的话,你给它指定初始容量,这样我就能减少了很多次扩容和元素拷贝,从而提升性能(在一定程度上提升性能),
当元素比较少的时候,你就可以默认不指定初始容量
ArrayList容量有上限吗?
元素是有上限的,它的容量是Integer的最大值,在源码里面可以清楚的看到它的设定:
/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;(写的很清楚,它的大小不能超过Integer的最大值(21亿))

我们可以看到它减了一个8,因为到后期就已经不需要扩容了,最后几个元素它是一个一个添加的,添加一个扩一个容,添加一个扩一个容。

值得一提的是,当它的容量超过10亿后,性能就有所衰减,这个实验是有人做过的,也被证实了,10亿之前它的性能都表现良好

List-LinkedList

首先,LinkedList底层是一个链表结构,并且是双向链表;

 增删快 、查询慢 

并分为 单向链表跟双向链表

单向链表

单向链表,每个元素都称之为一个节点,每个节点都由两部分组成分别是,数据 、指向下一个节点的指针;

单向链表每一个节点在内存中存储上、空间位置上、都是无无序的;

链表查询效率较低

单向链表中的每个元素在空间的存储位置上没有规律,也没有顺序,那么在查找某个元素的时候,必须从头节点挨着往后找,直到找到为止;

链表增 删 效率高

因为链表每个元素在存储的空间是没有顺序的,,删除或者添加某个元素,只需要让指针重新指向即可,不需要将其他元素位移。所以随机增删效率较高

双向链表

双向链表的查找方式是交替查找,就是左表查找一个,右边查找一个,最终左边跟右边谁先返回,那么谁就先找到;

双向链表就是双向开工,最后离谁近我返回谁,说白就是谁查的次数最少,我返回谁;

LinkedList底层讲解

那么,链表结构在底层保存的是什么结构呢?

Node(节点),它底层保存的是Node,一个节点一个节点的;

我们点进源码后我们开始进行分析:

1

transient int size = 0;

  我们点进源码后,看到的第一个属性就是 size =0;

这句话的意思就是,LinkedList初始值是0,也就是,你没有给它任何数据的时候,它的长度为0;

再往下看:

1

2

3

4

5

6

7

transient Node<E> first;//代表第一个节点

 

 /**

  * Pointer to last node.

  * Invariant: (first == null && last == null) ||

  *            (last.next == null && last.item != null)

  */

  我们看到了Node,Node这个对象,就代表链表中的每一个元素;

我们点进去看一下,Node里面有什么:

1

2

3

4

5

6

7

8

9

10

11

private static class Node<E> {

       E item;

       Node<E> next;

       Node<E> prev;

 

       Node(Node<E> prev, E element, Node<E> next) {

           this.item = element;

           this.next = next;

           this.prev = prev;

       }

   }

  我们点进去后,第一行有一个E,这个E是干什么的呢?

这个E就代表,本节点的信息,比如说你这里插入的数据的类型都是String类型,那么这个节点的类型就是String,另外一个节点可能是int类型,double类型,那么节点类型也就不同,所以这个E是个泛型;

再往下走,有一个next跟一个prev;

next代表下一个,prev代表前一个;

为什么会有这两个呢,是因为便于双向查找的时候能够找到对方;

    transient Node<E> last;//代表最后一个节点

    /**
     * Constructs an empty list.
     */

我们看到在底层源码中,还有一个代表最后一个节点的方法,我们发现,两个方法分别声明,为什么不写在一起呢?

因为,最后一个节点的信息,跟中间的信息保存的不一样!

第一个节点只需要保存下一个节点,而最后一个节点只需要保存上一个节点;

总结:

我需要知道的是LinkedList是一个链表结构,链表结构的特点是查询慢 增删快;

还有链表结构的每一个元素都是一个Node(节点),而Node的底层,就是一个双向链表;

每个Node都会存储三个信息,prev item next;

Set集合

set集合特点:

无序、不重复;

这里的无序是指,没有添加顺序;

它有两个实现类,分别是 HashSet、TreeSet

首先我们先关注HashSet;

HashSet

创建一个HashSet集合:

Set<泛型> 对象名 = new HashSet<String>();

添加元素:

1

对象名.add("");

下面我们就做一些例子来更好的讲一下HashSet集合;

1.创建一个HashSet赋值,并用增强for循环打印,添加相同元素观察状态;

Set<String> set01 = new HashSet<String>();

set.add("hh");

set.add("aa");

set.add("cc");

set.add("hh");

for(String str : set01){

syso(str);

}

执行结果我们会发现,我们明明添加进去了两个,为什么却值打印出来一个hh?

这就是HashSet的特性,值不能重复;

那么它是如何做到的呢,它是如何保证元素不重复的?

我们现在可以假设一下HashSet的底层是什么,我们假设它底层是一个数组,那么,数组是如何做到元素不重复的呢?

是不是要从头开始遍历,直到遍历结束后发现这个元素没有出现过,那么就表明这个元素确实为唯一未重复的;

但是,如果数组的长度非常长,这个时候,你这样的方法,还能行得通吗?当然不行了,因为性能太低了!

它的底层确实是数组,但是,缺不是这样的遍历方式,而是hash算法;

所有元素存储的时候,存的是hashcode的元素值,那个这个值是可以当成它索引;

hash算法

*任何一种hashcode的算法都无法达到绝对的完美*

*必然会存在hashcode值的冲突*

假如我现在要添加一个元素,加进来之后首先会计算这个值的hashcode值,如果这个hashcode值 = 50,那我就把你这个元素存到下标为50的数组的对应位置上;

这个时候又假设又存进来一个“bb”,首先第一步还是需要先计算它的hashcode值,假设bb的hashcode值 = 25,那么,它就会被分配到下标为25的数组里;

这个时候我又存进来了一个元素“cc”,当你插入cc元素的时候,首先还是需要先算一个你这个元素的hashcode值,假设这个cc还等与25,那么这个cc是不是也要去找相对应的下标为25的位置了,但是发现,这个25这个位置已经被bb占用了,这个时候就会触发底层的equals方法进行内容比较,如果内容相同,则不让你插入,如果不同,那么就会以列表的方式进行插入,就是挂在“bb”的下面;

在java1.5的时候,以上这个结构被称之为,hashset桶表+链表;

在java1.8点时候,以上转增结构被称之为,桶表+链表+二叉树;

为什么要加二叉树?

假设有许多hashcode的值 = 25,那么你是不是就需要一直的往下挂呀;

大家都知道链表是有缺陷的,就是查找慢!那你查的时候,是不是就是从第一个开始遍历,去寻找啊,假设这个元素刚好在链表的最末端,那么你需要查多久啊;

所以到1.8之后,为了避免这种情况出现,所以它对这个链表做了一个优化,链表深度超过8的时候,就会转化成二叉树

hashset底层代码分析

进去之后,首先第一句话:

1

2

3

private transient HashMap<E,Object> map;

 

// Dummy value to associate with an Object in the backing Map

  看到这个map,就说明set跟map是有一定关系的,你说白了set的底层实现,事实上就是map的底层实现;

看一下set的底层构造方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

public HashSet() {

    map = new HashMap<>();

}

 

/**

 * Constructs a new set containing the elements in the specified

 * collection.  The <tt>HashMap</tt> is created with default load factor

 * (0.75) and an initial capacity sufficient to contain the elements in

 * the specified collection.

 *

 * @param c the collection whose elements are to be placed into this set

 * @throws NullPointerException if the specified collection is null

 */

  是HashMap;

所以点进去后你会发现,最后还是到了HashMap的底层里面去了;

首先看第一行:

1

2

3

4

/**

  * The default initial capacity - MUST be a power of two.

  */

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

  默认长度是 16;

再往下走:

1

2

3

4

5

6

/**

 * The maximum capacity, used if a higher value is implicitly specified

 * by either of the constructors with arguments.

 * MUST be a power of two <= 1<<30.

 */

static final int MAXIMUM_CAPACITY = 1 << 30;

  这个是最大容量,int的最大值/2;10亿左右

再往下:

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

这个是默认加载因子;

也就是16元素,到第16*0.75(12);

也就是到达12个元素即将到达第13个元素的时候,它就开始扩容,并以二倍的速度开始扩的;

再往下看:

 /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

这个就是树结构控制;

每个链表达到8之后,就开始自动转化为二叉树结构;

什么是时候会触发链表啊?

hash算法相同的时候会把值相同的放到同一个链表上;

转载请注明出处和署名,谢谢!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值