1.集合体系补充(1)

1.接口式引用

集合的构造,我们需要采用接口类型引用的的方式,这样做的好处就是方便根据业务或者设计上的变化,快速更换具体的实现。

事实上,Java集合设计体系者也是支持我们这样做的,并且集合体系的设计也是如此的。

创建一个ArrayList实例化变量,赋值给List引用。

java 复制代码
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.forEach(System.out::println);

但是当我们思考业务之后,感觉使用栈比列表更好:

直接无缝衔接到Stack上,切换之后调用的API也不用转变,代码可维护性极高。

java 复制代码
List<Integer> list = new Stack<>();
list.add(1);
list.add(2);
list.add(3);
list.forEach(System.out::println);

之所以可以这样做的原因是:接口不会决定实现的具体效果,表现出来的效果最终由创建的实际对象来决定,又由于这样对象都实现了List接口,调用List接口的方法时会触发多态机制。

2.集合设计体系

2.1利于扩展的体系

Java集合设计者不仅仅去实现了种类繁多的集合,还为开发者方便扩展集合体系做足了准备。

从Collection入手:

Java设计者设计Collection接口的原因不只是为了设计Java的集合体系结构,更是为用户拓展提供了一些标准的实用方法声明。

Java类库使用者在使用Java集合标准实现自己的集合时,可以实现Collection接口,并实现其中的方法。

但是如果每次定义自己的集合时,都需要实现这些实用方法,就太麻烦了,于是Java集合设计者声明了新的抽象方法AbstractCollection。

AbstractCollection提供了实用的抽象方法的默认实现,除了基础方法(如size,iterator这种不同集合的实现差异性极大的以外),均提供了相应默认实现,意味着集合设计者在设计集合体系时,直接去继承AbstractCollection即可。

虽然AbstractCollection提供了默认实现,但是如果设计的集合中开发者认为这些实用的方法有更好的实现,完全可以自定义重写。

设计体系:

但是现在这种方案已经逐渐在被淘汰了,在接口默认方法(defalult关键字)出现之前,使用接口设计抽象行为,抽象类实现默认行为是被认可的,但是现在更为主流的方案是接口中实现default默认方法。

2.2集合接口体系

Java集合框架为不同类型的集合定义了大量接口。

Java集合体系被划分为:1.Collection(单数据存储)2.Map(键值对映射存储)

单数据存储的Collection又被分为了三大体系:1.List(列表)2.Set(集)3.Queue(队列)

还有两个附加的体系,迭代器机制体系Iterator接口,快速随机访问机制RandomAccess接口。

2.3集合实现类体系

具体集合体系是比较庞大的:

1.Collection体系

Collection体系主要是通过AbstractCollection默认实现了Collection常用的方法是,下面三大分支,List体系(AbstractList在AbstractCollection的基础上扩展出List常用的一些方法),Set体系(借助AbstractSet拓展),Queue体系(一部分借助AbstractQueue扩展,剩下一部分直接继承的AbstractCollection)

List体系主要是有:1.ArrayList(借助数组结构实现的列表),2.LinkedList(借助链表结构实现的列表)

Set体系主要是有:1.HashSet(无序存储,借助Hash值和Hash算法实现的,底层数据结构是HashMao)2.EnumSet(存储枚举类型数据)3.TreeSet(在HashSet的基础上增加了有序性的特点,底层数据结构是TreeMap)

