Java中的容器
Java中经常会用到容器,也就是相关的集合类。各容器之间的关系如下:
其中淡绿色的表示接口,红色的表示我们经常使用的类。
1:基本概念
Java容器类类库的用途是保存对象,可以将其分为2个概念。
1.1:Collection
一个独立元素的序列,这些元素都服从一条或多条规则。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)
1.2:Map
一组成对的值键对对象,允许用键来查找值。ArrayList允许我们用数字来查找值,它是将数字和对象联系在一起。而Map允许我们使用一个对象来查找某个对象,它也被称为关联数组。或者叫做字典。
2:List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上加入了大量的方法,使得可以在List中间可以插入和移除元素。下面主要介绍2种List
2.1:基本的ArrayList
它的优点在于随机访问元素快,但是在中间插入和移除比较慢
那么现在我们就一起来看看为什么ArrayList随机访问快,而插入移除比较慢。先说关于ArrayList的初始化。
ArrayList有三种方式进行初始化如下
private transient Object[] elementData;
public ArrayList() {
this(10);
} // 无参构造器,创建一个长度为10的数组
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
} // 创建一个长度为 initialCapacity 的 Object 类型数组
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); // toArray() 返回一个包含该集合中所有元素的数组
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} // 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
我们可以看出ArrayList其实就是采用的是数组(默认是长度为10的数组)。所有ArrayList在读取的时候是具有和数组一样的效率,它的时间复杂度为1。
插入尾部就是elementData[size++] = e;当然中间会进行扩容。现在主要说插入中间为什么相对来说比较慢源码如下:
public void add(int index, E element) {
rangeCheckForAdd(index);//验证(可以不考虑)
ensureCapacityInternal(size + 1); // Increments modCount!!(超过当前数组长度进行扩容)
// System.arraycopy(elementData, index, elementData, index + 1, size - index)
// 第一个参数是源数组,源数组起始位置,目标数组,目标数组起始位置,复制数组元素数目。
// 那么这个意思就是从index索性处每个元素向后移动一位,最后把索引为index空出来,
// 并将element赋值给它。这样一来我们并不知道要插入哪个位置,所以会进行匹配那么它的时间赋值度就为n。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);(核心代码)
elementData[index] = element;
size++;
}
2.2:LinkedList
它是通过代价较低在List中间进行插入和移除,提供了优化的顺序访问,但是在随机访问方面相对较慢。但是他的特性功能要比ArrayList强大的多。支持Queue和Stack
Link1edList采用的是链式存储。链式存储就会定一个节点Node。包括三部分前驱节点、后继节点以及data值。所以存储存储的时候他的物理地址不一定是连续的。
我们看下它的中间插入实现:
从代码我们可以看出先获取插入索引元素的前驱节点,然后把这个元素作为后继节点,然后在创建新的节点,而新的节点前驱节点和获取前驱节点相同,而后继节点则等于要移动的这个元素。所以这里是不需要循环的,从而在插入和删除的时候效率比较高。
我们在来看看查询(我们可以分析出它的效率要比ArrayList低了不少)
3:Set
Set也是一个集合,但是他的特点是不可以有重复的对象,所以Set最常用的就是测试归属性,很容易的询问出某个对象是否存在Set中。并且Set是具有和Collection完全一样的接口,没有额外的功能,只是表现的行为不同。
3.1:HashSet
HashSet查询速度比较快,但是存储的元素是随机的并没有排序,下面我写一段程序看一下
public static void main(String[] args){
/**
* 没有顺序可循,这是因为hashset采用的是散列(处于速度考虑)
*/
Random random=new Random();
Set<Integer> intset=new HashSet<Integer>();
for (int i=0;i<10000;i++){
intset.add(random.nextInt(30)); // random.nextInt() 返回一个 Int 类型随机数,并从给定的随机数发生器的序列中均匀分布 Int 值
}
System.out.print(intset);
}
可以看到在HashSet列表中元素的存储是随机且没有顺序的,如果你需要让所存取的数据有序则可以考虑Set集合的另一种形式:TreeSet
public static void main(String[] args){
Random random=new Random();
Set<Integer> intset=new TreeSet<Integer>();
for (int i=0;i<10000;i++){
intset.add(random.nextInt(30));
}
System.out.print(intset);
}
4:Queue
Queue是队列,队列是典型的先进先出的容器,就是从容器的一端放入元素,从另一端取出,并且元素放入容器的顺序和取出的顺序是相同的。LinkedList提供了对Queue的实现,LinkedList向上转型为Queue。其中Queue有offer、peek、element、pool、remove等方法
offer是将元素插入队尾,返回false表示添加失败。peek和element都将在不移除的情况下返回对头,但是peek在对头为null的时候返回null,而element会抛出NoSuchElementException异常。poll和remove方法将移除并返回对头,但是poll在队列为null,而remove会抛出NoSuchElementException异常,以下是例子
public static void main(String[] args){
Queue<Integer> queue=new LinkedList<Integer>();
Random rand=new Random();
for (int i=0;i<10;i++){
queue.offer(rand.nextInt(i+10));
}
printQ(queue);
Queue<Character> qc=new LinkedList<Character>();
for (char c:"HelloWorld".toCharArray()){
qc.offer(c);
}
System.out.println(qc.peek());
printQ(qc);
List<String> mystrings=new LinkedList<String>();
mystrings.add("1");
mystrings.get(0);
Set<String> a=new HashSet<String>();
Set<String> set=new HashSet<String>();
set.add("1");
}
public static void printQ(Queue queue){
while (queue.peek() != null){
System.out.print(queue.poll());
}
System.out.println();
}
同样的,从上面的输出的结果我们可以看出结果并不是一个顺序的,没有规则的,这个时候如果想让队列按照规则输出那么这个时候我们就要考虑优先级了,这个时候我们就应该使用PriorityQueue,这个时候如果在调用offer方法插入一个对象的时候,这个对象就会按照优先级在对列中进行排序,默认的情况是自然排序,当然我们可以通过Comparator来修改这个顺序(在下一篇讲解)。PriorityQueue可以确保当你调用peek、pool、remove方法时,获取的元素将是对列中优先级最高的元素。ok我们再次通过代码查看
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();
Random rand = new Random();
for (int i = 0; i < 10; i++) {
priorityQueue.offer(rand.nextInt(i + 10));
}
QueueDemo.printQ(priorityQueue);
List<Integer>ints= Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25);
priorityQueue=new PriorityQueue<Integer>(ints);
QueueDemo.printQ(priorityQueue);
}
PriorityQueue 实现比较器 Comparator 代码示例:
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return -o1.compareTo(o2); // 从大到小排序
}
});
Random rand = new Random();
for (int i = 0; i < 10; i++) {
priorityQueue.offer(rand.nextInt(i + 10));
}
printQ(priorityQueue);
List<Integer>ints= Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25);
priorityQueue=new PriorityQueue<Integer>(ints);
printQ(priorityQueue);
}
public static void printQ(Queue queue){
while (queue.peek() != null){
System.out.print(queue.poll() + " ");
}
System.out.println();
}
从输出可以看到,重复是允许的,最小值拥有最高优先级(如果是String,空格也可以算作值,并且比字母具有更高的优先级)如果你想消除重复,可以采用Set进行存储,然后把Set作为priorityQueue对象的初始值即可。
Queue 集合依然可以使用循环遍历的方式读取数据:
import java.util.*;
public class QueueDemo {
public static void main(String[] args) {
//add()和remove()方法在失败的时候会抛出异常(不推荐)
Queue<String> queue = new LinkedList<String>();
//添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
for(String q : queue){
System.out.print(q + " ");
}
System.out.print("|| ");
System.out.print("poll="+queue.poll() + "|"); //返回第一个元素,并在队列中删除
for(String q : queue){
System.out.print(q + " ");
}
System.out.print("|| ");
System.out.print("element="+queue.element() + "|"); //返回第一个元素
for(String q : queue){
System.out.print(q + " ");
}
System.out.print("|| ");
System.out.print("peek="+queue.peek() + "|"); //返回第一个元素
for(String q : queue){
System.out.print(q + " ");
}
}
}
5:Map
Map在实际开发中使用非常广,特别是HashMap,想象一下我们要保存一个对象中某些元素的值,如果我们在创建一个对象显得有点麻烦,这个时候我们就可以用上map了,HashMap采用是散列函数所以查询的效率是比较高的,如果我们需要一个有序的我们就可以考虑使用TreeMap。这里主要介绍一下HashMap的方法,注意HashMap的键可以是null,而且键值不可以重复,如果重复了以后就会对第一个进行键值进行覆盖。
put进行添加值键对,containsKey验证主要是否存在、containsValue验证值是否存在、keySet获取所有的键集合、values获取所有值集合、entrySet获取键值对。
public static void main(String[] args){
//Map<String,String> pets=new HashMap<String, String>();
Map<String,String> pets=new TreeMap<String, String>();
pets.put("1","张三");
pets.put("2","李四");
pets.put("3","王五");
if (pets.containsKey("1")){
System.out.println("已存在键1");
}
if (pets.containsValue("张三")){
System.out.println("已存在值张三");
}
Set<String> sets=pets.keySet();
Set<Map.Entry<String , String>> entrySet= pets.entrySet(); // Map.Entry 可以理解为一个新的类型,类似于常见的 String 类型
Collection<String> values= pets.values();
for (String value:values){
System.out.println(value+";");
}
for (String key:sets){
System.out.print(key+";");
}
for (Map.Entry entry:entrySet){
System.out.println("键:"+entry.getKey());
System.out.println("值:"+entry.getValue());
}
}
6:Iterator和Foreach
现在foreach语法主要作用于数组,但是他也可以应用于所有的Collection对象。Collection之所以能够使用foreach是由于继承了Iterator这个接口。
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
Iterator iterator = new IteratorClass().iterator();
for (Iterator it=iterator;iterator.hasNext();) {
System.out.println(iterator.next());
}
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class IteratorClass{
public Iterator<String> iterator(){
return new Itr();
}
private class Itr implements Iterator<String>{
protected String[] words=("Hello Java").split(" ");
private int index=0;
public boolean hasNext() {
return index<words.length;
}
public String next() {
return words[index++];
}
public void remove() {
}
}
}
从中我们可以看出foreach循环最终是转换成 for (Iterator it=iterator;iterators.hasNext();)只不过jdk帮我们隐藏了我们无法查看。下面我们再来分析一个问题,当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException
的异常。
import java.util.Iterator;
import java.util.List;
public class AddRemoveListElement {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
for (String s : list) {
if (s.equals("B")) {
list.remove(s);
}
}
//foreach循环等效于迭代器
/*Iterator<String> iterator=list.iterator();
while(iterator.hasNext()){
String s=iterator.next();
if (s.equals("B")) {
list.remove(s);
}
}*/
}
}
异常原因
ArrayList的父类AbstarctList中有一个域modCount
,每次对集合进行修改(增添元素,删除元素……)时都会modCount++
而foreach的背后实现原理其实就是Iterator,等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量expectedModCount
,该变量会初始化和modCount
相等,但如果接下来如果集合进行修改modCount
改变,就会造成expectedModCount!=modCount
,此时就会抛出java.util.ConcurrentModificationException异常
过程如下图:
我们再来根据源码详细的走一遍这个过程
/*
*AbstarctList的内部类,用于迭代
*/
private class Itr implements Iterator<E> {
int cursor = 0; //将要访问的元素的索引
int lastRet = -1; //上一个访问元素的索引
int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
//判断是否还有下一个元素
public boolean hasNext() {
return cursor != size();
}
//取出下一个元素
public E next() {
checkForComodification(); //关键的一行代码,判断expectedModCount和modCount是否相等
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
根据代码可知,每次迭代list时,会初始化Itr的三个成员变量
int cursor = 0; //将要访问的元素的索引
int lastRet = -1; //上一个访问元素的索引
int expectedModCount = modCount; //预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
接着调用hasNext()
循环判断访问元素的下标是否到达末尾。如果没有,调用next()
方法,取出元素。
而最上面测试代码出现异常的原因在于,next()
方法调用checkForComodification()
时,发现了modCount != expectedModCount
接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。
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的add、remove、clear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。
异常的解决
1. 单线程环境
好,现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。
我很任性,我就是想在迭代集合时删除集合的元素,怎么办?
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String str = iter.next();
if( str.equals("B") )
{
iter.remove();
}
}
细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;
保证了不会抛出java.util.ConcurrentModificationException异常。
但是,这个办法的有两个弊端
1.只能进行remove操作,add、clear等Itr中没有。
2.而且只适用单线程环境。
2. 多线程环境
在多线程环境下,我们再次试验下上面的代码
public class Test2 {
static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
new Thread() {
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName() + ":"
+ iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
new Thread() {
public synchronized void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName() + ":"
+ element);
if (element.equals("c")) {
iterator.remove();
}
}
};
}.start();
}
}
异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。
此时有两个办法:
- 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
public class Test2 {
static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
new Thread() {
public void run() {
Iterator<String> iterator = list.iterator();
synchronized (list) {
while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName()
+ ":" + iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
}.start();
new Thread() {
public synchronized void run() {
Iterator<String> iterator = list.iterator();
synchronized (list) {
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName()
+ ":" + element);
if (element.equals("c")) {
iterator.remove();
}
}
}
};
}.start();
}
}
- 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
public class Test2 {
static List<String> list = new CopyOnWriteArrayList<String>();
public static void main(String[] args) {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
new Thread() {
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName()
+ ":" + iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
}.start();
new Thread() {
public synchronized void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName()
+ ":" + element);
if (element.equals("c")) {
list.remove(element);
}
}
};
}.start();
}
}
CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。
深入理解异常—fail-fast机制
到这里,我们似乎已经理解完这个异常的产生缘由了。
但是,仔细思考,还是会有几点疑惑:
- 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
- ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
我们来看看源码中modCount的注解
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;
我们注意到,注解中频繁的出现了fail-fast
那么fail-fast
(快速失败)机制是什么呢?
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。
7:Collections和Arrays
这里只介绍2个常用的Collections.addAll和Arrays.asList
addAll:
asList采用的是数组
可以看出最终转换成ArrayList。
8:总结
1):数组是将数字和对象联系起来,它保存明确的对象,查询对象时候不需要对查询结果进行转换,它可以是多维的,可以保存基本类型的数据,但是数组一旦生成,其容量不能改变。所以数组是不可以直接删除和添加元素。
2):Collection保存单一的元素,而Map保存相关联的值键对,有了Java泛型,可以指定容器存放对象类型,不会将错误类型的对象放在容器中,取元素时候也不需要转型。而且Collection和Map都可以自动调整其尺寸。容器不可以持有基本类型。
3):像数组一样,List也建立数字索性和对象的关联,因此,数组和List都是排好序的容器,List可以自动扩容
4):如果需要大量的随机访问就要使用ArrayList,如果要经常从中间插入和删除就要使用LinkedList。
5):各种Queue和Stack由LinkedList支持
6):Map是一种将对象(而非数字)与对象相关联的设计。HashMap用于快速访问,TreeMap保持键始终处于排序状态,所以不如HashMap快,而LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问的能力
7):Set不接受重复的元素,HashSet提供最快的访问能力,TreeSet保持元素排序状态,LinkedHashSet以插入顺序保存元素。