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
- 随着技术的更新换代,Java提供了新的线程同步集合类,在java.util.concurrent(JUC)包下面,使用Lock锁或者volatile+CAS的无锁化。
新一代的并发集合类
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容器。
- 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
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值