Java集合框架

概述

Java集合框架源自两个最底层的接口Collection,Map,这种分类方式是从对集合元素访问的根本区别来决定的,Collection集合是基于对集合元素本身或特定位置进行相应的访问,而Map则是通过映射方式来操作集合中的元素,对集合中元素的访问是通过与之一一对应的的key来访问的,二者最本质的区别在于:对Collection的访问我们必须要清楚集合元素本身(或位置信息)才能从集合中获取该元素,而对于Map而言则是通过提供类似别名的方式进行访问集合中的元素,这在很多时候非常有用,让我们不必关心集合元组中本身的区别或其在集合中的位置信息,只需要一个相对简单的key就能够获取到该key对应的元素。

Collection分析

Java本身并没有直接提供任何Collection接口的直接实现类,而是根据实际需求对Collection进行了再次细分,因此我们很少直接使用Collection接口,是基于需要使用其相应的子接口。如何选择相应的子接口取决于实际的功能需求,在开始分析其子接口前先了解以下Collection接口提供了什么功能

  • add:向当前集合中增加一个元素
  • addAll:将另一个集合中的所有元素添加到当前集合中
  • clear:清空结合中的所有元素
  • contains:判断当前集合是否包含一个元素
  • containsAll:判断当前集合是否包含另一个集合中的所有元素
  • equals:判断集合本身的相等性,详见Object.equals
  • hashCode:同上
  • isEmpty:判断当前集合是否未空集合
  • iterator:获取该集合的一个迭代器
  • parallelStream:并行流,这是jdk1.8引入的新特性,属于java相对比较抽象且晦涩的一个概念,通常情况下我们访问集合中所有元素时,是基于迭代器显式的遍历集合元素,并在遍历的过程中对集合进行操作,而则是在迭代器的基础上进一步封装,这让我们只需要提供对应的操作,由自身内部的进行集合的遍历,这在使用上是不小的简化,同时由流本身特性规定了它只能是一次性、单向的遍历过程;parallelStream是属于并行流,其底层依赖于ForkJoinPool实现,ForkJoinPool是并行任务处理中分治理念的一种实现,此处不做过多介绍,简单来说parallelStream会基于多线程模式将集合拆分成多个子集合后利用多核多线程优势提高集合处理速度,因为多线程的原因对集合的遍历是非线性的。参见下面简单例子:
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        intList.parallelStream().forEach(item->System.out.print(item+","));
        System.out.println();
        intList.forEach(item->System.out.print(item+","));
    }

输出结果如下:parallelStream并行流输入顺序和集合顺序不一致,普通流输出顺序与集合顺序一致

7,3,6,5,9,4,10,2,8,1,
1,2,3,4,5,6,7,8,9,10,
  • remove:移除集合中的某个元素
  • removeAll:移除该集合中与指定集合中相同的元素,移除后该集合将不包含指定集合中的任意元素集合的差集
  • removeIf:参数是一个Predicate函数式接口,通过提供返回一个boolean值的方法实现可删除集合中满足对应条件的元素
  • retainAll:保留该集合中与指定集合相同的元素,取两个集合的交集
  • size:返回集合元素个数
  • splitIterator:分裂迭代器,jdk1.8引入的新特性,同parrallelStream一样提供了基于分治理念和多核多线程提高效率,同时还对源(集合)提供了相关属性报告,比如源是否支持SIZED、CONCURRENT等集合属性。
  • stream:获取集合的一个Stream对象
  • toArray:返回一个包含所有集合元素的数组,且返回的数组是安全的意味着集合即便本身是一个数组实现的它也必须返回一个新的数组,集合对删除元素不会导致新数组中对应元素被删除,但其内部元素还是引用的同一个对象。
  • toArray(T[] t),返回特定类型的数组,相对于上述toArray返回Object[],该方法将返回T[],如果传入的t数组长度小于集合大小,则返回一个新的数组,否则装在集合元素到t并返回t

从Colleciton接口定义方法来看其定义了集合作为容器对元素的基本操作包括添加元素、删除元素、判断是否包含元素、容器本身的属性(是否为空、元素数量等)、以及返回一个能够遍历元素的迭代器。从Collection接口方法来看没有定义如何获取结合中的元素香瓜你的方法,这是因为这些方法与具体的数据结构有段,其相应方法的定义放在子类中。值得注意的是jdk1.8提供的新特定对流、并行流、并行迭代器的支持,下面就Collection的几个子接口进行详细说明

List:有序结合或列表

