Day Twenty-Seven
集合的其他内容
Iterator
- Iterator专门为遍历集合而生,集合并没有提供专门的遍历的方法
- Iterator实际上就是迭代器设计模式在Java中的实现
- Iterator的常用的方法:
- boolean hashNext():判断是否存在另一个可访问的元素
- Object next():返回要访问的下一个元素
- void remove():删除上次访问返回的对象
- 哪些集合可以使用Iterator遍历
- Collection、List、Set可以;但是Map不可以,需要先转换成Set之后才可以
- 只要接口或者类提供了iterator()这个方法就可以将元素交给Iterator,进行遍历。
- 实现Iterator接口的集合类都可以使用迭代器遍历
- for-each循环和Iterator的联系
- for-each循环在遍历集合的时候,其底层使用的还是Iterator迭代器
- 凡是可以使用for-each循环进行遍历的集合,也一定能使用Iterator进行遍历
- for-each循环和Iterator的区别
- for-each还能遍历数组,但是Iterator只能遍历集合
- 在使用for-each遍历集合的时候,不能删除元素,会抛并发修改异常ConcurrentModificationException。
- 使用迭代器Iterator进行遍历的时候能够删除元素,但是要使用集合在调用iterator()方法时声明的局部变量,用局部变量来直接调用remove()方法,里面也不用写参数。如果直接使用集合去调用remove方法然后传要删除的值得话,也会报并发修改异常。
- Iterator是一个接口,那么它的实现类在哪?
在相应的集合实现类中,比如说在ArrayList中存在一个内部类 Itr 实现了Iterator。 - 为什么Iterator不设计成一个类,而是一个接口
不同的集合类其底层结构也不相同,迭代的方式也不相同,所以说提供了一个接口,让相应的实现类来实现。
Iterator的原理
- Iterator接口中包含三个基本方法,next(), hasNext(), remove(),其中对于List的遍历删除只能用Iterator的remove方法。
public interface Iterator<E> {
boolean hasNext();
E next();
//Java8的新特性:可以通过default在接口中写个方法的实现
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
- 我们通过ArrayList的Iterator的实现来分析Iterator的原理.
- 在ArrayList里面有一个迭代器方法,这个方法返回的是一个Itr对象,这个对象实现了iterator方法。
public Iterator<E> iterator() {
return new Itr();
}
- 看ArrayList中实现类Itr:我们主要就看hasNext()、next()、remove()这三个主要的方法。
private class Itr implements Iterator<E> {
int cursor; //下一个返回的位置
int lastRet = -1; //当前操作的位置
int expectedModCount = modCount;//这玩意可以理解为版本号,检查List是否有更新
Itr() {}//无参构造
//判断是否有下一个元素
public boolean hasNext() {
return cursor != size;
}
//返回下一个元素
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;//cursor记录的是下一个元素,所以调用next时返回的是cursor对应的元素
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//记录需要返回的元素
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;//记录下一个元素
return (E) elementData[lastRet = i];//返回当前元素
}
//移除元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();//检查是否有更改,remove或者add
try {
ArrayList.this.remove(lastRet);//删除当前元素
cursor = lastRet;//下一个返回的位置指向当前被删除的元素的位置
lastRet = -1;//当前操作的位置
expectedModCount = modCount;//保持版本号一致
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
- 从以上的代码中可以看出,对于Iterator的实现类中主要有cursor,lastRest,expectedModCount这三个变量,其中cursor将记录下一个位置,lastRest记录的是当前的位置,expectedModCount记录没有修改的List的版本号。
- 在上面的时候我们说到List中在iterator遍历的时候,不能随便添加和删除元素,我们来看一看这是为什么。
- 在iterator遍历的时候抛出的异常都是checkForComodification()这个方法进行检查的,我们先来看看这个方法的源码。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 这个源码的意思就是当modCount和expectedModCount不相等的时候就会抛出这个ConcurrentModificationException异常。
- 为什么不相等呢?
- 我们从ArrayList的add()和remove()方法的源码入手。
//add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//remove方法
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
- 从上面的代码中可以看出只要对ArrayList作了添加或删除操作都会增加modCount版本号,这样的意思是在迭代期间,会不断检查modCount和迭代器持有的expectedModCount两者是不是相等,如果不相等就抛出异常了。
- 这样在迭代器迭代期间不能对ArrayList作任何增删操作,但是可以通过iterator的remove作删除操作,从之前的代码可以看出,在iterator的remove()中有一行代码,expectedModCount = modCount; 这个赋值操作保证了iterator的remove是可用性的。
- 当然,iterator期间不能增删的根本原因是ArrayList遍历会不准,就像遍历数组的时候改变了数组的长度一样。
ListIterator
- ListIterator和Iterator的关系
- ListIterator这个接口继承了Iterator
- 都可以遍历List
- ListIterator和Iterator的区别
- 使用范围不同
- Iterator可以应用于更多的集合,Set,List和这些集合的子类型。
- ListIterator只能用于List及其子类型。
- 遍历顺序不同
- Iterator只能顺序向后遍历;ListIterator还可以逆序向前遍历
- Iterator可以在遍历的过程中remove();ListIterator可以在遍历的过程中remove()、add()、set()
- ListIterator可以定位到当前索引的位置,nextIndex()和previousIndex()可以实现。但是Iterator没有这个功能。
- 当ListIterator在进行逆序向前遍历的时候,必须要先执行正常的顺序向后遍历,再执行向前遍历,否则的话,直接执行向前遍历的结果就会为null。
Collections工具类
- 关于集合操作的工具类,好比Arrays,Math
- 唯一的构造方法private,不允许在类的外部创建对象
- 提供了大量的static方法,可以通过类名直接调用
public class TestCollections {
public static void main(String[] args) {
//给集合快速赋值
List<Integer> list = new ArrayList<>();
Collections.addAll(list,20,50,80,90,40,60,10,2);
System.out.println(list);
System.out.println("===================================");
//排序
Collections.sort(list);
System.out.println(list);
//查找元素(元素必须有序)
//调用Collections的工具方法binarySearch(在哪里找,找什么元素);
//其返回值是这个元素在集合中的索引位置,
//是一个int类型的值
int index = Collections.binarySearch(list, 60);
System.out.println(index);
//最大值
System.out.println("最大值:" + Collections.max(list));
//最小值
System.out.println("最小值:" + Collections.min(list));
//填充集合
//Collections.fill(哪个集合,全部用几去填充);
//结果:[0, 0, 0, 0, 0, 0, 0, 0]
//Collections.fill(list,0);
//System.out.println(list);
//复制集合
//Collections.copy(目的集合,源集合);
//目的集合的size要 >= 源集合的size
List<Integer> list2 = new ArrayList<>();
Collections.addAll(list2,0,0,0,0,0,0,0,0,0,0);
Collections.copy(list2,list);
System.out.println(list2);
//同步集合
StringBuffer buffer;//线程同步的
StringBuilder builder;//线程不同步
ArrayList<String> arrayList;//线程不安全,在多线程操作会有安全问题
//Collections.synchronizedList(不安全的集合);其返回值是一个安全的集合
List<Integer> synchronizedList = Collections.synchronizedList(list);
}
}
旧的集合类
- Vector
- 实现原理和ArrayList相同,功能相同,都是长度可变的数组结构,很多情况下可以互用。
- 两者的主要区别如下:
- Vector是早期JDK接口,ArrayList是代替Vector的新接口
- Vector线程安全,效率低;ArrayList效率高但是不安全。
- 当大小需要扩展的时候,Vector默认的是直接扩展一倍,ArrayList扩展50%
- Hashtable类
- 实现原理和HashMap相同,功能相同,底层都是哈希表结构,查询速度快,很多情况下可以互用。
- 两者的主要区别如下:
- Hashtable是早期JDK提供,HashMap是新版JDK提供的
- Hashtable继承了Dictionary类,HashMap实现Map接口
- Hashtable线程安全,HashMap线程不安全
- Hashtable不允许null值,HashMap允许null值
新一代并发集合类
集合类的发展历程
- 早期集合类Vector、Hashtable都是线程安全的,那么怎么保证线程安全的呢,是使用了synchronized修饰方法。
- 为了提高性能,使用了ArrayList、HashMap进行替换,虽然说性能好了,但是他们是线程不安全的。 那么怎么样使他们变成线程安全的呢?
- 使用Collections.synchronizedList(list)、Collections.synchronizedMap(m)解决,底层使用synchronized代码块锁。
- 虽然也是锁住了所有的代码,但是锁在方法里边,比锁在外面的性能会高一些,因为在进方法的时候本身就是要分配资源的。
- 在大量并发情况下该如何提高集合的效率和安全呢?
- 随着技术的更新换代,Java提供了新的线程同步集合类,在java.util.concurrent(JUC)包下面,使用Lock锁或者volatile+CAS的无锁化。
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteArraySet
新一代的并发集合类
ConcurrentHashMap
ConcurrentHashMap:Node数组+链表/红黑树
- Java 7中ConcurrentHashMap使用的是分段锁,每一个Segment上只有一个线程可以操作,每一个Segment都是类似HashMap结构,可以扩容,遇到冲突可以转化为链表,但是Segment的长度是固定的,一旦初始化就不能改变。
- Java 8中ConcurrentHashMap使用的是CAS和synchronized锁,机构也变为Node数组+链表/红黑树。它摒弃了Segment分段锁的概念,而是启用了一种全新的方式实现。利用volatile + CAS实现无锁化操作。为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
- ConcurrentHashMap初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl < 0 ,说明另外的线程执行CAS成功,正在进行初始化。
if ((sc = sizeCtl) < 0)
//让出CPU使用权
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap的初始化是通过自旋和CAS操作实现的,sizeCtl变量的值有如下几个含义:
(1)-1:说明正在初始化。
(2)-N:说明有N-1个线程正在进行扩容。
(3)如果table没有初始化,则表名table初始化大小。
(4)如果table已经初始化,则表示table容量。
- put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
//f = 目标位置元素
//fh 后面存放目标位置的元素 hash 值
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//数组桶为空,初始化数组桶(自旋 + CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//桶内为空,CAS放入,不加锁,成功了就直接break跳出
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//使用synchronized加锁加入节点
synchronized (f) {
if (tabAt(tab, i) == f) {
//说明是链表
if (fh >= 0) {
binCount = 1;
//循环加入新的或者覆盖节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
//红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
- 根据key计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用synchronized 锁写入数据。
- 如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。
- get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//key所在的hash位置
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
//如果指定位置元素存在,头结点hash值相同
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
//key的hash值相等,key值相等,直接返回元素value
return e.val;
}
else if (eh < 0)
//头结点hash值 < 0,说明正在扩容或者是红黑树,则要用find方法进行查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
//是链表,进行遍历查找
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- 根据 hash值计算位置。
- 查找到指定位置,如果头结点就是要找的,直接返回它的 value。
- 如果头结点hash 值小于 0 ,说明正在扩容或者是红黑树,find查找。
- 如果是链表,遍历查找。
CopyOnWriteArrayList
- CopyOnWriteArrayList:CopyOnWrite+Lock锁
对于set()、add()、remove()等方法使用ReentrantLock的lock和unlock来加锁和解锁。读操作不需要加锁(之前集合安全类,即使读操作也要加锁,保证数据的实时一致)。 - CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
- CopyOnWrite的缺点
- 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
- 针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
- 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
CopyOnWriteArraySet
- CopyOnWriteArraySet:CopyOnWrite + Lock锁
- 它是线程安全的无序集合,可以把它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过散列表(HashMap)实现的,而CopyOnWriteArraySet则是通过==动态数组(CopyOnWriteArrayList)==实现的,并不是散列表。
- CopyOnWriteArraySet在CopyOnWriteArrayList的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质上是一个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的集合
- CopyOnWriteArrayList中允许有重复的元素,但CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作。
集合常用概念辨析
集合和数组的比较
数组不是面向对象的,存在着明显的缺陷,集合完全弥补了数组的一些缺点,比数组更加灵活实用,可以大大提高软件的开发效率,而且不同的集合框架类可适用于不同场合。比如说:
- 数组容量固定且无法动态改变,集合类容量动态改变
- 数组能存放基本数据类型和引用数据类型的数据,而集合类中只能放引用数据类型的数据
- 数组无法判断其中实际存有多少元素,length只告诉了array的容量;集合可以判断实际存放了多少元素,而对总的容量不关心
- 集合有多种数据结构(顺序表、链表、哈希表、树等)、多种特征(是否有序,是否唯一)、不同适用场合(查询快、便于删除、有序),不像数组仅采用顺序表方式
- 集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性调用即可实现各种复杂的操作,大大的提高了软件的开发效率。
ArrayList和LinkedList的联系和区别
- 联系:
- 都实现了List接口
- 有序,不唯一(可重复)
- ArrayList
- 特点:在内存中分配连续的空间每个空间大小相同,逻辑顺序和物理顺序一致,实现了长度可变的数组
- 优点:遍历元素和随机访问元素的效率比较高,按照索引查询效率高,直接计算出地址,不需要逐个进行比较,第n个元素的地址=数组首地址+每个元素空间大小*索引
- 缺点:添加和删除需要大量的移动元素,效率低,按照内容查询效率低
- LinkedList
- 特点:采用链表存储方式,底层是双向链表。在内存中分配不连续的空间,每个空间大小相同;每个节点分为两部分:数据和指向下一个节点的指针。逻辑顺序和物理顺序不一致。
- 缺点:遍历和随机访问元素效率低。按照索引查询效率低,只能逐个进行查询,无法计算地址。
- 优点:插入、删除元素效率比较高(但是前提也是必须先低效率查询才可以。如果说插入和删除的操作发生在头和尾的话,可以减少查询次数)
哈希表的原理(HashMap的底层原理)
- 哈希表的特征
- 快:查询快、添加快
- 哈希表的结构
- 最常用、最容易理解的结构是JDK1.7的数组+链表结构
- 在JDK1.8改成了数组+链表/红黑树(当链表长度>=8的时候,链表就转换成了红黑树)
- 哈希表的添加原理
- 计算哈希码(hashCode())
- 计算存储位置(存储位置就是数组的索引)
- 存入指定位置(要处理冲突,可能重复。需要借助equals()方法进行比较)
- 哈希表的查询原理和添加的原理是相同
TreeMap的底层原理(红黑树的底层原理)
- 基本特征
- 二叉树、二叉查找树、二叉平衡树、红黑树
- 每个节点的结构
- 添加原理
- 从根节点开始比较
- 添加过程就是构造二叉平衡树的过程,会自动平衡
- 平衡离不开比较;外部比较器优先,然后是内部比较器,否则会出错
- 查询原理和添加原理基本类似
Collection和Collections的区别
- Collection是Java提供的集合接口,存储一组不唯一,无需的对象。它有两个子接口List和Set。
- Java中还有一个Collections类,专门用来操作集合类,它提供了一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
Vector和ArrayList的联系和区别
- 实现原理和ArrayList相同,功能相同,都是长度可变的数组结构,很多情况下可以互用。
- 两者的主要区别如下
- Vector是早期JDK接口,ArrayList是代替Vector的新接口
- Vector线程安全效率低下;ArrayList看重速度,不重视安全,线程非安全
- 在长度需要增加的时候,Vector默认增长一倍,ArrayList增长50%
HashMap和Hashtable的联系和区别
- 实现原理相同,功能相同,底层都是哈希表,查询速度快,在很多情况下可以互用
- 两者的主要区别如下:
- Hashtable是早期JDK提供的接口,HashMap是新版JDK提供的接口
- Hashtable继承Dictionary类,HashMap实现Map接口
- Hashtable线程安全,HashMap线程不安全
- Hashtable不允许null值,HashMap允许null值
a中还有一个Collections类,专门用来操作集合类,它提供了一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
Vector和ArrayList的联系和区别
- 实现原理和ArrayList相同,功能相同,都是长度可变的数组结构,很多情况下可以互用。
- 两者的主要区别如下
- Vector是早期JDK接口,ArrayList是代替Vector的新接口
- Vector线程安全效率低下;ArrayList看重速度,不重视安全,线程非安全
- 在长度需要增加的时候,Vector默认增长一倍,ArrayList增长50%
HashMap和Hashtable的联系和区别
- 实现原理相同,功能相同,底层都是哈希表,查询速度快,在很多情况下可以互用
- 两者的主要区别如下:
- Hashtable是早期JDK提供的接口,HashMap是新版JDK提供的接口
- Hashtable继承Dictionary类,HashMap实现Map接口
- Hashtable线程安全,HashMap线程不安全
- Hashtable不允许null值,HashMap允许null值