本文是我个人对ArrayList集合、HashSet集合、HashMap、Hashtable、ConcurrentHashMap这几个集合的理解,也希望能够帮助到看了此文章的你,如有不当之处,还望不吝赐教。
ArrayList:
成员变量:
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;//初始容量
private static final Object[] EMPTY_ELEMENTDATA = {};//空对象实例
//一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; //用于存储元素的数组,不参与序列化
private int size;//数组长度
-
首先创建这个集合的时候,如果指定初始长度会先判断长度是否大于0,如果大于0就创建一个object[长度] 赋值给elementData 它就是存储整个集合 否则就报一个一个异常 并且将类中的一个静态常量 值是空的object数组赋值给elementData
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else { if (initialCapacity != 0) { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } this.elementData = EMPTY_ELEMENTDATA; } }
-
上面是有参构造,如果创建集合的时候是无参的,也会将另一个静态常量 长度为0的object数组 赋值给elementData
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //Object[0]; }
-
第三种就是构造的时候传的参数是一个Collection集合,会直接会这个形参调用toArray()方法赋值给elementData;这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy 做深拷贝 为size赋值 判断形参传递过来的collection集合是否是一个空的集合,如果是则还是把静态成员变量一个空的object数组在赋值给elementData,如果不为空 则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
public ArrayList(Collection<? extends E> c) { this.elementData = c.toArray(); if ((this.size = this.elementData.length) != 0) { if (this.elementData.getClass() != Object[].class) { this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class); } } else { this.elementData = EMPTY_ELEMENTDATA; } }
-
上面就是list集合是三种构造方法,接下来的添加元素以及扩容的问题,第一次添加元素的时候,比较的是size+1 和当前的最大容量比较,所以第一次添加元素的时候会将当前elementData的长度赋值为10,也是静态常量。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
size+1 和 当前数组长度 进行比较,数组长度大,就允许添加,将新元素添加到位于size的位置上。否则当前容量会进行扩容,当前容量加上当前容量右移一位。如果扩充的长度小于与所需的最小长度,则长度变为数组所需的最小长度;如果扩充的长度大于MAX_ARRAY_SIZE,则调用hugeCapacity()方法来获得 最小长度minCapacity和MAX_ARRAY_SIZE 的小值 在进行copyOf方法
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) 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); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
HashSet
HashSet底层由HashMap实现,HashSet的值存放于HashMap的key位置上,HashMap的value统一为present
HashMap
HashMap的默认长度是16,默认负载因子是0.75 ,每一次添加 首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,尾插法 。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍+1,把原链表数组的搬移到新的数组中
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。当一个链的长度变为了6又会将红黑树变为链表。6与8直接空了一个7,这样的好处是避免长度一直在7和8之间来回切换从而频繁进行链表与红黑树的转换。那为什么是8的时候进行链表转红黑树呢?这是在jdk底层源码中可以发现,jdk的开发人员使用泊松分布列举出了1到8具体值,其实在为8的时候概率就已经很小了,再往后调整并没有很大意义。
加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低
新的理解:
初始值:16
负载因子:0.75
- 调用put()方法
- hash(key) & (当前容量-1) 进行位运行 得到该键值对存放在数组中的位置
- 是否hash冲突,如果在数组的这个位置没有产生hash冲突就直接存储
- 检查key是否相同,如果相同则执行替换并返回旧值的操作
- 如果key不相同则进行判断 红黑树/链表
- 如果是红黑树则进行红黑树的添加逻辑
- 如果是链表,会使用一个for循环,找到链表的最后一个null,然后存值
- 在进行循环遍历链表时如果发现了key相同,也会进行替换并返回旧值的操作
- 在进行遍历时也会进行判断当前链表是否会扩容或转换红黑树,转换红黑树的条件是链表长度>8并且数组长度>64,如果只满足其中一个条件只会进行扩容
扩容的步骤:
10. 当前容量不能超过2^30次方
11.当前容量<1 左移一位
源码中有一个size变量来记录已经存储了多少个数据,当进行put()方法后 ++size > 当前总容量*负载因子 就会进行扩容。
Hashtable
hashtable和hashmap其实没什么区别,只是它的底层方法都加了synchronized,是线程安全的,它的初始容量是11,负载因子也是0.75,put() 添加元素的时候首先判断value是否为空 如果的就报空指针异常,然后在通过key的hashCode() 得到哈希值 确保当前key在集合中没有重复的,这个时候如果添加元素的key是空,null.hashCode() 也会报空指针异常。
如果当前集合的存储个数>= 了集合的总容量*负载因子,这时候就会进行扩容。扩容机制也是当前容量左移一位 + 1;
ConcurrentHashMap
在jdk1.5的时候出现的,正是因为Hashtable的关系到存取的每个方法都加了synchronized关键字,所以在高并发的环境下效率很慢,然后就出现了ConcurrentHashMap,它的底层实现和HashMap息息相关,它内部有两个静态内部类:HashEnter和 Sngment 。HashEnter存储的是集合元素的键值对映射关系,Sngment是负责锁的。ConcurrentHashMap默认会分为16段锁,这也正是因为HashMap的默认初始化容量是16,锁其实就是锁的HashMap的每一条链表。
ConcurrentHashMap默认分成了16个segment,每个Segment都对应一个 Hash表,且都有独立的锁。所以这样就可以每个线程访问一个Segment,就可以并行访问了, 从而提高了效率。这就是锁分段。但是,java 8 又更新了,不再采用锁分段机制,也采用CAS算 法了。