作为Collection的子接口,List相对Collection接口对集合做了更进一步的约束,主要是提供了集合的有序性,一方面它提供了更加丰富的方法来操作集合,另一方面它对相关操作做了进一步具化性约束,List可称为有序集合或者列表集合,它在Collection集合的基础上增加了以下特性

  • 有序性:保存元素被加入到集合中的先后顺序,这在遍历集合时能按照元素被添加的顺序进行访问
  • 允许重复元素和null元素
  • 通过索引值访问特定位置的元素
  • 提供了一个新的迭代器:ListIterator,相对于Collection提供的迭代器,ListIterator不仅可以正向遍历,还可以反向遍历,同时ListIterator还提供了对元素的增删操作以及获取前一个或后一个元素的索引信息。
  • 提供了按照索引或集合元素进行搜索元素的方法,当然在使用这些方法的时候要注意List的实现方式,避免使用不当造成耗时的线性搜索,比如对于数组结构的List实现使用索引访问元素是非常好的体验,但如果频繁对其元素的增删操作将不被建议使用;对于链表结构的List实现则于数组结构实现刚好相反,这取决于具体实现的方式。
  • 提供两种方式在指定点增加或删除元素

下面是List结构的方法列表,其中大部分是继承自Collection的方法,发现一个现象就是在List接口中重新定义一遍了继承自Collection提供的接口,其主要原因是相对于Collection的方法,List对相应的方法有了新的约束和定义,不在细述每个方法的具体含义,与Collection相差不大,仅对特殊点做说明:
List接口

  • add(E)方法默认将元素添加到集合元素尾部,而add(int,E)则在指定的索引位置添加元素,同样的特定也体现在addAll上
  • get(int)方法根据索引位置搜索获取元素,indexOf(Object)则根据对象获取元素从前向后索引,如果不存在将返回-1,lastIndexOf则从后向前进行搜索
  • 提供了一个remove(int)方法移除指定索引位置的元素
  • replaceAll方法提供了一个规则替换集合元素的方法,需要一个UnaryOperator的函数接口,UnaryOperator是一个一元操作符,其继承了Function接口,具体看下面例子:
public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(list);
        list.replaceAll(t->t < 5 ? 100 : 200);
        System.out.println(list);
    }

测试结果如下:我们提供的替换规则是将<5的元素替换成100,其余替换为200

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[100, 100, 100, 100, 200, 200, 200, 200, 200, 200]
  • set(i,E)将指定索引处的元素设置(替换)为指定元素
  • sort方法将重新对集合元素进行排序,需要一个Comparator的函数时接口,下面实现整数集合的降序排列:
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(list);
        list.sort((e1,e2)-> e1 > e2 ? -1 : (e1 == e2 ? 0 : 1));
        System.out.println(list);
    }

测试结果:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
  • subList:返回指定索引范围内的子集,这里的子集是原集合的一个视图,因此对子集的操作将直接反应到原集中,同样对原集的操作将反应到子集中

ArrayList:可扩容的数组集合

ArrayList特性说明

在查看集合框架关于List的层次图发现一个有意思的现象,jdk提供了一个List接口的抽象基类AbstractList,其它具体实现类均继承了该基类,ArrayList则在继承该基类的基础上又实现了List接口,似乎显得有点多余,网络上有观点认为这是为了支持jdk动态代理而实现的,但实际上动态代理是在jdk1.3之后才有的,而List则是在1.2中就有的,如果需要支持动态代理,应该调整jdk动态代理逻辑使它支持父类实现的接口的代理更加合理,当然这是一种相对靠谱的说法了,下面是ArrayList的一系列特征

  • ArrayList是List的一种具体实现,其内部是基于一个可调整大小的数组来实现的,因此其相应的方法的性能和数组的操作相关联,例如对于指定索引位置获取元素ArrayList有非常高的性能,但对于在随机位置处进行元素的增加和移除就显得耗时且笨拙了。
  • 数组是一个内存大小固定的存储结构,ArrayList在集合元素超过集合容量阈值后将通过分配一个更大的数组来容纳更多的元素,这种容量扩张是比较耗时的,因此在使用ArrayList之前尽量预估所需容量大小一次分配到位,避免ArrayList自身的多次容量扩充导致性能下降。
  • ArrayList不是一个线程安全的集合,因此多线程的操作可能会导致数据不一致或者抛出ConcurrentModificationException异常(基于fast-fail快速失败机制,下面详说)
ArrayList源码分析

