Java集合体系
Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。
Collection体系
Collection是List、Set和Queue集合类的根接口,它定义了集合的基本操作和行为,如添加、删除、遍历等。
List接口:
List接口是一个有序的集合,允许重复元素,常用实现类包括ArrayList、LinkedList和Vector。
ArrayList:
ArrayList是基于数组实现的动态数组,它可以根据需要自动调整大小。它支持快速随机访问和通过索引进行元素操作。由于底层使用数组实现,所以适合频繁访问和更新元素的场景。但是,对于大量的插入和删除操作,性能可能较差。
添加相关方法:
boolean add(E element) | 将指定元素添加到列表末尾 |
void add(int index,E element) | 在指定位置插入指定元素 |
boolean addAll(collection<? entends E> collection) | 将指定集合中所有元素追加到列表末尾 |
删除相关方法:
E remove(int index) | 删除指定索引的元素并返回 |
boolean remove(object element) | 删除第一个与指定元素匹配的项目(如果存在 |
boolean removeAll(collection<?> collection) | 删除列表中与指定集合相匹配的所有元素 |
查询相关方法:
E get(int index) | 返回指定索引的元素 |
int indexOf(object element) | 返回指定元素第一次出现时的索引,不存在返回-1 |
int lastIndexOf(Object element) | 返回指定元素最后一次出现的索引,不存在返回-1 |
修改相关方法:
E set(int index,E element) | 替换指定索引的元素 |
其他方法:
int size() | 返回列表中元素的数量 |
boolean isEmpty() | 判断列表是否为空 |
void clear() | 清空列表中的元素 |
boolean contains(Object element) | 查询是否存在指定元素 |
ArrayList与Array的区别:
- Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
- Array 大小是固定的,ArrayList 的大小是动态变化的。
- ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。
ArrayList的扩容机制:
当我们创建一个新的ArryList时,默认使用无参的方式,此时初始容量为0。并不很多人说的默认容量是10,而是第一次从0第一次扩容至10。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
如果是使用有参构造的话,会使用我们指定的int参数来确认初始容量。
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);
}
}
如果使用集合来创建ArrayList那么就会根据集合大小来设置初始容量。
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
扩容机制
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
- oldCapacity保存当前数组的容量。
- 首先,检查ArrayList是否已经包含元素(即oldCapacity > 0)或者elementData不等于默认的空数组(即elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。这是为了判断是否需要进行扩容。
- 如果需要进行扩容,通过调用ArraysSupport.newLength方法计算出新的容量newCapacity。该方法根据当前容量、所需最小容量的增长量和当前容量的一半(作为首选增长量)来确定新的容量。
- 使用Arrays.copyOf方法创建一个新的数组,并将原始数组elementData中的元素复制到新数组中。然后,将新数组赋值给elementData,表示扩容完成。
- 如果不需要扩容(即ArrayList原本是空的),直接创建一个新的数组,长度为默认容量和所需最小容量的较大值。然后,将新数组赋值给elementData。
扩容时增长量的计算:
1.5倍并不是原容量乘1.5,而是原容量加上原容量进行进行右移一位之后的值。
我们拿默认情况下的第三次扩容来举例:
15加上15右移一位等于15+7=22。所以默认情况下第三次扩容的容量就变成了22。
上述为add()方法的容量计算,如果我们调用的是addAll()方法,那么在扩容时会从下次扩容后的容量大小与addAll的容量大小进行比较,选择其中较大的值作为新的容量。
比如说,我们在为一个空ArrayList添加一个长度为11的集合时,此时第一次扩容后的容量为10,那么新的ArrayList的容量便会选择为11。
如果添加集合长度为1,那么新的容量便是第一次扩容的结果10。
为什么ArrayList的增长因子是1.5
因为1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
为什么不是一个固定的扩容值
- 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制
- 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
LinkedList:
LinkedList使用双向链表来存储元素。每个节点包含一个指向前一个节点的引用(prev)和一个指向后一个节点的引用(next)。这种双向链表结构使得在LinkedList中插入、删除元素的操作更高效,因为只需要修改节点的引用,所以插入和删除的时间复杂度为O(1)。
常用方法:
void addFirst(E element) | 在链表的开头添加一个元素。 |
void addLast(E element) | 在链表的末尾添加一个元素 |
void add(int index, E element) | 在指定位置插入一个元素 |
E removeFirst() | 移除链表的第一个元素 |
E removeLast() | 移除链表的最后一个元素 |
E remove(int index) | 移除指定位置的元素 |
E getFirst() | 获取链表的第一个元素 |
E getLast() | 获取链表的最后一个元素 |
E get(int index) | 获取指定位置的元素 |
boolean contains(Object element) | 判断链表是否包含指定元素 |
int size() | 返回链表的大小,即元素的个数 |
boolean isEmpty() | 判断链表是否为空 |
void clear() | 清空链表,移除所有元素 |
Iterator<E> iterator() | 返回一个迭代器,用于遍历链表的元素 |
Object[] toArray() | 将链表转换为数组 |
底层原理:
LinkedList的底层实现是基于双向链表(doubly linked list)。它由一个个节点(Node)组成,每个节点包含一个存储的元素以及两个指针,分别指向前一个节点和后一个节点。
在LinkedList中,每个节点(Node)都是一个独立的对象,它包含以下两个属性:
- 元素值(Element):用于存储实际的数据元素。
- 前后指针(Prev和Next):分别指向前一个节点和后一个节点。
通过这种方式,将多个节点连接在一起,形成一个双向链表。双向链表允许在任意位置高效地插入和删除节点,因为只需要修改相邻节点的指针,而不需要像数组那样移动其他元素。
LinkedList类中还维护了两个特殊节点,即头节点(first)和尾节点(last)。头节点存储链表的第一个元素,尾节点存储链表的最后一个元素。这些特殊节点使得在链表的开头和末尾进行插入和删除操作更加高效。
Vector:
Vector是一个线程安全的动态数组,类似于ArrayList。它的操作方法都是同步的,可以保证在多线程环境下的安全性。然而,由于同步操作的开销,性能上可能不如ArrayList。在现代Java中,推荐使用ArrayList或LinkedList代替Vector。
不过多展开。
Set:
用于表示一组不包含重复元素的集合。Set接口的实现类通常用于存储和操作无序的集合数据。
Set接口的特点如下:
- 不允许重复元素:Set不允许包含重复的元素。当尝试向Set中添加重复元素时,添加操作将被忽略。
- 无序性:Set中的元素没有特定的顺序,即元素在Set中的存储顺序可能与添加顺序不同。
常见实现类
-
HashSet:HashSet是基于哈希表实现的Set接口的实现类。它使用哈希值来存储和检索元素,具有快速的插入、删除和查找操作。HashSet不保证元素的顺序,并且允许使用null元素。
-
TreeSet:TreeSet是基于红黑树实现的Set接口的实现类。它根据元素的自然顺序或自定义比较器进行排序,并保持元素的有序状态。TreeSet的插入、删除和查找操作具有较高的时间复杂度,但可以按照元素的顺序进行遍历。
-
LinkedHashSet:LinkedHashSet继承自HashSet,它在HashSet的基础上使用链表维护元素的插入顺序。LinkedHashSet保持元素的插入顺序,因此可以按照插入顺序进行遍历。