许多基础数据类型都和对象的集合有关。具体来说,数据类型的值就是一组对象的集合, 所有操作都是关于添加、删除或是访问集合中的对象。我们有三种这样的数据类型,分别是背色(Bag)、队列(Queue )和栈( Stack)。它们的不同之处在于删除或者访问对象的顺序不同。
照例,我们对集合型的抽象数据类型的讨论从定义它们的API开始, 如表1.3.1所示。每份API都含有一个无参数的构造函数、一个向集合中添加单个元素的方法、一个测试集合是否为空的方法和一个返回集合大小的方法。Stack和Queue都含有一个能够删除集合中的特定元素的方法。除了这些之外,还有API反映出的两种Java特性:泛型和迭代
背包 | |
---|---|
public class Bag
| |
Bag() | 创建一个空背包 |
void add(Item item) | 添加一个元素 |
boolean isEmpty() | 背包是否为空 |
int size() | 背包中的元素数量 |
先进先出(FIFO)队列 | |
---|---|
public class Queue
| |
Queue() | 创建空队列 |
void enqueue(Item item) | 添加一个元素 |
Item dequeue() | 删除最近添加的元素 |
boolean isEmpty() | 队列是否为空 |
int size() | 队列中的元索数量 |
下压(后进先出,LIFO) 栈 | |
---|---|
public class Stack implements Iterable | |
Stack() | 创建一个空栈 |
void push(Item item) | 添加一个元素 |
Item pop() | 删除最近添加的元素 |
boolean isEmpty() | 栈是否为空 |
int size() | 栈中的元素数量 |
集合类的抽象数据类型的一个关键特性是我们应该可以用它们存储任意类型的数据。一种特别的Java机制能够做到这一点, 它被称为泛型,也叫做参数化类型。在每份API中,类名后的记号将Item定义为一个类型参数,它一个象征性的占位符,表示的是用例将会使用的某种具体数据类型。可以将Stack
Stack<String> stack - new Stack<String>();
stack.push("Test");
...
String next - stack. pop();
并在以下代码中使用队列处理Date对象:
Queue<Date> queue = new Queue<Date>();
queue. enqueue(new Date(1, 18, 2024);
));
...
Date next - queue . dequeue();
如果你尝试向stack变量中添加一个Date对象(或是任何其他非String类型的数据)或者向queue变量中添加一个String对象(或是任何其他非Date类型的数据),你会得到一个编译时错误。
类型参数必须被实例化为引用类型,因此Java有-种特殊机制来使泛型代码能够处理原始数据类型。我们还记得Java的封装类型都是原始数据类型所对应的引用类型: Boolean、 Byte 、Character、Double、Float、Integer、Long 和Short分别对应着boolean、byte、 char、 double、float、 int、 long和short。在处理赋值语句、方法的参数和算术或逻辑表达式时,Java会自动在引用类型和对应的原始数据类型之间进行转换。在这里,这种转换有助于我们同时使122用泛型和原始数据类型。例如:
Stack<Integer> stack = new Stack<Integer> ();
stack. push(17);//自动装箱(int 一> Integer)
int i . stack.pop(); //自动拆箱(Integer 一> int)
自动将一个原始数据类型转换为一个封装类型被称为自动装箱,自动将一个封装类型转换为一个原始数据类型被称为自动拆箱。在这个例子中,当我们将一个原始类型的值17传递给push()方法时,Java 将它的类型自动转换(自动装箱)为Integer。pop()方法返回了一个Integer类型的值,Java在将它赋予变量i之前将它的类型自动转换(自动拆箱)为了int。
对于许多应用场最,用例的要求只是用某种方式处理集合中的每一个元素,或者叫做选代访问集合中的所有元素。有了它,我们能够写出清晰简洁的代码且不依赖于集合类型的具体实现。例如,假设用例在Queue中维护个交易集合,如下:
Queue<Transaction> collection = new Queue<Transaction>();
如果集合是可迭代的,用例用一行语句即可打印出交易的列表:
for (Transaction t : collection){
System.out.println(t);
}
这种语法叫做foreach语句:可以将for语句看做对于集合中的每个交易t(foreach),执行以下代码段。这段用例代码不需要知道集合的表示或实现的任何细节,它只想逐个处理集合中的元素。相同的for语句也可以处理交易的Bag对象或是任何可迭代的集合。Stack和Queue的API的唯一不同之处只是它们的名称和方法名。这让我们认识到无法简单地通过一列方法的签名说明一个数据类型的所有特点。只有自然语言的描述才能说明选择被删除元素(或是在foreach语句中下一个被处理的元素)的规则。
背包是一种不支持从中删除元素的集合数据类型——它的目的就是帮助用例收集元素并迭代遍历所有收集到的元素(用例也可以检查背包是否为空或者获取背包中元素的数量)。迭代的顺序不确定且与用例无关。
先进先出队列(或简称队列)是一种基于先进先出( FIFO )策略的集合类型,按照任务产生的顺序完成它们的策略我们每天都会遇到:在剧院门前排队的人们、在收费站前排队的汽车或是计算机上某种软件中等待处理的任务。任何服务性策略的基本原则都是公平。在提到公平时大多数人的第一个想法就是应该优先服务等待最久的人,这正是先进先出策略的准则。当用例使用foreach语句迭代访问队列中的元素时,元素的处理顺序就是它们被添加到队列中的顺序。在应用程序中使用队列的主要原因是在用集合保存元素的同时保存它们的相对顺序:使它们入列顺序和出列顺序相同。例如,下页的用例是我们的In类的静态方法readInts()的一种实现。 这个方法为用例解决的问题是用例无需预先知道文件的大小即可将文件中的所有整数读入一个数组中。我们首先将所有的整数读入队列中,然后使用Queue的size()方法得到所需数组的大小,创建数组并将队列中的所有整数移动到数组中。队列之所以合适是因为它能够将整数按照文件中的顺序放入数组中。这段代码使用了自动装箱和拆箱来转换用例中的int原始数据类型和队列的Integer封装类型。
public static int[] readInts(String name){
In in = new In(name);
Queue<Integer> q = new Queue<Integer>();
while(! in.isEmpty()){
q. enqueue(in. readInt());
}
int[] a= new int[];
for (int i =0; 1 <N; i++){
a[i] = q. dequeue() ;
}
return a;
}
下压栈(或简称栈) 是一种基于后进先出(LIFO)策略的集合类型。你在网上冲浪时很可能会遇到栈的一个例子。点击一个超链接,浏览器会显示一个新的页面(并将它压入一个栈)。你可以不断点击超链接并访向新页面,但总是可以通过点击“回退”按钮重新访问以前的页面(从栈中弹出)。栈的后进先出策略正好能够提供你所需要的行为。当用例使用foreach语句迭代遍历栈中的元素时,元素的处理顺序和它们被压入的顺序正好相反。在应用程序中使用栈迭代器的一个典型原因是在用集合保存元素的同时颠倒它们的相对顺序。例如,右侧的用例Reverse将会把标准输人中的所有整数逆序排列,同样它也无需预先知道整数的多少。具
public class Reverse{
public static void main(String[] args){
Stack<Integer> stack;
stack = new Stack<Integer>();
while (!StdIn. isEmpty()){
stack.push(StdIn.readInt());
}
for(int i : stack){
System.out.println(i);
}
}
}
我们要学习的另一个栈用例同时也是展示泛型的应用的一个经典例子。就是用来计算算术表达式的值的,例如:
( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
如果将4乘以5,把3加上2,取它们的积然后加1,就得到了101。但Java系统是如何完成这些运算的呢?我们也可以编写一个Java程序来解决这个问题。它接受一个输人字符串(表达式)并输出表达式的值。为了简化问题,首先来看一下这份明确的递归定义:算术表达式可能是一个数,或者是由一个左括号、 一个算术表达式、 一个运算符、另一个算术表达式和一个右括号组成的表达式。简单起见,这里定义的是未省略括号的算术表达式,它明确地说明了所有运算符的操作数——你可能更熟悉形如1 + 2 * 3的表达式,省略了括号,而采用优先级规则。我们的重点在于如何解析由括号、运算符和数字组成的字符串,并按照正确的顺序完成各种初级算术运算操作。如何才能够得到个(由字符串表示的)算术表达式的值呢?这里有一个非常简单的算法,用两个栈(一个用于保存运算符,一个用于保存操作数)完成了这个任务。
表达式由括号、 运算符和操作数(数字)组成。我们根据以下4种情况从左到右逐个将这些实休送入栈处理:
1.将操作数压入操作数栈;
2.将运算符压入运算符栈;
3.忽略左括号;
4.在遇到右括号时,弹-一个运算符,弹出所需数量的操作数,并将运算符和操作数的运算结果压人操作数栈。在处理完最后一个右括号之后,操作数栈上只会有一个值, 它就是表达式的值。