一、list
1。list集合的特点
它是一个元素存取有序的集合。简单来说就是队列方式存取。
它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
增删慢,查找快。原因:集合数据存储的结构是数组结构。
2.list的三个子集分别是:ArrayList,Vector,LinkList
1.ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高。
2.Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率高。
3.LinkList:底层数据是链表,查询慢,增删快,线程不安全,效率高。
3.常用的方法
public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
public E get(int index):返回集合中指定位置的元素。
public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
1.ArrayList
ArrayList 底层是基于数组来实现容量大小动态变化的。所以增删慢,查找快。
java.util.ArrayList 是大小可变的数组的实现,存储在内的数据称为元素。此类提供一些方法来操作内部存储 的元素。 ArrayList 中可不断添加元素,其大小也自动增长。
构造方法:public ArrayList() :构造一个内容为空的集合。
ArrayList list = new ArrayList<>();
方法:
public boolean add(E e) :将指定的元素添加到此集合的尾部。
public E remove(int index) :移除此集合中指定位置上的元素。返回被删除的元素。
public E get(int index) :返回此集合中指定位置上的元素。返回获取的元素。
public int size() :返回此集合中的元素数。遍历集合时,可以控制索引范围,防止越界。
2.LinkList
LinkedList是一个双向链表。增删快,查询慢。
所以他的方法涉及了很多头尾删除方法。
方法:
public void addFirst(E e):将指定元素插入此列表的开头。
public void addLast(E e):将指定元素添加到此列表的结尾。
public E getFirst():返回此列表的第一个元素。
public E getLast():返回此列表的最后一个元素。
public E removeFirst():移除并返回此列表的第一个元素。
public E removeLast():移除并返回此列表的最后一个元素。
public E pop():从此列表所表示的堆栈处弹出一个元素。
public void push(E e):将元素推入此列表所表示的堆栈。
public boolean isEmpty():如果列表不包含元素,则返回true。
3.Vector
Vector 可实现自动增长的对象数组。
对于预先不知或者不愿预先定义数组大小,并且需要频繁地进行查找,插入,删除工作的情况,可以考虑使用向量类。
向量类提供了三种构造方法:
public vector()
public vector(int initialcapacity,int capacityIncrement)
public vector(int initialcapacity)
使用第一种方法系统会自动对向量进行管理,若使用后两种方法,则系统将根据参数,initialcapacity设定向量对象的容量(即向量对象可存储数据的大小),当真正存放的数据个数超过容量时。系统会扩充向量对象存储容量。
二.queue队列
1.特点:先进先出,新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
2.实现形式
(一)、单队列:
移除头部的元素时,不会将后面的下标往前移动,如果插入数据超过了定义长度,此时,数组就会出现“假溢出”的现象,尾指针指向了一个不存在的下标,如果要解决这种情况,一般有两种方法:
1、无限扩充数组容量;
2、使用循环队列。
(二)、循环队列
当尾指针指向了一个不存在的下标时,即超过数组大小时,此时我们判断数组头部是否有空余的空间,如果有就把尾指针指向头部空余的空间,循环队列就是将单队列的头尾相连,形成一个圆形,这样就不会出现下标溢出的现象(distruptor实现)。
3.子类
1、Queue接口继承自Collection接口;
2、Queue接口分别有Deque子接口和AbstractQueue抽象类;
3、Deque子接口分别有LinkedList类和ArrayDeque类;
4、AbstractQueue抽象类有PriorityQueue实现类
4.方法
1.add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
2.remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
3.element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
4.offer 添加一个元素并返回true 如果队列已满,则返回false
5.poll 移除并返问队列头部的元素 如果队列为空,则返回null
6.peek 返回队列头部的元素 如果队列为空,则返回null
7.put 添加一个元素 如果队列满,则阻塞
8.take 移除并返回队列头部的元素 如果队列为空,则阻塞
三、Stack
1.特点:Stack即栈操作,采用的是一种先进后出的数据结构形式,即在栈中最早保存的数据最后才会取出,而最后保存的数据可以最先取出java.util.Stack 类是Vector的子类。
2.方法:
1 Object E push(E item) 数据入栈
2 Object E pop() 数据出栈,如果栈中没有数据,则调用此方法会抛出空栈异常(EmptyStackException)
3 boolean empty() 测试堆栈是否为空。
4 Object peek( )查看堆栈顶部的对象,但不从堆栈中移除它。
5 int search(Object element)返回对象在堆栈中的位置,以 1 为基数。
四、Map
1.特点:Map是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复,并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。
2子集:
1.HashMap
HashMap实现了Map 接口,它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全。HashMap中hash数组的默认大小是16,而且一定是2的指数。之所以是2的指数次,是因为计算元素在数组中的索引时,需要和数组长度-1按位与,2的指数次-1都是1111,容易减少hash碰撞;
2.Hashtable:Hashtable承自Dictionary类,并且是线程安全的,使用了synchronized进行方法同步,插入、读取数据都使用了synchronized,任一时间只有一个线程能写Hashtable,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。HashTable中的hash数组初始大小是11,增加的方式是 old*2+1。HashTable直接使用对象的hashCode。而HashMap重新计算hash值
3.LinkedHashMap
HashMap和双向链表合二为一, 非线程安全
4.ConcurrentHashMap 线程安全
1. 在读取数据的时候不需要加锁,因为public V get(Object key)方法不涉及到锁
2. put、remove方法涉及到锁,但是也并一定有锁争用,因为Concurrenthashmap是分段加锁的,如果要获取的对象正好不在同一个段(segment)上,也就不会存在争夺锁的情况,Concurrenthashmap默认有16个段
3. ConcurrentHashMap允许一边更新、一边遍历,也就是说在Iterator对象遍历的时候,ConcurrentHashMap也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式。Iterator是fail-fast迭代器,而enumerator迭代器不是fail-fast的。当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
ConcurrentHashMap插入元素详解
在进行put操作的时候,table数组可能还没有初始化,或者table中容纳的记录数量超过了阈值,前者需要进行table的初始化,而后者需要进行扩容。
首先,计算key的hashCode,然后计算在table中的index(和数组长度-1按位与),如果该位置上的Node为null,说明该位置上还没有记录,则通过调用casTabAt方法将新的记录插入到table的index上,否则,通过synchronized关键字对table的index位置加锁。需要注意的是,当前线程只会锁住table的index的Node,其他位置上没有锁住,所以此时其他线程可以安全的获得其他的table位置来进行操作。这也就提高了ConcurrentHashMap的并发度。
然后根据table的index位置上的第一个节点的hashCode值来判断此处的Node是链表还是红黑树,如果hashCode值小于0,那么就是一颗红黑树,因为ConcurrentHashMap在初始化红黑树时将hashCode设为了-2。如果Node是链表,那么就匹配和当前想要插入的记录是一致的key值。如果匹配到,那么这次put的效果就是replace,否则,将该记录添加到链表中去。
如果是一颗红黑树,那么就通过调用putTreeVal方法来进行插入。在插入操作完成之后,需要判断插入的链表的长度是否超过阈值8,超过则需要将链表结构改为红黑树。
另外,需要判断本次操作是否是更新操作(binCount为1则为更新),如果是更新操作,则不会造成size的变化,否则,就需要更新size,而size的更新涉及到并发环境,所以较为复杂,并且更新size的时候也可能带来table的扩容,这也是较为复杂的。
ConcurrentHashMap扩容
有两个操作会判断是否需要扩容:
1.新增元素后如果所在链表的元素个数达到了阈值8,则会调用treeifyBin方法把链表转换成红黑树,但是在结构转换之前,会对数组长度进行判断:如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。
2.新增元素之后,会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。
扩容步骤
1.计算每个线程可以处理的桶区间。默认 16.
2.初始化临时变量 nextTable,扩容 2 倍。
3.有一个死循环来计算下标。开始扩容。
4.synchronized同步转移桶内数据。如果这个桶是链表,那么就将这个链表的hash根据table的 length 按位与(nodehash & length)拆成两份,“&”结果是 0 的放在新表的低位,取于结果是 1 放在新表的高位;如果这个桶是红黑数,那么也拆成 2 份,方式和链表的方式一样,然后,判断拆分过的树的节点数量,如果数量小于等于 6,改造成链表。反之,继续使用红黑树结构。
在ConcurrentHashMap的扩容时,其他线程写会停止,从而一起参与扩容,读线程在扩容未完成时也会帮助一起扩容。sizeCtl变量为-1 代表table正在初始化,-N 表示有N-1个线程正在进行扩容操作。这其中用到了forwardNode这个结构,扩容时会将forwardNode放入table中,如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。
删除元素
在进行删除的时候需要对table的index位置加锁,ConcurrentHashMap使用synchronized关键字将table中的index位置锁住,然后进行删除,remove方法调用了replaceNode方法来进行实际的操作,而删除操作的步骤首先依然是计算key的hashCode,然后根据hashCode来获取index值,再根据index位置上的Node是链表还是红黑树来使用不同的方法来删除这个记录,删除记录的操作需要进行记录数量的更新