Queue体系主要是有:1.PriorityQueu((优先级队列,借助数组实现的堆结构,默认是小根堆,可以传入比较器或者自定义自然排序规则来改变排序规则)2.ArrayQueue(数组实现的对立)

2.Map体系

Map体系主要借助Map接口声明了Map中应该存在的被设计好的方法,规范Map结构是如何运行的,AbstractMap被设计为实现了Map接口中的通用方法,方便子类继承。

Map体系比较庞大:所有的实现类都依托于一个抽象类AbstractMap展开,被划分为六大Map实现类。

1.HashMap:底层数据结构是Hash表,借助的是Hash算法,数组,链表,平衡树等数据结构综合应用实现。具有无序性,可以通过key快速定为value的特性。

2.LinkedHashMap:底层数据结构是Hash表和链表实现,主要是在Hash表的基础上使用链表维护了数据的顺序,方便记录元素加入的顺序。

3.TreeMap:底层数据结构是红黑树,实现了键值对存储和有序存储的功能。

4.EnumMap:存储Enum对象。

5.WeakHashMap:弱引用的HashMap。

6.IdentityHashMap:判断两个key是否一致使用的是==指针判断,而不是equals判断。

3.迭代器机制

在Java集合体系中,有一个十分重要的机制就是迭代器机制。

作者认为需要掌握的迭代器机制的内容:

3.1集合中的迭代器体系

3.1.1Iterator接口

在集合Collection接口中声明了一个抽象方法iterator,返回Iterator

java 复制代码
Iterator<E> iterator();

Iterator接口中定义了几个方法:

java 复制代码
public interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

1.hasNext => 判断下一位置是否还有数据,有数据返回true,无数据返回false。

2.next() => 迭代器向后移动,并将越过去的数据返回(要非常注意这个越过去,这是Java实现迭代器的重要机制)、

3.remove() => 删除上一次返回的数据(即上一次越过去的数据)

为什么要多次强调越过去这个机制呢?是Java设计Iterator迭代器时设计的机制问题。

3.1.2编程届的迭代器机制

编程中有几种迭代器机制,一种是位置驱动迭代类(例如CPP的迭代器就是如此),另一种是位置变更驱动类(例如Java就是如此)

以图解的形式介绍两种迭代器机制:

3.1.2.1位置驱动迭代模型

位置驱动迭代更新模型机制如下:

其中最关键的是迭代器的游标指针,迭代器的游标指针指向元素当前位置index,需要返回数据的时候直接调用iterator(index)方法,将元素返回出去,当不想返回当前数据的时候,就将游标指针向后移动到下一个元素,这就是:位置驱动迭代更新机制,迭代的元素与位置紧密相关。

3.1.2.2位置变化驱动迭代模型

位置变化驱动迭代模型机制是Java设计者设计Iterator迭代器时规定的机制,迭代器的游标指针指向两个元素之间地带,定义出两个关键方法:next和hasNext方法。

hasNext方法即判断是否有下一个元素,如果有下一个元素就返回true,如果没有就返回false。

next方法即返回下一个元素,事先判断是否有下一个元素,如果没有就会抛出异常,如果有就返回元素,需要格外注意的是,返回的元素是越过去的元素,为什么被称为越过去的元素呢?游标指针的变化是从上一个元素和下一个元素之间的夹缝中,当调用next方法时,会越过去下一个元素,指向下一个元素和下下个元素之间夹缝中,所以给人一种越过去的感觉。

迭代遍历一般就是循环判断hasNext,查看是否有数据,如果有数据就使用next遍历下一个元素。

提供的remove方法是删除上一次返回的元素,这样设计也是符合逻辑的,先使用hasNext方法查看是否有下一个元素,然后使用next返回下一个元素,查看一下这个元素到底要不要删除,符合条件就使用remove删除元素。

需要注意的是remove是不能连续调用的,调用remove必须得先next一次,不可以连续调用remove删除,否则会抛出IllepgalStateException异常,告诉我们这次抛出的异常时违法的。

3.1.3更强大的遍历器 => ListIterator接口

3.1.3.1ListIterator接口的定义

ListIterator是更为强大的迭代器,他扩展了Iterator接口,提供了更加强大的功能。

ListIterator的定义如下:

继承了Iterator并在接口内部定义了一些方法

java 复制代码
public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int nextIndex();

    int previousIndex();

    void remove();

    void set(E e);

    void add(E e);
}
3.1.3.2ListIterator接口的原理