对ArrayList源码相对较为简单,分析如下:

  • 先看一下ArrayList的成员变量
    ArrayList成员变量

    • elementData:一个Object类型的数组
    • DEFAULT_CAPACITY:默认容器容量:10
    • EMPTY_ELEMENTDATA:默认空容器数据:{}
    • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认容器数据:{}
    • size:容器元素数量
    • ArrayListSpliterator:SplitIterator的具体实现
    • Itr:Iterator的具体实现
    • ListItr:ListIterator的具体实现
    • SubList:实现subList方法,用于返回一个elementData的视图
  • Arraylist提供了3个构造方法。

    • 默认构造方法:public ArrayList()容量为0,在后续会将容量初始化为DEFAULT_CAPACITY=10
    • public ArrayList(Collection<? extends E> c):基于已有集合创建实例
    • public ArrayList(int initialCapacity):创建指定大小容量的实例
  • 关键方法:add(E)

    public boolean add(E e) {
    	//确保集合容量符合要求
        ensureCapacityInternal(size + 1);
        //这里使用size++,是因为size为容器元素大小,而数组下标从0开始,因此新元素的位置=size
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal用于确保容器的容量满足新增需求

    private void ensureCapacityInternal(int minCapacity) {
    	//先calculateCapacity来获取一个合理的新容量值,在判断该新值大小是否操作数组长度
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	//如果使用默认构造器创建ArrayList实例,则判断默认容量和新容量大小返回大值,否则返回新容量值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
    	//modCount标识修改的次数,主要用于fast-fail,对容器元素的增删都会执行modCount++操作
        modCount++;
		//如果新容量已经超过了数组的长度,则说明需要对数组容量进行扩充
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //先尝试扩充现有容量一半大小,如果还不满足就直接使用新容量的的值
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量的值已经超过了数组最大值,则使用Integer.MAX_VALUE作为新容量值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 当扩充容量后将原数组全部拷贝到新数组中,该操作相对耗时,尤其是涉及大量元素的拷贝时
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

上述逻辑说明为什么有必要预估所需集合容量的大小了,这避免了数组拷贝带来的性能消耗,在看一下add(int,E)方法

    public void add(int index, E element) {
    	//检查index是否超出elementData索引范围(index>=0 && index<elementData.length),如果超出范围将抛出IndexOutOfBoundsException异常
        rangeCheckForAdd(index);
		//同上保证容量满足要求
        ensureCapacityInternal(size + 1); 
        //将大于index的数组元素向后移动一个位置腾出index位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //设置index位置的为指定元素的值
        elementData[index] = element;
        size++;
    }

add(int,E)实现了随机位置的元素插入操作,其原理是将index向后的元素整体向后挪动一个位置,删除原理差不多,是将指定index的元素整体向前挪动一个位置,这种操作是相对耗时的,因此说ArrayList不适合做随机位置的增删操作

  • clear方法将所有元素设置为null,同时置size=1
ArrayList中的fast-fail

fast-fail是一种快速失败机制,为什么要有这种机制?主要是为了基于异常形式告知开发者你的代码存在严重问题,ArrayList中的fast-fail主要通过对所有集合元素的增删均会执行一个modCount++操作,而对集合的遍历操作会在遍历期间检测modCount是否发生变化,如果发生变化就会导致快速失败抛出ConcurrentModificationException异常

但是这种快速失败不是百分之百保证的,这是因为modCount++操作本身不是原子操作,因此对modCount的变化的判断也就不能保证绝对正确了,下面分析迭代器遍历的源码:
iterator方法会返回一个ArrayList的迭代器(内部Itr的实例)

    public Iterator<E> iterator() {
    	返回一个Itr的实例
        return new Itr();
    }
//Itr默认构造器如下:
        int cursor;       
        int lastRet = -1; 
        //保存当前modCountd的值
        int expectedModCount = modCount;

        Itr() {}

迭代器的next方法如下:

        public E next() {
        	//检查modCount
            checkForComodification();
            int i = cursor;
            //如果在进入next后到执行此处代码期间有其它线程删除集合元素导致i>=size则将抛出NoSuchElementException
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            //如果迭代执行到此处时有其他线程调用了trimToSize方法则会抛出ConcurrentModificationException
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        final void checkForComodification() {
        	//如果发生了变化则抛出ConcurrentModificationException
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

下面给出快速失败的示例代码

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>(3);
        list.add(1); list.add(2); list.add(3);
        Iterator<Integer> iter = list.iterator();
            //模拟其它线程在此时删除元素,因为已经创建对应的迭代器,迭代器中copy了modCount的值,后续如果又增删操作两个值将不在相等
            list.remove(1);
        while(iter.hasNext()){
            Integer next = iter.next();
            System.out.println(next);
        }
    }

测试结果:增加元素也将fast-fail

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at com.test.collection.CommonTest.main(CommonTest.java:18)
遍历ArrayList的错误示范

初学Java时容易出现基于for循环来增删集合元素的写法,这很容易导致不正常的结果或抛出异常,代码如下:

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>(3);
        list.add(1); list.add(2); list.add(3);
        list.forEach(item->{
            if(item == 1)
                list.remove(item);
        });
    }

测试结果:尝试通过forEach遍历元素删除元素将抛出异常,这是因为forEach也会赋值modCount的值作为副本,在遍历过程中会一致检测modCount是否发生更改

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList.forEach(ArrayList.java:1262)
	at com.test.collection.CommonTest.main(CommonTest.java:15)

另一种错误时通过for循环来增删元素,这时候可能不会抛出异常但会导致数据结果不正确
抛出IndexOutOfBoundsException示例代码

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>(3);
        list.add(1); list.add(2); list.add(3);
        int sum = 0;
        for(int i = 0,len = list.size();i<len;i++){
            if(list.get(i) == 1){
                list.remove(i);
            }else{
                sum += list.get(i);
            }
        }
        System.out.println("去除为1的元素并求和结果:"+sum);
    }

计算结果不正确实例代码,计算结果为:3,实际理论结果为:5

public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>(3);
        list.add(1); list.add(2); list.add(3);
        int sum = 0;
        for(int i = 0;i<list.size();i++){
            if(list.get(i) == 1){
                list.remove(i);
            }else{
                sum += list.get(i);
            }
        }
        System.out.println("去除为1的元素并求和结果:"+sum);
    }

