[关闭]
@Darcy 2017-08-11T11:13:27.000000Z 字数 8136 阅读 2573

第九章 保存游戏分数

JavaForKids

程序运行完毕后就会从内存中清除。这就意味着除非你重新运行这段程序,否则所有的类,方法和变量都不存在了。如果你想要保存一些程序的执行结果,那就必须把这些结果存放到磁盘,磁带,记忆卡或其他能长时间保存数据的设备中。在这一章节,你将会学习如何使用Java流来将数据存放到磁盘。从根本上说,你会在你的程序和磁盘上的文件之间建立数据流。如果你需要从磁盘上读取数据,就需要采用输入流,如果你想要将数据写到磁盘上,需要采用输出流。举个例子,如果一个玩家赢得了一轮游戏,你想将分数保存下来,就可以使用输出流将数据存放在文件scores.txt中。

程序从流中连续地读取/写入数据,采用逐个字节,逐个字符等方式。由于你的程序可能使用不同的数据类型比如Stringintdouble等等,所以你需要使用合适的Java流,比如字节流,字符流或者是数据类型流。

和流相关的类在java.iojava.nio包中。

无论你要采用哪一种流,都必须在程序中完成以下三个步骤:

1. 字节流

如果你创建一个程序来读取文件,并将文件内容展示到屏幕上,那么你需要知道这个文件中存储的是什么数据类型。另一方面,如果程序只是将文件从一个地方复制到另一个地方,那么就不需要知道这个文件是图片,文本,还是音乐文件了。这类程序按字节读取原始文件到内存中,然后逐个字节地将内容写到目标文件夹中。这个过程可以用Java中的FileInputStreamFileOutputStream类来实现。

下一个程序示例展示了如何使用FileInputStream 从文件夹c:\practice中读取图像文件abc.gif。如果你使用的是微软Windows系统,因为Java是使用反斜杠来进行一些特殊字符的转义的,反斜杠本身也是一种特殊字符,所以你可以使用两条反斜杠来表示来分隔文件夹和文件的路径:c:\\practice。这个简短的程序不会将图片展示出来,而是打印了一些数字,这些数字也是图片在磁盘中的存放方式。每个字节都是0到255之间的整数,我们用ByteReader来把这些值读出来,并用空格间隔打印出来。

请注意到ByteReader是在finally块中将流关闭的。永远不要刚完成读取文件就在try/catch块中调用close()方法,而应该在finally中调用。如果在读取文件过程中发生了异常,程序会直接跳过close()语句,导致流并没有关闭。当FileInputStream.read()方法返回负数时,读取就会结束。

  1. public class ByteReader {
  2. public static void main(String[] args) {
  3. FileInputStream myFile = null;
  4. try {
  5. // 打开一个指向文件的字节流
  6. myFile = new FileInputStream("c:\\temp\\abc.gif");
  7. while (true) {
  8. int intValueOfByte = myFile.read();
  9. System.out.print(" " + intValueOfByte);
  10. if (intValueOfByte == -1) {
  11. // 我们已经读取到文件的末尾了,让我们结束这个循环吧
  12. break;
  13. }
  14. }
  15. // myFile.close(); 不要在这里关闭流
  16. } catch (IOException e) {
  17. System.out.println("Could not read file: " + e.toString());
  18. } finally {
  19. try {
  20. myFile.close();
  21. } catch (Exception e1) {
  22. e1.printStackTrace();
  23. }
  24. System.out.println(" Finished reading the file");
  25. }
  26. }
  27. }

下面的代码片段使用FileOutputStream类用字节的方式向文件xyz.dat写入了一些整型数字:

  1. int somedata[]= {56,230,123,43,11,37};
  2. FileOutputStream myFile = null;
  3. try {
  4. // 打开xyz.dat文件,并把数组的内容写进去
  5. myFile = new FileOutputStream("xyz.dat");
  6. for (int i = 0; i <some data.length; i++){
  7. file.write(data[i]);
  8. }
  9. } catch (IOException e) {
  10. System.out.println("Could not write to a file: "+e.toString());
  11. } finally{
  12. try{
  13. myFile.close();
  14. } catch (Exception e1){
  15. e1.printStackTrace();
  16. }
  17. }

