集合概念的由来
集合是为了方便存储和操作一组对象而设计,例如在实际开发中,我们经常需要处理一组数据,例如存储用户信息、商品信息等,而集合类提供了一种方便的方式来管理这些数据。
集合与数组相比不同之处
- 动态扩展:集合类可以根据需要动态扩展大小,而数组的大小是固定的。 当然数组也是可以实现自动扩容,但需要手动进行实现。数组的默认大小是根据你创建数组时候进行,指定数组长度,例如int[] arr = new int[10],可以存放10个整数,如果不指定数组长度是不可进行使用。
- 更多的功能:集合类提供了许多实用的功能,例如迭代器、排序、查找等,可以大大提高开发效率
- 更高效的内存使用:集合类可以根据实际需要分配内存,而数组需要一次性分配所有内存,可能会造成内存浪费。
集合全家福
集合家庭成员分类
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
对象,该对象可以对集合中的元素进行分割、遍历和处理。
1. Collection集合
Collection集合设计解读
Collection 是 Java 中的一个接口,它是一个集合框架的基础接口,定义了一些通用的方法,用于操作集合中的元素。它定义了一些通用的对集合操作方法,如添加元素、删除元素、判断元素是否存在、获取集合大小等。接口允许我们操作集合时不必关注具体实现, 从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。其他集合接口和类(Map 除外)都扩展或实现了Collection interface。
List接口特点
- 继承 Collection集合接口
- 有序:有序(元素存入集合的顺序和取出的顺序一致)。List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素。
- 可重复:List允许加入重复的元素。更确切地讲,List通常允许满足条件的元素重复加入容器
List接口具体实现
ArrayList
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优缺点:
优点:
- 可以动态地添加和删除元素,不需要预先指定数组的大小;
- 可以存储任何类型的对象,包括基本数据类型的包装类;
- 可以通过索引快速访问元素;
- 可以使用迭代器遍历集合中的元素;
- 可以使用泛型来保证集合中元素的类型安全。
缺点:
- 在插入或删除元素时,需要移动其他元素,效率较低;
- 在大量元素的情况下,可能会占用较大的内存空间;
- 不支持多线程并发访问,需要手动进行同步处理;
- 在进行元素查找时,效率较低,因为需要遍历整个集合。
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支持两种删除
- remove(int index) 按照下标删除 常用
- 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总结
- 底层数组实现,使用默认构造方法初始化容量是10
- 扩容的长度是在原长度基础上加二分之一
- 实现了RandomAccess接口,底层又是数组,读取元素性能很好
- 线程不安全,所有的方法均不是同步方法也没有加锁,因此多线程下慎用
- 顺序添加快删除和插入需要复制数组 性能很差