一、Iterator(迭代)
1.定义:
- 在Java中Iterator为一个接口(java.util.Iterator),它只提供了迭代了基本规则
- 一个标准化遍历各类容器里面的所有对象的方法类,它是一个典型的设计模式
- Iterator模式是用于遍历集合类的标准访问方法
- 把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构
- 客户端自身不需要来维护集合的内部结构,所有的内部状态都由Iterator来维护
- 客户端从不直接和集合类打交道,它总是控制Iterator,向它发送向前,向后,取当前元素的命令,就可以间接遍历整个集合
java.util.Iterator接口定义:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
/*Object next():返回迭代器刚越过的元素的引用,
返回值是Object,需要强制转换成自己需要的类型*/
//boolean hasNext():判断容器内是否还有可供访问的元素
//void remove():删除迭代器刚越过的元素
一般只需使用next()、hasNext()两个方法即可完成迭代
for(Iterator it = c.iterator(); it.hasNext(); ) {
Object o = it.next();
do something
}
2. Iterator和Enumeration
迭代器取代了 Java Collections Framework 中的 Enumeration。
迭代器与枚举有两点不同:
- 迭代器允许调用者利用定义良好的语义在迭代期间从迭代器所指向的 collection 移除元素
- 方法名称得到了改进
3.ArrayList的Iterator实现:
在ArrayList内部首先是定义一个内部类Itr,该内部类实现Iterator接口:
private class Itr implements IteratorE {
do something
}
而ArrayList的iterator()方法实现:
public IteratorE iterator() {
return new Itr();
}
所以通过使用ArrayList.iterator()方法返回的是Itr()内部类,所以现在我们需要关心的就是Itr()内部类的实现:
在Itr内部定义了三个int型的变量:
- cursor:下一个元素的索引位置
- lastRet:上一个元素的索引位置
- expectedModCount
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
ArrayList中 Iterator接口中 hasNext()实现方法 :
//判断cursor是否和集合中元素个数相等,
//若是相等则集合中无可访问的元素
//cursor默认为0
public boolean hasNext() {
return cursor != size;
}
ArrayList中 Iterator接口中 Next()实现方法 :
public E next() {
checkForComodification();
//记录索引位置
int i = cursor;
//如果获取元素大于集合元素个数,则抛出异常
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//cursor + 1
cursor = i + 1;
//lastRet + 1 且返回cursor处元素
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Next()实现方法执行过程:
前置条件:
- 该集合元素个数为3,即size=3
- cursor 默认是 0
- lastRet = -1
- expectedModCount = modCount = 3
第一次执行Next :
语句:checkForComodification();
首先通过checkForComodification()判断该集合在遍历过程中是否有被其他线程修改过,若有则抛出异常
语句:int i = cursor;
此时cursor = 0;即i被赋值为0
语句:if (i >= size)
throw new NoSuchElementException();
判断 i 是否大于 size
0 > 3 。条件不成立,不执行里面语句
语句:Object[] elementData = ArrayList.this.elementData;
这行代码意思是将ArrayList里的底层数组赋值给了elementData
语句:if (i >= elementData.length)
throw new ConcurrentModificationException();
判断 i 是否 大于这个数组的长度。
此时 elementData.length = 10
条件不成立,不执行里面语句。
语句:cursor = i + 1;
此时 cursor = 1
语句:return (E) elementData[lastRet = i];
返回 elementData[lastRet = i];
前面说过 lastRet = -1,现在将 i 赋值给 lastRet。
此时 lastRet = 0
所以返回的是 elementData[0] 。也就是你存入的 0 索引的数据
第一次结束后的值:
lastRet = 0
cursor = 1
- checkForComodification()主要用来判断集合的修改次数是否合法,即用来判断遍历过程中集合是否被修改过
- modCount用于记录ArrayList集合的修改次数,初始化为0,每当集合被修改一次(结构上面的修改,内部update不算),如add、remove等方法,modCount + 1,所以如果modCount不变,则表示集合内容没有被修改
ArrayList中 Iterator接口中 remove()实现方法 :
public void remove() {
if (lastRet 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
二、fail-fast机制
1.定义:
- Java集合的一种错误检测机制
- 当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制
fail-fast示例
/**
* @desc:线程one迭代list
* @Project:test
* @file:FailFastTest.java
* @Authro:chenssy
* @data:2014年7月26日
*/
private static class threadOne extends Thread{
public void run() {
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
int i = iterator.next();
System.out.println("ThreadOne 遍历:" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @desc:当i == 3时,修改list
* @Project:test
* @file:FailFastTest.java
* @Authro:chenssy
* @data:2014年7月26日
*/
private static class threadTwo extends Thread{
public void run(){
int i = 0 ;
while(i < 6){
System.out.println("ThreadTwo run:" + i);
if(i == 3){
list.remove(i);
}
i++;
}
}
}
public static void main(String[] args) {
for(int i = 0 ; i < 10;i++){
list.add(i);
}
new threadOne().start();
new threadTwo().start();
}
/**
*ThreadOne 遍历0
*ThreadTwo run:0
*ThreadTwo run:1
*ThreadTwo run:2
*ThreadTwo run:3
*ThreadTwo run:4
*ThreadTwo run:5
*Exception in thread Thread-0 java.util.ConcurrentModificationException
*at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
*at java.util.ArrayList$Itr.next(Unknown Source)
*at test.ArrayListTest$threadOne.run(ArrayListTest.java23)
*/
2.产生原因:
fail-fast产生的原因:
- 程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast
- ConcurrentModificationException 异常为方法检测到对象的并发修改,但不允许这种修改时,就抛出该异常
- ConcurrentModificationException 异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常
- 迭代器的fail-fast行为无法得到保证,它不能保证一定会出现该错误,但是fail-fast操作会尽最大努力抛出ConcurrentModificationException异常
- 所以为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是ConcurrentModificationException 应该仅用于检测 bug
ArrayList中迭代器的源代码:
private class Itr implements IteratorE {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification();
省略此处代码
}
public void remove() {
if (this.lastRet 0)
throw new IllegalStateException();
checkForComodification();
省略此处代码
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
在ArrayList中的迭代器在调用next()、remove()等方法时都调用checkForComodification()方法,该方法主要就是检测modCount 是否与expectedModCount相等, 若不等,则抛出ConcurrentModificationException 异常,从而产生fail-fast机制
3.为什么会产生modCount != expectedModCount
- expectedModCount 是在Itr中定义的(即它的值是不可能会修改的,所以会变的就是modCount):int expectedModCount = ArrayList.this.modCount;
- modCount是在 AbstractList 中定义的,为全局变量:protected transient int modCount = 0;
ArrayList的源码:
public boolean add(E paramE) {
ensureCapacityInternal(this.size + 1);
}
private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1; //修改modCount
}
public boolean remove(Object paramObject) {
int i;
if (paramObject == null) {
for (i = 0; i this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else{
for (i = 0; i this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i); return true; }
return false;
}
private void fastRemove(int paramInt) {
this.modCount += 1; //修改modCount
}
public void clear() {
this.modCount += 1; //修改modCount
}
从ArrayList的源码中得出,ArrayList中无论add、remove、clear方法,只要涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。
所以可以初步得出expectedModCount 值与modCount的改变不同步,导致两者之间不等,从而产生fail-fast机制,即:
有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。
线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制
3.解决办法:
- 在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList(不推荐:因为增删造成的同步锁可能会阻塞遍历操作)
- 使用CopyOnWriteArrayList来替换ArrayList(推荐)
4.CopyOnWriteArrayList
- ArrayList 的一个线程安全的变体
- 数据结构、定义都和ArrayList一样
- 和ArrayList一样,同样是实现List接口,底层使用数组实现
- 在方法上也包含add、remove、clear、iterator等方法
- CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制
- CopyOnWriterArrayList所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的
- 该类产生的开销比较大
- 适用于以下两种情况:
- 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时
- 当遍历操作的数量大大超过可变操作的数量时
//CopyOnWriterArrayList中Iterator实现
private static class COWIteratorE implements ListIteratorE {
public E next() {
if (!(hasNext()))
throw new NoSuchElementException();
return this.snapshot[(this.cursor++)];
}
}
CopyOnWriterArrayList方法没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等
add方法为例:
public boolean add(E paramE) {
ReentrantLock localReentrantLock = this.lock;
localReentrantLock.lock();
try {
Object[] arrayOfObject1 = getArray();
int i = arrayOfObject1.length;
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);
int j = 1;
return j;
} finally {
localReentrantLock.unlock();
}
}
final void setArray(Object[] paramArrayOfObject) {
this.array = paramArrayOfObject;
}
CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);
这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常,因为它copy原来的array,再在copy数组上进行相关操作,这样做就完全不会影响COWIterator中的array了。
CopyOnWriterArrayList核心概念:
- 任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据
- 修改完成之后改变原有数据的引用
- 同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的
三、Comparable 和 Comparator
1.Comparable 和 Comparator的区别:
- Comparable 在 java.lang包下,是一个接口,内部只有一个方法 compareTo();Comparator 在java.util包下,是一个抽象度极高的接口,内部包含两个方法:compare()和equals(Object obj)
public interface ComparableT {
public int compareTo(T o);
}
package java.util;
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
- Comparable接口将比较代码嵌入自身类中,而Comparator既可以嵌入到自身类中,也可以在一个独立的类中实现比较
- 实现Comparable接口需要重写compareTo()方法,实现Comparator方法需要重写compare()方法,这两个方法的类型都是int
- Comparable是排序接口,相当于内部比较器,称为自然排序,Comparator是比较器,相当于外部比较器,称为比较器排序,外部比较器可以对内部比较器进行扩充
- 对于String, Integer, Double等这些基本类型的JAVA封装类都已经实现了Comparable接口,这些类对象本身就支持Comparable接口(自比较),直接调用
Collections.sort()
就可以。对集合中元素的排序,无需自己去实现Comparable接口 - 自定义类的List序列,当这个对象不支持自比较或者自比较函数不能满足你的要求时,可以适用Comparator来完成两个对象之间大小的比较,即临时规则排序,也称作专门规则排序
- Comparable 自然排序(实体类实现);Comparator 是定制排序(无法修改实体类时,直接在调用方创建),同时存在时采用 Comparator(定制排序)的规则进行比较
2.Comparable
compareTo方法的返回值是int,有三种情况:
- 比较者大于被比较者(也就是compareTo方法里面的对象),那么返回正整数
- 比较者等于被比较者,那么返回0
- 比较者小于被比较者,那么返回负整数
注意(求解惑):
- 由于 null 不是一个类,也不是一个对象,因此在重写 compareTo 方法时应该注意 e.compareTo(null) 的情况,即使 e.equals(null) 返回 false,compareTo 方法也应该主动抛出一个空指针异常 NullPointerException。
- Comparable 实现类重写 compareTo 方法时一般要求 e1.compareTo(e2) == 0 的结果要和 e1.equals(e2) 一致。这样将来使用 SortedSet 等根据类的自然排序进行排序的集合容器时可以保证保存的数据的顺序和想象中一致。 有人可能好奇上面的第二点如果违反了会怎样呢?
举个例子,如果你往一个 SortedSet 中先后添加两个对象 a 和 b,a b 满足 (!a.equals(b) && a.compareTo(b) == 0),同时也没有另外指定个 Comparator,那当你添加完 a 再添加 b 时会添加失败返回 false, SortedSet 的 size 也不会增加,因为在 SortedSet 看来它们是相同的,而 SortedSet 中是不允许重复的。
- 实际上所有实现了 Comparable 接口的 Java 核心类的结果都和 equlas 方法保持一致。 实现了 Comparable 接口的 List 或则数组可以使用 Collections.sort() 或者 Arrays.sort() 方法进行排序。
- 实现了 Comparable 接口的对象才能够直接被用作 SortedMap (SortedSet) 的 key,要不然得在外边指定 Comparator 排序规则。
因此自己定义的类如果想要使用有序的集合类,需要实现 Comparable 接口
例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author :Handoking
* @date : 2019/5/24
*/
//内部比较器
class Students implements Comparable<Students> {
private int age;
private String name;
public Students(int age, String name) {
this.age = age;
this.name = name;
}
public Students() {
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Students s1){
if (this.getAge()>s1.getAge()){
return 1;
}else if (this.getAge()==s1.getAge()){
return 0;
}else{
return -1;
}
}
@Override
public String toString(){
return "Name:" + name +",Age:"+age;
}
}
public class TestCompare{
public static void main(String[] args){
List<Students> arr = new ArrayList<>();
arr.add(new Students(20,"mark"));
arr.add(new Students(22,"kaven"));
arr.add(new Students(16,"daxia"));
arr.add(new Students(25,"handoking"));
Collections.sort(arr);
System.out.println(arr.get(0).toString());
}
}
3.Comparator
- Comparator最常用在排序和分组,排序常使用Arrays.sort()和Collections.sort方法,而分组则可以使用提供的divider方法
- 排序和分组的区别在于: 排序时,两个对象比较的结果有三种:大于,等于,小于。 分组时,两个对象比较的结果只有两种:等于(两个对象属于同一组),不等于(两个对象属于不同组)
例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author :Handoking
* @date : 2019/5/24
*/
//外部比较器
class MyCompare implements Comparator<Students> {
@Override
public int compare(Students s1,Students s2){
if (s1.getAge()>s2.getAge()){
return 1;
}else if (s1.getAge()==s2.getAge()){
return 0;
}else{
return -1;
}
}
}
class Students{
private int age;
private String name;
public Students(int age, String name) {
this.age = age;
this.name = name;
}
public Students() {
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString(){
return "Name:"+name+",Age:"+age;
}
}
public class TestCompare{
public static void main(String[] args){
List<Students> arr = new ArrayList<>();
arr.add(new Students(20,"mark"));
arr.add(new Students(22,"kaven"));
arr.add(new Students(16,"daxia"));
arr.add(new Students(25,"handoking"));
Collections.sort(arr,new MyCompare());
System.out.println(arr.get(1).toStirng);
}
}
四、Java8中使用lambda实现比较器
1.Lambda表达式的基本语法:
- (parameters) -> expression或(请注意语句的花括号)(parameters) -> { statements; }
- 箭头的作用:把参数列表与Lambda主体分隔开,箭头左边的是参数列表,右边的是Lambda主体
- Lambda表达式可以包含多行语句
比较器写法:
//比较器写法
Comparator<Developer> byName = new Comparator<Developer>() {
@Override
public int compare(Developer o1, Developer o2) {
return o1.getName().compareTo(o2.getName());
}
};
//Lambda 表达式的写法
Comparator<Developer> byName =
(Developer o1, Developer o2)->o1.getName().compareTo(o2.getName());
Collections.sort()写法:
public class TestSorting {
public static void main(String[] args) {
List<Developer> listDevs = getDevelopers();
System.out.println("Before Sort");
for (Developer developer : listDevs) {
System.out.println(developer);
}
//安装年龄排序
Collections.sort(listDevs, new Comparator<Developer>() {
@Override
public int compare(Developer o1, Developer o2) {
return o1.getAge() - o2.getAge();
}
});
System.out.println("After Sort");
for (Developer developer : listDevs) {
System.out.println(developer);
}
}
private static List<Developer> getDevelopers() {
List<Developer> result = new ArrayList<Developer>();
result.add(new Developer("mkyong", new BigDecimal("70000"), 33));
result.add(new Developer("alvin", new BigDecimal("80000"), 20));
result.add(new Developer("jason", new BigDecimal("100000"), 10));
result.add(new Developer("iris", new BigDecimal("170000"), 55));
return result;
}
}
输出结果:
Before Sort
Developer [name=mkyong, salary=70000, age=33]
Developer [name=alvin, salary=80000, age=20]
Developer [name=jason, salary=100000, age=10]
Developer [name=iris, salary=170000, age=55]
After Sort
Developer [name=jason, salary=100000, age=10]
Developer [name=alvin, salary=80000, age=20]
Developer [name=mkyong, salary=70000, age=33]
Developer [name=iris, salary=170000, age=55]
//lambda写法;
public class TestSorting {
public static void main(String[] args) {
List<Developer> listDevs = getDevelopers();
System.out.println("Before Sort");
for (Developer developer : listDevs) {
System.out.println(developer);
}
System.out.println("After Sort");
//对比上面的代码
listDevs.sort((Developer o1, Developer o2)->o1.getAge()-o2.getAge());
//这样打印感觉也不错
listDevs.forEach((developer)->System.out.println(developer));
}
private static List<Developer> getDevelopers() {
List<Developer> result = new ArrayList<Developer>();
result.add(new Developer("mkyong", new BigDecimal("70000"), 33));
result.add(new Developer("alvin", new BigDecimal("80000"), 20));
result.add(new Developer("jason", new BigDecimal("100000"), 10));
result.add(new Developer("iris", new BigDecimal("170000"), 55));
return result;
}
}
输出结果:
Before Sort
Developer [name=mkyong, salary=70000, age=33]
Developer [name=alvin, salary=80000, age=20]
Developer [name=jason, salary=100000, age=10]
Developer [name=iris, salary=170000, age=55]
After Sort
Developer [name=jason, salary=100000, age=10]
Developer [name=alvin, salary=80000, age=20]
Developer [name=mkyong, salary=70000, age=33]
Developer [name=iris, salary=170000, age=55]