概述
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除操作的一端称为栈顶,另一端称则为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
入栈:栈的插入操作叫做压栈或进栈。
出栈:栈的删除操作叫做出栈。
Stack
从上面的关系图可以看出Stack继承自Vector类,而Vertor类又继承了抽象类AbstractList,同时实现了List接口,所以已经可以猜到stack底层的结构其实是一个Collection集合。
由于Stack继承了Vector类,所以在Stack里面主要实现了下面几个方法:
方法名 | 返回值类型 | 说明 |
---|---|---|
push(E item) | E | 将元素压入栈中 |
pop( ) | E | 弹出栈顶元素 |
peek( ) | E | 返回栈顶元素(不删除) |
empty( ) | boolean | 判断栈是否为空 |
search(Object o) | int | 返回一个对象在此堆栈上的基于1的位置 |
下面两个方法需要说明一下:
1.push
入栈操作在Java中的源码:
public E push(E item) {
addElement(item);
return item;
}
在源码中并没有看到入栈过程的详细过程,而是用addElement方法把后面的细节都封装起来了,其实主要过程就是将要入栈的元素在数组中进行尾插,再将数组使用长度加1。
深究一下就会发现一个问题,数组的长度是有限的,当我们在new一个栈的时候,就会给我们生成一个初始内部容量为10的数组。插满后便需要进行扩容,而扩容并不是无限制,所以当达到一定程度的时候便不能扩容了,就需要抛出异常。
好在这两点它的父类Vector类都考虑到了:
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
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;
}
可以看到在grow()方法中,用一个capacityIncrement来指定扩容长度。默认的情况下相当于数组长度翻倍,如果设置了capacityIncrement变量就增加这个变量指定的这么多。
然后再调用Arrays.copyOf( )方法,将当前数组的元素给拷贝过去。
2.pop
pop()方法即弹出栈顶元素,如果栈顶没有元素就抛出异常。
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
在源码中可以看到在pop()方法中调用了peek()方法,并将其作为返回值返回,其中栈为空则抛出异常的部分也在peek()方法中实现。
在返回之前使用removeElementAt()方法来删除栈顶元素。
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
删除的方式很简单,就是用待删除元素的后面元素依次覆盖前面一个元素。
有一个小问题:
通过peek()取到顶端的元素之后,为什么要用removeElementAt()方法将最顶端的元素移除呢?明明有一个elementCount来记录栈的长度,所以不可以不管这个元素么?
实际上,如果不管它,那么在程序运行的时候就会有一个潜在的内存泄露的问题。因为在java里面,如果我们普通定义的类型属于强引用类型。比如这里vector就底层用的Object[]这个数组强类型来保存数据。强类型在jvm中做gc的时候,只要程序中有引用到它,它是不会被回收的。这就意味着,只要我们一直在用着stack,那么stack里面所有关联的元素就都释放不了。这样运行时间一长就会导致内存泄露的问题。所以,为了解决这个问题,这里就使用了removeElementAt()方法。
用数组模拟实现一个栈
import java.util.Arrays;
class MyStack<E> {
int capacity = 10;
private E[] array =(E[]) new Object[capacity];
private int size = 0;
public void push(E e) {
//插满了则按照2倍的方式进行扩容
if(size == capacity) {
capacity *= 2;
array = Arrays.copyOf(array,capacity);
}
array[size] = e;
size++;
}
public E pop() {
if(empty()){
//源码中是抛出一个异常,简易起见直接返回null
return null;
}
E e = array[size-1];
size--;
return e;
}
public E peek() {
if(empty()){
//也是该抛出一个异常
return null;
}
return array[size-1];
}
boolean empty() {
return size == 0;
}
public int size() {
return size;
}
}
public class TestDemo {
public static void main(String[] args) {
MyStack<Integer> ms = new MyStack<>();
ms.push(1);
ms.push(2);
ms.push(3);
ms.push(4);
System.out.println(ms.size());
System.out.println(ms.peek());
ms.pop();
ms.pop();
System.out.println(ms.size());
System.out.println(ms.peek());
}
}
//执行结果
4
4
2
2