Vector:线程安全的可扩容数组集合

Vector的代码和原理基本与ArrayList相同,这里不在进行细述,主要讨论以下其不同之处

Vector中的线程安全机制

Vector是线程安全的集合,其线程安全性通过synchronized关键字保证

Vector中的fast-fail

Vector中也会出现fast-fail,不是说Vector是线程安全的吗,怎么也会出现快速失败,这是因为遍历操作的是通过锁迭代器对象,而对元素的增删改则是锁的是ArrayList对象,两把不同的锁无法保证线程对modCount的同步访问

Stack:基于数组的栈

Stack继承自Vector集合,因此其具备Vector的相关特性,Stack顾名思义提供了一个栈(先进后出)的相关操作,像入栈操作push,出栈操作pop以及获取栈顶元素信息等,代码简单不做细述

Queue:队列

队列接口,通常是使用先入先出(FIFO)对元素进行排序,当然也有例外情况,比如优先级队列(根据提供的比较器对元素进行排序)或LIFO的栈队列。队列结构通常对每一种操作都提供了两种形式,其主要目的在于当操作失败时的处理结果不同。

其数据结构通常是链表结构,当然也有数组的,链表结构的集合决定了它适合于随机位置的元素增删操作而不适合随机位置的访问操作,其时间复杂度为(n)

接口方法如下:两两一对的元素操作方法,区别在于是否允许操作失败
Queue接口

  • add:向队列里添加元素,成功返回true,失败通常抛出异常
  • offer:像队列添加元素:成功返回true,失败通常返回false(特定情况也抛出异常,如不允许null元素或者不允许的元素类型)
  • element:返回队首元素:空队列抛出NoSuchElementException
  • peek:返回队首元素:空队列返回null
  • remove:移除并返回队首元素:空队列将抛出NoSuchElementException
  • poll:移除并返回队首元素:空队列将返回null

Deque:双端队列

双端队列顾名思义支持在两端进行插入和删除的线性集合,发音"dek",通常情况下队列对容量没有限制,但也支持特定容量的双端队列

其继承了Queue接口,也继承了对相关操作提供两种形式的特性,Deque提供了Queue中对元素的增加、删除、查看的双端操作,这些双端操作也同时具备两种形式,即操作失败是否抛出异常

Deque还提供了栈结构的相关操作:posh、pop、peek,如果需要使用栈结构的集合,建议使用Deque而不是Stack。

此外Deque还提供了删除内部元素的方法: removeFirstOccurrence删除首次遇到的元素和removeLastOccurrence 删除最后一次遇到的元素

通常情况不建议插入null元素,因为peek操作在集合为空时将返回null,如果允许插入null元素将无法区分peek返回null是集合为空还是元素本身为null

该接口中定义的很多方法其含义是相同的,仅仅是为了特定数据结构相匹配的方法名,具体如下:

Queue中的方法等效的Deque中的方法反向的Deque方法等效的栈操作
add(e)addLast(e)addFirst(e)push(e)
offer(e)offerLast(e)offerFist(e)
remove()removeFirst()removeLastpop()
poll()pollFirst()pollLast()
element()getFirstgetLast()
peek()peekFirst()peekLast()peek()

LinkedList:链表列表

从集合框架图中可以看到LinkedList即实现了Queue(实现Deque)又实现了List(继承AbstractList),那是不是说Linked具备了二者的优点呢?实际上不是的,LinkedList是基于显现链表结构存储数据的,因此只具备了Queue快速随机增删的操作,实现List接口只是为了提供List的相关方法访问集合元素。

LinkedList是双向链表的实现、同时提供了List相关对元素的操作方法,并允许null元素,索引列表元素并未确定从开头或结尾进行索引,而是以索引结果更接近索引值为准

LinkedList并未实现线程安全,如果需要线程安全的LinkedList,则建议使用Collections.synchronizedList(new LinkedList())封装后的LinkedList结构,迭代器和ArrayList一样也是fast-fail。

LinkedList的源码相对简单不在细说

PriorityQueue:优先级队列

  • PriorityQueue实现的是Queue接口,其默认是基于原始的Comparable接口进行排序,当然也可以在构造器中提供一个Comparator对象实现特定的排队算法,如果既没有提供Comparator也没有实现Comparable则将抛出ClassCastException,PriorityQueue内部是使用二叉堆数组结构
  • 队列不允许使用null元素
  • 默认情况下队首为最小元素
  • 无界队列(add=offer)且自动增长
  • 非线程安全的队列
  • 迭代器迭代过程并不保证任何迭代顺序,如果需要顺序遍历,可使用Arrays.sort(pq.toArray());
  • PriorityQueue使用二叉堆的最小堆存储结构,其增加元素和删除元素均满足二叉堆的的新增和删除逻辑,具体二叉堆算法不在此处详述
  • 二叉堆结构决定了使用数组下标访问元素毫无意义,因为随着元素的增删其它元素的下标会发生变化。

