.2.2 数组的改进版本——Vector类
与数组对象相比,Vector对象除了能很好地实现元素的插入和删除功能外,也拥有动态增长的 特性。
下面,我们通过学习Vector对象的常用方法来体会该对象的语法和用途。为了使用Vector类的方法,我们要在引入java.util.*包。
l 构造函数
Vector类有四种重载方式的构造函数,分别为:
1 Vector(int initialCapacity,int capacityIncrement)
2 Vector(int initialCapacity)
3 Vector()
4 Vector(<E> c)
通过第一类的构造函数,我们能为初始化的Vector对象分配长度为initialCapacity的容量,并且,该Vector可以在必要的时候以capacityIncrement的速度,自增长其容量空间。
通过第二类构造函数,我们可以在创建Vector对象时,指定其初始存储容量。
第三个构造函数既不指定初始的存储容量也不指定capacityIncrement,在调用结束后返回一个Vector对象的句柄。
一般来说,上述的第三类的构造函数最实用,因为,在实际的项目使用中,我们无法预知Vector将要容纳对象的数量,所以我们可以不必指定参数。
而在第四种形式里,体现出了JDK1.6的新特性:泛型(generic type),我们可以这样用: Vector<String> v = new Vector<String> ();,在初始化的Vector内,只可以容纳String的对象。只有当我们在使用Vector前,明确知道其中容纳对象的类型,才可以使用这种类型的构造函数。
l 向Vector中添加元素的addElement方法
在这个方法里,同样体现到了JDK1.6的泛型特性,它的函数原型是:
void addElement(E obj)
通过这个方法,我们可把obj对象添加到该Vector对象的尾部,同时Vector的size加1。
这里我们需要注意: 如果我们在构造Vector时已经通过泛型,明确了这个Vector容纳对象的类型,在使用这个方法时只能添加同样类型的对象。
如果没有使用泛型,则我们可以把任意的Object类型对象添加进Vector。由此我们可以理解泛型的使用特性,如果我们指定了泛型的类型,它是确定的,反之如果我们没有使用泛型,那我们可以把“泛型E”理解成Object类型。
不过请大家注意,如果我们没有使用泛型,那不能把基本数据类型(比如int类型)放到Vector中,因为int不是Object的子类。比如:有以下的代码:
int i = 10;
Vector v = new Vector();
v.addElement(i);
这样做是错误的,因为int不是Object的子类。为了实现上述功能,我们要用到int基本数据类型的封装类,修改后的代码如下所示:
Integer i = new Integer(10);
Vector v = new Vector();
v.addElement(i);
l 在指定索引处添加元素的insertElementAt(E obj, int index) 方法
通过这个方法,我们可以把obj对象添加到参数指定的index索引处,此后的Vector对象里的各内容自动向后移动1个单位。
l 替换指定索引处元素的setElementAt(E obj, int index)方法
这个方法同上面提到的insertElementAt方法很相似。只不过这个方法是替换指定索引处的原来元素,而不是添加。
l 删除Vector对象里指定元素的boolean removeElement(Object obj)方法
通过这个方法,程序员可以删除Vector中的第一个obj内容的对象,这个方法返回一个布尔类型的值,用来表示是否在Vector里找到并删除指定对象。
另外,我们还可以通过void removeElementAt(int index) 删除指定索引的元素。
l 删除Vector里所有元素的void removeAllElements()方法
通过这个方法,我们以删除Vector中的所有对象,操作完成后,该Vector对象的size重置为0。
l 获得Vector当前长度的int size()方法
通过这个方法,我们可以统计出当前Vector中含有多少个元素,这个方法返回一个int类型的变量,通常用在遍历Vector的场合下。
l 依次访问Vector中各元素的E elementAt(int index)方法
我们可以通过这个方法,将Vector对象里索引号为i的元素以泛型或Object类型返回。
这里,我们需要注意:如果我们在构造Vector对象时没有用到泛型,Vector对象由于事先不知道何种类型的对象将会放入其中,所以它会用Object类型来容纳对象。
这样一来,我们通过elementAt方法得到其中元素的时候,得到的一律是Object类型的,也就是说,放入Vector中的对象会丢失其原始类型,我们在得到其中的对象后必须用类型强制转换的方式,把该对象还原成它放入Vector前的类型。
我们学习Java集合类,首先要了解它所包含的方法以及这些方法有什么作用,其次要学会如何在我们的程序中综合使用这些类,并要在这个基础上,学会如何通过这些类,更好地实现实际中的需求。下面,我们通过一些例子来学习一下Vector的具体操作,在以下的代码里,我们将综合使用Vector对象提供的插入和删除元素的方法。
//引入包
import java.util.Vector;
public class vectorTest
{
public static void main(String args[])
{
//定义一个Vector
Vector v=new Vector();
//使用addElement方法,往Vector中插入元素
v.addElement("one ");
v.addElement("two ");
v.addElement("three ");
//注释1
//在第0个索引处插入"zero"
v.insertElementAt("zero",0);
//注释2
v.insertElementAt("anotherThree",3);
//注释3
//替换索引为4的元素
v.setElementAt("four ",4);
//注释4
//清空
v.removeAllElements();
//注释5
}
}
在这段代码里,我们首先通过构造函数,初始化了名为v的一个Vector对象,其次,通过addElement方法,往v里添加了若干对象,此时请注意,这里插入的对象是直接插在v的尾部的。完成后,通过insertElementAt方法,根据指定的索引,再向v里添加了2个对象。完成后,通过setElementAt的方法,替换掉了在索引为4的位置上的对象,最后,通过removeAllElement方法,清空了v对象。
我们可以通过下表3-1,来观察不同时刻下,Vector元素的分布情况。
表3-1 Vector元素分布情况一览表
时刻
索引0
索引1
索引2
索引3
索引4
运行到注释1时
one
two
three
无
无
运行到注释2时
zero
one
two
three
无
运行到注释3时
zero
one
two
anotherThree
three
运行到注释4时
zero
one
two
anotherThree
four
运行到注释5时
无
无
无
无
无
从上述的描述里,我们知道,Vector会丢失放入其中对象的类型信息。而在下面的代码里,我们将看到如何在遍历Vector的过程中,有效地避免这一问题。
import java.util.*;
//首先定义两个类
class Boy
{
private int boyId;
Boy(int i)
{
boyId = i;
}
void print()
{
System.out.println("Boy #" + boyId);
}
}
class Girl
{
private int girlId;
Girl(int i)
{
girlId = i;
}
void print()
{
System.out.println("Girl #" + girlId);
}
}
//主方法
public class BoysAndGirls
{
public static void main(String[] args)
{
//定义Vector
Vector boys = new Vector();
//通过For循环,往boys的Vector里加5个元素
for(int i = 0; i < 5; i++)
{
boys.addElement(new Boy(i));
}
//这里,我们试图在boys里加入girl类型的对象
boys.addElement(new Girl(5));
for(int i = 0; i < boys.size(); i++)
{
((Boy)boys.elementAt(i)).print();
}
}
}
这段代码的主要业务逻辑是:
1. 首先定义了用于容纳到Vector里的Boy和Girl两个对象,其中,定义了男孩和女孩的编号,并定义了打印编号的print方法。
2. 在main函数里,定义了名为boys的Vector类型对象,并通过for循环,向其中添加了5个Boy类型的对象。
3. 添加完Boy类型对象后,又通过addElement方法,向boys对象里添加了一个Girl类型的 对象。
需要指出的是,对于boys对象来说,在其中既添加Boy又添加Girl类型对象的做法并没有语法上的问题,因为Vector是个可以容纳Object类型对象的容器,所以,不论男孩类还是女孩类,都是以Object的身份被放置到boys对象里的。
4. 在代码的最后部分,我们通过for循环来遍历boys对象。此时,我们通过boys.elementAt(i)方法得到的对象是Object类型的,而不是期望中的Boy对象。
并且,通过for循环遍历到boys里的最后一个元素的时候,系统会出错。因为我们已经通过了boys.addElement(new Girl(5));,把Girl类型的对象放入了Vector,而此时,又企图通过访问Boy对象的方式来访问Girl,这就是出错的原因。
从这段代码里,我们可以看到Vector的灵活性,因为Vector可以容纳Object类型的对象,所以我们可以把不同类型的对象放入同一个Vector中,不过,这种灵活性是以牺牲存放在其中的对象类型为代价的。
而从以下的代码里,我们可以看到在Vector里泛型的一些使用方式。
import java.util.Vector;
public class VectorGenerics
{
public static void main(String[] args)
{
Integer i = new Integer(10);
Vector<String> v = new Vector<String>();
// 错误的代码,企图向String泛型里插入Integer类型的对象
// v.addElement(i)
// 添加了两个String类型的对象
v.addElement("Generics1");
v.addElement("Generics2");
v.insertElementAt("InsertedGenerics", 1);
// 遍历
for (int index = 0; index < v.size(); index++)
{
System.out.println(v.elementAt(index));
}
}
}
这段代码的执行结果是:
Generics1
InsertedGenerics
Generics2
这段代码里,我们首先定义了一个String泛型的Vector对象,并通过了addElement方法向其中插入了若干元素,不过请大家注意,我们不能把String以外类型的元素再插入到v里,否则系统会 报错。
插入完成后,我们通过一个for循环来实现了遍历动作,同样请大家注意,由于我们指定了泛型,所以elementAt方法返回的,是String类型的对象,所以不必再进行强制的类型转换动作。
3.2.3 先进后出的Stack类
Stack,它的中文含义是“堆栈”,同Vector对象一样,它也是属于线性表类型的数据结构。
不过,同Vector相比,Stack是个“后进先出”(Last In First Out)类型的容器,对于该对象来说,最后一个被“压(push)”进堆栈中的对象,会被第一个“弹(pop)”出来。
我们可以从码头卸载货物的过程中,看到“堆栈”的原型,从货船上卸载下来的集装箱被依次叠放,后卸下来的放在先卸下来的集装箱上面,当完成卸载,开始取这些集装箱的时候,最后卸载的是最先取到的,反之,最先从船上卸载下来的集装箱是最后才能被取到的。
堆栈的压入和弹出的步骤如图3-1所示,从中可以看到,我们只可以从堆栈的顶端,执行从堆栈里弹出其中元素和压入元素的动作。
图3-1 堆栈示意图
通过对比,我们发现Stack同Vector对象很相似,它们均是以线性表的方式存储对象,另外,Stack“后进先出”的访问其中对象的方式可以看成是Vector访问数据方式的特例。所以,Java类库的设计者们根据面向对象思想,把Stack设计成了Vector的子类,即,Stack重用了Vector的存储对象的空间和访问线性表的方法,并在此基础上扩展了以“后进先出”方式访问数据的方法。
正是由于Stack继承了Vector类,所以它封装的方法不是很多,主要如下所述。
l 构造函数
Stack的构造函数的原型是:Stack();,不带参数,用来一个可支持“后进先出”访问方式的对象。
同样,我们可以使用泛型的方式,来创建Stack对象,比如通过如下代码:
Stack <String> st = new Stack();
创建的Stack对象st,在其中只能容纳String类型的对象。
l 用来访问堆栈顶端的方法
Stack类里提供了E peek()和E pop()两方法,用来返回栈顶的元素,它们的返回值是泛型类的对象——返回由泛型指定的或者是Object类型的对象。
其中,pop的做法是,弹出栈顶元素,并返回其中的对象,而peek虽然可是返回栈顶元素的对象,但没有弹出栈顶元素的动作。
l 向堆栈顶端压入元素的push方法
这个方法的原型是:E push(E item),用来在堆栈的顶端压入item对象,同时将item对象返回。
这个方法和pop方法,是Stack类最重要的两个方法,其中反映了堆栈“后进先出”的重要特性。
l 判断堆栈是否为空的empty方法
该方法的原型是:boolean empty(),如果该堆栈为空,返回true,反之返回false。
下面我们通过一段代码来详细说明Stack类的一些用法和注意点。
import java.util.Stack;
public class StackTest
{
//主方法
public static void main(String[] args)
{
//定义一个堆栈
Stack st = new Stack();
//使用泛型的形式
//Stack <String> st = new Stack();
//往堆栈里压入元素
st.push("First Element");
st.push("second Element");
st.push("third Element");
//通过pop方法,依次弹出堆栈里的内容
while(st.empty() != true)
{
System.out.println(st.pop().toString());
//以下的做法会导致死循环
//System.out.println(st.peek().toString());
}
//不好的用法,把堆栈当成Vector用
int i = 0;
st.addElement("bad usage1");
st.addElement("bad usage2");
st.addElement("bad usage3");
//破坏了堆栈的特性
for(i=0;i<st.size();i++)
{
System.out.println(st.elementAt(i));
}
}
}
这段代码里,首先通过构造函数,初始化了名为st的Stack类,构造的时候,我们可以使用泛型,指明st对象里,只能容纳String类型的对象。
构造完成后,我们通过了push方法,往其中压入了三个String类型的对象。
完成push的动作后,我们在代码里通过一个while的循环,在其中调用堆栈的pop方法,依次读取栈顶元素。
这里我们可以看到,正是因为执行pop动作后,栈顶元素会弹出,所以当栈内元素被全部弹出后,while循环的st.empty()条件将会为true,这个while循环将会被终止。如果执行被注释掉的System.out.println(st.peek().toString());语句,由于peek函数不会弹出栈顶元素,所以会导致死循环。
通过这里的while循环,代码会输出如下的语句:
third Element
second Element
First Element
需要说明的是,这里的输出次序很好地符合了堆栈“后进先出”的特性,并且,如果我们没有在构造时用到泛型,所以通过pop方法得到的对象是Object类型的,因此要用toString()方法,把pop后的结果还原成String类型。
而在后继的代码里,我们把Stack对象当作了Vector处理,由于Stack继承了Vector类,所以从语法上来讲,不会有问题。它输出的结果是:
bad usage1
bad usage2
bad usage3
需要指出的是,for循环里的st.elementAt(i)方法,返回的是一个Object类型对象,而不是我们想象当中的String类型。不过,System.out.println方法如果发现其参数不是String类型,会让它的参数自动调用toString()方法,也就是说,这里的语句其实等价于:System.out.println(st.elementAt(i). toString());,所以我们看到的依然是字符串输出的效果。
但是这种做法相当不好,是顺序访问了Stack对象,从而破坏了它的“后进先出”特性,所以,不推荐这种用法。
3.2.4 链表式的List接口以及LinkedList实现类
在线性表的数据结构里,有一种以“链条”的方式存储和管理数据的结构,我们称它为“链表”。
同数组相比,用链表存储数据的方式略显不同:数组是“顺序存储”,而链表是“链式存储”,它们的区别如图3-2所示。
图3-2 数组与链表数据结构对比图
从上图我们可以看到,在数组中,数据是依次存储在相邻的内存空间里,所以我们可以根据数据起始地址和其中元素的索引号,很快地定位到数组中的任意元素,反观链表,由于其中存放的元素不是连续存放在内存空间里,而是通过“链条”连接不相邻的元素,所以,在“链表”里实现元素访问的代价要比数组高——如果要访问第100号元素,那不得不依次从第1号元素开始,通过“链条关系”,依次查找。
不过,同数组相比,链表的优势在于,能很方便地完成元素的插入和删除工作,这同我们现实生活中的情况也非常相似,比如,我们要删除图3-2里链表中的1号元素,可通过把0号元素的链条指向2号元素,同时破坏1号元素指向2号元素的链条,如果我们要在1和2号元素之间插入3号元素,也可通过适当修改对应的链条来完成。
根据面向对象的思想,接口(Interface)可以实现封装同类的功能特性,所以,在Java集合类里,专门给出了名为List的接口(Interface),在其中封装了“以链表形式存储和管理数据”的功能,如果我们使用实现(implements)List接口的类,比如LinkedList类,就可以使用链表的方式来存储 数据。
事实上,我们刚才讲到的Vector和Stack这两个类,都是通过实现了List接口来体现它们的“存储并管理数据”的功能,所以说,这两个类都具有链表的特性,都属于链表形式的数据结构。
3.2.4.1 List接口里的方法
由于List是个接口,所以没有构造函数,在这个接口里,主要提供了如下的方法。
l 用于往链表里插入对象的add方法
这个方法有两类重载的方式:
void add(int index, E element)
boolean add(E o)
第一种重载形式里,是在索引号index后插入element元素,而第二种重载形式里,是直接把o对象插入到链表的最后。这里,我们同样是用到了泛型。
l 从链表里删除指定元素的remove方法
这个方法也有两类重载的方式:
E remove(int index)
boolean remove(Object o)
第一种方式里,是删除链表里指定索引里的元素,而在第二种方式里,是删除在链表里的第一个指定内容的元素。
l 从链表里获取指定索引元素的E get(int index)方法
通过这个方法,我们可以得到链表里指定索引的元素。
如果我们没有用到泛型,那么这个方法的返回类型是Object,所以在这种情况下我们要根据实际情况,把这个返回对象转换成它原来的类型。
l 统计链表中所有元素个数的int size()方法
这个方法主要用在遍历链表的过程中。
l 判断链表里是否有指定元素的int indexOf(Object obj)方法
如果在链表里找到了obj元素,则返回这个元素的索引值,反之如果没有找到,返回–1。
截取链表里元素的List<E> subList(int fromIndex, int toIndex)方法,使用这个方法,我们可以得到链表里的从fromIndex开始,到toIndex结束的子链表。
l 清空链表的void clear()方法
通过这个方法,我们可以把链表里存储的元素全部清除掉,这个方法一般在链表使用完成后调用。
3.2.4.2 List实现类及其功能
接口本身只是封装功能的载体,它是通过具体实现类来体现价值,我们刚才提到的Vector和Stack这两个类是实现了List接口后,而下面讲到的LinkedList类也是实现了List接口,所以它们都具有链式存储的特性。
LinkedList具有List提供的向链表里插入和删除元素、统计元素个数和清空链表等方法,除此以外,它还具有如下的重要方法。
l 构造函数
它的构造函数有如下的重载方式:
LinkedList()
LinkedList(Collection c)
LinkedList(<E > c)
除了常规的不带参数的构造函数以外,在第二种形式的构造函数里,它携带一个Collection类型的参数,Collection是Java里的线性表类集合的基类,所以通过第二类形式的构造函数,我们可以把Collection内的各元素装载到LinkedList对象里。
同样,在它的构造函数里我们可以使用泛型,用来指定LinkedList里可以容纳对象的类型。
l 添加元素的void addFirst(E obj)和void addLast(E obj)方法
在List的接口里,已经提供了add方法,在LinkedList这个实现类里,更提供了这两个能在链表头和链表尾添加元素的方法。
l 获取元素的E getFirst()和Object getLast方法
LinkedList类在List的get方法基础上,更添加了这两个方法,用来获取链表头和尾的元素。
l 删除元素的E removeFirst()和Object removeLast()方法
同样,LinkedList类也在List接口的基础上扩展了这两个删除头部和尾部元素的方法。
从LinkedList提供的主要方法里,我们可以看到,正是因为List接口已经封装好了实现链表功能的主要方法,所以LinkedList类是在这些方法的基础上,扩展出了具有自身特色的方法。
List的实现类还有ArrayList类等,它们虽然同LinkedList类在封装的方法上略微有些不同,但由于它们都是链表类型的数据结构,所以在使用语法上,是大同小异的,因此我们不再详细描述。
3.2.4.3 List代码示例
下面,我们通过一段实际的代码,来综合学习LinkedList的语言应用。
import java.util.LinkedList;
import java.util.List;
public class ListTest
{
public static void main(String[] args)
{
//使用了泛型
List<String> l1 = new LinkedList<String>();
//List l1 = new LinkedList();
int index = 0;
//插入元素
l1.add("firstElement");
l1.add("secondElement");
l1.add("thirdElement");
//错误的插入
//Integer errorItem = new Integer(10);
//l1.add(errorItem);
//访问索引
index = l1.indexOf("firstElement");
System.out.println("the index of firstElement is " + index);
index = l1.indexOf("fourElement");
System.out.println("the index of fourElement is " + index);
//删除元素
l1.remove("secondElement");
index = l1.indexOf("secondElement");
System.out.println("the index of secondElement is " + index);
//依次遍历
System.out.println("Begin get the list");
for(int i = 0;i<l1.size();i++)
{
System.out.println(l1.get(i));
}
//清空链表
l1.clear();
System.out.println("After clear");
System.out.println(l1.size());
}
}
在这段代码里,我们首先通过List<String> l1 = new LinkedList<String>();语句,创建了l1对象,请大家注意:我们是用实现类LinkedList来初始化接口List类型的,由于创建的时候用到了泛型,所以在l1里,我们只能插入String类型的对象。
创建完l1以后,我们通过了add方法,向LinkedList里添加了三个类型为String的对象,如果我们企图把Integer类型的对象插入其中,系统会出错。
完成元素的添加动作后,我们通过indexOf方法,来获取指定元素在LinkedList里的索引。
代码执行到这里,会有如下的输出语句:
the index of firstElement is 0
the index of fourElement is -1
the index of secondElement is -1
正是因为fourElement和secondElement这两个元素或者不存在,或者已经被删除掉,所以它们的索引值是–1,而firstElement的索引值是0,因为在LinkedList里,索引值是从0开始的。
之后,我们通过一个for循环,使用了get方法,依次遍历了l1里的对象,同样由于我们使用了String的泛型,所以get方法返回的对象是String类型的,不需要再做强制的类型转换。这里遍历时打印的输出语句是:
Begin get the list
firstElement
thirdElement
遍历以后,我们通过clear方法来清空链表,清空后,链表的长度会被重置成0,所以后继动作的输出语句是:
After clear
0
3.2.5 不允许有重复元素的Set接口
在刚才讲到的Vector或LinkedList等集合对象里,允许出现重复的元素。但是,在有些项目应用中,不允许把相同的元素插入到同一个数据结构中,比如在银行的应用项目中,由于每个客户的账号是不同的,所以不允许在同一个数据结构里插入相同的账号。
我们固然可以通过插入前,执行一段检验代码,如果没有检测到重复元素才允许插入,但这样做将会影响到代码的执行效率。
在Java集合类里专门给出了Set接口,这个接口里,不仅封装了用线性表管理对象的方法,更封装了“不允许插入重复元素”的功能,所以我们用Set可以用较高的效率来避免出现重复元素的情况。
需要说明的是,在Set接口里,通过了“散列表(即Hash表)”和“红黑树”这两种方式来避免插入相同的元素。
3.2.5.1 Set接口里的方法
由于Set是个接口,所以它同样没有构造函数,这个接口的主要方法如下所示。
1. 向Set里添加元素的boolean add(E o)方法,这个方法同样用到了泛型。如果待插入的元素不存在于Set里,add动作执行后会返回true,反之,如果我们调用add方法把已有的元素插入到其中,那么这个待插入的元素不会被再次插入,并且add方法将返回false。
2. 删除Set里指定元素的boolean remove(Object o) 方法,如果这个方法成功地在Set里删除了指定元素,返回true,反之,如果因为Set不存在o元素而导致了删除失败,则返回false。
3. 判断Set是否是空的boolean isEmpty()方法,如果Set里有元素,这个方法返回true,反之返回false。
4. 返回Set大小的int size()方法。
3.2.5.2 Set实现类及其功能
我们来思考一下Set接口避免插入重复元素的实现方式,如果在插入前,依次从Set里现有的元素中判断是否存在待插入的元素,这样做的代价是相当大的。
于是,在诸多Set接口的实现类里,采用了“基于散列表”和“基于树结构”的两种不同策略来提高“检测Set里是否有重复元素”的效率,其中,TreeSet类实现了“基于树结构”的重复元素检测动作,由于它的实现原理比较复杂,所以不做讨论。
而HashSet体现了“基于散列表”的检测重复元素的策略(关于散列表数据结构的详细描述可以参见下文的3.3.1部分),简而言之,HashSet里的元素值同这个元素在Set里所存放的索引位置有个对应关系(这个对应关系就是下文里提到的散列函数),在HashSet里插入元素前,可根据这个元素值,根据对应关系,计算出这个元素在HashSet里的插入位置,如果在这个位置里(或位置周围)已经存在了待插入元素的值,则不能插入。由此可以看出,在往HashSet里插入元素时,只需要访问少量的索引位置就可以检测到这个元素是否可插入。
HashSet类的主要方法如下所述:
l 构造函数
它的构造函数主要有以下两种重载的方式。
HashSet()
HashSet(<E> c)
除了常规的不带参数的构造函数以外,在第二种构造函数里,我们可以通过输入泛型的类型,指定创建好的HashSet里可以容纳的对象。
l 判断是否存在指定元素的boolean contains(Object o)方法
如果在HashSet里存在o元素,则返回true,否则返回false。
下面我们通过一段代码来演示HashSet类的一些语法。
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
public class HashSetTest
{
public static void main(String[] args)
{
//通过泛型,创建HashSet
Set<String> set = new HashSet<String>();
set.add("One");
//尝试插入相同的元素
set.add("One");
//输出结果是1,而不是2
System.out.println(set.size());
set.add("Two");
set.add("Three");
System.out.println(set.size());
//查找指定元素
boolean flag = set.contains("One");
System.out.println("the flag is " + flag);
}
}
在这段代码里,我们首先通过了泛型,创建了名为set的HashSet对象,在其中只能容纳String类型的元素。随后,我们企图把两个重复的对象插入到set里,不过HashSet不会让我们把第二个“One”也插入set里,所以,随后输出HashSet的长度,是1而不是2,当我们再插入两个不重复的元素后,它的长度会变为3。
插入完成后,我们通过contains方法,判断set是否有指定的“One”元素,这里的输出是true。
整段代码的输出结果是:
1
3
the flag is true
3.2.6 对第一类集合的归纳
第一类集合有着共同的特性就是它们存储的对象都是一元的,只不过存储的方式和使用的数据结构不同。
对于所属于List下面的类来说它们是线性存储的,Vector和ArrayList都是基于数组来存放数据的,而LinkedList是基于线性链表来存放数据的,前者有很好的随机反问特性,即根据给出的位置快速的定位相应的元素,后者适合经常的从集合里面调入调出元素的场景,因为线性链表在因添加或删除元素而造成的剩余数据大批量位移表现及其突出。
对于所属于Set下面的类来说最大的一个特点就是它们不允许有重复的元素,HashSet是基于哈希算法来存放数据的,数据的顺序有可能是会发生变化的,TreeSet是基于数据结构来存放数据的,因此存放在此的数据是有顺序的。
在使用第一类集合的时候我们一定要掌握它们的特性,根据实际的需求,选择合适的类型。
3.2.7 使用Vector模拟Stack的动作
Java集合类里提供的Stack类,其实是Vector的子类,也就是说,我们可以通过在Stack类里使用Vector的方法,破坏Stack的特性。
为此,在如下的代码里,我们给出了改进后的Stack类,在其中,由于我们彻底地向程序员屏蔽了违规的操作,所以通过使用MyStack类,程序员能完全实现Stack的“先入后出”的规范。
import java.util.Vector;
//封装堆栈的方法
class MyStack
{
//用Vector来模拟Stack动作
private Vector<Object> stkBuf = new Vector<Object>();
private int curSize = 0;
//改进后的push方法
void myPush(Object obj)
{
//向栈顶插入元素
stkBuf.addElement(obj);
curSize ++;
}
//改进后的pop方法
Object myPop()
{
//从栈顶取出元素
Object popItem = stkBuf.elementAt(curSize-1);
stkBuf.removeElementAt(curSize-1);
curSize --;
return popItem;
}
//改进后的peek方法
Object myPeek()
{
//没有弹出堆栈的动作
Object popItem = stkBuf.elementAt(curSize);
return popItem;
}
//返回堆栈的长度
public int size()
{
return stkBuf.size();
}
//判断队形还是否为空
boolean isEmpty()
{
if(stkBuf.size() == 0)
{
return false;
}
else
{
return true;
}
}
}
//主类
public class VectorAsStack
{
public static void main(String[] args)
{
MyStack ms = new MyStack();
//压入堆栈
ms.myPush("one item");
ms.myPush("two item");
ms.myPush("three item");
//得到堆栈的大小
System.out.println("the stack size is " + ms.size());
//依次遍历堆栈
while(ms.isEmpty()!=false)
{
//如果用peek,会出现死循环
//System.out.println(ms.myPeek());
System.out.println(ms.myPop());
}
}
}
在MyStack类里,我们定义了一个Vector对象,用此模拟Stack动作。在这个类的压栈(myPush)方法里,我们是通过addElement方法,把元素插入到Stack的尾部,而在出栈(myPop)方法里,则是通过elementAt(curSize-1)等方法,返回并弹出Vector最尾部的元素。
通过这两个主要动作,我们可以保证,最先压入堆栈的元素是最后被弹出堆栈的,另外,由于MyStack类里的Vector对象是私有的,而我们在代码里并没有提供违背堆栈特性的操作,所以我们不能随意地访问堆栈里的任何元素。
在VectorAsStack类的main函数里,我们测试了堆栈的一些动作。
当我们向堆栈里压入三个元素后,从随后的输出语句里我们可以看到堆栈的当前长度。
the stack size is 3
随后,我们通过一个while循环,依次打印从堆栈里弹出的元素。
three item
two item
one item
从输出结果上来看,这段代码所定义的数据结构很好地符合了堆栈“先入后出”的特性。另外,如果我们把代码里System.out.println(ms.myPeek());的注释语句打开,由于myPeek只是获取栈顶元素,并没有弹出的动作,所以while里的isEmpty条件不会得到满足,从而会导致出现死循环。
3.2.8 使用Vector模拟队列的动作
我们在实际应用中,需要队列类型的数据结构,队列采用了我们日常生活中的“排队”里的“先来先服务”工作历程,即后到的对象是被插入到队列的尾部,而服务程序只能是操作队列头的对象。
队列的“先来先服务”这种特性与其说是限制了队列操作动作,倒不如说是队列的优势,在特定的场合里,比如在CPU处理内核请求任务时,在不考虑优先级的情况下,只能是本着“先来先服务”原则,所以我们可以用队列来容纳内核的请求任务。
下面,我们同样是根据Vector对象,来设计符合规范的队列类代码。
import java.util.Vector;
//封装队列的动作
class MyQueue
{
//定义容纳队列的Vector 对象和队列的长度
private Vector<Object> queue = new Vector<Object>();
private int queueSize = 0;
//向队列头插入元素的方法
void myInsertQueue(Object obj)
{
//插入元素,同时队列长度加1
queue.addElement(obj);
queueSize ++;
}
//从队列头得到元素
Object getFromHead()
{
Object headItem = queue.elementAt(0);
queue.removeElementAt(0);
queueSize --;
return headItem;
}
//返回队列的大小
public int size()
{
return queue.size();
}
//判断队列是否为空
boolean isEmpty()
{
if(queue.size() == 0)
{
return false;
}
else
{
return true;
}
}
}
//主类
public class VectorAsQueue
{
public static void main(String[] args)
{
MyQueue myqueue = new MyQueue();
//向队列里插入元素
myqueue.myInsertQueue("First comer");
myqueue.myInsertQueue("Second comer");
myqueue.myInsertQueue("Third comer");
//依次遍历队列
while(myqueue.size()>0)
{
System.out.println(myqueue.getFromHead());
}
}
}
在MyQueue类里,我们同样是通过了Vector类存储的队列里的元素。
在MyQueue的myInsertQueue方法里,我们定义了向队列里插入元素的动作,在这个方法里,我们通过Vector提供的addElement方法,向队列的尾部插入元素,这样的做法是符合 “后到者等待”特性的。而在getFromHead方法里,是通过Vector的elementAt(0)方法,获取头元素,这也是符合“先来者先服务”特性的。同样,在这个类里,也屏蔽了针对队列的非常规操作。
在主类的main函数里,我们通过MyQueue对象的实例myqueue,先向队列插入了三个String类型的元素,随后通过while循环,依次遍历的队列。这段代码的输出语句是:
First comer
Second comer
Third comer
从中可以看出,我们最先插入队列的元素,是最先得到处理的,并且,我们无法使用MyQueue类提供的方法,做出违背队列规则