Collection集合体系全景图(一)

1,029 阅读12分钟

集合概念的由来

集合是为了方便存储和操作一组对象而设计,例如在实际开发中,我们经常需要处理一组数据,例如存储用户信息、商品信息等,而集合类提供了一种方便的方式来管理这些数据。

集合与数组相比不同之处

  1. 动态扩展:集合类可以根据需要动态扩展大小,而数组的大小是固定的。 当然数组也是可以实现自动扩容,但需要手动进行实现。数组的默认大小是根据你创建数组时候进行,指定数组长度,例如int[] arr = new int[10],可以存放10个整数,如果不指定数组长度是不可进行使用。
  1. 更多的功能:集合类提供了许多实用的功能,例如迭代器、排序、查找等,可以大大提高开发效率
  1. 更高效的内存使用:集合类可以根据实际需要分配内存,而数组需要一次性分配所有内存,可能会造成内存浪费。

集合全家福

image.png

集合家庭成员分类

Collection集合是指一组单个数据元素的集合,这些元素可以是任何类型的对象,例如字符串、数字、自定义对象等。Collection集合包括List、Set和Queue三种类型。

  • List:是一种有序的集合,可以存储重复的元素。List集合中的元素可以通过索引访问和修改,例如ArrayList、LinkedList等。
  • Set:是一种不允许重复元素的集合,元素的顺序是不确定的。Set集合中的元素不可以通过索引访问和修改,例如HashSet、TreeSet等。
  • Queue:是一种先进先出(FIFO)的集合,通常用于实现队列。Queue集合中的元素不可以通过索引访问和修改,例如LinkedList、PriorityQueue等。

Map集合是指一组键/值对映射关系的集合,每个键对应一个值。Map集合中的键是唯一的,但是值可以重复。Map集合包括HashMap、TreeMap、LinkedHashMap等类型。常用的实现有:

  • HashMap:基于哈希表实现,无序的,允许键和值。
  • TreeMap:基于红黑树实现,有序的,不允许键,但允许值。
  • LinkedHashMap:基于哈希表和双向链表实现,有序的,允许键和值。

本章只写关于Collection集合成员家庭,Map家庭可以在后续章节进行补充

Collection集合家庭成员介绍

Iterable接口介绍:

Iterable接口是Java集合框架中的一个接口,它是所有集合类的父接口,定义了一种迭代器的行为,使得集合类可以被迭代。Iterable接口中只有一个方法iterator(),该方法返回一个Iterator对象,用于遍历集合中的元素。

简单使用小案例

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

上述代码中,通过List集合的iterator()方法获取一个Iterator对象,然后使用while循环和next()方法遍历集合中的元素并输出。

Iterable 源码解读

Java中的Iterable接口是一个集合框架中的基本接口,Iterable接口的作用是提供一种统一的遍历集合元素的方式,使得不同的集合类可以使用相同的遍历方式,从而方便程序员进行集合操作。

Iterable 接口包含一个方法 iterator(),该方法返回一个 Iterator 对象,该对象可以用于遍历集合中的元素

forEach方法是用来对集合中的每个元素执行指定的操作,这个操作由传入的Consumer函数式接口实现。默认实现是通过迭代器遍历集合中的元素,然后对每个元素执行操作。

spliterator方法是用来创建一个Spliterator对象,该对象可以对集合中的元素进行分割、遍历和处理。

image.png

1. Collection集合

image.png

Collection集合设计解读

Collection 是 Java 中的一个接口,它是一个集合框架的基础接口,定义了一些通用的方法,用于操作集合中的元素。它定义了一些通用的对集合操作方法,如添加元素、删除元素、判断元素是否存在、获取集合大小等。接口允许我们操作集合时不必关注具体实现, 从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。其他集合接口和类(Map 除外)都扩展或实现了Collection interface。

List接口特点

  • 继承 Collection集合接口