ArrayDeque:双端数组队列

  • 双端数组队列实现Deque接口,具备Deque的相关特性,其内部通过一个数组来存储元素,同时定义int head和int tail来分别维护队首的指针偏移量和队尾的指针偏移量
  • 不允许null元素
  • 建议使用ArrayDeque代替Stack作为栈操作,其效率更高,分析代码原因在于Stack中对入栈和出战过程做了更多的if判断
  • ArrayDeque查询判断均是线性时间
  • 同上属于fast-fail
ArrayDeque源码分析

add/addLast源码

    public void addLast(E e) {
    	//不允许null元素
        if (e == null)
            throw new NullPointerException();
        //将元素入队到队尾处,tail默认值为0
        elements[tail] = e;
        //如果队首与队尾相遇,进行双倍容量扩容
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

doubleCapacity双倍扩容;双端队列存储格式是add或addFirst从数组的index=0位置起开始添加元素,而addLast则是从数组的index=array.length-1位置处开始添加元素

    private void doubleCapacity() {
    	//进行扩容的断言时队首和队尾相遇
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // 数组的右侧数据
        int newCapacity = n << 1;
        //溢出检测
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
		//将原数组末尾部分(addFirst从队尾开始添加)copy到新数组的队首,该新数组的新的可用位置是r
        System.arraycopy(elements, p, a, 0, r);
        //将原数组的队首部分(add或addLast从队首开始添加)copy到新数组并衔接新的数组的r位置
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;//重置head=0
        tail = n;
    }

关于双端队列容量扩充后的数据存储位置参见下面说明:

    public static void main(String[] args) throws InterruptedException, IllegalAccessException, NoSuchFieldException {
        ArrayDeque<Integer> arrayDeque = new ArrayDeque<>();
        for (int i = 0; i < 13; i++) {
            arrayDeque.add(i);
        }
        arrayDeque.addFirst(9);
        arrayDeque.addFirst(10);
        //arrayDeque.addFirst(11);
        Field f = ArrayDeque.class.getDeclaredField("elements");
        f.setAccessible(true);
        Object arr = f.get(arrayDeque);
        System.out.println(Arrays.toString((Object[]) arr));
    }

测试结果如下:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, 10, 9]

通过add或addLast添加的元素从数组开始向后入队,通过addFirst添加到的元素从数组尾部向前开始入队,到仅剩余一个空闲位置时在执行addFirst将进行扩容,扩容后的结构如下图

[ 11,10, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,··· ··· ··· ···null, null, null, null]

此后再次执行addFirst其数组结构如下:addFirst又开始从数组尾端开始向前入队元素了

[ 11,10, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,··· ··· ··· ···null, null, null, 12]

Set:不重复集

Set集合的最大特点就是去除重复的元素,如果集合中已经存在一个元素A,再向集合中添加一个元素B,且B.equals(A)==true,则A元素将被B元素替换,Set集合除了继承自Collection集合外,没有在定义任何额外的功能。所有从这里可以看到Set集合相对于其它两个继承自Collection的接口(List,Queue)有着明显的缺点,list提供基于索引获取元素,Queue提供基于队首或队尾获取元素,而Set除了通过遍历集合外,我们无法从集合中获取元素。

  • 元素不重复性

SortedSet:排序不重复集

实现元素排序的不重复集,默认情况下基于元素本身Comparable属性进行排序,当然也可以通过传入一个Comparator类型参数的构造器实现元素排序,如果元素不满足二者之一,将抛出ClassCastException异常,

所有实现排序的集合都应该实现的4个构造器

  • 无参构造器,基于元素Comparable进行排序
  • 具备Comparator单个参数的构造器,通过该参数进行排序
  • 一个Collection单个参数的构造器
  • 一个具备SortedSet单个参数的构造器
    既然提供了排序功能,那么必然存在队首和队尾的概念,SortedSet再Set的基础上增加了以下几种方法:
  • E first():获取队首元素
  • E last():获取队尾元素
  • SortedSet headSet(E toElement):返回一个小于toElement元素的视图集,该视图集是原视图的一个子集,因此对返回是图集的修改都会影响到原始图,当尝试向视图集中插入大于toElement的元素时,将抛出IllegalArgumentException,toElement为空时将抛出NPE
  • SortedSet tailSet(E fromElement):返回一个大于等于fromElement元素的视图,其相关特性和headSet相同
  • Comparator<? super E> comparator():返回一个比较器用于实现自定义排序
  • default Spliterator spliterator():实现了该方法,且不允许子类修改

NavigableSet:导航集

