项目档案

建议阅读

第13.5、20.2、21.2、21.3节

一系列的例子

在CMSC 250的前三分之一中,我们将专注于面向对象编程。在这部分课程中,我将展示一系列示例,演示如何使用Java类库中的一些集合类,如何创建我们自己的类,以及如何构建由多个交互类组成的更复杂的程序。

使用Java集合类

Java类库包含许多用于存储对象集合的类。在本系列的前几节课中,我将向您介绍一些更常用的集合类,并解释如何为给定的赢博体育程序选择最佳的集合类。

第一节课将介绍ArrayList和TreeSet类。

一个问题

第一部分的赢博体育课程都围绕着问题展开。在每节课中,我会介绍要解决的问题,然后提出解决问题的方法。每堂课的最后都会有一个作业,要求你们解决一个密切相关的问题。

这是我们第一节课要解决的问题。在这个问题中,我们有两个存储正整数列表的文本文件。每个文本文件包含一个没有重复的随机整数列表。我们的问题是构造第三个文件,该文件包含恰好出现在两个列表中的一个中的赢博体育数字。换句话说,我们想要构造这两个列表的并集然后拿走这两个列表交点上的赢博体育数。

生成列表

解决这个问题的第一步是编写一个程序来生成这两个列表。具体来说,我们希望能够生成一个由N个随机整数组成的列表,这些整数的范围从0到N-1。此外,我们希望列表中没有重复。(由于我们不会在列表中放入重复列表,因此我们生成的最终列表中的数字将少于N个。)

生成列表的程序

下面是为我们生成这些列表的程序的源代码。我将在下面讨论代码的重要部分。