image.png

  • 有序:有序(元素存入集合的顺序和取出的顺序一致)。List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素。
  • 可重复:List允许加入重复的元素。更确切地讲,List通常允许满足条件的元素重复加入容器

List接口具体实现

image.png

ArrayList

image.png

ArrayList实际上是一个动态数组,容量可以动态的增长,其继承了AbstractList,实现了List, RandomAccess(随机访问), Cloneable, java.io.Serializable这些接口。

RandomAccess的作用是允许在数据结构中随机访问元素,而不需要按照顺序遍历整个数据结构。这种随机访问可以大大提高数据结构的访问效率,特别是对于大型数据结构来说。RandomAccess通常用于数组、列表等数据结构中,可以通过索引或下标来访问元素。在Java中,实现了RandomAccess接口的数据结构可以使用快速随机访问的算法,而没有实现该接口的数据结构则需要使用迭代器等方式来遍历元素。

简单说就是,如果实现RandomAccess接口就下标遍历,反之迭代器遍历 实现了Cloneable, java.io.Serializable意味着可以被克隆和序列化,LinkedList由于底层采用数据结构时双向链表(内存空间不连续))所以就不支持快速随机访问的能力,必须依次进行遍历。

Cloneable 接口是 Java 中的一个标记接口,用于指示一个类可以被克隆。实现 Cloneable 接口的类可以使用 Object 类中的 clone() 方法来创建一个新的对象,该对象与原始对象具有相同的状态。Cloneable 接口的作用是提供一种简单的方式来实现对象的复制,而不需要重新创建一个新的对象并将其初始化为原始对象的副本。这在某些情况下可以提高程序的性能和效率。但是需要注意的是,clone() 方法是浅拷贝,即只复制对象的基本类型和引用类型的地址,而不会复制引用类型所指向的对象。因此,在使用 clone() 方法时需要特别小心,避免出现意外的错误。

浅拷贝和深拷贝区别

浅拷贝只复制对象的引用,而不是对象本身。也就是说,新对象和原对象共享同一个引用类型的属性,修改其中一个对象的属性会影响另一个对象的属性。

Array.prototype.concat():返回一个新数组,包含原数组和其他数组或值的拷贝

深拷贝则是将对象及其引用类型的属性全部复制一份,新对象和原对象互不影响。

JSON.parseObject(result) 方法属于深拷贝

需要注意的是,深拷贝可能会因为对象的循环引用而导致栈溢出或死循环

Serializable是Java中的一个接口,用于实现对象的序列化和反序列化。一个类实现了Serializable接口后,就可以将该类的对象转换成字节序列,以便在网络上传输或者保存到本地文件中。同时,也可以将字节序列反序列化成对象,以便在程序中使用。

ArrayList 使用方式

// 初始化一个 String 类型的数组 
String[] stringArr = new String[]{"hello", "world", "!"}; 
// 修改数组元素的值
stringArr[0] = "goodbye"; System.out.println(Arrays.toString(stringArr));
// [goodbye, world, !] 
// 删除数组中的元素,需要手动移动后面的元素 
for (int i = 0; i < stringArr.length - 1; i++) { 
stringArr[i] = stringArr[i + 1]; 
} 
stringArr[stringArr.length - 1] = null;
System.out.println(Arrays.toString(stringArr));

ArrayList优缺点:

优点

  1. 可以动态地添加和删除元素,不需要预先指定数组的大小;
  2. 可以存储任何类型的对象,包括基本数据类型的包装类;
  3. 可以通过索引快速访问元素
  4. 可以使用迭代器遍历集合中的元素;
  5. 可以使用泛型来保证集合中元素的类型安全

缺点

  1. 在插入或删除元素时,需要移动其他元素,效率较低;
  2. 在大量元素的情况下,可能会占用较大的内存空间
  3. 不支持多线程并发访问,需要手动进行同步处理;
  4. 在进行元素查找时,效率较低,因为需要遍历整个集合。

