1. 异常处理
异常是程序中的一些错误,但并不是所有的错误都是异常,并且有些错误是可以避免的。
异常通常发生的原因:
- 用户输入了非法数据
- 要打开的文件不存在
- 网络通信时连接中断,或者JVM内存溢出
异常的类型:
- 检查性(可查)异常:最具有代表性的是用户错误或问题引起的异常,这往往是开发时较难遇见的
- 运行时异常:运行时异常可能被避免
-
错误
图1. 异常类层次结构图
1.3 处理异常机制:
- 抛出异常
当一个方法出现错误引发异常时,方法创建异常对象,并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。
public class CodeExercise {
static void pop() throws NegativeArraySizeException{
// 定义方法并抛出异常
int[] arr = new int[-1];
}
public static void main(String[] args) {
// try 语句处理异常信息
try{
pop();
}
// catch来创建异常对象并且捕捉异常
catch(NegativeArraySizeException e){
System.out.println("pop()方法抛出异常");
}
}
}
- 捕获异常
在方法抛出异常之后,运行时系统将寻找合适的异常处理器(Exception Handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并去执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止,同时Java程序终止。
由于运行时异常的不可查性,为了更合理、更容易地实现应用程序。Java规定,运行时异常将由Java运行时,系统自动抛出,允许应用程序忽略运行时异常。
对于所有可查异常,Java规定,一个方法必须捕捉,或者声明抛出方法。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的情况,也属于合理的应用程序不该捕捉的异常。
能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时,系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单来说,异常总是先被抛出,后被捕捉的。
从方法中抛出的任何异常都必须使用throws子句。
总体来讲,对于可查异常必须捕捉,或者声明抛出。允许忽略不可查的RuntimeException和Error。
1.3.1 抛出异常的三种形式
- 系统自动抛出:当程序出现一些逻辑错误、转换错误时,系统会自动抛出异常。例如:“abc” 转int,除数为0等。
- throws用来标明一个成员函数可能抛出的各种异常。出现在方法头。
public void add() throws NumberFormatException{ // 可以明确抛出某个异常
// Code
}
public void add() throws Exception{ // 也可以抛出全部异常
// Code
}
- throw语句用来明确的抛出一个异常。出现在方法体。
public void add() {
int a = 5;
int b = 1;
if(b == 0){
// 抛出的异常必须明确
throw new ArithmeticException();
}
else{
System.out.println(a/b);
}
}
1.3.2 捕获异常:try、catch 和finally
1. try-catch 语句
在Java中,异常通过try-catch 语句捕获。
try{
// 监控区域 可能会发生异常的代码
}catch(Type1 id1){
// 捕获并处置try抛出的异常率类型Type1
}catch(Type2 id2){
// 捕获并处置try抛出的异常率类型Type2
}
例1:捕捉throw语句抛出的“除数为0”的异常
public class CodeExercise {
public static void main(String[] args) {
int a = 6;
int b = 0;
try{
if(b == 0) throw new ArithmeticException();
System.out.println("a/b的值是: " + a/b);
}catch(ArithmeticException e){
System.out.println("出现异常,分母不能为零");
}
System.out.println("End");
}
}
在例1中,监控区域通过if语句进行判断,当“除数为0”的错误条件成立时引发ArithmeticException
异常,创建ArithmeticException
异常对象,并有throw
语句将异常抛给Java运行系统,由系统寻找匹配的异常处理器catch
并运行相应异常处理代码,打印输出。
事实上,“除数为0”等ArithmeticException
,是RuntimeException
的子类。故运行时由系统自动抛出,不需要使用throw
语句。
需要注意的是,一旦某个catch
捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch
语句结束,其他的catch
子句将不再有匹配。
所以,对于有多个catch
子句的异常程序而言,应该尽量将底层异常类的catch
子句放在前面,同事尽量将相对高层的异常类的catch
子句放在后面。否则,底层异常类的catch
子句可能会被屏蔽。
2. try-catch-finally 语句
finally
语句表示,无论是否发生异常,都应执行的语块。
try{
// 监控区域 可能会发生异常的代码
}catch(Type1 id1){
// 捕获并处置try抛出的异常率类型Type1
}catch(Type2 id2){
// 捕获并处置try抛出的异常率类型Type2
}finally{
// 无论是否发生异常,都将执行的语句块
}
public class CodeExercise {
public static void main(String[] args) {
int i = 0;
int[] b = {1,2,3,4};
while(i < 5){
try{
System.out.println(b[i++]);
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界");
}finally {
System.out.println("-----------------");
}
}
}
}
output:
1
-----------------
2
-----------------
3
-----------------
4
-----------------
数组越界
-----------------
注:如果try
子句中语句块的设计如下,将会出现死循环。
try{
System.out.println(b[i]);
i++;
}
output:
1
-----------------
2
-----------------
3
-----------------
4
-----------------
数组越界
-----------------
数组越界
-----------------
数组越界
-----------------
原因:
是因为当i = 4
时,b[4]
已经outofbound,故走不到i++
,故i
恒等于4
,因此导致无限循环。
注:如果try/catch
中出现return
语句时,finally
语句块将在return
之前被执行,代码及输出结果如下。
try{
System.out.println(b[i++]);
return;
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界");
}finally {
System.out.println("-----------------");
}
output:
0
-----------------
3. try-catch-finally
规则(异常处理语句的语法规则)
- 在try之后必须添加
catch
或finally
块,或同时接它们两个。 - 一个
try
块可能有多个catch
块,执行第一个匹配块。 - 可嵌套
try-catch-finally
结构 - 除了下列情况,总将执行
finally
作为结束:JVM过早终止System.exit(int)
、在finally
块中抛出一个未处理的异常。
4. try-catch-finally
中return
的执行顺序问题
// 实例一
try{
retrun 3;
}catch{
e.printStackTrace();
}finally{
return 4;
}
// 实例二
try{
int x = 3;
retrun x;
}catch{
e.printStackTrace();
}finally{
x++;
}
output:
// 实例一
4
// 实例二
3
原因:finally
的业务操作是在try
中return
调用之前。实例一中,执行完try
中的代码,return
的操作会先存储到一个临时的堆栈中,此时不返回,随后执行finally
中的业务代码,如果finally
中有return
操作,那么就会把finally
中的return
值与try
中的return
值进行替换。故,实例一中x
被替换,
1.4 区别error
和exception
- 首先Exception和Error都是继承于Throwable类,在Java中,只有Throwalbe类型的实例才可以被抛出或者捕获,Exception和Error体现了Java对于异常处理的两种方式。
- Exception是Java程序运行中可预料的异常情况,我们可以获取到这种异常,并且对这种异常进行业务外的处理。它分为检查性异常和非检查性异常(RuntimeException)。两个根本的区别在于,检查性异常必须使用try-catch捕获(IOException)。非检查性异常在编写代码时,可以忽略捕获操作(ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。
- Error是Java程序运行中不可预料的异常情况。这种异常情况发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能被抓取到,如OutOfMemoryError、NoClassDefFoundError等。
1.5 区别runtimeException
和一般的exception
一般的Exception 必须被显式地捕获,而RuntimeException往往可以不必捕获或抛出。
自定义Exception实例:
以下实例是一个银行账户的模拟,可以进行deposit和withdraw操作。
自定义exceptionInsufficientFundsException.java
public class InsufficientFundsException extends Exception{
private double amount;
public InsufficientFundsException(double amount){
this.amount = amount;
}
public double getAmount(){
return amount;
}
}
银行账户
public class CheckingAccount {
// 余额
private double balance;
// 卡号
private int number;
public CheckingAccount(int number){
this.number = number;
}
// 存钱
public void deposit(double amount){
balance += amount;
}
// 取钱
public void withdraw(double amount) throws InsufficientFundsException{
if(amount <= balance){
balance -= amount;
}else{
double needs = amount - balance;
// 把 差额(needs) 传递到 InsufficientFundsException
throw new InsufficientFundsException(needs);
}
}
public double getBalance(){
return balance;
}
public int getNumber(){
return number;
}
}
应用实例
public class BankDemo {
public static void main(String[] args) {
CheckingAccount c = new CheckingAccount(101);
try{
c.deposit(500);
System.out.println("the balance is "+ c.getBalance());
c.withdraw(600);
// 不会有输出,是因为 在 withdraw -> else -> throw new InsufficientFundsException
System.out.println("the balance is "+ c.getBalance());
}catch (InsufficientFundsException e){
// e.getAmount()
System.out.println("Sorry insufficient funds, there is a short of $" + e.getAmount());
e.printStackTrace();
}
}
}
2. 容器应用
2.1 数组与容器
Java中常用的存储容器就是数组和容器,二者有以下区别:
- 存储大小是否固定:
- 数组的长度固定
- 容器的长度可变
- 数据类型
- 数组可以存储基本数据类型,也可以存储引用数据类型
- 容器只能存储引用数据类型,基本数据类型的变量要转换成对应的包装类才能放入容器中
数据类型的详细概念请参考,Java基本数据类型
2.2 容器的框架
从上图可以看到,Java容器主要分为
Collection
和Map
两种。其中,Collection
又分为List
, Set
以及Queue
。
-
Collection
==> 一个独立元素的序列,这些元素都服从一条或者多条规则。-
List
==> 必须按照插入的顺序保存元素 -
Set
==> 不能有重复的元素 -
Queue
==> 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)
-
-
Map
==> 一组成对的“键值对”对象,允许你使用键来查找值。
2.3 常用容器使用及他们特性、区别
Collection
1. Set
-
TreeSet
:基于红黑树实现,支持有序性操作。例如,根据一个范围查找元素的操作。但是查找效率不如HashSet
,HashSet
查找的时间复杂度为 O(1),TreeSet
则为 O(logN) -
HashSet
:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序,也就是说,使用Iterator
遍历HashSet
得到的结果是不确定的。 -
LinkedHashSet
:具有HashSet的查找效率,并且内部使用双向链表维护元素的插入顺序。
2. List
-
ArrayList
:
基于动态数组实现,支持随机访问。
ArrayList源码分析:
因为ArrayList
是基于数组实现的,所以支持快速随机访问。RandomAccess
接口标识着该类支持快速随机访问。
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
数组的默认大小为10。
private static final int DEFAULT_CAPACITY = 10;
ArraryList 的扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
}
添加元素时,使用ensureCapacityInternal()
方法来保证容量足够,如果不够时,需要使用grow()
方法进行扩容,新容量的大小为oldCapacity + (oldCapacity >> 1)
(>> 1
是二进制向右移1位,也就是说是“/2”),即oldCapacity + oldCapacity/2。其中`oldCapacity >>1```需要取整,所以新容量大约是旧容量的1.5倍左右。
扩容操作需要调用Arrays.copyOf()
把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,减少扩容操作的次数。
删除元素
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;
}
需要调用System.arraycopy()
将index+1
后面的元素都复制到index位置上,该操作的时间复杂度为O(N),可以看到ArrayList删除元素的代价是非常高的。
序列化
序列化:Java提供了一种对象序列化的机制,该机制中,一个对象被转换为一个字节序列。该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
// non-private to simplify nested class access
transient Object[] elementData;
ArrayList基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会别使用,那么就没必要全部进行序列化。
保存元素的数组elementData
使用transient修饰,该关键字声明数组默认不会被序列化。
ArrayList实现了writeObject()
和readObject()
来控制只序列化数组中有元素填充那部分内容。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i
序列化时需要使用ObjectOutputStream
的writeObject()
将对象转换为字节流并输出。 而writeObject()
方法在传入的对象存在writeObject()
的时候会去反射调用该对象的writeObject()
来实现序列化。反序列化使用的是ObjectInputStream
的 readObject()
方法,原理类似。
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
Fail-Fast
modCount
用来记录ArrayList
结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后modCount
是否改变,如果改变了需要抛出ConcurrentModificationException
。代码参考上节序列化中的writeObject()
方法。示例代码请参考Fail-Fast 示例
List
和ArrayList
的区别:
List
是一个接口,而ArrayList
是List
接口的一个实现类。所以,ArrayList
类是继承AbstractList
抽象类和实现List
接口的一个实现类。
因此,List
接口不能被构造,也就是我们说的不能创建实例对象,但是我们可以为List
接口创建一个指向自己的对象引用,而ArrayList
实现类的实例对象就在这充当了这个指向List
接口的对象引用。例如:
public class ArraryListDemo {
public static void main(String[] args) {
Animal a = new Animal(); // error:"Animal" is abstract, cannot be instantiated.
Animal a1 = new Dog();
Dog a2 = new Dog();
a1.bark(); // output: Wow! Wow!
a1.guard(); // error: cannot resovle method "guard"
a2.bark(); // output: Wow! Wow!
a2.guard(); // output: Guard! Guard!
}
}
abstract class Animal{
String name;
public void bark(){
System.out.println("Bark!");
}
}
class Dog extends Animal{
String name;
String name1;
@Override
public void bark(){
System.out.println("Wow! Wow!");
}
public void guard(){
System.out.println("Guard! Guard!");
}
}
在上述例子中,
Animal a = new Animal();
是错误的用法,因为Animal
是一个抽象类,抽象类无法实例化。
Animal a1 = new Dog();
是创建了一个Dog
实现类的对象后,把它上溯到了Animal
抽象类。此时,它就是一个Animal
的对象了。所以,a1.guard();
会报错,因为Animal
的对象无法调用其子类的方法。a1.bark();
会输出 output: Wow! Wow!
的结果是因为,bark()
的方法在后面被Overrided。
Dog a2 = new Dog();
是创建了一个Dog
子类的对象,故它保留了所有Dog
的属性与方法。
import java.util.*;
public class Demo{
public static void main(String[] args){
List list = new ArrayList();
ArrayList arrayList = new ArrayList();
list.trimToSize(); //错误,没有该方法。
arrayList.trimToSize(); //ArrayList里有该方法。
}
}
所以,回到List
和ArrayList
。List
接口中并没有trimToSize()
方法,但这个方法在它的实现类ArrayList
中有。
在List a=new ArrayList();
中,a
拥有List
的所有属性和方法,不会拥有其实现类ArrayList
的独有的属性和方法。
如果List
和ArrayList
中有相同的属性(如int i
)和相同的方法(如void f()
),则a.i
调用的是List
中的i
。
a.f()
调用的是ArrayList
中的f()
。
那么,为什么推荐使用List a=new ArrayList();
而不是ArrayList a=new ArrayList();
?
问题就在于,List
接口有多个实现类,如果在一开始就声明ArrayList a=new ArrayList();
,在之后的代码开发中,需要换成其他的实现类,如LinkedList
或Vector
等时,则需要重新构造,因为很有可能下面调用了很多ArrayList
特有的方法和属性。而List a=new ArrayList();
则不用担心这个问题,大大增加了代码后期的可维护性,可复用性和灵活性。
ArrayList遍历方式:
ArrayList
有三种遍历方式。
- 迭代器遍历
Iterator it = arrayList.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
- 索引值遍历
for(int i = 0; i < arrayList.size(); i++){
System.out.print(arrayList.get(i) + " ");
}
- for-each循环遍历
for(Integer number : arrayList){
System.out.print(number + " ");
}
注:遍历ArrayList
时,通过索引值遍历效率最高 > for-each循环遍历 > 迭代器遍历。
toArray()
的使用
当我们调用ArrayList
中的toArray()
,可能遇到过抛出java.lang.ClassCastException
异常的情况,这是由于toArray()
返回的是object[]
数组,将Object[]
转换为其他类型(如,将Object[]
转换为Integer[]
)则会抛出java.lang.ClassCastException
异常,因为Java不支持向下转型。
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo2 {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
// 第一种
Integer[] integer = list.toArray(new Integer[0]);
System.out.println("toArray 第一种");
for(int i=0; i
ArrayList
用法示例:
// 指定位置添加指定元素
list.add(2,4);
System.out.println(list);
// 删除指定index上的元素
list.remove(4);
System.out.println(list);
// 删除指定元素
list.remove((Object)2);
System.out.println(list);
// 判断是否为空
System.out.println("the list contains 2? " + list.contains(2));
// 清空
list.clear();
System.out.println(list);
// 判断是否为空
System.out.println("the list is empty? " + list.isEmpty());
CopyOnWriteArrayList源码分析:
1. 读写分离
如其名,CopyOnWrite, 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后,需要把原始数组指向新的复制数组。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
2. 适用场景
CopyOnWriteArrayList
在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是CopyOnWriteArrayList
有其缺陷:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右。
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以,CopyOnWriteArrayList
不适合内存敏感以及对实时性有很高要求的场景。
-
Vector
:
和ArrayList
类似,但它是线程安全的。
Vector源码分析:
1. 同步
它的实现与ArrayList
类似,但是使用了synchronized
进行同步。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
2. 扩容
Vector
的构造函数可以传入capacityIncrement
参数,它的作用是在扩容时是容量增长capacityIncrement
。如果这个参数的值小于0,扩容时每次都令capacity为原来的两倍。
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
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);
}
调用没有capacityIncrement
的Vector
的构造函数时,默认值为0,即默认情况下Vector
每次扩容时容量都会翻倍。
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
3. 与ArrayList
的比较
-
Vector
是同步的,因此开销就比ArrayList
要大,访问速度更慢。故更推荐使用ArrayList
,因为同步操作完全可以自己来控制。 -
Vector
每次扩容请求其大小的至少2倍,而ArrayList
是1.5倍。
4. 替代方案
可以使用Collection.synchronizedList();
,得到一个线程安全的ArrayList
。
List list = new ArrayList<>();
List synList = Collections.synchronizedList(list);
也可以使用concurrent
并发包下的CopyOnWriteArrayList
类。
List list = new CopyOnWriteArrayList<>();
-
LinkedList
:
基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList
还可以用作栈、队列和双向队列。
LinkedList源码分析:
1. 概览
基于双向链表的实现,使用Node
存储链表节点信息。
private static class Node {
E item;
Node next;
Node prev;
}
每个链表存储了first
和last
指针:
transient Node first;
transient Node last;
LinkedList
继承了AbstractSequentialList
类。
LinkedList
实现了Queue
接口,可作为队列使用。
LinkedList
实现了List
接口,可进行列表的相关操作。
LinkedList
实现了Deque
接口,可作为队列使用。
LinkedList
实现了Cloneable
接口,可实现克隆。
LinkedList
实现了java.io.Serializable
接口,即可支持序列化,能通过序列化去传输。
2. 与ArrayList
的比较
ArrayList
是基于动态数组实现的,LinkedList
是基于双向链表实现的。它们之间的区别可以归结为数组和链表的区别:
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素。
- 链表不支持随机访问,但插入删除只需要改变指针。
所以,与ArrayList
相比,LinkedList
的增加和删除对操作效率更高,而查找和修改的操作效率较低。
以下情况使用ArrayList
:
- 频繁访问列表中的某一元素。
- 只需要在列表末尾进行添加和删除元素操作。
以下情况使用LinkedList
:
- 需要通过循环迭代来访问列表中的某些元素。
- 需要频繁的在列表开头,中间,末尾等位置进行添加和删除元素操作。
3. LinkedList
用法示例
add()
:添加
addFirst()
:添加首元素
addLast()
:添加尾元素
removeFirst()
:删除首元素
removeLast()
:删除尾元素
getFirst()
:查看首元素
getLast()
:查看尾元素
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList list = new LinkedList<>();
list.add("One");
list.add("Two");
list.add("Three");
list.add("Four");
list.addFirst("addFirst");
list.addLast("addLast");
list.removeFirst();
list.removeLast();
System.out.println(list.getFirst());
System.out.println(list.getLast());
}
}
3. Queue
-
LinkedList
:可以用它来实现双向队列。 -
PriorityQueue
:基于堆结构,可以用它来实现优先队列。
Map
Java为数据结构中的映射定义了一个接口
java.util.Map
,此接口主要有四个常用的实现类,分别是HashMap
, Hashtable
, LinkedHashMap
和TreeMap
。类继承关系如上图所示。
HashMap:它根据Key的HashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序确实不确定的。HashMap最多只允许一条记录的Key为null,允许多条记录的Value为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,故可能导致数据的不一致。如果需要满足线程安全,可以用
Collections
的synchronizedMap
方法使HashMap具有线程安全能力,或者使用ConcurrentHashMap
。Hashtable:Hashtabl是遗留类,很多映射的常用功能与HashMap类似,不同的是它继承自
Dictionary
类,并且是线程安全的,任一时间只有一个线程能写Hashtable。但并发性不如ConcurrentHashMap
,因为ConcurrentHashMap
引入了分段锁。所以,Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的可以用ConcurrentHashMap
替换。LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用
Iterator
遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。TreeMap:TreeMap实现
SortedMap
接口,能够把它保存的记录根据Key排序,默认是按Key的值升序排序的,也可以指定排序的比较器。当用Iterator
遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,Key必须实现Comparable
接口或者在构造TreeMap传入自定义的Comparator
,否则会在运行时抛出java.lang.ClassCastException
异常。
对于上述四种Map类型的类,要求映射中的Key是不可变对象。不可变对象是指该对象在创建后,它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
通过上面的比较,我们知道了HashMap是Java的Map家族中的一个普通成员。鉴于它可以满足大多数场景的使用条件,所以是使用频率最高的一个。下文我们主要结合源码,从存储结构、常用方法分析、扩容以及安全性等方面,深入讲解HashMap的工作原理
HashMap源码分析:
1. 存储结构
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树的部分)实现的,如下所示:
这里需要讲明白两个问题:数据底层具体存储的是什么?这样存储的方式有什么优点?
(1)从源码可知,HashMap类中有一个非常重要的字段——
Node[] table
,即哈希桶数组,明显它是一个Node
的数组。
static class Node implements Map.Entry {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node next; //链表的下一个node
Node(int hash, K key, V value, Node next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
Node是HashMap的一个内部类,实现了Map.Entry
接口,本质就是一个映射(Key-Value pair)。上图中的每个黑色圆点就是一个Node对象。
(2)HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题。Java中HashMap采用了链地址法。链地址法简单来说,就是数组加链表的结合。在每个数组元素上都有一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
例如程序执行下面代码:
map.put("Hello","World");
-
- 系统将调用 “Hello” 这个Key的
hashCode()
方法得到其hashCode
值(该方法适用于每个Java对象)。
- 系统将调用 “Hello” 这个Key的
-
- 然后再通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置。有时两个Key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小。Map的存取效率就越高
如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组很小,即使好的Hash算法也会出现比较多的碰撞。所以就需要在空间成本和时间成本之间权衡。在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的Hash算法减少Hash碰撞。那么通过什么方式来控制Map使得Hash碰撞的概率又小,哈希桶数组(Node[] table
)占用空间又少呢?答案就是好的Hash算法和扩容机制来实现。
- 然后再通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置。有时两个Key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小。Map的存取效率就越高
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化:
transient int size;
transient int modCount;
int threshold; //负载因子
final float loadFactor; //所能容纳的Key-Value对极限
首先,Node[] table
的初始化长度static final int DEFAULT_INITIAL_CAPACITY = 16;
(默认值是16),loadFactor
是负载因子,默认值是0.75(static final float DEFAULT_LOAD_FACTOR = 0.75F;
),threshold
是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold
= DEFAULT_INITIAL_CAPACITY
* DEFAULT_LOAD_FACTOR
。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
结合负载因子的定义公式可知,threshold
就是在此负载因子和数组长度对应下允许的最大元素数目,超过threshold
就需要重新resize(扩容),扩容后的HashMap容量是之前的两倍。默认的DEFAULT_LOAD_FACTOR
为0.75是对空间和时间效率的一个平衡选择,一般不做更改。除非在相对特殊的情况下:如果内存空间很多而对时间效率要求很高,可以降低loadFactor
的值;相反,如果内存空间紧张而时间效率要求不高,可以增加loadFactor
的值,loadFactor
可以大于1。
size
这个字段就很好理解——HashMap中实际存在的键值对数量。注意threshold
是能够容纳键值对的最大数量,DEFAULT_INITIAL_CAPACITY
是默认Node
数组长度。
modCount
字段同在ArrayList
中的用法一样,用来记录HashMap中内部结构发生变化的次数,主要用于Fail-Fast。内部结构发生变化指的是如put新的键值对的结构变化,而非某个Key对应的Value值被覆盖。
但,即使负载因子和Hash算法设计的再合理,也不免会出现拉链过长的情况。一旦拉链过长,则会影响HashMap的性能。于是,在JDK1.8版本中,加入了红黑树。当链表长度超过8时,链表就转换为红黑树。利用红黑树快速增删改查的特点,提高HashMap的性能。
2. 功能实现
本节主要从根据Key获取哈希桶数组index位置、put方法的详细执行以及扩容过程三个方面来开展。
2.1确定哈希桶数组index位置
由于HashMap的数据结构是数组和链表的结合,所以理想状态就是其中的元素尽量分布均匀,使得每个位置上的元素只有一个,不用遍历链表。所以,HashMap定位数组index位置,直接决定了hash方法的离散性能。
源码实现:
static final int hash(Object var0) {
// var1 = var0.hashCode() 为第一步 取hashCode值
// var1 ^ (var1 >>> 16) 为第二步 高位运算
int var1;
return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
}
static int indexFor(int var1, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return var1 & (length-1); // 为第三步 取模运算
}
Hash算法本质上就是三步:取Key的hashCode值,高位运算,取模运算。
如下举例:
注:
101 & 100 => 100
对应位都是1 结果为1,否则为0
1101 ^ 1010 => 0111
对应位相同为0,否则为1
1111 0000 >>> 4 => 1111
向右移4位
在JDK1.8的hashCode实现中,通过高16位 异或 底16位来优化高位运算的的算法(
var1 ^ var1 >>> 16
高 ^ 低)。主要是从速度、功效、质量来考虑的,这么做可以在数组长度比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
在第三步取模运算中,由于模运算的开销比较大,故通过var1 & (length-1)
来得到该对象的保存位。取模(%
)的本质就是为了当数据相对离散时,所得的结果也相对平均且分散。但&
运算比%
运算具有更高的效率。而HashMap底层数组的长度总是2的n次方(这是HashMap在速度上的优化)。当length总是2的n次方时,h& (length-1)运算等价于对length取模。
2.2 分析HashMap的put方法
2.3 扩容过程
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
2.4 线程安全性
在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurrentHashMap。在并发的多线程使用场景中使用HashMap可能造成死循环。
2.5 JDK1.8与JDK1.7的对比
3 HashMap 应用示例
HashMap hashmap = new HashMap<>();
hashmap.put(1, "Hello")
hashmap.get(1)
hashmap.remove(1)
hashmap.clear()
hashmap.size()
import java.util.HashMap;
public class HashMapDemo {
public static void main(String[] args) {
HashMap hashmap = new HashMap<>();
hashmap.put(1,"hello");
hashmap.put(2,"oppo");
hashmap.put(3,"Matthew");
// keySet()返回所有key的集合
for(Integer i : hashmap.keySet()){
System.out.println("key is " + i + " value is " + hashmap.get(i));
}
// values() 返回所有value的值
for(String i : hashmap.values()){
System.out.print(i + ", ");
}
}
}
4 HashMap与Hashtable的比较
外部可调用的方法
Null Key & Null Value
HashMap是支持null键和null值的。
而Hashtable在遇到null时,会抛出NullPointerException异常
线程安全
Hashtable是同步的,HashMap不是,也就是说Hashtable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行。
也正是因为Hashtable是线程安全的,所以效率比较低下。
并且Hashtable是继承自Dictionary类,而Dictionary类是一个已经被废弃的类。
ConcurrentHashMap源码简析:
存储结构:
ConcurrentHashMap
和HashMap实现上类似,最主要的差别是ConcurrentHashMap
采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是Segment的个数)。
Segment继承自ReentrantLock。
static final class Segment extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
final Segment[] segments;
默认的并发级别为16,也就是说默认创建16个Segment。
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
size操作:
每个Segment维护了一个count变量来统计该Segment中的键值对个数。
/**
* The number of elements. Accessed only either within locks
* or among other volatile reads that maintain visibility.
*/
transient int count;
在执行size操作时,需要遍历所有Segment然后把count累计起来。
ConcurrentHashMap
在执行size操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
尝试次数使用RETRIES_BEFORE_LOCK
定义,该值为2,retries
初始值为-1,因此尝试次数为3。
如果尝试的次数超过3次,就需要对每个Segment加锁。
/**
* Number of unsynchronized retries in size and containsValue
* methods before resorting to locking. This is used to avoid
* unbounded retries if tables undergo continuous modification
* which would make it impossible to obtain an accurate result.
*/
static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
1. ConcurrentHashMap 应用示例
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentHashMapDemo implements Runnable{
private final static ConcurrentHashMap conMap = new ConcurrentHashMap<>();
// 注意:latch 的个数 与 main中的for循环次数要匹配
// 例如: 如果 latch 大于 循环次数,则运行不会结束。因为latch.countDown永远无法到0;
// 如果latch 小于 循环次数,map.size要取决于 线程池的大小。
private final static CountDownLatch latch = new CountDownLatch(100);
private String key;
private Integer value = 0;
public ConcurrentHashMapDemo(String key,Integer value){
this.key = key;
this.value = value;
}
@Override
public void run() {
conMap.put(key,value);
latch.countDown();
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
try {
for (int i = 1; i <= 100; i++) {
ConcurrentHashMapDemo demo = new ConcurrentHashMapDemo("Key :" + i, i);
threadPool.execute(demo);
}
latch.await();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
threadPool.shutdown();
}
System.out.println("the size of conMap is "+conMap.size());
System.out.println(conMap);
}
}
output:
the size of conMap is 100
{Key :9=9, Key :19=19, Key :18=18, Key :20=20, Key :26=26,....}
更多的方法,可以参考:链接
2. ConcurrentHashMapJDK 1.7和 JDK 1.8的区别
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
LinkedHashMap源码简析:
参考资料
继承自HashMap,因此具有和HashMap一样的快速查找特性。
public class LinkedHashMap extends HashMap implements Map
内部维护了一个双向链表,用来维护插入顺序或者LRU顺序。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry tail;
accessOrder 决定了顺序,默认为false,此时维护的是插入顺序。
final boolean accessOrder;
LinkedHashMap最重要的是以下用于维护顺序的函数,它们会在put
,get
等方法中调用。
void afterNodeAccess(Node p) { }
void afterNodeInsertion(boolean evict) { }
afterNodeAccess()
:
当一个节点被访问时,如果accessOrder
为true
,则会将该节点移到链表尾部。也就是说,指定为LRU顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
void afterNodeAccess(Node e) { // move node to last
LinkedHashMap.Entry last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
afterNodeInsertion()
在put
等操作之后执行,当removeEldestEntry()
方法返回true
时,会移除最晚的节点,也就是链表首部节点first。
evict只有在构建Map的时候才会为false,在这里为true。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
removeEldestEntry()
默认为false,如果需要让它为true,需要继承LinkedHashMap并且覆盖这个方法的实现,这在实现LRU的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
LRU缓存
以下是使用LinkedHashMap实现的一个LRU缓存:
- 设定最大缓存空间MAX_ENTRIES为3;
- 使用LinkedHashMap的构造函数将
accessOrder
设置为true,开启LRU顺序; - 覆盖
removeEldestEntry()
方法实现,在节点多于MAX_ENTRIES就会将最近最久未使用的数据移除。
class LRUCache extends LinkedHashMap {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
public static void main(String[] args) {
LRUCache cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);
cache.put(4, "d");
System.out.println(cache.keySet());
}
output:
[3, 1, 4]
2.4 容器的基本机制
Java的容器具有一定的共性,它们或全部或部分依赖一下技术。所以,学习一下技术点,对于理解Java容器的特性和原理有很大的帮助。
2.4.1 泛型
Java容器通过泛型技术来保证其数据的类型安全。
类型安全:如果有一个List
容器,Java编译器在编译时不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
未使用泛型:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
使用泛型:
List list = new ArrayList();
list.add("hello");
String s = list.get(0); // no cast
想深入了解Java泛型技术的用法和原理,可以参考:[泛型深入]。(https://github.com/dunwu/javacore/blob/master/docs/basics/java-generic.md)
2.4.2 Iterable 和 Iterator
Iterable
和Iterator
目的在于遍历访问容器中的元素。
Iterator
接口定义:
public interface Iterator {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Iterable
接口定义:
public interface Iterable {
Iterator iterator();
default void forEach(Consumer action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Collection
接口扩展了 iterable
接口。
迭代可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的接口。它是一个经典的设计模式——迭代器模式(Iterator)。
迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又无需暴露该对象的内部表示。
示例:迭代器遍历
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CodeDemo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(4);
list.add(2);
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
2.4.3 Comparable 和 Comparator
Comparable
是排序接口。若一个类实现了Comparable
接口,表示该类的实例可以比较,也就意味着支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort
或Arrays.sort
进行自动排序。
Comparable
接口的定义:
public interface Comparable {
public int compareTo(T o);
}
Comparator
是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable
接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator
接口即可。也就是说,我们可以通过实现Comparator
来新建一个比较器,然后通过这个比较器对类进行排序。
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
// 反转
default Comparator reversed() {
return Collections.reverseOrder(this);
}
default Comparator thenComparing(Comparator other) {
Objects.requireNonNull(other);
return (Comparator & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
// thenComparingXXX 方法略
// 静态方法略
}
在Java容器中,一些可以排序的容器,如TreeMap
、TreeSet
,都可以通过传入Comaprator
,来定义内部元素的排序规则。
示例:Comparator
2.4.4 Cloneable
Java中一个类要实现clone
功能,必须实现Cloneable
接口,否则在调用clone()
时会报CloneNotSupportedException
异常。
Java中所有类都默认继承java.lang.object
类,在java.lang.object
类中有一个方法clone()
,这个方法将返回Object
对象的一个拷贝。Object
类里的clone()
方法仅仅用于浅拷贝(即拷贝基本成员属性,对于引用类型仅返回指向该地址的引用)。
如果需要深拷贝,需要Override clone()
方法。
2.4.5 fail-fast
fail-fast 的要点
fail-fast 是java容器的一种错误检测机制。当多个线程对容器进行结构上的改变的操作时,就可能触发fail-fast机制。
例如:线程1通过Iterator
在遍历容器A中的元素,在某个时候,线程2修改了容器A的结构(是结构上的修改,而非简单地修改容器元素的内容)。那么这个时候程序就会抛出ConcurrentModificationException
异常,从而产生fail-fast机制。容器在迭代操作中改变元素个数(添加,删除)都可能会导致fail-fast。
fail-fast示例
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Fail_fastDemo {
private static int MAX = 100;
private static List list = new ArrayList<>();
public static void main(String[] args) {
for(int i=0; i iterator = list.iterator();
while(iterator.hasNext()){
System.out.println("访问元素:" + iterator.next());
try{
TimeUnit.MILLISECONDS.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
// MyThreadB删除所有的偶数
public static class MyThreadB implements Runnable{
@Override
public void run() {
for(int i=0; i
解决fail-fast
fail-fast 有两种解决方案:
- 在遍历过程中,所有涉及到改变容器个数的地方全部加上``synchronized
或者直接使用
Collections.synchronizedXXX```容器,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作,影响吞吐。 - 使用并发容器,如:
CopyOnWriterArrayList
。
2.4.6 适配器模式
java.util.Arrays#asList()
可以把数组类型转换成List类型。
@SafeVarags
public static List asList(T... a)
注意:asList()
的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组作为参数。例如:
Integer[] arr = {1,2,3};
List list = Arrays.asList(arr);
也可以使用一下方式调用asList()
List list = Arrays.asList(1,2,3);s
Java容器的一些基本问题