Stack继承自Vector
,实现了栈元素的基本操作,最典型的就是push
压栈和pop
出栈,整个Stack.java
的代码很简单:
public class Stack<E> extends Vector<E> {
public Stack() {
}
/**
* 压入一个元素入栈,实际就是调用Vector的addElement方法
* @see java.util.Vector#addElement
*/
public E push(E item) {
addElement(item);
return item;
}
/**
* 弹元素出栈:拿到栈顶元素,从栈顶移除元素,并将元素返回
*/
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
/**
* 只通过elementAt方法去取到栈顶元素
*/
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
/**
* 返回栈是否为空
*/
public boolean empty() {
return size() == 0;
}
/**
* 搜索元素在栈内的index,如果栈内有多个相同元素,则拿到的是距离栈顶最近的index
*/
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = 1224463164541339165L;
}
栈的实现很大程度上是通过父类Vector
来实现的,Vector内部通过一个对象数组来存储元素:
//Vector.java
protected Object[] elementData
当 elementData
数组满了之后,添加新元素时,会动态扩展数组长度,最大的容量为Integer.MAX_VALUE
入栈
通过代码可以看到,Stack通过父类 Vector 的addElement()
方法来实现入栈,这个方法是多线程安全的:
//Vector.java
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
实际上这个方法就是每次判断数组是否满了,未满则直接将元素放入栈顶,如果满了则将栈的空间在旧elementData.length
的基础上扩充一倍,然后将新的元素放入栈顶。
下面来看具体是怎么实现空间的扩充的,从ensureCapacityHelper
入手,代码如下:
//Vector.java
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
从上面可以看到 minCapacity 的值为elementCount + 1
,即当前元素个数+1,此时的elementData
数组的长度是多少呢?我们可以看看默认情况下的构造方法,传入的初始容量initialCapacity
为10,增量capacityIncrement
为0
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
所以第一次stack压入元素时,elementData
数组长度为10,不会扩展空间,只是将数组的elementData[elementCount++]
位置指向插入的元素。当元素个数大于数组长度10时,if 里面成立,所以会执行grow
方法来扩大数组长度,代码如下:
//Vector.java
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);
}
这里的capacityIncrement
是 vector 容量的增量值,如果capacityIncrement
值为0,那么通过上面的代码可以知道,newCapacity
拿到的是2倍的 oldCapacity,也就是原来的数组长度的2倍。从capacityIncrement
的注释文档里可以看到:
//Vector.java
/**
* The amount by which the capacity of the vector is automatically
* incremented when its size becomes greater than its capacity. If
* the capacity increment is less than or equal to zero, the capacity
* of the vector is doubled each time it needs to grow.
*
* 翻译:capacityIncrement是vector容量的增量值,当vector的尺寸变大时,
* 会自动增加capacityIncrement大小,而当capacityIncrement小于等于0,则容量会双倍地增加
*/
protected int capacityIncrement;
由于 Vector 的默认构造方法里传入的initialCapacity
是10,所以最开始的数组长度为10,在超过10后,每次容量翻倍,这点可以举个例子测试下:
/**
* 通过反射拿到Vector里面的elementData数组,打印出数组长度
*/
private Object[] getElements(Vector vector) {
try {
Field capacityField = vector.getClass().getDeclaredField("elementData");
//设置Field可访问,protected类型的elementData无法直接访问
capacityField.setAccessible(true);
Object[] elementData = (Object[]) capacityField.get(vector);
Log.e("lpc","elementData: " + elementData.length) ;
return elementData ;
} catch (NoSuchFieldException e) {
Log.e("lpc","NoSuchFieldException: " + e.getMessage()) ;
e.printStackTrace();
} catch (IllegalAccessException e) {
Log.e("lpc","IllegalAccessException: " + e.getMessage()) ;
e.printStackTrace();
}
return null ;
}
public void init() {
//默认new Vector时,容量为10,elementData数组长度为10
Vector<Integer> vector = new Vector<>();
//手动决定初始容量和增量
//Vector<Integer> vector = new Vector<>(10,1);
for (int i=0;i<9;i++) {
vector.add(i);
}
//此时有9个元素,数组长度应该为10
getElements(vector);
vector.add(10);
//此时有10个元素,数组长度也应该为10
getElements(vector);
vector.add(11);
//此时有11个元素,数组扩大一倍,因此应该是20
getElements(vector);
for (int i=0;i<9;i++) {
vector.add(i);
}
vector.add(12) ;
//连续添加了9+1个元素,此时有21个元素,数组长度应该再翻一倍,为40
getElements(vector);
vector.add(13) ;
//22个元素,长度为40
getElements(vector);
}
日志如下:
E/lpc: elementData: 10
E/lpc: elementData: 10
E/lpc: elementData: 20
E/lpc: elementData: 40
E/lpc: elementData: 40
如我们猜想的一样,当 capacityIncrement = 0
时,每次扩容都是在原来基础上加倍,现在我们改动一句话,在Vector的构造方法里传入 initialCapacity
为10,capacityIncrement
为1,意思是初始容量为10,每次扩容数组长度加1 :
Vector<Integer> vector = new Vector<>(10,1);
这种情况下,当数组元素个数为9,10,11,21,22 时的数组长度结果如下,每次扩容时,正好是当前元素的个数+1:
E/lpc: elementData: 10
E/lpc: elementData: 10
E/lpc: elementData: 11
E/lpc: elementData: 21
E/lpc: elementData: 22
再回到栈上面来,由于Stack
的构造方法只有一个默认的public Stack() {}
,所以其存储数据时,遵循的扩容原则就是默认的双倍扩容方式,因此每次扩容时,空间上的浪费还是很大的。
回到 grow
方法,确定扩容后的新容量newCapacity
后,调用Arrays.copyOf
方法,来复制出一个长度为newCapacity
的 elementData 数组。接着回到addElement(E object)
方法里,执行elementData[elementCount++] = obj;
,将新的元素存入数组最顶部。
出栈
出栈时,先通过peek方法拿到栈顶元素,然后删除栈顶元素,返回元素。pop
方法也是线程安全的:
//Stack.java
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
//移除栈顶的元素
removeElementAt(len - 1);
return obj;
}
删除元素也是通过父类Vector里面的removeElementAt
方法实现,代码如下:
//Vector.java
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 */
}
这里的System.arraycopy(elementData, index + 1, elementData, index, j);
实际上就是将整个elementData
数组从index+1开始向前移一位,将index所在位置的值覆盖掉,以此来达到remove的效果,复制后将栈顶置空,方便GC回收内存。在pop
方法中,传入的是len-1
,因此每次 pop 都只会移除栈顶上的元素。System.arrayCopy
方法如下:
//System.java
/**
*将 源数组src 从起点位置srcPos开始的lenght长度的部分,
*复制到目标数组的destPos位置
* @param src 源数组.
* @param srcPos 源数组的起点位置.
* @param dest 目标数组.
* @param destPos (复制的内容放在)目标数组的开始位置.
* @param length 需要copy的数组长度.
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);