其中添加了一些新功能:add(为集合新增数据),set(为集合中上一次被迭代器越过的元素设置数据值),previous/hasPrevious(反向遍历集合),nextIndex(查看正向遍历下一个数据时的索引),previousIndex(查看反向遍历下一个数据时的索引)

3.1.3.3ListIterator接口的出现价值

ListIterator接口被广泛用于各种有序集合中,其实这也体现出迭代器的设计理念。

ListIterator接口相对于Iterator接口最重要的就是增加了添加元素机制。

Iterator迭代器是通用的,所有Collection中都可以使用,但是ListIterator只能在实现了List接口的集合中使用:

List接口中定义了listIterator方法,可以返回ListIterator类型的对象。

java 复制代码
public interface List<E> extends Collection<E> {
    ListIterator<E> listIterator();
}

Set接口中没有定义这个方法,只能获取Set集合的Iterator迭代器对象。

Queue接口中也没有定义这个方法,Queue的实现类中,只能获取Queue集合的Iterator迭代器对象。

为什么只有List接口中的迭代器可以添加元素呢?

这个问题需要从Collection的三大分支的特性去理解,Collection三大分支:List,Set,Queue。

1.List和Set相比,List拥有有序性的特点,Set存储元素是无序的,前面已经多次提到Java中依赖的是位置变化驱动迭代机制,List是有序集合,即位置是确定,有规律的,所以是可以让Java的迭代器添加元素的,但是Set是无序的,存储元素的顺序没有规律,没有顺序,迭代器无法借助位置变化驱动机制去添加数据。

2.Queue根本不能在中间插入元素,没有这个机制。

3.1.3.4ListIterator的高级特性

ListIterator支持从集合中的指定位置开始遍历。

在AbstractList抽象集合中有如下定义:

抽象集合中定义了一个类ListItr实现了ListIterator接口,且实现了List接口中的listIterator方法返回一个实例化的ListItr对象。

可以发现ListItr中提供了有参构造器,接收一个int类型的索引,决定了迭代器从哪个位置开始迭代遍历。

java 复制代码
public ListIterator<E> listIterator() {
    return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
    rangeCheckForAdd(index);

    return new ListItr(index);
}
private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        cursor = index;
    }
}    

要点:ListIterator提供的高级特性可以决定迭代器游标指针的初始位置。

3.1.4迭代器遍历集合的顺序性

迭代器遍历集合时是具有顺序性的,List有序集合遍历时就是按照有序集合的顺序遍历,Set无序集合遍历时是用一种感觉随机的感觉遍历的。

一般需要依赖有序性时,建议使用List有序集合。

如果不需要依赖于有序性时,建议使用Set集合,没有顺序,但是依然可以完成很多事情:最小值,最大值,总和等。

3.2迭代器的基本使用

3.2.1基本迭代

java 复制代码
// 测试集合遍历
private static void traverse(ArrayList<Integer> list) {
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

ArrayList<Integer> list = new ArrayList<Integer>() {
    {
        add(1);
        add(2);
        add(3);
        add(4);
        add(5);
    }
};
traverse(list);

3.2.2删除集合中指定位置的元素

java 复制代码
// 测试清空集合
private static void clearList(ArrayList<Integer> list) {
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        if (iterator.next() == 2) {
            iterator.remove();
        }
    }
}

clearList(list);
System.out.println(list);

3.2.3Iterator循环消费

循环消费机制是集合迭代器体系提供的一个默认方法。

java 复制代码
// 测试迭代消费机制
public static void testConsumer() {
    List<Integer> list = new ArrayList<>();
    Collections.addAll(list, 1, 2, 3, 4, 5);
    Iterator<Integer> iterator = list.iterator();
    iterator.forEachRemaining((item) -> {
        System.out.println("蛋蛋" + item);
    });
}

forEachRemaining的源码:

java 复制代码
default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
        action.accept(next());
}

3.2.4有序集合添加元素

