内容


第 21 单元:I/O

在 Java 程序中收集和处理外部数据

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解 java.io.File 类的主要用途
  • 了解如何使用字节流和字符流
  • 了解如何从文件读取数据和向其中写入数据

处理外部数据

您在 Java 程序中使用的数据通常来自外部数据来源,比如数据库、通过套接字直接传输的字节或文件存储。大部分收集和处理外部数据的 Java 工具都包含在 java.io 包中。

文件

在所有可用于 Java 应用程序的数据来源中,文件是最常见的,通常也是最方便的。如果想在 Java 应用程序中读取某个文件,必须使用将它的传入字节解析为 Java 语言类型的

java.io.File 是一个在文件系统上定义资源并以某种抽象方式表示该资源的类。创建 File 对象很容易:

File f = new File("temp.txt");

File f2 = new File("/home/steve/testFile.txt");

File 构造方法接受它创建的文件的名称。第一个调用在指定的目录中创建一个名为 temp.txt 的文件。第二个调用在我的 Linux 系统上的特定位置创建一个文件。您可以将任何 String 传递到 File 的构造方法,只要它是对您的操作系统有效的文件名,无论它引用的文件是否存在。

此代码向新创建的 File 对象询问该文件是否存在:

File f2 = new File("/home/steve/testFile.txt");
if (f2.exists()) {
  // File exists. Process it...
} else {
  // File doesn't exist. Create it...
  f2.createNewFile();
}

java.io.File 还有其他一些方便的方法可用于:

  • 删除文件
  • 创建目录(通过将目录名称作为参数传递给 File 的构造方法)
  • 确定资源是文件、目录还是符号链接
  • 等等

Java I/O 的主要操作是写入和读出数据来源,这时就需要使用流。

在 Java I/O 中使用流

您可以使用流来访问文件系统上的文件。最低限度上,流允许程序从来源接收字节或将输出发送到目标。一些流可以处理所有类型的 16 位字符(ReaderWriter 类型)。其他流仅能处理 8 位字节(InputStreamOutputStream 类型)。这些分层结构中包含多种风格的流,均可在 java.io 包中找到。

字节流读(InputStream 和子类)和写(OutputStream 和子类)8 位字节。换句话说,可以将字节流视为一种更加原始的流类型。下面总结了两种常见的字节流和它们的用法:

  • FileInputStream / FileOutputStream:从文件读取字节,将字节写入文件
  • ByteArrayInputStream / ByteArrayOutputStream:从内存型中的数组读取字节,将字节写入内存中的数组

字符流

字符流读(Reader 和它的子类)和写(Writer 和它的子类)16 位字符。下面挑选了一些字符流和它们的用法:

  • StringReader / StringWriter:在内存中的 String 中读取和写入字符。
  • InputStreamReader / InputStreamWriter(和子类 FileReader / FileWriter):充当字节流和字符流之间的桥梁。Reader 喜欢从字节流读取字节并转换为字符。Writer 喜欢将字符转换为字节,以便将它们放在字节流上。
  • BufferedReader / BufferedWriter:在读取或写入另一个流时缓冲数据,使读写操作更高效。

我在这里没有尝试介绍所有流,而是主要介绍了读写文件的推荐流。在大多数情况下,这些都是字符流。

从 File 读取数据

可通过多种方式从 File 中读取数据。可能最简单的方法是:

  1. 在想要从中读取数据的 File 上创建一个 InputStreamReader
  2. 调用 read() 来一次读取一个字符,直至到达文件末尾。

清单 1 是一个从 File 读取的示例:

清单 1. 从 File 中读取数据
public List<Employee> readFromDisk(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file))) {
    StringBuilder sb = new StringBuilder();
    int numberOfEmployees = 0;
    int character = reader.read();
    while (character != -1) {
        sb.append((char)character);
        character = reader.read();
    }
    log.info("Read file: \n" + sb.toString());
    int index = 0;
    while (index < sb.length()-1) {
      StringBuilder line = new StringBuilder();
      while ((char)sb.charAt(index) != '\n') {
        line.append(sb.charAt(index++));
      }
      StringTokenizer strtok = new StringTokenizer(line.toString(), Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      index++;
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

将数据写入到 File

与从 File 中读取数据一样,可通过多种方式将数据写入到 File。同样地,我介绍最简单的方法:

  1. 在想要写入数据的 File 上创建一个 FileOutputStream
  2. 调用 write() 来写入字符序列。

清单 2 是一个将数据写入 File 的例子:

清单 2. 将数据写入到 File
public boolean saveToDisk(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

缓冲流

一次一个字符地读取和写入字符流的效率很低,所以在大部分情况下,您可能希望使用缓冲的 I/O。要使用缓冲的 I/O 从文件中读取数据,代码类似于 清单 1,但将 InputStreamReader 包装在一个 BufferedReader 中,如清单 3 所示。

清单 3. 使用缓冲的 I/O 从 File 中读取数据
public List<Employee> readFromDiskBuffered(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
    String line = reader.readLine();
    int numberOfEmployees = 0;
    while (line != null) {
      StringTokenizer strtok = new StringTokenizer(line, Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      // Read next line
      line = reader.readLine();
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

使用缓冲的 I/O 写入文件的过程相同:将 OutputStreamWriter 包装在一个 BufferedWriter 中,如清单 4 所示。

清单 4. 使用缓冲的 I/O 写入 File
public boolean saveToDiskBuffered(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

测试您的理解情况

  1. 对或错:在创建 File 对象来表示一个文件之前,该文件必须存在于磁盘上。
  2. 哪句话最恰当地描述了字符流
    1. 字符流是 8 位流,用于通过 ByteArrayProcessor 接口从文件中读取数据并写入内存中。
    2. 字符流不应用于读取二进制数据。
    3. 字符流主要用于读取和写入文本文件。
    4. 字符流是 16 位流,用于通过 ReaderWriter 子类在文件中读取和写入数据。
    5. 上述选项都不是
  3. 哪句话最恰当地描述了为什么使用 BufferedReader
    1. 使用 BufferedReader 包装 OutputStreamWriter,可以通过缓存输入和输出来帮助它更高效地处理 16 位字符流。
    2. 结合使用 StreamReaderStreamWriter 时,必须小心不要跨越流,
    3. 当用作 InputStreamReader 的包装器时,BufferedReader 通过缓存输入来帮助它更高效地处理 16 位字符流。
    4. BufferedReader 绝不应用于从输入流读取数据。
    5. 上述选项都不是
  4. 创建一个名为 lorem.txt 的文件,其中包含 lorem ipsem 的前 250 个单词(可以使用 generator.lorem-ipsum.info 或类似网站生成此文本)。将该文件保存到 Java 项目的根目录。现在编写一个名为 Unit21 的类,其中包含一个名为 readFile() 的方法,该方法读取文件,使用 JDK Logger 实例打印它的内容,并将(包含文件内容的)String 返回给调用方。

    编写一个名为 Unit21Test 的 JUnit 类作为测试平台。

    要考虑的问题:
    • 您如何指定 readFile() 要读取的文件?
    • 您如何处理异常?
    • 如何确保在完成时关闭了所有文件资源?
  5. 将一个名为 writeFile() 的方法添加到问题 4 中的 Unit21 类,该方法将您读取的文件写入到一个名为 lorem2.txt 的新文件。向 Unit21Test 添加一个新测试方法作为测试平台。备注:您在问题 4 的解决方案中解决的问题同样也适用于输出流。
  6. 改进问题 4 的解决方案,让每行的长度不超过 maxCharactersPerLine 字符数。(在到达第 maxCharactersPerLine 个字符时,可以尝试保留字。)编写 JUnit 测试,指定 80 作为 maxCharactersPerLine 的值。
  7. 改进问题 6 的解决方案,防止截断包含第 maxCharactersPerLine 个字符的单词。而在下一行输出该单词(和任何后续单词)。这类似于文字处理程序的工作。编写 JUnit 测试,指定 80 作为 maxCharactersPerLine 的值。提示:更简单的方法可能是,一次一个单词地处理当前行,检查当前单词是否导致该行超出 maxCharactersPerLine,如果超出,则将该单词放在下一行。

核对您的答案

进一步探索

Java 教程:I/O 流

Java I/O 教程

Benjamin J. Evans 和 David Flanagan 合著的 Java 技术手册第 6 版(参阅第 10 章 “文件处理和 I/O”)

在 Java 中读取纯文本文件

在 Java 中将二进制输入流读入一个字节数组中

上一单元:泛型下一单元:Java 序列化


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1039321
ArticleTitle=第 21 单元:I/O
publish-date=11012016