Java知识回顾总结(2)

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规则(异常处理语句的语法规则)

  1. 在try之后必须添加catchfinally块,或同时接它们两个。
  2. 一个try块可能有多个catch块,执行第一个匹配块。
  3. 可嵌套try-catch-finally结构
  4. 除了下列情况,总将执行finally作为结束:JVM过早终止System.exit(int)、在finally块中抛出一个未处理的异常。

4. try-catch-finallyreturn的执行顺序问题

// 实例一
try{
    retrun 3;
}catch{
    e.printStackTrace();
}finally{
    return 4;
}

// 实例二
try{
    int x = 3;
    retrun x;
}catch{
    e.printStackTrace();
}finally{
    x++;
}

output:

// 实例一
4
// 实例二
3

原因:finally的业务操作是在tryreturn调用之前。实例一中,执行完try中的代码,return的操作会先存储到一个临时的堆栈中,此时不返回,随后执行finally中的业务代码,如果finally中有return操作,那么就会把finally中的return值与try中的return值进行替换。故,实例一中x被替换,

1.4 区别errorexception

  • 首先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 容器的框架

容器框架1

容器框架2

从上图可以看到,Java容器主要分为CollectionMap两种。其中,Collection又分为List, Set 以及Queue

  • Collection ==> 一个独立元素的序列,这些元素都服从一条或者多条规则。
    • List ==> 必须按照插入的顺序保存元素
    • Set ==> 不能有重复的元素
    • Queue ==> 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)
  • Map ==> 一组成对的“键值对”对象,允许你使用键来查找值。

2.3 常用容器使用及他们特性、区别

Collection

Collection

1. Set

  • TreeSet:基于红黑树实现,支持有序性操作。例如,根据一个范围查找元素的操作。但是查找效率不如HashSetHashSet查找的时间复杂度为 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

序列化时需要使用ObjectOutputStreamwriteObject()将对象转换为字节流并输出。 而writeObject()方法在传入的对象存在writeObject()的时候会去反射调用该对象的writeObject()来实现序列化。反序列化使用的是ObjectInputStreamreadObject()方法,原理类似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
Fail-Fast

modCount用来记录ArrayList结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化
在进行序列化或者迭代等操作时,需要比较操作前后modCount是否改变,如果改变了需要抛出ConcurrentModificationException。代码参考上节序列化中的writeObject()方法。示例代码请参考Fail-Fast 示例

List list = new ArrayList<>()ArrayList list = new ArrayList<>()的区别:
List是一个接口,而ArrayListList接口的一个实现类。所以,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里有该方法。
  } 
}

所以,回到ListArrayListList接口中并没有trimToSize()方法,但这个方法在它的实现类ArrayList中有。
List a=new ArrayList();中,a拥有List的所有属性和方法,不会拥有其实现类ArrayList独有的属性和方法。
如果ListArrayList中有相同的属性(如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();,在之后的代码开发中,需要换成其他的实现类,如LinkedListVector等时,则需要重新构造,因为很有可能下面调用了很多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);
}

调用没有capacityIncrementVector的构造函数时,默认值为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;
}

每个链表存储了firstlast指针:

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, LinkedHashMapTreeMap。类继承关系如上图所示。

  • HashMap:它根据Key的HashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序确实不确定的。HashMap最多只允许一条记录的Key为null,允许多条记录的Value为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,故可能导致数据的不一致。如果需要满足线程安全,可以用CollectionssynchronizedMap方法使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");
    1. 系统将调用 “Hello” 这个Key的hashCode()方法得到其hashCode(该方法适用于每个Java对象)。
    1. 然后再通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置。有时两个Key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小。Map的存取效率就越高
      如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组很小,即使好的Hash算法也会出现比较多的碰撞。所以就需要在空间成本和时间成本之间权衡。在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的Hash算法减少Hash碰撞。那么通过什么方式来控制Map使得Hash碰撞的概率又小,哈希桶数组(Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制来实现

在理解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() :
当一个节点被访问时,如果accessOrdertrue,则会将该节点移到链表尾部。也就是说,指定为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

IterableIterator目的在于遍历访问容器中的元素。
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.sortArrays.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容器中,一些可以排序的容器,如TreeMapTreeSet,都可以通过传入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容器的一些基本问题

你可能感兴趣的:(Java知识回顾总结(2))