包不同;进口java.io.File;进口java.io.PrintWriter;进口java.util.ArrayList;进口java.util.Random;/**生成N个范围从0到N *的随机整数,并存储在文件名为filename的文件中。* *在保存时跳过任何重复-这将导致*在文件中保存的数字少于N个。*/公共静态无效makeNumbers(字符串文件名,int N){随机随机=新随机();ArrayList<Integer> numbers = new ArrayList<Integer>()//生成N个随机整数并将它们添加到数字中//仅当它们尚未出现在该列表中时。for(int n = 0;n < n;n++) {int nextInt = random.nextInt(n);如果(! numbers.contains (nextInt) numbers.add (nextInt);} //将数字保存到文件中。printwwriter输出= null;try {output = new printwwriter (new File(filename));} catch(Exception ex) {ex. printstacktrace ();返回;} for(int n: numbers) {output.println(n);} output.close ();}公共静态void main(String[] args) {final int N = 200000;介绍船名(“one.txt”,N);介绍船名(“two.txt”,N);System.out.println(“完成”);}}

生成随机整数

在构建这个程序时,我们需要解决的第一个技术问题是生成一个随机整数序列。与Java编程中的许多问题一样,Java类库已经有一个类可以帮助解决这个问题,即Java中的Random类。util包。(关于这个类的完整文档可以在这里找到。)

要使用Random类,我们首先创建一个Random类型的对象。

随机随机= new Random();

Random类提供了许多生成随机数的方法。为了生成随机整数,我们使用nextInt()方法之一。nextInt()的第一个变体不接受任何参数,并返回一个随机整数,该整数可以是正的,也可以是负的。此方法对返回的整数大小也没有限制。第二个变体以正整数N作为参数,并返回0到N-1范围内的随机正整数。这就是我们将在我们的解决方案中使用的版本。

int nextInt = random.nextInt(N);

避免重复

这个问题的一个重要要求是,我们生成的整数列表不能包含任何重复项。为了执行这一要求,我将使用以下策略:

  1. 创建一个空列表来存储整数。
  2. 生成N个随机整数。
  3. 对于我们生成的每个整数,首先检查该整数是否已经出现在列表中。
  4. 如果没有,将其添加到列表中。
  5. 当我们生成了赢博体育N个随机整数后,我们将把列表的内容写到一个文件中。

使用ArrayList类

Java类库包含许多用于存储对象集合的集合类。最基本和最广泛使用的集合类是ArrayList类,它将数据项存储在内部数组中。与赢博体育集合类一样,ArrayList类是一个参数化类,这意味着在创建ArrayList时必须指定计划在ArrayList中存储的对象类型。在我们的例子中,我们将这样做:

ArrayList<Integer> numbers = new ArrayList<Integer>()

这里我们必须遵守的一个重要限制是集合类只能存储对象。在这个赢博体育程序中,这是一个问题,因为我们想要存储一个整数列表。通常我们在Java中使用int数据类型来存储整数。不幸的是,int数据类型是原始数据类型的一个例子,而不是对象类型。幸运的是,Java类库提供了一组特殊的类,它们可以作为基本数据类型的容器。其中一个类是Integer类,它是一个对象,用作单个int的简单容器。为了存储整型列表,首先将每个整型放入一个Integer对象中,然后将该Integer对象存储在ArrayList中。

ArrayList类提供了许多有用的方法,使ArrayList类成为比简单使用数组更好的选择。在这个例子中,我们将使用ArrayList类的两个最重要的方法:add()方法和contains()方法。

下面是我使用这两种方法的程序部分。

for(int n = 0;n < n;n++) {int nextInt = random.nextInt(n);如果(! numbers.contains (nextInt) numbers.add (nextInt);}

我们的任务是生成一个N个随机整数的列表。这里的循环解决了这个问题。对于我们生成的每个随机整数,我们首先要确保该数字不是重复的。为了检查这一点,我们使用ArrayList的contains()方法来确保我们生成的整数不在列表中。如果不是,则使用go ahead并使用add()方法将其添加到列表中。

这里需要指出的一个重要技术问题与我们调用contains()和add()方法的方式有关。在这两种情况下,我们都将变量nextInt传递给这些方法。这看起来像是类型不匹配,因为nextInt是int类型,而这两个方法实际上都期望一个Integer类型的参数。Java通过使用一种称为自动装箱的技术为我们自动解决了这个问题。如果我们传递一个int给一个需要Integer形参的方法,Java会自动将int放入一个Integer中,并将该Integer对象传递给该方法。

与打印机一起工作

一旦构造了整型列表,我们就需要将该列表保存到一个文本文件中。在Java中将数据写入文本文件的最简单方法是使用printwwriter类。该类提供了println()方法,我们可以使用该方法将一行数据打印到文本文件中。

下面是程序的相关部分,我们在其中设置printwwriter并将整数写入文件。

printwwriter输出= null;try {output = new printwwriter (new File(filename));} catch(Exception ex) {ex. printstacktrace ();返回;} for(int n: numbers) {output.println(n);} output.close ();

要创建一个写文件的printwwriter,我们使用printwwriter的构造函数版本,该构造函数接受file对象作为参数。用要写入的文件的名称初始化File对象。

output = new printwwriter (new File(filename));

这里我们必须处理的一个技术问题是File构造函数可能抛出异常。这意味着我们必须做一些事情来处理潜在的异常。处理潜在异常的最佳方法是将可能生成异常的代码放入try/catch块中:

try {output = new printwwriter (new File(filename));} catch(Exception ex) {ex. printstacktrace ();返回;}

如果在try块中的某个地方确实发生了异常,我们可以在catch块中捕获该异常。一旦捕获到异常,就可以要求它使用printStackTrace()方法将异常的详细信息打印到标准输出中。而且,一旦我们知道打开文件的尝试失败了,我们几乎不得不放弃将数字写入文件。放弃的最简单方法是从我们所在的函数返回,因为现在没有什么可做的了。

设置好printwwriter之后,就可以将赢博体育的整数写入文件了。下面是我用来做这个的循环:

For (int n: numbers) {output.println(n);}

这里我使用了Java for循环的一种特殊变体,称为增强型for循环。每当需要遍历集合中的赢博体育元素时,就经常使用这种类型的for循环。在这个例子中,我们的集合number是一个整数数组列表。这里的for循环遍历该集合,将列表中的每个数字依次放入int变量n中。(注意,这里再次出现类型不匹配:ArrayList存储Integer对象,但我们将数字放入int变量中。)Java再次通过自动拆箱为我们解决了这个问题:对于列表中的每个Integer, Java将从Integer中复制int并将其存储在变量n中。

一旦我们将数字写入文件,我们就必须处理一个列表细节。我们需要调用printwwriter的close()方法:

output.close ();

最后一步很重要,因为printwwriter使用了一种称为缓存策略的优化。当我们要求printwwriter将一些文本打印到文件时,它不会立即将文本写出来。相反,它将文本存储在内部文本缓存中。当缓存填满时,它会一次将整个缓存写入文件,这比每次向文件中写入一行文本更有效。这种优化的缺点是,我们没有调用close()方法,我们可能会导致部分或全部文本仍然卡在缓存中。调用close()将赢博体育剩余的文本从缓存中刷新到文件中。

计算差值列表

一旦我们有了两个整数文件,我们就准备好解决主要问题了,即找到恰好出现在两个列表中的一个中的赢博体育数字的问题。由于这个问题相当于找到两个列表的并集,然后删除出现在两个列表中的赢博体育数字,我们可以使用以下策略来解决这个问题:

  1. 将第一个列表中的赢博体育数字放入ArrayList中。
  2. 对于第二个列表中的每个数字,我们检查是否已经在数组列表中。如果是,则该数字位于交叉点,必须从数组列表中删除。如果不是,则将其添加到ArrayList中。
  3. 处理完第二个列表中的每个数字后,将数组列表的内容写入输出文件。

下面是执行这个策略的程序代码:

程序查找恰好出现在两个*文件one.txt和two.txt中的数字。*/ public static void main(String[] args){/**读取第一个文件中的数字并将它们放入列表中。**/扫描器输入= null;try {input = new Scanner(new File("one.txt"));} catch(Exception ex) {ex. printstacktrace ();返回;} ArrayList<Integer> numbers = new ArrayList<Integer>();while(input.hasNextInt()) {numbers.add(input.nextInt());} input.close ();/**读取和处理第二个列表中的数字。**/ try {input = new Scanner(new File("two.txt"));} catch(Exception ex) {ex. printstacktrace ();返回;} //下面是我们查找所需数字的方法。对于我们从第二个文件中读取的每个数字,我们首先检查它是否已经出现在第一个文件中(因此是数字)。// If已经在numbers中了,从numbers中删除它,因为我们不想保留两个文件中出现的任何数字。如果不是数字形式的//,则将其加到数字中,因为现在我们知道数字//只出现在文件2中。while(input.hasNextInt()) {Integer next = input.nextInt();如果(numbers.contains(下))numbers.remove(下);其他numbers.add(下);} input.close ();/**将结果写入文件。**/ printwwriter输出= null;try {output = new printwwriter (new File("result.txt"));} catch(Exception ex) {ex. printstacktrace ();返回;} for(int n: numbers) {output.println(n);} output.close ();}}

速度问题

上面的程序解决了手头的问题,但它有一个非常重要的问题。如果我们增加两个列表的大小,这个解决方案的速度就会大大降低。问题的根源在于这部分代码:

while(input.hasNextInt()) {Integer next = input.nextInt();如果(numbers.contains(下))numbers.remove(下);其他numbers.add(下);}

这个循环遍历第二个列表中的赢博体育数字。对于第二个列表中的每个数字,我们首先必须通过调用contains()方法来检查它是否已经出现在列表中。因为列表中的数字没有特定的顺序,所以contains()确定给定数字不在列表中的唯一方法是搜索列表中的赢博体育项。如果数字在列表中,我们通常必须在列表中搜索大约一半才能找到它。已知赢博体育这些,如果两个列表中都有≤k N个元素那么调用contains()所做的总功的上界是k2 N2。这种二次缩放行为是个坏消息:这意味着如果我们将N增加一倍,算法将花费4倍的时间来处理数据,如果我们将N增加10倍,算法将花费100倍的时间来处理数据。

生成初始随机列表的代码也遇到了同样的问题。

速度问题的修复

幸运的是,我们的速度问题很容易解决。我们所要做的就是用一个提供了更有效的contains()方法的容器类替换我们一直用来存储数字的ArrayList。用于此目的的最佳类是TreeSet类。TreeSet内部将数据项存储在称为二叉搜索树的特殊结构中。这里有一个二叉搜索树的例子。

在树状结构中,数据被安排在节点集合中。在二叉树中,每个节点最多可以有两个连接到它的子节点。此外,二叉搜索树对其子树施加了特殊的排序属性。如果一个节点包含一个整数k,那么它的左子节点必须包含一个小于k的整数,而它的右子节点必须包含一个大于k的整数。排序规则还影响我们如何向树中添加新项目。要向树中添加新项,我们从顶部(或根)节点向下搜索,直到找到一个空白的数字空间。在我们到达的每个节点上,我们都将我们要插入的数字与当前节点上的数字进行比较。如果传入的编号小于节点的编号,我们继续在左子节点中搜索,如果大于节点的编号,我们继续在右子节点中搜索。如果我们遇到的节点已经有我们要插入的数字,我们只需停止。如果我们从树的末端掉下来,我们只需在我们掉下来的地方添加一个新节点。

在树中搜索数字的算法是类似的。同样,我们从根开始,使用与插入新数字相同的过程。如果我们到达一个节点,它有我们正在搜索的数字,我们停止并宣布成功。如果我们从树的末端掉下来,我们就停止并宣布失败。

这种数据结构的主要优点是,对于包含N个项的树,添加新项的算法和搜索算法所需的时间都与log2n成正比。

用TreeSet替换上面两个程序中的ArrayList非常简单。我们所要做的就是替换语句

ArrayList<Integer> numbers = new ArrayList<Integer>()

TreeSet<Integer> numbers = new TreeSet<Integer>();

如果我们看一下之前给我们带来问题的代码

while(input.hasNextInt()) {Integer next = input.nextInt();如果(numbers.contains(下))numbers.remove(下);其他numbers.add(下);}

我们会看到,我们有一个while循环要处理k N个数字。对于这些数字中的每一个,我们必须在包含大约k个数字的TreeSet上调用contains()。赢博体育对contains()的调用所做的总功现在是knlog2 (kn)它正比于nlog2n而不是我们之前的N2。

为什么这么容易?

我们只需要修改一行代码就能显著提高程序的性能,这一事实并非偶然。我们在这里看到的是Java类库设计者所追求的经过深思熟虑的设计策略的结果。

以下是该设计策略的概要:

  1. 首先构造一个接口,该接口列出了我们希望集合类共享的赢博体育方法。
  2. 然后确保赢博体育集合类都实现相同的接口。
  3. 这将保证我们赢博体育的类共享一组公共方法,例如add ()包含(),remove ()
  4. 当我们编写与特定集合类一起工作的代码时,我们试图限制自己只使用来自公共接口的方法。
  5. 当我们决定切换到实现相同接口的不同集合时,除了创建集合的语句之外,不需要修改代码。

通过查看java.util.Collection接口的文档,您可以了解该策略在实践中是如何工作的。ArrayList和TreeSet类都实现了这个接口。