NavigableSet继承SortedSet,对SortedSet进行了功能拓展,相对于SortedSet只能获取队首队尾的元素,NavigableSet可以获取大于、大于等于、小于、小于等于某元素的元素,具体如下:

  • E lower(E e):返回小于e的一个元素
  • E flower(E e):返回小于等于e的一个元素
  • E ceiling(E e):返大于e的一个元素
  • E higher(E e):返回大于等于e的一个元素
  • E pollFirst():弹出队首元素
  • E pollLast():弹出队尾元素
  • NavigableSet descendingSet():获取将序集
  • Iterator descendingIterator():获取升序集
  • NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive):返回指定范围内的集合

HashSet:哈希集

HashSet实现了Set接口,因此仅具备Set的相关特性,其本质是一个HashMap的实例,关于HashMap后续详说

  • 允许null元素
  • 相关操作的时间复杂度线性不变,这得益于基于HashMap的Hash散列函数
  • 不是线程安全的集合,如需要使用线程安全的HashSet,可使用Collecitons.synchronizedSet(new HashSet(…))封装后的集
  • 迭代器支持fast-failed
  • 如果使用该集合进行大量的迭代工作,那么别将初始容量设置过高或负载因子设置过低,这是因为二者将导致过多的空桶遍历降低效率
  • 内部使用HashMap存储元素,因此不在此分析器源码结构

LinkedHashSet:哈希集的链表实现

再HashSet的基础上提供了入队顺序保证,意味着可以更具元素被添加到集合的顺序进行遍历

  • 具备HashSet的去重复功能,其基本操作例如add,remove等由于需要额外的维护链表结构,因此效率相对HashSet略有降低
  • 允许null元素
  • 基于其链表特性,其迭代开销只与集合元素数量有关,与初始容量和负载因子无关
  • 非线程安全的集合,若有需要可使用Collections.new synchronizedSet(new LinkedHashMap());
  • 支持快速失败
  • 内部通过一个LinkedHashMap来存储元素,因此不在此讨论其原理结构

TreeSet:排序集

TreeSet实现了 NavigableSet接口,内部通过一个TreeMap来存储元素,其相关特性(不重复、排序)通过TreeMap保证

  • 排序通过Comparable或Comparator实现
  • 非线程安全的集合
  • 支持fast-failed

Map:映射集

Map类型的集合区别于前面提到的各种Collection集合,通过前面的了解我们知道要获取Collection中的一个元素只能通过特定位置来进行获取(队首、队尾、索引位置),而Map则不关心元素在集合中的位置,我们只需要一个和元素一一映射的key就可以获取到元素了,可以将该key看作是元素的别名

Map提供了三种集合视图:key的集合视图、元素的集合视图以及key-value组成的对象集合视图,必须要了解视图只是集合的一种展现形式,因此集合的任何修改都必然导致视图的变化,相关视图的变化也必然导致集合的变化

通常作为key的对象是不可更该的对象,比如是string,基本类型的封装类型等,如果定义成一个可变更的对象作为key,可能会导致一些错误的映射结果,比如key的equals和hashCode方法因为对象属性的变更而变更等,详见下面例子:

public class CommonTest {
    static class MyObj{
        private int id;
        private String name;

        public MyObj(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public int hashCode() {
            return id;
        }

        @Override
        public boolean equals(Object obj) {
            MyObj obj1 = (MyObj) obj;
            return this.id == obj1.id;
        }

        @Override
        public String toString() {
            return "MyObj{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) throws InterruptedException, IllegalAccessException, NoSuchFieldException {
        Map<MyObj, MyObj> map = new HashMap<>();
        MyObj zs = new MyObj(1, "zhang san");
        MyObj ls = new MyObj(2, "li si");
        map.put(zs,zs);
        map.put(ls,ls);
        System.out.println(map.get(zs));
        zs.id = 2;
        System.out.println(map.get(zs));
    }
}

测试结果:当修改key的id值后原本指向zs->zs的映射突然变成了zs->ls了

MyObj{id=1, name='zhang san'}
MyObj{id=2, name='li si'}

根据Map的具体实现不同,有的实现允许null key,有的不允许,有的对key的类型有限制,尝试添加不符合要求的类型将抛出ClassCastException(实现排序的Map),下面介绍Map接口定义的各类方法:
Map接口

  • put将键值对K/V添加到Map中,返回覆盖前的值,如果不存在,则返回null
  • putAll:将Map集合添加到当前Map中,
  • get:根据指定Key从集合中获取对应元素
  • getOrDefault:从集合中获取对应元素,如果不存在对应Key,则返回入参中的默认值
  • remove(key):从Map中移除指定key对应的元素并返回该值,返回被移除的值,如果不存在返回null
  • remove(key,value):从Map中删除对应的的键值对,要求key和value均相等,如果存在则返回true,不存在返回false
  • replace(K,V):如果Map中存在对应的K的键,则使用V替换其value,并返回旧value,如果不存在则返回null
  • replace(K,V,V):相对于replace(K,V)需要V相等才替换成新的V,存在则返回true,否则返回false
  • replaceAll:入参是一个BiFunction的函数接口,定义替换规则批量替换
  • size():Map集合元素数量
  • clear():清空map
  • values:返回一个value的Collection集合
  • keySet():返回一个key的Set集合,从这里可以看到values返回的集合是允许重复的,而keySet返回的集合是不重复的。
  • entrySet:返回一个key、value封装成entry的Set集合
  • containsKey():判断是否包含特定的key
  • containValue()判断是否博阿寒特定的value

HashMap:

HashMap应该算是集合框架中最重要的一个实现之一,其实质是基于hash表的实现Map接口,具备以下特性