ArrayList源码阅读

List list = new ArrayList<>(); 创建一个 ArrayList一个执行流程是

// 通过构造方法进行初始化
    /**  
    *默认无参构造函数  
     *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,
      也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10  
    */
    public ArrayList() {  
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  
    }
    /**  
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)  
*/  
public ArrayList(int initialCapacity) {  
  if (initialCapacity > 0) {  
   this.elementData = new Object[initialCapacity];  
} else if (initialCapacity == 0) {  
   this.elementData = EMPTY_ELEMENTDATA;  
} else {  
   throw new IllegalArgumentException("Illegal Capacity: "+  
   initialCapacity);  
}  
}
    

elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA解读

    private static final long serialVersionUID = 8683452581122892189L;  
    /**  
    * 默认初始容量大小  
    */  
    private static final int DEFAULT_CAPACITY = 10;  
    /**  
    * 空数组(用于空实例)。  
    */  
    private static final Object[] EMPTY_ELEMENTDATA = {};  
    /**  
    * 空数组(用于空实例)。  
    */  
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};  
      
    /**  
    * 保存ArrayList数据的数组  
    */  
    transient Object[] elementData;  
    /**  
    * 保存ArrayList数据的数组  
    */  
    private int size;

    //在这里可以看到我们不解的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    //实际上是一个空的对象数组,当添加第一个元素的时候数组容量才变成10
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    

