Java 集合框架:Vector、Stack 的介绍、使用、原理与源码解析

CSDN 2024-07-23 13:05:06 阅读 87

大家好,我是栗筝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 与 Stack1.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 如果需要线程安全,可以使用 <code>Collections.synchronizedList(new ArrayList(...)); 方法,获取一个线程安全的 List。

1.2、Stack 概述

Stack 是 Java 集合框架中的一个类,它继承自 Vector,因此它本质上也是一个线程安全的动态数组。但是,Stack 类被设计为了一个后进先出(LIFO, Last In First Out)的数据结构,通常被称为栈。

image-20240618133542754

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶;出栈:栈的删除操作叫做出栈。出数据在栈顶。


2、Vector 的具体实现原理

2.1、Vector 底层的数据结构

与 ArrayList 类似(基本一致),Vector 内部也使用了动态数组来存储元素。

<code>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转换为线程安全的集合使用 CopyOnWriteArrayListCopyOnWriteArrayListjava.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类。




声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。