有序集合添加元素需要使用到有序集合增强迭代器ListIterator。

java 复制代码
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
ListIterator<Integer> iterator = list.listIterator();
iterator.next();
iterator.next();
iterator.add(999);
System.out.println(list);

3.2.5指定位置初始化加强迭代器

ListIterator是支持将游标初始化到指定位置的:

java 复制代码
// 测试指定位置初始化ListIterator
public static void testIndexInit() {
    List<Integer> list = new ArrayList<>();
    Collections.addAll(list, 1, 2, 3, 4, 5);
    ListIterator<Integer> iterator = list.listIterator(3);
    System.out.println(iterator.next());
}

执行结果:

3.3加强for循环(for-each)

3.3.1基本使用

使用加强for循环可以实现集合的遍历:

java 复制代码
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
for (Integer item : list) {
    System.out.println(item);
}

遍历结果:

3.3.2底层原理

加强for循环的底层原理是迭代器机制,可以通过反编译字节码文件后表现出:

java 复制代码
List<Integer> list = new ArrayList();
Collections.addAll(list, new Integer[]{1, 2, 3, 4, 5});
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    Integer item = (Integer)var2.next();
    System.out.println(item);
}

3.4快速失败机制

3.4.1什么是快速失败机制

java.util包下的集合体系是不支持多线程操作的,仅仅支持单线程使用,在多线程下没有同步机制,很有可能因为集合体系结构的改变,导致原本应该正常执行的代码在运行中出现异常,Java类库设计者为了避免这种情况的发生,推出了Fail-Fast快速失败机制,尽可能的抛出错误,避免更大的问题出现。

要点:提前抛出错误,避免多线程下,集合体系结构的改变,导致出现更大的问题。

集合中的快速失败机制是为了在迭代过程中,如果有其他线程调用了remove/add等修改结构的方法,就会抛出comodification异常,而不是将问题拖到后面,再去使用比较复杂的检测算法判断是否出错。

注意:快速失败机制(fail-fast)只能在迭代时起作用,其他时刻多线程并发执行多个操作是不会触发fail-fast的。

可以看到在ArrayList的父类中定义了一个modCount变量。

java 复制代码
protected transient int modCount = 0;

modCount记录的是集合的结构修改次数,每当使用影响结构的操作的时(例如add/remove),会增加modCount,在迭代中,迭代器会记录住生成迭代器的modCount,在迭代过程会判断集合的modCount是否发生改变,如果发生改变就会抛出comodification异常。

3.4.2快速失败机制的执行流程

从ArrayList的角度看待快速失败机制fail-fast。

remove方法:

remove方法会对集合的结构体系发生一次改变,modCount增加了一次。

javascript 复制代码
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;
}

迭代器Iterator:

1.创建迭代器时会初始化expectedModCount为modCount。

java 复制代码
int expectedModCount = modCount;

2.使用迭代器的next元素时,调用checkForComodification方法检查Iterator迭代器中的modCount和集合中的modCount是否一致

java 复制代码
@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

3.checkForComodification方法的定义:

使用这个方法可以检查集合的modCount是否在迭代过程中发生变化,如果发生变化就会抛出ConcurrentModificationExcption异常。

java 复制代码
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

3.4.3总结快速失败机制

快速失败机制的原理其实就是提前发现迭代器迭代过程中集合发生变化引起的错误,提前抛出错误防止出现更大的错误。

要点:快速失败机制是用来提前发现迭代器遍历过程中,集合结构改变会引发的问题的,当集合一改变就会立刻抛出错误,防止迭代器迭代过程中引发更大的错误。

相关推荐
Daniel 大东8 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞14 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen14 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)20 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿20 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
修道-032321 分钟前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
九圣残炎26 分钟前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
当归102439 分钟前
若依项目-结构解读
java
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
iiiiiankor1 小时前
C/C++内存管理 | new的机制 | 重载自己的operator new
java·c语言·c++