ArrayList:查询快,增删慢,底层使用数组(objects[]和elementData)实现,线程不安全,但是一般情况下都使用这种集合,因为平时的使用主要是从里面查数据,一般只有第一次会往里面放一次数据,之后不会有频繁的插入操作,一般都是查询所以,日常用的最多的还是ArrayList
ArrayList在JDK1.8之后初始化时,会根据构造函数是否有入参来进行初始化,如果是有参构造器,则会初始化数组的大小和集合的底层数组容量,初始化为入参值的大小,如果是默认的构造器,则只会初始化集合的大小为默认的10(10是因为这里面定义的初始化默认大小就是10) ,不会初始化集合的底层数组容量,只有当第一次往里面插入值的时候,会初始化集合的底层数组容量,JDK1.8之前的无参构造器初始化时就会初始化底层数组容量为10。所以在默认构造器初始化数组之后必须先使用add先添加,不能直接使用set进行指定位置插入,因为,在这种情况下,list的size其实还是0
扩容:由于数组的容量在初始化的时候就已经固定了,所以集合的底层数组的容量在初始化时也是固定的,而集合是允许源源不断的向其中插入数据的,所以在插入数据超过了底层数组的容量大小时,他会自动扩容,默认的扩容机制就是(原数组容量为T)新初始化一个(T+T/2)大小的数组,然后将原数组的值COPY到新数组中,将集合的地址指向新数组,销毁旧的数组。
新增:而ArrayList新增和删除较慢的原因就在这里,当他需要新增一个元素时,会先对集合的底层数组容量进行判断,看看是否达到了极限,如果是的话,需要进行扩容,如果未达到极限,在未指定位置新增时,会插入到数组的末尾,如果是指定位置新增的话,他会复制一个新数组,将指定的index值位置之后的值复制到index+1位置开始,然后将新增的值放入到index位置,最后将集合的索引指向新的数组
删除:同理删除也是这样,在删除时,会先COPY一个新数组将需要删除位置之后的值COPY到删除位置进行覆盖操作,这样的话,删除的点,离数组的末端越远就越慢
数组适合用来做队列而集合不适合
在遍历的情况下,ArrayList的效率比LinkedList的快许多,因为ArrayList的底层是数组,他所持有的是一块连续的内存空间,而LInkedList底层的链表的内存空间是不连续的内存片段,在遍历是还需要通过内存地址进行寻找,大大增加了内存开销,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销
HashMap的底层是有数组和链表共同组成的,数组中的每个索引位都存储了KEY-VALUE格式数据,这个在JDK7中也叫作Entry,在8中也叫作Node
因为其本身所在的位置都为Null,所以在插入数据的时候回去判断一下,这个数据的key的hash值去计算插入的索引值,而我们数组中的容量是有限的,而且当我们使用hash时,其实我们的KEY的hash值是有一定的几率重复的,当我们出现重复的hash值时,则将在数组当前的索引位中,将重复hash的KEY的ENTRY插入到链表的下一个节点,在JDK8之前这个值插入的位置就是从链表的头部插入,将原来位置的值都向后顺移一位,而在JDK8之后,开发者认为其实在链表中去取数据的话,其实是会从尾部开始取的,所以又改为默认从尾部将新值插入
因为数组的容量是有限的,所以,我们认为当在数据多次插入,达到一定的容量限制时,会自动进行扩容
而对于扩容的影响有两个因素:
一、Capacity:hashMap的当前的长度
二、LoadFactor:负载因子,默认值为0.75f
就是说当hashmap的容量为10时,如果超过了7,就必须进行扩容了,当为100时,超过了75,就必须扩容了
扩容时会先创建一个新的entry空数组,他的容量默认就是原有容量的两倍大小,然后遍历原有的数组中的entry进行rehash进去
之所以说是重新hash而不是说直接进行copy,是由于在进行扩容之后,数组的容量大小改变了,这种情况下,hash值也会随着变化,原有的hash对应的索引值已经不适用了
索引计算公式:
index = HashCode(Key) & (Length - 1)//这个公式就是说,将KEY的值和数组长度进行二进制与计算一下,得到索引值
从上述公式,我们可以很容易的看出来,如果发生了数组容量变化的话,index的值是会变化的
而我们可以对比一下头插入和尾插入的方式,当我们使用头插入时,如果我们在需要插入时,碰上了需要扩容,而由于扩容时,是需要将entry进行重新hash进去的,这样则会出现一种情况就是,当我们进行hash到新的数组中去时,由于都是头插入方式,会导致原有的链表指针指向了不对应的位置,导致链表中的值位置出现错乱,和出现死循环
而对于JDK8之后新的插入方式,尾部插入的话,其实他的内部是引用了红黑树的结构进行插入的,这样做的话的好处就在于头投插入的话,最后一个节点就会出现在链表的头部,而如果是尾插入的话,那么则是还会按照正常的节点顺序和指针指向下一个正常节点可以保证扩容之后的链表中的节点顺序不变,而且能够保证之前的节点的顺序的引用关系
而且由于hashmap的get和put操作是没有加锁的,这样一来的话,如果将这个容器应用在多线程中的话,还是不安全的
hashmap的初始化容量为16,这是根据内部定义的一个默认的容量公式:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
之所以在这里用上了位运算是为了切合后续插入的entry的KEY的hash的索引值配合使用,因为后续的索引计算公式其实也是通过位运算来获取到插入entry的位置索引的