类集:保存数据的集合,类集 = 数据结构 + 线程安全
为了解决数组定长问题,产生类集思想 - Collection(理解为一个动态数组)
1. Collection 接口常用操作方法
Collection接口是Java中保存单个对象的最顶层接口。
Collection有两个子接口:LIst与Set。
List接口:允许数据重复
Set接口:不允许数据重复。
- 添加元素:
boolean add(E e) - 删除元素:
boolean remove(Object o) - 查找元素:(如果有返回true,没有返回false)
boolean contains(Object o); - 转为数组:
Object[] toArray(); - 取得类集的迭代器(用于集合遍历)
Iterator iterator(); - Collection.EMPTY_LIST,这是一个空集合。
2. List接口
2.1 常用方法
- 根据下标返回元素:
E get(int index);
- 根据下标修改元素,返回修改前的元素
E set(int index,E element);
- 根据下标删除元素
E remove(int index); E remove(new Integer(int value));
例:
import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);//添加元素
list.add(3);
list.add(5);
list.add(7);
list.add(1);
System.out.println(list);//输出数组
System.out.println(list.size());//返回数组大小
System.out.println(list.contains(7));//判断是否有这个元素
System.out.println(list.get(3));//返回数组下标为3的数
System.out.println(list.set(3,9));//将数组下标为3的数修改为9
System.out.println(list.get(3));//再次返回数组下标为3的数,看是否修改成功
System.out.println(list.remove(4));//删除的是索引位置的数
System.out.println(list.remove(new Integer(5)));//删除数字5
System.out.println(list);//打印数组
}
}
结果:
2.2 List集合中添加自定义类,及比较方法
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
class Person{
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class ListTest {
public static void main(String[] args) {
List<Person> people = new LinkedList<>();
Person per1 = new Person("ll",20);
Person per2 = new Person("hh",30);
Person per3 = new Person("fwb",1);
people.add(per1);
people.add(per2);
people.add(per3);
System.out.println(people.contains(new Person("ll",20)));
System.out.println(people.remove(new Person("zs",20)));
}
}
这样添加的结果是输出了两个false,因为他需要覆写equals方法以获得比较元素的支持:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
class Person{
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/*
这是原方法中所调用的equals方法,只是比较地址是否相同
public boolean equals(Object obj) {
return (this == obj);
}
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
//instanceof用来判断是否是这类的对象
if (!(obj instanceof Person))
return false;
//向下转型取出属性值比较
Person per = (Person)obj;
return this.age.equals(per.age) && this.name.equals(per.name);
}
}
public class ListTest {
public static void main(String[] args) {
List<Person> people = new LinkedList<>();
Person per1 = new Person("aaa",20);
Person per2 = new Person("bbb",30);
Person per3 = new Person("ccc",1);
people.add(per1);
people.add(per2);
people.add(per3);
//Object obj = new Person("aaa",20);
//父类 父类引用 = new 子类();这是向上转型
System.out.println(people.contains(new Person("aaa",20)));
System.out.println(people.remove(new Person("张三",20)));
}
}
此时的输出结果是true与false是我们想要的结果。
2.3 List接口中ArrayList,LinkedList,Vector的区别
2.3.1 ArrayList: JKD1.2
- 数组实现
1.1 初始化策略:Lazy-Load(懒加载策略):只有当对象第一次被使用(add)时,内部的数组才会初始化为长度为10的数组。
1.2 扩容策略:每次扩容为原先数组的1.5倍 - 线程不安全
源码:
private int size;
private static final int DEFAULT_CAPACITY = 10;
1.
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//二者取最大值
}
ensureExplicitCapacity(minCapacity);
}
3.
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
4.
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);
}
2.3.2 Vector: JDK1.0
- 数组实现
1.1 初始化策略:当产生Vector对象时,内部数组就初始化为大小为10的数组(不管用不用)
1.2 扩容:每次扩容为原先数组的2倍。 - 线程安全性:使用synchronized同步方法来确保线程安全性。
但是因为使用了synchronized同步方法所以效率会很低(读写互斥)(锁的是当前整个Vector对象,操作任意一个方法,其他线程全部阻塞了,无法实现多个线程同时读这个Vector对象) - 特殊点:JDK内置的Stack继承Vector。Java中自定义的栈是它的子类。
- Vector独有枚举输出。
源码:
1.
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
2.
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
3.
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
4.
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.3.3 LinkedList: JDK1.2
- 基于双向链表的动态数组(所以不存在初始化,扩容问题),可以被当作栈、队列或双端队列来使用
- 线程不安全
- 特殊:JDK Queue(队列)的一个实现子类
.
2.3.4 ArrayList与LinkedList的选择:
ArrayList 适合查找(取得)和更改。LinkedList适合添加和删除。但是在自己测试的时候,低数量级的数据(1w以下)是ArrayList更快,之后更大量级的都是LinkedList更快。同时,在使用LinkedList时需要考虑到他是链表结构的使用了大量的小对象,因此会影响整个进程的性能。
可以看看这个问题:https://stackoverflow.com/questions/322715/when-to-use-linkedlist-over-arraylist-in-java/322742#322742
3. Set接口(本质是一个Map接口)
set接口中没有扩充任何方法,set中有的方法都是Collection中有的方法。
源码:
//HashSet底层用来存储元素的结构,实际上使用HashMap来存储
private transient HashMap<E,Object> map;
//HashMap中的value值,HashSet只关注key值,所以所有的value值都为Object对象
private static final Object PRESENT = new Object();
//HashSet的无参构造,直接创建了一个HashMap对象
public HashSet() {
map = new HashMap<>();
}
//指定初始化容量和负载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//给定初始化容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet的构造方法底层都是调用 HashMap的构造方法, 所以HashSet底层实际上是使用 HashMap 来作为存储结构.
当使用无参构造创建 HashSet对象时, 其实调用了 HashMap的无参构造创建了一个 HashMap对象, 所以 HashSet 的初始化容量也为16, 负载因子也为 0.75.
再来看看 HashSet 的 add() 方法的实现:
HashSet 的 add() 方法底层实际也是调用了 HashMap 的 put() 方法, 这里的key为我们传入的将要添加到 set集合中的元素, 而value值则为 PERSENT,其实就是上面分析的 HashSet类中的一个静态字段, 默认为 Object对象.
HashSet并不关注value元素, 只使用 HashMap来存储 key元素, 这就使得 HashSet判断元素相等的条件与 HashMap中 key相等的条件其实是一样的, 两个元素的 hashCode值相同且通过equals()方法比较返回 true.
所以HashSet应该重写 equals()和hashCode()方法, 两个元素的 HashCode相同, 保证通过equals() 方法比较返回 true.
1. Set下的两个实现类类:
1.HashSet(无序存储),底层实现哈希表 + 红黑树(JDK1.8以后),JDK1.8以前只有哈希表。
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("Hello");
set.add("Java");
set.add("SE");
set.add("EE");
set.add("Hello");//重复元素
set.add(null);
set.add(null);//重复元素
System.out.println(set);
}
}
结果:
2.TreeSet(如果没有自定义类则按照ASCII码,升序存储),不允许存放null,底层实现:红黑树
public class SetTest {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("e");
set.add("a");
set.add("a");
set.add("c");
set.add("A");
System.out.println(set);
}
}
2. TreeSet中自定义类如何保证元素升序与不重复?
首先看一下使用自定义类时,直接使用Java定义好的工具类什么都不做出现的结果:
class Person{
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class SetTest {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
set.add(new Person(20,"张三"));
set.add(new Person(25,"李四"));
System.out.println(set);
}
}
结果:自定义的类如果不实现Comparable接口,或者comparator接口则不能比较。
非自定义的类可以比较是因为已经自己实现了Compara接口。
3. 所以想让自定义类升序有两种方法:
1.java.lang.Comparable:内部排序接口
int compareTo(T o);
大于0:大于目标对象
等于0:等于目标对象
小于0:小于目标对象
类现了Comparable接口就表示此类具备可比较的性质,所以我们自定义的Person类想要能比较就需要实现Comparable接口。
class Person implements Comparable<Person>{
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
//首先比较年龄,如果年龄相同,则比较名字(名字按ASCII码排序)
public int compareTo(Person o) {
if (this.age < o.age)
return -1;
if (this.age > o.age)
return 1;
return this.name.compareTo(o.name);
}
}
public class SetTest {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
set.add(new Person(25,"王五"));
set.add(new Person(25,"李四"));
set.add(new Person(20,"张三"));
System.out.println(set);
}
}
结果:
2.java.util.Comparator:外部排序接口(推荐使用)策略模式,不用修改源代码
类本身不具备可比较的特性,专门有一个类来比较自定义类的大小,此类专门用于比较两个Person的大小,以后如果需要更换比较策略,只需再写一个新的类就可以,不需要再次更改原代码。
int cpmpare(T o1,T o2);
package fwb.collection.Set;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
class Person2 {
private Integer age;
private String name;
public Person2(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person2{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
/*
此类专门用于比较两个Person的大小,以后如果需要更换比较策略,
只需再写一个新的类就可以,不需要再次更改原代码
*/
class PersonAgeSec implements Comparator<Person2>{
@Override
public int compare(Person2 o1, Person2 o2) {
if(o1.getAge() < o2.getAge()){
return -1;
}
if (o1.getAge() > o2.getAge()){
return 1;
}
return 0;
}
}
//若以后需要更改比较策略,只需要重新写一个类即可
class PersonAgeDesc implements Comparator<Person2>{
@Override
public int compare(Person2 o1, Person2 o2) {
if(o1.getAge() < o2.getAge()){
return 1;
}
if (o1.getAge() > o2.getAge()){
return -1;
}
return 0;
}
}
public class SetTest2 {
public static void main(String[] args) {
PersonAgeSec personAgeSec = new PersonAgeSec();
//TreeSet有一个构造方法可以接收Comparator的子类
Set<Person2> set = new TreeSet<>(personAgeSec);
set.add(new Person2(25,"王五"));
set.add(new Person2(25,"李四"));
set.add(new Person2(20,"张三"));
System.out.println(set);
}
}
结果:
总结:
自定义类要想使用TreeSet:
- 自定义类自己实现了Comparable
- 要么是从外部传入一个该类的比较器对象(实现了Comparator接口)
通过compare的返回值来判断是否相等,如果为0则重复。
4. HashSet如何保证元素不重复?
HashSet使用equals与hashCode共同来判断元素是否重复
equals:判断两个对象属性是否相同。
hashCode:对象在内存中的地址根据Hash算法转为int。
所以如果自定义类需要覆写equals与hashCode方法:
package fwb.collection.Set;
import java.util.*;
/**
* @program: collection
* @description: HashSet
* @author: fwb
* @create: 2019-07-15 15:06
**/
class Person3 {
private Integer age;
private String name;
public Person3(Integer age, String name) {
this.age = age;
this.name = name;
}
//覆写equals方法
@Override
public boolean equals(Object obj){
//当前对象 == obj(自己和自己比)
if (this == obj){
return true;
}
//如果obj不是当前类对象
if (!(obj instanceof Person3)){
return false;
}
//传进来的时此类对象,并且和自己不是同一个地址
Person3 per = (Person3) obj;
return this.age.equals(per.age)&& this.name.equals(per.name);
}
//覆写hashCode()方法
@Override
public int hashCode() {
return Objects.hash(name,age);
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person3{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class SetTest3 {
public static void main(String[] args) {
Set<Person3> set = new HashSet<>();
set.add(new Person3(25,"王五"));
set.add(new Person3(25,"李四"));
set.add(new Person3(20,"张三"));
set.add(new Person3(20,"张三"));
System.out.println(set);
}
}
注:
- equlas相同,hashCode一定返回相同
- hashCode相同,equals不一定相同,因为hash算法不同。例:
//hash算法1:
//x = 10
hash(int i){
return i + 1;
}
//hash算法2:
//x = 20
hash(int i){
return i - 9;
}
4. 集合遍历与输出
Collection的输出:
1.使用Iterator迭代器输出(Collection的迭代器接口,专门用来输出Collection接口内容)
Iterator<E> iterator(); //取得该Collection接口对象的迭代器。
boolean hasNext(); //判断当前集合是否还有未遍历的元素。
E next(); //取得当前正在遍历的内容。
2.foreach:
Collection接口可以使用foreach输出的本质在于,所有的子类都实现了Iterator接口。(foreach输出其实就是一种语法糖(只存在于代码编译阶段),本质和第一种方式一样)
3.LIstIterator(双向迭代输出——只有List接口有)
扩展了两个方法:
boolean hasPrevious();
E previous();
要想使用从后向前输出,必须先从前向后走一遍。(要使用previous方法必须先试用next方法)
4.Enumeration(枚举输出——只有Vector及其子类Stack才有)
boolean hasMoreElements();
E nextElement();
代码:
1. 使用迭代器,for—each,双向输出
package fwb.collection.iterator_test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("java");
list.add("hello");
list.add("fwb");
System.out.println("1. 使用迭代器输出:");
Iterator<String> iterator = list.iterator();//取得迭代器
while(iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
}
System.out.println("2. 使用foreach:");
for (String str: list
) {
System.out.println(str);
}
System.out.println("-------------------------");
System.out.println("3. 双输出,只有list接口有");
ListIterator<String> iterator2 = list.listIterator();
System.out.println("正向");
while(iterator2.hasNext()){
String str = iterator2.next();
System.out.println(str);
}
System.out.println("反向");
while (iterator2.hasPrevious()){
String str = iterator2.previous();
System.out.println(str);
}
}
}
运行结果:
2.使用remove删除
public class ListIteratorTest2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("java");
list.add("hello");
list.add("fwb");
// list.add("1998");
Iterator<String> iterator = list.iterator();//取得迭代器
while(iterator.hasNext()){
String str = iterator.next();
if (str.equals("fwb")){
//list.remove(str);
iterator.remove();
}
System.out.println(str);
}
Iterator<String> iterator2 = list.iterator();
System.out.println("----------");
while(iterator2.hasNext()){
String str = iterator2.next();
System.out.println(str);
}
}
}
结果:使用remove()删除的时候,虽然会将删除的元素打印出来,但实际上是删除了的。
5. 栈和队列
栈是一种先进后出的数据结构。
Java集合中提供Stack类,他是 Vector的子类。
- 入栈 : public E push(E item)
- 出栈 : public synchronized E pop()
- 观察栈顶元素 : public synchronized E peek()
public class StackTest {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(3);
stack.push(5);
System.out.println(stack.peek());
System.out.println(stack.pop());
System.out.println(stack.peek());
}
}
结果:
Queue:(有一个子类是linkedlist)
- public E element() 普通 找到链表的表头。
- public boolean offer(E o) 普通 将指定元素增加到链表的表尾。
- public E peek() 普通 找到但并不删除链表的头。
- public E poll() 普通 找到并删除链表的头。
- public E remove() 普通 检索并移除表头
public class QueueTest {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
queue.add(3);
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.peek());
}
}
6. Map接口
Map集合中会一次性保存两个对象,且这两个对象的关系: key=value结构。这种结构最大的特点是可以通过key找到对应的value内容。
Map接口中有如下常用方法:
几个常用的子类有以下4个:
HashMap:HashSet就是一个value值相同的map结构,key和value都可以为空,但是key只能有一个相同的。
JDK1.2,线程不安全,底层实现是:哈希表 + 红黑树(JDK8)
HashMap问总结
TreeMap:key值不可以为空,value可以为空
JDK1.2,线程不安全,底层实现:红黑树
Hashtable:key与value均不为空
线程安全,JDK1.0,底层实现是:哈希表
ConcurrentHashMap
map常用方法实例:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class HashMapTest {
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
map.put(1,"hello");
map.put(2,"java");
map.put(3,"hello");
map.put(4,"fwb");
map.put(4,"ffwwwbbbb");
map.put(null,null);
map.put(null,"哈哈");
System.out.println(map.get(4));
System.out.println(map.get(7));
System.out.println(map);
System.out.println(map.get(null));
Set<Integer> keySet = map.keySet();//取得key信息,//key不能重复
Iterator<Integer> iterator = keySet.iterator();
while(iterator.hasNext()){
Integer i = iterator.next();//key
System.out.println(i + "=" + map.get(i));
}
}
}
结果:
如果同一个key有多个value,以最后一个为准。
如果没有put进去,那么get输出为空
key为空,value不为空,可以添加
7. Map集合的输出
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class HashMapTest2 {
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
map.put(1,"hello");
map.put(2,"java");
map.put(3,"hello");
map.put(4,"fwb");
map.put(4,"ffwwwbbbb");
map.put(null,null);
map.put(null,"哈哈");
// map -> set
// map标准输出
Set<Map.Entry<Integer,String>> set = map.entrySet();
Iterator<Map.Entry<Integer,String>> iterator = set.iterator();
while (iterator.hasNext()){
Map.Entry<Integer,String> entry = iterator.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
}
8. properties属性文件操作
保存的都是键值对 key = value,key与value均为String类型
key = value
- 设置属性 : public synchronized Object setProperty(String key, String value)
- 取得属性 : public String getProperty(String key),如果没有指定的key则返回null
- 取得属性 : public String getProperty(String key, String defaultValue),如果没有指定的key则返回默认值
- 将属性资源持久化到外部:store(OutputStream out, String comments)
- 从外部资源中读取配置 void load(InputStream inStream)
9. Collection工具类
Collections是一个集合操作的工具类,包含有集合反转、排序等操作。
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.List;
public class TestDemo { .
public static void main(String[] args) throws IOException {
List<String> list = new ArrayList<>() ;
// 相当于调用了三次add()方法
Collections.addAll(list,"A","B","C") ;
System.out.println(list) ;
// 集合反转
Collections.reverse(list) ;
System.out.println(list) ;
}
}
10. Stream数据流
最简单的一些操作:
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list,"Java","python","C++","JavaScript");
//返回list集合中带Java关键字的关键字的个数
System.out.println(list.stream().filter((e) ->
e.contains("Java")).count());
//返回list集合中带Java关键字的关键字的个数,并形成新的List集合
List<String> resultList = list.stream().
filter((e -> e.contains("Java"))).
collect(Collectors.toList());
System.out.println(resultList);
}
}
11.快速失败策略(fail - fast)
即为优先处理产生异常的情况,当异常情况发生时,直接向用户抛出异常,程序终止。
例:
public static int div(int a,int b){
if (b == 0){
throw new IllegalArgumentException("除数不能为0");
}
return a / b;
}
当我们执行这种代码的时候会出现如下情况:
public class ListIteratorTest2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//modcount = 1
list.add("hello");
//modcount = 2
list.add("java");
//modcount = 3
list.add("hello");
//modcount = 4
list.add("fwb");
// expectedModCount = modcount (4)
Iterator<String> iterator = list.iterator();//取得迭代器
while(iterator.hasNext()){
String str = iterator.next();
if (str.equals("fwb")){
list.remove(str);
//modcount++(5)
}
System.out.println(str);
}
}
}
java.util.ConcurrentModificationException,产生原因:当迭代输出集合时,并发修改了集合的结构。
源码中的 protected transient int modCount 记录了当前集合被修改的(add,remove等方法)次数。.
源码中当使用迭代器的时候,会执行这样一个代码int expectedModCount = modCount;
以及这个方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
当我们执行: list.remove(str);这行代码的时候 modCount++,则modCount != expectedModCount,产生了异常,之所以前四个可以正常输出是因为在remove之后modcount才加1,所以在此之前都是正常的。(见代码)
意义:例如有3个线程在同时运行,2个线程在读取ArrayList的内容,1个线程修改了该集合的值,那么这两个线程读取的内容就有可能是不正确的。fail—fast机制保证集合在多线程场景下读到最新的值。 java.util 的集合类大多都采用此策略(如:ArrayList、Vector、LinkedList、HashSet,读写都在同一个副本中)
与其相关的有:
fail—safe:
java.util.concurrent:CopyOnWriteArrayList/conCurrentHashMap
CopyOnWriteArrayList采用读写分离,所有的读为异步操作,写为同步操作,且读写不在同一个副本。
如何处理:
1.迭代输出的时候,不要修改集合的内容
2.使用迭代器的修改方法(在源码中,他虽然最终调用的还是ArrayList的方法。但是在此之后将expectModCount重新赋值了一次)
if (str.equals("fwb")){
//list.remove(str);
iterator.remove();
}