Appendix-Standard-IO
附录:标准 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
来使用,这要求我们用 InputStreamReader
把 System.in
转换2成 Reader
。下面这个例子将键入的每一行显示出来:
// standardio/Echo.java
// How to read from standard input
import java.io.*;
import onjava.TimedAbort;
public class Echo {
public static void main(String[] args) {
TimedAbort abort = new TimedAbort(2);
new BufferedReader(
new InputStreamReader(System.in))
.lines()
.peek(ln -> abort.restart())
.forEach(System.out::println);
// Ctrl-Z or two seconds inactivity
// terminates the program
}
}
BufferedReader
提供了 lines()
方法,返回类型是 Stream<String>
。这显示出流模型的的灵活性:仅使用标准输入就能很好地工作。 peek()
方法重启 TimeAbort
,只要保证至少每隔两秒有输入就能够使程序保持开启状态。
将 System.out 转换成 PrintWriter
System.out
是一个 PrintStream
,而 PrintStream
是一个OutputStream
。 PrintWriter
有一个把 OutputStream
作为参数的构造器。因此,如果你需要的话,可以使用这个构造器把 System.out
转换成 PrintWriter
。
// standardio/ChangeSystemOut.java
// Turn System.out into a PrintWriter
import java.io.*;
public class ChangeSystemOut {
public static void main(String[] args) {
PrintWriter out =
new PrintWriter(System.out, true);
out.println("Hello, world");
}
}
输出结果:
Hello, world
要使用 PrintWriter
带有两个参数的构造器,并设置第二个参数为 true
,从而使能自动刷新到输出缓冲区的功能;否则,可能无法看到打印输出。
重定向标准 I/O
Java 的 System
类提供了简单的 static
方法调用,从而能够重定向标准输入流、标准输出流和标准错误流:
- setIn(InputStream)
- setOut(PrintStream)
- setErr(PrintStream)
如果我们突然需要在显示器上创建大量的输出,而这些输出滚动的速度太快以至于无法阅读时,重定向输出就显得格外有用,可把输出内容重定向到文件中供后续查看。对于我们想重复测试特定的用户输入序列的命令行程序来说,重定向输入就很有价值。下例简单演示了这些方法的使用:
// standardio/Redirecting.java
// Demonstrates standard I/O redirection
import java.io.*;
public class Redirecting {
public static void main(String[] args) {
PrintStream console = System.out;
try (
BufferedInputStream in = new BufferedInputStream(
new FileInputStream("Redirecting.java"));
PrintStream out = new PrintStream(
new BufferedOutputStream(
new FileOutputStream("Redirecting.txt")))
) {
System.setIn(in);
System.setOut(out);
System.setErr(out);
new BufferedReader(
new InputStreamReader(System.in))
.lines()
.forEach(System.out::println);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.setOut(console);
}
}
}
该程序将文件中内容载入到标准输入,并把标准输出和标准错误重定向到另一个文件。它在程序的开始保存了最初对 System.out
对象的引用,并且在程序结束时将系统输出恢复到了该对象上。
I/O 重定向操作的是字节流而不是字符流,因此使用 InputStream
和 OutputStream
,而不是 Reader
和 Writer
。
执行控制
你经常需要在 Java 内部直接执行操作系统的程序,并控制这些程序的输入输出,Java 类库提供了执行这些操作的类。
一项常见的任务是运行程序并将输出结果发送到控制台。本节包含了一个可以简化此任务的实用工具。
在使用这个工具时可能会产生两种类型的错误:导致异常的普通错误——对于这些错误我们只需要重新抛出一个 RuntimeException
即可,以及进程自身的执行过程中导致的错误——我们需要用单独的异常来报告这些错误:
// onjava/OSExecuteException.java
package onjava;
public class OSExecuteException extends RuntimeException {
public OSExecuteException(String why) {
super(why);
}
}
为了运行程序,我们需要传递给 OSExecute.command()
一个 String command
,我们可以在控制台键入同样的指令运行程序。该命令传递给 java.lang.ProcessBuilder
的构造器(需要将其作为 String
对象的序列),然后启动生成的 ProcessBuilder
对象。
// onjava/OSExecute.java
// Run an operating system command
// and send the output to the console
package onjava;
import java.io.*;
public class OSExecute {
public static void command(String command) {
boolean err = false;
try {
Process process = new ProcessBuilder(
command.split(" ")).start();
try (
BufferedReader results = new BufferedReader(
new InputStreamReader(
process.getInputStream()));
BufferedReader errors = new BufferedReader(
new InputStreamReader(
process.getErrorStream()))
) {
results.lines()
.forEach(System.out::println);
err = errors.lines()
.peek(System.err::println)
.count() > 0;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (err)
throw new OSExecuteException(
"Errors executing " + command);
}
}
为了捕获在程序执行时产生的标准输出流,我们可以调用 getInputStream()
。这是因为 InputStream
是我们可以从中读取信息的流。
这里这些行只是被打印了出来,但是你也可以从 command()
捕获和返回它们。
该程序的错误被发送到了标准错误流,可以调用 getErrorStream()
捕获。如果存在任何错误,它们都会被打印并且抛出 OSExcuteException
,以便调用程序处理这个问题。
下面是展示如何使用 OSExecute
的示例:
// standardio/OSExecuteDemo.java
// Demonstrates standard I/O redirection
// {javap -cp build/classes/main OSExecuteDemo}
import onjava.*;
public class OSExecuteDemo {}
这里使用 javap
反编译器(随 JDK 发布)来反编译程序,编译结果:
Compiled from "OSExecuteDemo.java"
public class OSExecuteDemo {
public OSExecuteDemo();
}