添加方法add

    ```
  /**
     * 将指定的元素追加到此列表的末尾。
     */
    public boolean add(E e) {  
      ensureCapacityInternal(size + 1); // Increments modCount!!  
      elementData[size++] = e;  
      return true;  
     }
    /** 在此列表中的指定位置插入指定的元素。
    *先调用 rangeCheckForAdd 对index进行界限检查然后调用 ensureCapacityInternal 方法保证capacity足够大;
    *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
    */ 
    public void add(int index, E element) { 
    rangeCheckForAdd(index); 
    ensureCapacityInternal(size + 1); 
    // Increments modCount!! 
    //arraycopy()这个实现数组之间复制的方法一定要看一下,
    //下面就用到了arraycopy()方法实现数组自己复制自己 
    //System.arraycopy 是 Java 中的一个方法,
    //用于将一个数组的一部分复制到另一个数组中的指定位置。
    //它的作用是快速地将一个数组的部分内容复制到另一个数组中,
    //可以用于数组的拷贝、合并、排序等操作,
    //其中,src 表示源数组,srcPos 表示源数组的起始位置,
    //dest 表示目标数组,destPos 表示目标数组的起始位置,
    //length 表示要复制的元素个数。该方法是一个静态方法,可以直接通过类名调用
    System.arraycopy(elementData, index, elementData, index + 1, size - index); 
    elementData[index] = element;
    size++; }

这个方法就是将一个元素增加到数组的size++位置上。ensureCapacityInternal,它是用来扩容的,准确说是用来进行扩容检查的

    private void ensureCapacityInternal(int minCapacity) {  
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));  
    }
    private void ensureExplicitCapacity(int minCapacity) {  
    modCount++;  
    if (minCapacity - elementData.length > 0)  
    grow(minCapacity);  
    }
    private static int calculateCapacity(Object[] elementData, int minCapacity) {  
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  
    return Math.max(DEFAULT_CAPACITY, minCapacity);  
    }  
    return minCapacity;
    private void grow(int minCapacity) {  
    int oldCapacity = elementData.length;  
    int newCapacity = oldCapacity + (oldCapacity >> 1);  
    if (newCapacity - minCapacity < 0)  
    newCapacity = minCapacity;  
    if (newCapacity - MAX_ARRAY_SIZE > 0)  
    newCapacity = hugeCapacity(minCapacity);  
    elementData = Arrays.copyOf(elementData, newCapacity);  
}
    private static int hugeCapacity(int minCapacity) {  
    if (minCapacity < 0) // overflow  
    throw new OutOfMemoryError();  
    return (minCapacity > MAX_ARRAY_SIZE) ?  
    Integer.MAX_VALUE :  
    MAX_ARRAY_SIZE;  
}
    }
  • 参数 minCapacity 表示需要存储的元素数量的最小值。

  • calculateCapacity(elementData, minCapacity) 是一个内部方法,用于计算实际需要的容量大小。

  • ensureExplicitCapacity() 是另一个内部方法,用于检查当前容量是否足够,如果不够则进行扩容。

  • 因此,ensureCapacityInternal() 的作用就是先计算需要的容量大小,然后检查当前容量是否足够,如果不够则进行扩容,以确保 ArrayList 内部的容量足够存储指定的元素数量。

  • grow方法具体实现是先将原数组的容量(oldCapacity)右移一位(相当于除以2),然后将得到的结果加上原数组的容量,得到新的容量(newCapacity)。如果新容量仍然小于指定的最小容量,则将新容量设置为指定的最小容量。

  • 如果新容量超过了数组的最大容量(Integer最大值-8),则调用另一个方法(hugeCapacity)来获取一个合适的容量值。这个方法呢判断当前值大于Arrary最大值(Integer最大值-8 )返回Integer最大值否则返回Arrary最大值(Integer-8)

  • 最后,使用Arrays.copyOf方法将原数组的元素复制到新数组中,并将新数组赋值给原数组变量elementData。

ArrayList支持两种删除

  1. remove(int index) 按照下标删除 常用
  2. remove(Object o) 按照元素删除 会删除和参数匹配的第一个元素
    /** 移除list中指定位置的元素 所有后续元素左移 下标减1
    *参数是要移除元素的下标
    返回值是被移除的元素 
    */
    public E remove(int index) {
    //下标越界检查 rangeCheck(index); 
    //修改次数统计 modCount++; 
    //获取这个下标上的值 
    E oldValue = elementData(index); 
    //计算出需要移动的元素个数 (因为需要将后续元素左移一位 此处计算出来的是后续元素的位数) 
    int numMoved = size - index - 1; 
    //如果这个值大于0 说明后续有元素需要左移 是0说明被移除的对象就是最后一位元素 
    if (numMoved > 0)
    //索引index只有的所有元素左移一位 覆盖掉index位置上的元素 
    System.arraycopy(elementData, index+1, elementData, index,numMoved); 
    // 将最后一个元素赋值为null 这样就可以被gc回收了
    elementData[--size] = null; 
    //返回index位置上的元素 return oldValue; } 
    //移除特定元素 public boolean remove(Object o) { 
    //如果元素是null 遍历数组移除第一个null 
    if (o == null) { 
    for (int index = 0; index < size; index++) 
    if (elementData[index] == null) {
   //遍历找到第一个null元素的下标 调用下标移除元素的方法 
    fastRemove(index); 
     return true;
    } 
    } else { 
    //找到元素对应的下标 调用下标移除元素的方法
    for (int index = 0; 
     index < size; index++) 
    if (o.equals(elementData[index])) { 
    fastRemove(index); return true; 
    } 
    } 
    return false;
    } 
    //按照下标移除元素
    private void fastRemove(int index)    { 
    modCount++; int numMoved = size - index - 1; 
    if (numMoved > 0) 
    System.arraycopy(elementData, index+1, elementData, index, numMoved); 
    elementData[--size] = null;
    }

ArrayList总结

  1. 底层数组实现,使用默认构造方法初始化容量是10
  2. 扩容的长度是在原长度基础上加二分之一
  3. 实现了RandomAccess接口,底层又是数组,读取元素性能很好
  4. 线程不安全,所有的方法均不是同步方法也没有加锁,因此多线程下慎用
  5. 顺序添加快删除和插入需要复制数组 性能很差

赠送一张总结整理

38e7b7942d6529e3c4fe909c34e9357.png