附录:标准IO

标准 I/O这个术语参考Unix中的概念,指程序所使用的单一信息流(这种思想在大多数操作系统中,也有相似形式的实现)。

程序的所有输入都可以来自于标准输入,其所有输出都可以流向标准输出,并且其所有错误信息均可以发送到标准错误标准 I/O 的意义在于程序之间可以很容易地连接起来,一个程序的标准输出可以作为另一个程序的标准输入。这是一个非常强大的工具。

从标准输入中读取

遵循标准 I/O 模型,Java 提供了标准输入流 System.in、标准输出流 System.out 和标准错误流 System.err。在本书中,你已经了解到如何使用 System.out将数据写到标准输出。 System.out 已经预先包装^1成了 PrintStream 对象。标准错误流 System.err 也预先包装为 PrintStream 对象,但是标准输入流 System.in 是原生的没有经过包装的 InputStream。这意味着尽管可以直接使用标准输出流 System.out 和标准错误流 System.err,但是在读取 System.in 之前必须先对其进行包装。

我们通常一次一行地读取输入。为了实现这个功能,将 System.in 包装成 BufferedReader 来使用,这要求我们用 InputStreamReaderSystem.in 转换^2Reader 。下面这个例子将键入的每一行显示出来:

  1. // standardio/Echo.java
  2. // How to read from standard input
  3. import java.io.*;
  4. import onjava.TimedAbort;
  5. public class Echo {
  6. public static void main(String[] args) {
  7. TimedAbort abort = new TimedAbort(2);
  8. new BufferedReader(
  9. new InputStreamReader(System.in))
  10. .lines()
  11. .peek(ln -> abort.restart())
  12. .forEach(System.out::println);
  13. // Ctrl-Z or two seconds inactivity
  14. // terminates the program
  15. }
  16. }

BufferedReader 提供了 lines() 方法,返回类型是 Stream<String> 。这显示出流模型的的灵活性:仅使用标准输入就能很好地工作。 peek() 方法重启 TimeAbort,只要保证至少每隔两秒有输入就能够使程序保持开启状态。

将 System.out 转换成 PrintWriter

System.out 是一个 PrintStream,而 PrintStream 是一个OutputStreamPrintWriter 有一个把 OutputStream 作为参数的构造器。因此,如果你需要的话,可以使用这个构造器把 System.out 转换成 PrintWriter

  1. // standardio/ChangeSystemOut.java
  2. // Turn System.out into a PrintWriter
  3. import java.io.*;
  4. public class ChangeSystemOut {
  5. public static void main(String[] args) {
  6. PrintWriter out =
  7. new PrintWriter(System.out, true);
  8. out.println("Hello, world");
  9. }
  10. }

输出结果:

  1. Hello, world

要使用 PrintWriter 带有两个参数的构造器,并设置第二个参数为 true,从而使能自动刷新到输出缓冲区的功能;否则,可能无法看到打印输出。

重定向标准 I/O

Java的 System 类提供了简单的 static 方法调用,从而能够重定向标准输入流、标准输出流和标准错误流:

  • setIn(InputStream)
  • setOut(PrintStream)
  • setErr(PrintStream)

如果我们突然需要在显示器上创建大量的输出,而这些输出滚动的速度太快以至于无法阅读时,重定向输出就显得格外有用,可把输出内容重定向到文件中供后续查看。对于我们想重复测试特定的用户输入序列的命令行程序来说,重定向输入就很有价值。下例简单演示了这些方法的使用:

  1. // standardio/Redirecting.java
  2. // Demonstrates standard I/O redirection
  3. import java.io.*;
  4. public class Redirecting {
  5. public static void main(String[] args) {
  6. PrintStream console = System.out;
  7. try (
  8. BufferedInputStream in = new BufferedInputStream(
  9. new FileInputStream("Redirecting.java"));
  10. PrintStream out = new PrintStream(
  11. new BufferedOutputStream(
  12. new FileOutputStream("Redirecting.txt")))
  13. ) {
  14. System.setIn(in);
  15. System.setOut(out);
  16. System.setErr(out);
  17. new BufferedReader(
  18. new InputStreamReader(System.in))
  19. .lines()
  20. .forEach(System.out::println);
  21. } catch (IOException e) {
  22. throw new RuntimeException(e);
  23. } finally {
  24. System.setOut(console);
  25. }
  26. }
  27. }

该程序将文件中内容载入到标准输入,并把标准输出和标准错误重定向到另一个文件。它在程序的开始保存了最初对 System.out 对象的引用,并且在程序结束时将系统输出恢复到了该对象上。

I/O重定向操作的是字节流而不是字符流,因此使用 InputStreamOutputStream,而不是 ReaderWriter

执行控制

你经常需要在Java内部直接执行操作系统的程序,并控制这些程序的输入输出,Java类库提供了执行这些操作的类。

一项常见的任务是运行程序并将输出结果发送到控制台。本节包含了一个可以简化此任务的实用工具。

在使用这个工具时可能会产生两种类型的错误:导致异常的普通错误——对于这些错误我们只需要重新抛出一个 RuntimeException 即可,以及进程自身的执行过程中导致的错误——我们需要用单独的异常来报告这些错误:

  1. // onjava/OSExecuteException.java
  2. package onjava;
  3. public class OSExecuteException extends RuntimeException {
  4. public OSExecuteException(String why) {
  5. super(why);
  6. }
  7. }

为了运行程序,我们需要传递给 OSExecute.command() 一个 String command,我们可以在控制台键入同样的指令运行程序。该命令传递给 java.lang.ProcessBuilder 的构造器(需要将其作为 String 对象的序列),然后启动生成的 ProcessBuilder 对象。

  1. // onjava/OSExecute.java
  2. // Run an operating system command
  3. // and send the output to the console
  4. package onjava;
  5. import java.io.*;
  6. public class OSExecute {
  7. public static void command(String command) {
  8. boolean err = false;
  9. try {
  10. Process process = new ProcessBuilder(
  11. command.split(" ")).start();
  12. try (
  13. BufferedReader results = new BufferedReader(
  14. new InputStreamReader(
  15. process.getInputStream()));
  16. BufferedReader errors = new BufferedReader(
  17. new InputStreamReader(
  18. process.getErrorStream()))
  19. ) {
  20. results.lines()
  21. .forEach(System.out::println);
  22. err = errors.lines()
  23. .peek(System.err::println)
  24. .count() > 0;
  25. }
  26. } catch (IOException e) {
  27. throw new RuntimeException(e);
  28. }
  29. if (err)
  30. throw new OSExecuteException(
  31. "Errors executing " + command);
  32. }
  33. }

为了捕获在程序执行时产生的标准输出流,我们可以调用 getInputStream()。这是因为 InputStream 是我们可以从中读取信息的流。

这里这些行只是被打印了出来,但是你也可以从 command() 捕获和返回它们。

该程序的错误被发送到了标准错误流,可以调用 getErrorStream() 捕获。如果存在任何错误,它们都会被打印并且抛出 OSExcuteException ,以便调用程序处理这个问题。

下面是展示如何使用 OSExecute 的示例:

  1. // standardio/OSExecuteDemo.java
  2. // Demonstrates standard I/O redirection
  3. // {javap -cp build/classes/main OSExecuteDemo}
  4. import onjava.*;
  5. public class OSExecuteDemo {}

这里使用 javap 反编译器(随JDK发布)来反编译程序,编译结果:

  1. Compiled from "OSExecuteDemo.java"
  2. public class OSExecuteDemo {
  3. public OSExecuteDemo();
  4. }