大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 015 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。
–
Java 集合框架(Java Collections Framework)是 Java 标准库中的一个核心组件,它提供了一套用于处理数据集合的接口和类。作为其中的重要成员,Vector 和 Stack 在特定场景中扮演着关键角色。Vector 是一种同步的动态数组,实现了 List 接口,适用于需要线程安全的场景;而 Stack 是 Vector 的子类,提供了后进先出(LIFO)的数据结构操作。本文将对 Vector 和 Stack 进行全面的介绍,探讨它们的使用方法、工作原理以及源码实现,以帮助开发者深入理解和高效使用这些集合类。同时,通过源码解析,我们将揭示其内部机制,为优化和定制提供有力支持。
文章目录
- 1、 Vector 与 Stack
- 1.1、Vector 概述
- 1.2、Stack 概述
- 2、Vector 的具体实现原理
- 2.1、Vector 底层的数据结构
- 2.2、Vector 的扩容
- 2.3、Vector 对线程安全的实现
- 3、Stack 的具体实现原理
- 4、Vector 与 Stack 的过时原因与替代类
- 4.1、不推荐使用 Vector 来实现线程安全的原因
- 4.2、Stack 过时的原因
1、 Vector 与 Stack
写在前面:在开始介绍 Vector 与 Stack 之前,我们首先应该了解的是 Vector 与 Stack 这两个类在如今的 Java 版本中都早已过时,在 Java 出于对向后兼容性的考虑,才没有删除。但是我们不会因此认为 Vector 与 Stack 的实现是没有必要了解了,因为二者依旧会偶尔出现在面试问题当中。
1.1、Vector 概述
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList慢。
Vector 的思路和 ArrayList 基本是相同的,底层是数组保存元素,Vector 默认的容量是10,有一个增量系数,如果指定,那么每次都会增加一个系数的大小,否则就扩大一倍。
Vector 扩容的时候,其实就是数组的复制,其实还是比较耗时间的,所以,我们使用的时候应该尽量避免比较消耗时间的扩容操作。
Vector 和 ArrayList 最大的不同,是它是线程安全的,几乎每一个方法都加上了 Synchronize 关键字,所以它的效率相对也比较低一点。ArrayList 如果需要线程安全,可以使用 Collections.synchronizedList(new ArrayList(...));
方法,获取一个线程安全的 List。
1.2、Stack 概述
Stack
是 Java 集合框架中的一个类,它继承自 Vector
,因此它本质上也是一个线程安全的动态数组。但是,Stack
类被设计为了一个后进先出(LIFO, Last In First Out)的数据结构,通常被称为栈。
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶;
- 出栈:栈的删除操作叫做出栈。出数据在栈顶。
2、Vector 的具体实现原理
2.1、Vector 底层的数据结构
与 ArrayList 类似(基本一致),Vector 内部也使用了动态数组来存储元素。
package java.util;import ...public class Vector<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{// 省略其他方法和实现细节...// 存储元素的数组 protected Object[] elementData; // 当前向量中元素的数量 protected int elementCount; // 容量增量,用于指定每次容量不足时增长的大小,如果小于等于0,则每次增长一倍 protected int capacityIncrement; /** * 构造一个具有指定初始容量和容量增量的空向量。 * * @param initialCapacity 向量的初始容量 * @param capacityIncrement 向量的容量增量 * @throws IllegalArgumentException 如果指定的初始容量是负数 */ public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } /** * 构造一个具有指定初始容量的空向量,其容量增量等于零。 * * @param initialCapacity 向量的初始容量 * @throws IllegalArgumentException 如果指定的初始容量是负数 */ public Vector(int initialCapacity) { this(initialCapacity, 0); } /** * 构造一个空的向量,使其内部数据数组的大小为10,标准容量增量为零。 */ public Vector() { this(10); } // 省略其他方法和实现细节...
}
2.2、Vector 的扩容
Vector 的扩容方式依旧与 ArrayList 相同,当元素数量超过当前容量时,Vector 会复制一个新的更大容量的数组。可能在扩容上的主要不同就是 Array 的一次扩容为原数组的 1.5 倍(int newCapacity = oldCapacity + (oldCapacity >> 1)
) 而 Vector 的扩容在不指定 capacityIncrement
的值得情况下为 2 倍(int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
)。
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // ... 其他成员变量和方法 ... // 容量增量,用于指定每次容量不足时增长的大小,如果小于等于 0,则每次增长一倍 protected int capacityIncrement; // ... 其他成员变量和方法 ... /** * 将指定的元素作为此向量的最后一个元素插入。 * * @param obj 要添加到此向量的元素 * @throws NullPointerException 如果指定的元素为 {@code null} 并且此向量不允许 null 元素 * (由具体实现决定) */ public synchronized void addElement(E obj) { modCount++; // 修改计数器增加,表示结构已修改 ensureCapacityHelper(elementCount + 1); // 确保容量足够 elementData[elementCount++] = obj; // 在当前末尾添加元素,并更新元素计数 } /** * 辅助方法,确保此向量的容量至少为指定的最小容量。 * 如果当前容量小于最小容量,则增加容量。 * * @param minCapacity 所需的最小容量 */ private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code(防止溢出的代码) if (minCapacity - elementData.length > 0) grow(minCapacity); // 如果最小容量大于当前容量,则增加容量 } /** * 增加此向量的容量,以确保其至少能容纳指定的最小容量。 * 如果指定的最小容量大于当前容量,则此向量的容量将增加至大于或等于最小容量的最小可能值。 * * @param minCapacity 所需的最小容量 */ private void grow(int minCapacity) { // overflow-conscious code(防止溢出的代码) int oldCapacity = elementData.length; // 根据 capacityIncrement(如果大于0)或当前容量来计算新容量 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); // 如果新容量仍然小于最小容量,则直接使用最小容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 检查新容量是否超过 MAX_ARRAY_SIZE,如果是,则调用 hugeCapacity 方法获取适当的大小 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 使用新的容量复制数组 elementData = Arrays.copyOf(elementData, newCapacity); } // ... 其他成员变量和方法 ... }
2.3、Vector 对线程安全的实现
Vector 类和 ArrayList 类的最大区别在于线程安全性。Vector 类是线程安全的,可以在多线程环境中使用,但是性能相对较低。而 ArrayList 类不是线程安全的,性能相对较高。
而 Vector 的线程安全性的实现方式就是在与 Array 基本相同的方法之前加 synchronized
关键字。
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // ... 其他成员变量和方法 ... public synchronized E get(int index) {...}public synchronized E set(int index, E element) {...}public boolean remove(Object o) {...}public void add(int index, E element) {...}// ... 其他成员变量和方法 ... }
3、Stack 的具体实现原理
Stack 的具体实现十分简单“
/** * Stack 类表示后进先出(LIFO)的对象堆栈。 * 它继承自 Vector 类,提供了堆栈的一些基本操作,如 push、pop、peek 等。 * * @param <E> 堆栈中元素的类型 */
public class Stack<E> extends Vector<E> { /** * 无参构造函数,创建一个空的 Stack 实例。 */ public Stack() { } /** * 将指定的元素推入此堆栈顶部。 * * @param item 要推入堆栈顶部的元素 * @return 被推入堆栈的元素 */ public E push(E item) { addElement(item); return item; } /** * 从此堆栈中弹出顶部对象,并返回该对象作为此函数的值。 * * @return 堆栈顶部的元素(即最后一个添加的元素) * @throws EmptyStackException 如果此堆栈为空 */ public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } /** * 查看堆栈顶部的对象,但不从堆栈中移除它。 * * @return 堆栈顶部的元素(即最后一个添加的元素) * @throws EmptyStackException 如果此堆栈为空 */ public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } /** * 测试此堆栈是否为空。 * * @return 如果此堆栈不包含任何项,则返回 true;否则返回 false */ public boolean empty() { return size() == 0; } /** * 返回此堆栈中最后一次出现的指定元素的索引(从 1 开始), * 如果堆栈不包含此元素,则返回 -1。 * * @param o 要搜索的元素 * @return 最后一次出现指定元素的索引(从 1 开始),如果未找到则返回 -1 */ public synchronized int search(Object o) { int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1; } // 序列化 ID 用于版本控制 private static final long serialVersionUID = 1224463164541339165L;
}
4、Vector 与 Stack 的过时原因与替代类
4.1、不推荐使用 Vector 来实现线程安全的原因
在 Java 中,不推荐使用 Vector 来实现线程安全的 ArrayList,主要原因有以下几点:
-
同步机制效率低下:
Vector
的所有方法都被同步(synchronized)了,这意味着每次对Vector
的操作都会获取并释放锁。这种方式会导致大量的上下文切换和锁竞争,影响性能。特别是在高并发环境下,这种同步机制会成为瓶颈。 -
过时的设计:
Vector
是 Java 1.0 引入的类,当时并没有考虑到并发编程的高效实现。后来,Java 引入了更高效的并发集合类,如java.util.concurrent
包下的集合类,这些类在设计时充分考虑了并发环境下的性能问题。 -
更好的替代品:Java引入了
Collections.synchronizedList
方法,可以将普通的ArrayList
包装成线程安全的集合。此外,java.util.concurrent
包下提供了更现代化的并发集合类,如CopyOnWriteArrayList
,它们在并发场景下表现更好。
一些替代Vector
的推荐方法和类:
- 使用
Collections.synchronizedList
:可以将一个普通的ArrayList
转换为线程安全的集合 - 使用
CopyOnWriteArrayList
:CopyOnWriteArrayList
是java.util.concurrent
包提供的线程安全的列表实现,它在写操作时进行复制,因此在并发读多写少的场景下性能较好
4.2、Stack 过时的原因
同样的,Java 中的 Stack 类已经被标记为过时(deprecated)。从 Java 1.2 版本开始,Stack 类就不再被推荐使用。Java 官方建议使用 Deque(双端队列)来替代 Stack,因为 Deque 提供了与 Stack 类似的操作方法,如 push()
、pop()
、peek()
和 isEmpty()
等,同时还具有更灵活和高效的特点。
具体来说,Deque是一个接口,它支持在两端插入和删除元素,因此可以很方便地实现栈(Stack)的功能。而且,Deque接口的实现类(如LinkedList)在性能上通常优于Stack类。
以下是关于 Stack 类过时的一些关键点和原因:
- 历史遗留:Stack 类作为 Java 早期版本中的一个类,虽然提供了栈的基本功能,但随着 Java 集合框架的不断发展,更先进、更灵活的数据结构被引入,使得 Stack 类逐渐失去了其优势地位。
- 功能限制:Stack 类在功能上存在一些限制,比如它不支持并发访问,这在多线程编程中可能会导致问题。此外,Stack类的继承结构也使其在某些场景下使用不够灵活。
- 性能考虑:与 Deque 等现代数据结构相比,Stack 类在性能上可能存在不足。例如,Stack 类在插入和删除元素时可能需要进行额外的操作,从而降低了其效率。
- 替代方案:Java 提供了多种替代 Stack 类的方案,其中最常用的是 Deque 接口的实现类(如 LinkedList)。这些替代方案不仅提供了与 Stack 类类似的功能,而且在性能、灵活性和可扩展性方面都优于 Stack类。