2. 缓冲流

到目前为止,我们都是一个一个字节的读写数据,这就意味着ByteReader要读取1000个字节的文件时需要访问磁盘1000次。但是在磁盘上获取数据要比在内存中操作数据慢得多。为了减少程序访问磁盘的次数,Java提供了类似“数据存储库”的缓冲区。

BufferedInputStream类帮助我们从FileInputStream中快速读取数据填充到内存缓冲区中。缓冲流从文件中一次性读取大量的字节存放到内存缓冲区,read()方法再从缓冲区中单个读取字节时速度就会快得多了。

你的程序连接流就像管道工连接两根管道。让我们来修改读取文件的例子。首先数据从FileInputStream存放到BufferedInputStream,然后read()方法来操作了:

  1. FileInputStream myFile = null;
  2. BufferedInputStream buff =null;
  3. try {
  4. myFile = new FileInputStream("abc.dat");
  5. // 连接到缓冲流
  6. buff = new BufferedInputStream(myFile);
  7. while (true) {
  8. int byteValue = buff.read();
  9. System.out.print(byteValue + " ");
  10. if (byteValue == -1)
  11. break;
  12. }
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }finally{
  16. try{
  17. buff.close();
  18. myFile.close();
  19. } catch(IOException e1){
  20. e1.printStackTrace();
  21. }
  22. }

这个缓存区可以有多大呢?这取决于JVM,但是你可以设置缓冲区的大小来尝试是否可以让文件读取更快一些。举个例子,使用带有两个参数的构造函数设置缓冲区大小为5000字节:

  1. BufferedInputStream buff = new BufferedInputStream(myFile, 5000);

缓冲流没有改变读取数据的方式,只是让读取更快了而已。

BufferedOutputStream工作机制也是类似,只不过采用的是FileOutputStream类。

  1. int somedata[]= {56,230,123,43,11,37};
  2. FileOutputStream myFile = null;
  3. BufferedOutputStream buff =null;
  4. try {
  5. myFile = new FileOutputStream("abc.dat");
  6. buff = new BufferedOutputStream(myFile);
  7. for (int i = 0; i <somedata.length; i++){
  8. buff.write(somedata[i]);
  9. }
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }finally{
  13. try{
  14. buff.flush();
  15. buff.close();
  16. myFile.close();
  17. } catch(IOException e1){
  18. e1.printStackTrace();
  19. }
  20. }

BufferedOutputStream中写入数据结束后要调用flush()方法,以保证缓冲区中的所有字节都有放到文件流中。

3. 命令行参数

我们的ByteReader程序将文件abc.gif的文件名直接写在了代码中,用程序员的话来说,文件名称在程序中是硬编码。这就意味着如果需要创建一个相同的程序来读取文件xyz.gif,你需要修改代码并重新编译程序,显然这是不好的。更好的方式是在运行程序的时候,将文件名从命令行输入给程序。

你可以运行任意带有命令行参数的Java程序,例如:

java ByteReader xyz.gif