  • 允许null key和null value
  • 非线程安全
  • HashMap的迭代顺序不能保证,且随着get/put操作发生变化,这是由于HashMap在扩容后会进行重新hash,这会导致其存储位置发生变化
  • get/put操作的时间复杂度线性不变(在具备良好hash散列基础上时)
  • HashMap中最重要的两个参数时初始容量和负载因子,这两个参数将直接影响hash散列结果,通常来说容量越大,负载因子越小散列效果越好,但是这会带来内存的浪费不利于频繁的集合遍历。
  • 线程不安全,可以使用Collections.synchronizedMap来封装获得一个线程安全的实现
  • 支持fast-failed
  • 预估容量在使用任何可变容量的集合中都是一个提高效率的手段,HashMap设置合适的容量将避免resize操作,数据量非常大时对性能影响很大

快速了解

HashMap涉及到的内容相对较多,源码也比较复杂,是Java面试的重点之一,因此有必要熟悉HashMap的各个知识点,先简单的描述一下HashMap的三个最重要的操作(添加、删除、查找)的基本原理

HashMap的数据结构为:数组+链表+红黑树,如下图,橘黄色矩形表示数组的一个元素(通常称这样的一个元素为哈希桶),数组可以存储链表的起始结点(白色平行四边形)和红黑树的根节点(黑色的圆形),其中红黑树继承自链接结点。
在这里插入图片描述

  • put(K,V),向集合中添加元素,其中当链表的长度大于等于8时将转换为红黑树,同样当小于等于6时将转换为链表结构。
    • 如果hash表table还没有初始化,则初始化table数组,初始化长度为16
    • 通过内部hash()方法计算K的hash值(int类型),如果K==null则hash()返回0,从这里可以看出hashMap支持null K,V,hash()方法将当前K的hashcode低16位雨高16位求或计算得到低16位,高16位保持不变得到最终结果,由于是通过K的hash值来计算其在桶的槽位值(数组下标),其计算方法为:hash值 & (table.length - 1),其中table.length为2的n次幂(二进制格式为全1),因此为了避免产生hash碰撞,hash值应该是一个与自身hashCode值高度相关且尽可能的保持二进制位为1,上述计算可以很好的实现这个目的
    • 将hash值对数组的长度的取模运算(hash & (table.length - 1))得到一个小于等于数组长度的int值
    • 将取模结果视为数组下标并进行以下判断
      • 如果该数组下标处无值(=null),则将K和V以及hash值封装成一个链表结点存放在数组下标位置处完成操作
      • 如果该数组下边处有值且值的类型为链表结点,则将K和V及hash封装成Node对象放在链表尾部,遍历链表时计算链表长度,如果超过8则进行链表的树化,如果遍历过程中发现已经存在K,则将当前V覆盖结点的值
      • 如果该数组下标处有值且位红黑树,则将K,V及hash封装成红黑树的结点添加到红黑树中,同上所示如果已存在K,则将当前值覆盖红黑树结点的value值。
  • get(K):根据K从集合中获取对应的V值,根据K的hash值计算table槽位,根据槽位的值分下面三种情况:
    • 槽位值为空返回null
    • 槽位值为链表,判断当前结点K是否等于当K,相等返回结点value,否则遍历链表查找与当前K相同的结点,找到返回结点对应value,找不到返回null
    • 槽位值为红黑树,红黑树查找和当前K相同的结点,找到返回对应value,找不到返回null
  • remove(K):从集合中移除K对应的元素,桶get(K)逻辑找到移除结点,找不到返回null,判断结点类型,如果为红黑树结点,则基于红黑树规则删除该结点;否则从链表中移除该结点;

源码分析

成员变量说明
//hash表,node数组结构,TreeNode继承该Node,
transient Node<K,V>[] table;

//调用entrySet()返回该对象,如果为null,则初始化,该对象表示并非实际的一个集合,而是table的一个Set集合视图,调用该对象的方法实际上访问的时table表,因此任何操作将反应到table中
transient Set<Map.Entry<K,V>> entrySet;

//集合元素数量
transient int size;

//用于快速失败,对元素的增删该值会加1
transient int modCount;

//扩容阈值,到集合元素数量到达该值时将进行扩容
int threshold;

//负载因子,threshold = table.length * loadFactor
final float loadFactor;
构造方法
	//无参构造器,初始化负载因子为0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	//初始化集合容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	//根据初始化容量和负载因子初始化结合
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
		//设置负载因子
        this.loadFactor = loadFactor;
        //设置扩容阈值,该值为初始化容量的最近的2的n次幂的值且大于当前的值
        this.threshold = tableSizeFor(initialCapacity);
    }
	//根据已有map构建创建hashmap实例
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
集合关键方法分析
public V put(K key, V value) : 向集合中新增键值对
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal相关说明:

  • 计算数组下标(槽位):为了提高基于K的hash码计算其在数组中的下标,实际上应该使用求余操作,但数组长度等于2的n次幂时,对数组的长度的求余操作等同于对数组长度长度-1的与运算
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果table == null 或table数组长度=0,则初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果hash对应数组下标不存在值,则直接将该槽位上新增Node,属于快速插入操作,如果hash散列效果较好,则大多put操作到这就结束了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果数组下标处结点即为当前K对应的结点,则说明该结点将被覆盖,记录e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果时红黑树结点,基于红黑树操作将当前结点插入到树中,并返回新结点,如果红黑树中已存在该节点则返回该结点
            //红黑树的相关只是不在这里细述
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//否则为普通链表结点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                    	//基于K,V构建新结点并追加在链表尾端
                        p.next = newNode(hash, key, value, null);
                        //如果达到树化阈值,则将当前链表树化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果链表中存在该K,则说明找到覆盖结点,返回该覆盖结点,退出遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e代表前面遍历结点或树过程中发现相同K,则进行覆盖操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //onlyIfAbsent = true表示不允许覆盖,但value==null除外
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //空方法,留待LinkedHahMap实现
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //累加修改次数
        ++modCount;
        //如果超过容量阈值进行扩容
        if (++size > threshold)
            resize();
        //空方法,留待LinkedHahMap实现   
        afterNodeInsertion(evict);
        return null;
    }
final Node<K,V>[] resize():集合hash表扩容
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
        	//如果集合数组长度已经达到最大值,则将扩容阈值设置为Integer.MAX_VALUE,不在进行扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //双倍扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //初始容量使用阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	//初始化容量为16,newThr=16* 0.75 = 12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {//基于自定义负载因子计算阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //创建新的table并将table表执行该新table
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {//遍历旧table数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)//如果槽位不存在多个结点,则直接在该结点新增到新table的相应槽位上
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//如果是红黑树结点则通过split方法进行rehash方法
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    	//链表上的每个结点在旧table产生了hash碰撞,即对数组长度求余结果相同,但对新table数组长度求余可能不同
                    	//这种求余结果要么还是原来的值,要么是原来的值+原数组长度
                    	//比如原数组长度=16,新数组长度=32,K的hash=33,则无论是对16还是32求余其结果都是1(这里实际上是33 & 15 和33 & 31)
                    	//因此loHead表示旧table上的所有经过hash过后任然位于旧槽位的结点,loHead为链表头,loTail为链表尾
                    	//hiHead为新槽位的链表头,hiTail为新槽位尾
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//e.hash * oldCap == 0说明e.hash在新数组中位置不变
                                if (loTail == null)
                                    loHead = e;//初始化loTail
                                else
                                    loTail.next = e;//追加到尾部,
                                loTail = e;//将当前结点设置为新的尾部
                            }
                            else {//结点槽位发生变化为旧槽位+旧table.length
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;//该节点可能还保留旧table中下一个结点、删除
                            newTab[j] = loHead;//将低位链表设置到新table中
                        }
                        if (hiTail != null) {//同上
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//返回新table
    }
final void treeifyBin(Node<K,V>[] tab, int hash)初始化红黑树结点
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //树化的条件是数组长度大于等于64,否则执行resize操作
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;//hd==head,tl=tail,维护链表属性
        //遍历链表
        do {
        	//创建Node对应的TreeNode结点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;//初始化链表头
            else {
                p.prev = tl;//双向链表维护
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
final void treeify(Node<K,V>[] tab)结点树化
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {//设置红黑树根节点
            x.parent = null;
            x.red = false;//根节点颜色位黑色
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {//二叉树循环查找树新增位置
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;//向p结点左侧遍历
                else if (ph < h)
                    dir = 1;//向p结点右侧遍历
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||//非Comparable对象
                         (dir = compareComparables(kc, k, pk)) == 0)//Comparalbe对象相等无法判断带线啊哦
                    dir = tieBreakOrder(k, pk);//使用类名的字符串进行比较

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {//已遍历到叶子结点
                    x.parent = xp;//设置新增结点父结点
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);//新增后红黑叔重新平衡
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);//将当前根节点设置为table的数组下标槽位结点,同时链接上上next和pre属性
}

LinkedHashMap

  • LinkedHashMap继承HashMap,其实现基本与HashMap相差不大,区别在于其内部维护了一个贯穿全局的链表表姐,因此保存LinkedHashMap是有序的, 通常情况下集合的迭代顺序为元素的加入顺序。
  • 由于维护了全局的链表结构,因此迭代不需要对table字段进行迭代,这在一定程度上比HashMap的迭代速度要快,因为HashMap要遍历并判断未命中的槽位
  • 但由于LinkedHashMap增加了额外的链表维护,增删性能可能有所下降

TreeMap

  • 基于红黑树结构实现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值