在这个例子中我们给ByteReadermain()方法传递了一个参数xyz.gif。如果你还记得,main()方法有一个参数:

  1. public static void main(String[] args) {

是的,这个参数是一个由JVM传递给main方法的String类型的数组。如果你不带有任何参数启动程序,则这个数组为空。否则,你通过命令行传入多少参数,这个数组就有多少元素。

一起来看看如何在一个非常简单的类中使用这些命令行参数并打印出来:

  1. public class TestArguments {
  2. public static void main(String[] args) {
  3. // 参数的个数
  4. int numberOfArgs = args.length;
  5. for (int i=0; i<numberOfArgs; i++){
  6. System.out.println("I've got " + args[i]);
  7. }
  8. }
  9. }

下面的截图向你展示了如果你带有两个参数xyz.gif250运行这个程序时,程序会做什么。xyz.gif会被JVM存档到元素args[0]中,而第二个参数250被存放到args[1]中。

命令行参数通常采用String类型来传递数据给程序,所以程序需要将这些数据转化为合适的数据类型,比如:

  1. int myScore = Integer.parseInt(args[1]);

检查命令行的参数个数是否正确是很有必要的。在main()方法开始时就可以做这个操作。如果程序没有接收到合适的参数个数,则应该打印出一条简短的信息,并用System.exit()来退出程序。

  1. public static void main(String[] args) {
  2. if (args.length != 2){
  3. System.out.println("Please provide arguments, for example:");
  4. System.out.println("java TestArguments xyz.gif 250");
  5. // 退出程序
  6. System.exit(0);
  7. }
  8. }

结束这一章节学习后,你需要写一个程序来复制文件。这个程序应该适用于任何文件类型,源文件名和目标文件名都需要通过命令行参数来传递给程序。

Eclipse为你的程序提供了命令行参数执行方式,你可以通过Eclipse来测试你的程序。在Run的窗口选项中,选择标签(x)=Arguments,在Program Arguments中输入要求的值。

VM arguments允许你传递参数给JVM。这些参数可以为程序要求更多的内存,或者性能更优的JVM等等。这一章节的扩展阅读中有个链接具体描述了这些参数。

4. 读取文本文件

Java使用两个字节的字符来存储字母,FileReaderFileWriter是为了方便操作文本文件。这些类可以使用read()方法一次读取一个字符,也可以使用readLine()方法一次读取整行数据。FileReaderFileWriter同样有对应的BufferedReaderBufferedWriter类可以提高操作文件的速度。

下一个类ScoreReader逐行读取文件scores.txt,当readLine()方法返回null时程序结束。

使用任意纯文本编辑器创建文件c:\scores.txt,包含内容如下:

David 235
Brian 190
Anna 225
Zachary 160

运行程序ScoreReader,它将打印出这个文件的内容。向文件中再添加几行分数内容,然后重新运行该程序,可以看到新添加的行也会被打印出来。

  1. import java.io.FileReader;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. public class ScoreReader {
  5. public static void main(String[] args) {
  6. FileReader myFile = null;
  7. BufferedReader buff = null;
  8. try {
  9. myFile = new FileReader("c:\\scores.txt");
  10. buff = new BufferedReader(myFile);
  11. while (true) {
  12. // 从scores.txt中读取一行
  13. String line = buff.readLine();
  14. // 检查是否到了文件尾
  15. if (line == null)
  16. break;
  17. System.out.println(line);
  18. }
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. } finally {
  22. try {
  23. buff.close();
  24. myFile.close();
  25. } catch (IOException e1) {
  26. e1.printStackTrace();
  27. }
  28. }
  29. }
  30. }

如果你的程序需要向磁盘写入文本文件,使用FileWriterwrite()write()有多个重载方法,通过这些方法,你可以写入字符,字符串或者是整个字符类型的数组。

FileWriter有不止一个的重载构造方法,如果你仅仅提供文件名来打开文件写入内容,那么你每次运行程序都会新生成一个文件来替代旧文件:

  1. FileWriter fOut = new FileWriter("Scores.txt");

如果你需要向一个已经存在的文件末尾添加数据,使用带有两个参数的构造方法(true表示使用追加模式):

  1. FileWriter fOut = new FileWriter("Scores.txt", true);

下一个类ScoreWriter从数组scores中读取三行写入到文件c:\scores.txt中。

  1. import java.io.FileWriter;
  2. import java.io.BufferedWriter;
  3. import java.io.IOException;
  4. public class ScoreWriter {
  5. public static void main(String[] args) {
  6. FileWriter myFile = null;
  7. BufferedWriter buff = null;
  8. String[] scores = new String[3];
  9. //填充数组scores
  10. scores[0] = "Mr. Smith 240";
  11. scores[1] = "Ms. Lee 300";
  12. scores[2] = "Mr. Dolittle 190";
  13. try {
  14. myFile = new FileWriter("c:\\scores2.txt");
  15. buff = new BufferedWriter(myFile);
  16. for (int i = 0; i < scores.length; i++) {
  17. // 把数组的内容写入scores2.txt中
  18. buff.write(scores[i]);
  19. System.out.println("Writing " + scores[i]);
  20. }
  21. System.out.println("File writing is complete");
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. } finally {
  25. try {
  26. buff.flush();
  27. buff.close();
  28. myFile.close();
  29. } catch (IOException e1) {
  30. e1.printStackTrace();
  31. }
  32. }
  33. }
  34. }

这个程序的输出如下所示:

Writing Mr. Smith 240
Writing Ms. Lee 300
Writing Mr. Dolittle 190
File writing is complete

5. File类

java.io.File类包含了许多便捷的方法,让你可以重命名文件,删除文件,检查类文件是否存在等等。假设你的程序要将一些数据存到文件中,当文件已经存在时,程序需要展示一条警告信息告知用户这个文件已经存在了。为了完成这个功能,你需要在创建对象File的实例时给这个文件命名,然后调用方法exist()。如果这个方法的返回是true,就表示abc.txt已经存在了,你需要输出警告信息,否则表示这个文件不存在:

  1. File aFile = new File("abc.txt");
  2. if (aFile.exists()){
  3. // 输出一些信息或者用JOptionPane来显示一个警告弹窗
  4. }

File类的构造方法并不是真的创建了文件:它只是在内存中创建了对象的实例来指向真实的文件。如果你需要创建文件,应该使用createNewFile()方法。

File类的一些有用的方法如下面列表所示。

方法名 做什么
createNewFile() 创建一个新的空文件
delete() 删除一个文件或者目录
renameTo() 重命名文件或者目录
length() 文件的字节数
exists() 判断文件或者目录是否存在
list() 返回该路径下的所有文件和目录名
lastModified() 返回文件上次的修改时间
mkDir() 创建一个文件目录

下一个代码片段将文件customers.txt重命名为customers.txt.bak。如果后者已经存在,则会直接覆盖。

  1. File file = new File("customers.txt");
  2. File backup = new File("customers.txt.bak");
  3. if (backup.exists()) {
  4. backup.delete();
  5. }
  6. file.renameTo(backup);

尽管这一章节阐述的是操作你电脑磁盘上的文件,Java也支持通过计算机网络创建流指向远程机器,这些机器在地理位置上可以相距非常远。比如说,NASA[1]使用Java来控制火星探测器,而且我可以确定他们只是将输入输出流指向火星而已。

6. 扩展阅读

1.JVM命令行选项
http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/java.html
2.如何使用文件流
http://java.sun.com/docs/books/tutorial/essential/io/filestreams.html

7. 练习

写一个文件复制程序FileCopy,可以参考字节流这个章节中出现的一些代码。

同时打开输入和输出流,然后在同一个循环中调用方法read()write()。使用命令行参数将源文件名和目的文件名传递给程序,例如:

java FileCopy c:\\temp\\scores.txt  c:\\backup\\scores2.txt

8. 进一步的练习

JFileChooser类可以创建标准的文件选择窗口,使用JFileChooser创建一个图形程序,这个程序允许用户选择需要复制的文件。当用户点击任意一个Browse按钮时,文件选择窗口会弹出。你需要写几行代码实现在对应的文本框中显示选中的文件名。

当用户点击按钮Copy后,actionPerformed()方法中的代码应该复制选中的文件。尝试重用之前练习题中的代码,而不是简单的复制粘贴。


創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 2.5 中國大陸 授權條款授權.


[1] 美国国家航空航天局(National Aeronautics and Space Administration
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注