15.6 Java虚拟机读写其他进程的数据

在第7章已经介绍过,使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生个Process对象,Process对象代表由该Java程序启动的子进程

Process类提供了如下三个方法,用于让程序和其子讲程讲行通信

方法 描述
abstract InputStream getErrorStream() 获取子进程的错误流。
abstract InputStream getInputStream() 获取子进程的输入流。
abstract OutputStream getOutputStream() 获取子进程的输出流。

要从java程序的角度来看输入输出流

此处的输入流、输出流非常容易混淆,如果试图让子进程读取程序中的数据,那么应该用输岀流
要站在Java程序的角度来看问题,而不是从子进程的角度来看问题:
子进程读取Java程序的数据,就是让Java程序把数据输岀到子进程中
这就像Java程序把数据输出到文件中一样,只是现在由子进程节点代替了文件节点,所以
子进程要读取java程序的内容应该使用输出流

程序 java读取子进程的错误输出

下面程序示范了读取其他进程的输出信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class ReadFromProcess {
public static void main(String[] args) throws IOException {
// 运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");// 代码1
try (
// 以p进程的错误流创建BufferedReader对象
// 这个错误流对本程序是输入流,对p进程则是输出流
BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()))// 代码2
) {
String buff = null;
// 采取循环方式来读取p进程的错误输出
while ((buff = br.readLine()) != null) {
System.out.println("子程序的错误输出:"+buff);
}
}
}
}

上面程序中的代码1使用Runtime启动了Javac程序,获得了运行该程序对应的子进程,
代码2以p进程的错误输入流创建了BufferedReader,这个输入流的流向如图15.11所示。
这里有一张图片
如图15.11所示的数据流对p进程(Javac进程)而言,它是输出流;但对本程序(ReadFromProcess)而言,它是输入流。
衡量输入、输出时总是站在运行本程序所在内存的角度,所以该数据流应该是输入流
运行上面程序,会看到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.6>javac -encoding utf-8 ReadFromProcess.java

G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.6>java ReadFromProcess
子程序的错误输出:用法: javac <options> <source files>
子程序的错误输出:其中, 可能的选项包括:
子程序的错误输出: -g 生成所有调试信息
子程序的错误输出: -g:none 不生成任何调试信息
子程序的错误输出: -g:{lines,vars,source} 只生成某些调试信息
子程序的错误输出: -nowarn 不生成任何警告
子程序的错误输出: -verbose 输出有关编译器正在执行的操作的消息
子程序的错误输出: -deprecation 输出使用已过时的 API 的源位置
子程序的错误输出: -classpath <路径> 指定查找用户类文件和注释处理程序的位置
子程序的错误输出: -cp <路径> 指定查找用户类文件和注释处理程序的位置
子程序的错误输出: -sourcepath <路径> 指定查找输入源文件的位置
子程序的错误输出: -bootclasspath <路径> 覆盖引导类文件的位置
子程序的错误输出: -extdirs <目录> 覆盖所安装扩展的位置
子程序的错误输出: -endorseddirs <目录> 覆盖签名的标准路径的位置
子程序的错误输出: -proc:{none,only} 控制是否执行注释处理和/或编译。
子程序的错误输出: -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
子程序的错误输出: -processorpath <路径> 指定查找注释处理程序的位置
子程序的错误输出: -parameters 生成元数据以用于方法参数的反射
子程序的错误输出: -d <目录> 指定放置生成的类文件的位置
子程序的错误输出: -s <目录> 指定放置生成的源文件的位置
子程序的错误输出: -h <目录> 指定放置生成的本机标头文件的位置
子程序的错误输出: -implicit:{none,class} 指定是否为隐式引用文件生成类文件
子程序的错误输出: -encoding <编码> 指定源文件使用的字符编码
子程序的错误输出: -source <发行版> 提供与指定发行版的源兼容性
子程序的错误输出: -target <发行版> 生成特定 VM 版本的类文件
子程序的错误输出: -profile <配置文件> 请确保使用的 API 在指定的配置文件中可用
子程序的错误输出: -version 版本信息
子程序的错误输出: -help 输出标准选项的提要
子程序的错误输出: -A关键字[=值] 传递给注释处理程序的选项
子程序的错误输出: -X 输出非标准选项的提要
子程序的错误输出: -J<标记> 直接将 <标记> 传递给运行时系统
子程序的错误输出: -Werror 出现警告时终止编译
子程序的错误输出: @<文件名> 从文件读取选项和文件名
子程序的错误输出:

程序 java输出数据到子进程

不仅如此,也可以通过ProcessgetOutputStream方法获得向子进程输入数据的流(该流对Java程序来说是输出流,对子进程来说是输入流),如下程序实现了在Java程序中启动Java虚拟机运行另一个Java程序,并向另一个Java程序中输入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.*;
import java.util.*;

public class WriteToProcess {
public static void main(String[] args) throws IOException {
// 运行java ReadStandard命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("java ReadStandard");// 代码1
try (
// 以p进程的输出流创建PrintStream对象
// 这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream())// 代码2
) {
// 向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("普通字符串");
ps.println(new WriteToProcess());
}
}
}

// 定义一个ReadStandard类,该类可以接受标准输入,
// 并将标准输入写入out.txt文件。
class ReadStandard {
public static void main(String[] args) {
try (
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))) {
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while (sc.hasNext()) {
// 输出输入项
ps.println("键盘输入的内容是:" + sc.next());
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

上面程序中的ReadStandard是一个使用Scanner获取标准输入的类,该类提供了main()方法,可以被运行,但此处不打算直接运行该类,而是由WriteToProcess类来运行ReadStandard类。

在程序的代码1中,程序使用Runtimeexec()方法运行了java Readstandard命令,该命令将运行ReadStandard类,并返回运行该程序的子进程;
程序的代码2获得当前程序对进程p的输出流:
程序通过该输出流向进程p(也就是ReadStandard程序)输出数据,这些数据将被ReadStandard类读到。
运行上面的WriteToProcess类,程序运行结束将看到产生了一个out.txt文件,该文件由ReadStandard类产生,该文件的内容由WriteToProcess类写入ReadStandard进程里,并由ReadStandard读取这些数据,并将这些数据保存到out.txt文件中.

15.5 重定向标准输入 输出

第7章介绍过,Java的标准输入输出分别通过System.inSystem.out来代表,在默认情况下它们分别代表键盘和显示器。

  • 当程序通过System.in来获取输入时,实际上是从键盘读取输入;
  • 当程序试图通过System.out执行输出时,程序总是输出到屏幕。

System类重定向标准输入输出的方法

System类里提供了如下三个重定向标准输入输出的方法

方法 描述
static void setErr(PrintStream err) 重定向“标准”错误输出流。
static void setIn(InputStream in) 重定向“标准”输入流。
static void setOut(PrintStream out) 重定向“标准”输出流。

程序示例 输出重定向到文件中

下面程序通过重定向标准输出流,将System.out的输出重定向到文件输出,而不是在屏幕上输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;

public class RedirectOut {
public static void main(String[] args) {
try (
// 一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))) {
// 将标准输出重定向到ps输出流
System.setOut(ps);
// 向标准输出输出一个字符串
System.out.println("普通字符串");
// 向标准输出输出一个对象
System.out.println(new RedirectOut());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序中创建了一个PrintStream输出流,并将系统的标准输出重定向到该PrintStream输出流。运行上面程序时将看不到任何输出——这意味着标准输出不再输出到屏幕,而是输出到out.txt文件,运行结束后,打开系统当前路径下的out.txt文件,即可看到文件里的内容,正好与程序中的输出一致。

程序示例 输入重定向到文件

下面程序重定向标准输入,从而可以将System.in重定向到指定文件,而不是键盘输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.*;
import java.io.*;

public class RedirectIn {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("RedirectIn.java")) {
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while (sc.hasNext()) {
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
sc.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序中的粗体字代码创建了一个FileInputStrean输入流,并使用SystemsetIn()方法将系统标准输入重定向到该文件输入流。运行上面程序,程序不会等待用户输入,而是直接输出了RedirectIn.java文件的内容,这表明程序不再使用键盘作为标准输入,而是使用RedirectIon.java文件作为标准输入源.

15.4.4 推回输入流

在输入/输出流体系中,有两个特殊的流与众不同,就是PushbackInputStreamPushbackReader,它们都提供了如下三个方法

PushbackInputStream的unread方法

方法 描述
void unread(int b) 将一个字节推回到推回缓冲区里,从而允许重复读取刚刚读取的内容
void unread(byte[] b) 将一个字节数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容
void unread(byte[] b, int off, int len) 将一个字节数组里从off开始,长度为len字节的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

PushbackReader的unread方法

方法 描述
void unread(int c) 将一个字符推回到推回缓冲区里,从而允许重复读取刚刚读取的内容
void unread(char[] cbuf) 将一个字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容
void unread(char[] cbuf, int off, int len) 将一个字符数组里从off开始,长度为len字节的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

细心的读者可能已经发现了这三个方法与InputStreamReader中的三个read方法一一对应,没错,这三个方法就是PushbackInputStreamPushbackReader的奥秘所在。

推回缓冲区

read方法优先从退回缓冲器中读取

这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread方法时,系统将会把指定数组的内容推回到该缓冲区里,而**推回输入流每次调用read方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read所需的数组时才会从原输入流中读取**。图15.10显示了这种推回输入流的处理示意图。
这里有一张图片
根据上面的介绍可以知道,当程序创建一个PushbackInputStreamPushbackReader时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflowIOException异常。
虽然图15.10中的推回缓冲区的长度看似比read方法的数组参数的长度小,但实际上,推回缓冲区的长度与read方法的数组参数的长度没有任何关系,完全可以更大。

程序示例

下面程序试图找出程序中的"new PushbackReader"这个字符串,当找到该字符串后,程序只是打印出目标字符串之前的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.*;

public class PushbackTest
{
public static void main(String[] args)
{
try(
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"PushbackTest.java") , 64))
{
char[] buf = new char[32];
// 用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
// 循环读取文件内容
while ((hasRead = pr.read(buf)) > 0)
{
// 将读取的内容转换成字符串
String content = new String(buf , 0 , hasRead);
int targetIndex = 0;
// 将上次读取的字符串和本次读取的字符串拼起来,
// 然后在拼接得到的字符串中查找目标字符串,
// 如果找到返回目标字符串开始的下标
if ((targetIndex = (lastContent + content)
.indexOf("new PushbackReader")) > 0)
{
// 将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
// 重新定义一个长度为targetIndex的char数组
if(targetIndex > 32)
{
buf = new char[targetIndex];
}
// 再次 目标字符串下标 之前的内容(也就是不包括目标字符串的内容)
pr.read(buf , 0 , targetIndex);
// 打印目标字符串前面的内容
System.out.print(new String(buf , 0 ,targetIndex));
System.exit(0);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

上面程序中将指定内容推回到推回缓冲区,于是当程序再次调用read()方法时实际上只是读取了推回缓冲区的部分内容,从而实现了只打印目标字符串前面内容的功能。
运行结果:

1
2
3
4
5
6
7
8
9
import java.io.*;

public class PushbackTest
{
public static void main(String[] args)
{
try(
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr =

PushbackReader其他方法

方法 描述
void close() Closes the stream and releases any system resources associated with it.
void mark(int readAheadLimit) Marks the present position in the stream.
boolean markSupported() Tells whether this stream supports the mark() operation, which it does not.
int read() Reads a single character.
int read(char[] cbuf, int off, int len) Reads characters into a portion of an array.
boolean ready() Tells whether this stream is ready to be read.
void reset() Resets the stream.
long skip(long n) Skips characters.

PushbackInputStream其他方法

方法 描述
int available() Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without blocking by the next invocation of a method for this input stream.
void close() Closes this input stream and releases any system resources associated with the stream.
void mark(int readlimit) Marks the current position in this input stream.
boolean markSupported() Tests if this input stream supports the mark and reset methods, which it does not.
int read() Reads the next byte of data from this input stream.
int read(byte[] b, int off, int len) Reads up to len bytes of data from this input stream into an array of bytes.
void reset() Repositions this stream to the position at the time the mark method was last called on this input stream.
long skip(long n) Skips over and discards n bytes of data from this input stream.

15.4.3 转换流

转换流 将字节流转成字符流

输入/输岀流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换成字符流,其中

  • InputStreamReader字节输入流转换成字符输入流,
  • OutputStreamWriter字节输出流转换成字符输出流

为什么没有把字符流转换成字节流的转换流

  • 字节流比字符流的使用范围更广,
  • 字符流比字节流操作方便

字节流转成字符流是为了使用更方便

如果现在有一个字节流,但可以确定这个字节流的内容都是文本容,那么把它转换成字符流来处理就会更方便一些.

没有必要将 使用方便的 字符流转成 使用更麻烦 的字节流

如果有一个流已经是字符流了,也就是说,是个用起来更方便的流,没有必要转换成使用起来不方便的字节流

所以Java只提供了将字节流转换成字符流的转换流,没有提供将字符流转换成字节流的转换流。

程序 读取键盘输入

下面以获取键盘输入为例来介绍转换流的用法。Java使用System.in代表标准输入,即键盘输入,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用InputStreamReader将其转换成字符输入流,普通的Reader读取输入内容时依然不太方便,可以将普通的Reader再次包装成BufferedReader,利用BufferedReaderheadLine方法可以一次读取一行内容。
如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.*;

public class KeyinTest
{
public static void main(String[] args)
{
try(
// 将Sytem.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader))
{
String line = null;
// 采用循环方式来一行一行的读取
while ((line = br.readLine()) != null)
{
// 如果读取的字符串为"exit",程序退出
if (line.equals("exit"))
{
System.exit(1);
}
// 打印读取的内容
System.out.println("输入内容为:" + line);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

上面程序将System.in包装成BufferedReader,BufferedReader流具有缓冲功能它可以一次读取一行文本(以换行符为标志),如果它没有读到换行符,则程序阻塞,等到读到换行符为止。
运行上面程序可以发现这个特征,在控制台执行输入时,只有按下回车键,程序才会打印出刚刚输入的内容。

通常把读取文本内容的输入流包装成BufferedReader

由于BufferedReader具有一个readLine()方法,可以非常方便地一次读入一行内容,所以经常把读取文本内容的输入流包装成BufferedReader,用来方便地读取输入流的文本内容.

15.4.2 输入 输出流体系

Java的输入/输岀流体系提供了近40个类,这些类看上去杂乱而没有规律,但如果将其按功能进行分类,则不难发现其是非常规律的。表15.1显示了Java输入输出流体系中常用的流分类

展开/折叠

这里有一张图片

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 Printstream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream

注:表15.1中的

  • 粗体字标出的类代表节点流,必须直接与指定的物理节点关联;
  • 斜体字标出的类代表抽象基类,无法直接创建实例。

从表15.1中可以看出,Java的输入/输出流体系之所以如此复杂,主要是因为Java为了实现更好的设计,它把IO流按功能分成了许多类,而每类中又分别提供了字节流和字符流(当然有些流无法提供字节流,有些流无法提供字符流),字节流和字符流里又分别提供了输入流和输岀流两大类,所以导致整个输入输出流体系格外复杂。

其他IO流

表15.1仅仅总结了输入/输出流体系中位于Java.io包下的流,还有一些诸如AudioInputStream,CipherInputStreamDeflaterInputStreamZipInputStream等具有访问音频文件加密解密压缩/解压等功能的字节流,它们具有特殊的功能,位于JDK的其他包下,本书不打算介绍这些特殊的IO流。

字节流功能比字符流强大 但复杂

通常来说,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件.但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。

文本内容使用字符流

所以通常有一个规则:如果进行输入/输出的内容是文本内容,则应该考虑使用字符流;

二进制内容使用字节流

如果进行输入/输岀的内容是二进制内容,则应该考虑使用字节流

字符集

计算机的文件常被分为文本文件二进制文件两大类。
所有能用记事本打开并看到其中字符内容的文件称为文本文件,反之则称为二进制文件。
但实质是,计算机里的所有文件都是二进制文件,文本文件只是二进制文件的一种特例,当二进制文件里的内容恰好能被正常解析成字符时,则该二进制文件就变成了文本文件。更甚至于,即使是正常的文本文件,如果打开该文件时强制使用了“错误”的字符集,例如使用EditPlus打开刚刚生成的poem.txt文件时使用的字符集不对,则将看到打开的poem.txt文件内容变成了乱码。
因此,如果希望看到正常的文本文件内容,则必须在打开文件时与保存文件时使用相同的字符集(Windows简体中文默认使用GBK字符集,而Linux下简体中文默认使用UTF8字符集)。

读写数组的节点流

表15.1中还列出了一种以数组为物理节点的节点流,

  • 字节流字节数组为节点,
  • 字符流字符数组为节点;

这种以数组为物理节点的节点流除在创建节点流对象时需要传入一个字节数组或者字符数组之外,用法上与文件节点流完全相似。

读写字符串的节点流

与此类似的是,字符流还可以使用字符串作为物理节点,用于实现从字符串读取内容,或将内容写入字符串(用StringBuffer充当字符串)的功能。

程序 读写字符串的输入输出流

下面程序示范了使用字符串作为物理节点的字符输入/输出流的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.*;

public class StringNodeTest {
public static void main(String[] args) {
String src = "从明天起,做一个幸福的人\n" + "喂马,劈柴,周游世界\n" + "从明天起,关心粮食和蔬菜\n" + "我有一所房子,面朝大海,春暖花开\n" + "从明天起,和每一个亲人通信\n"
+ "告诉他们我的幸福\n";
char[] buffer = new char[32];
int hasRead = 0;
try (StringReader sr = new StringReader(src)) {
// 采用循环读取的访问读取字符串
while ((hasRead = sr.read(buffer)) > 0) {
System.out.print(new String(buffer, 0, hasRead));
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
try (
// 创建StringWriter时,实际上以一个StringBuffer作为输出节点
// 下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter()) {
// 调用StringWriter的方法执行输出
sw.write("有一个美丽的新世界,\n");
sw.write("她在远方等我,\n");
sw.write("哪里有天真的孩子,\n");
sw.write("还有姑娘的酒窝\n");
System.out.println("----下面是sw的字符串节点里的内容----");
// 使用toString()方法返回StringWriter的字符串节点的内容
System.out.println(sw.toString());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序与前面使用FileReaderFileWriter的程序基本相似,只是在创建StringReaderStringWriter对象时传入的是字符串节点,而不是文件节点。由于String是不可变的字符串对象,所以StringWriter使用StringBuffer作为输出节点。

管道流

表15.1中列出了4个访问管道的流:PipedInputStreamPipedOutputStreamPipedReaderPipedWriter,它们都是用于实现进程之间通信功能的,分别是字节输入流、字节输出流、字符输入流和字符输出流。
本书将在第16章介绍这4个流的用法。

缓冲流

表15.1中的4个缓冲流则增加了缓冲功能,增加缓冲功能可以提高输入、输岀的效率,增加缓冲功能后需要使用flush才可以将缓冲区的内容写入实际的物理节点表

对象流

15.1中的对象流主要用于实现对象的序列化,本章的15.8节将系统介绍对象序列化

15.4 输入输出流体系

节点流存在差异 代码不统一

上一节介绍了输入输出流的4个抽象基类,并介绍了4个访问文件的节点流的用法。通过上面示例程序不难发现,4个基类使用起来有些烦琐。

处理流屏蔽底层节点流的区别 代码统一

如果希望简化编程,这就需要借助于处理流了。
这里有一张图片
图15.7显示了处理流的功能,处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法,让程序员只需关心高级流的操作

使用处理流的典型思路

使用处理流时的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入/输岀功能让节点流与底层的I/O设备、文件交互。

如何区分处理流和节点流

处理流的构造器参数是另一个流

实际识别处理流非常简单,只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流;

节点流构造器参数是物理节点

而所有节点流都是直接以物理IO节点作为构造器参数的.

使用处理流的优势

关于使用处理流的优势,归纳起来就是两点:

  • 对开发人员来说,使用处理流进行输入/输出操作更简单;
  • 使用处理流的执行效率更高

程序 使用处理流PrintStream

下面程序使用PrintStream处理流来包装OutputStream,使用处理流后的输出流在输出时将更加方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;

public class PrintStreamTest {
public static void main(String[] args) {
try (
// 物理流的构造器参数是物理节点(文件路径)
FileOutputStream fos = new FileOutputStream("test.txt");
// 处理流的构造器参数是另一个流对象
PrintStream ps = new PrintStream(fos)) {
// 使用PrintStream执行输出
ps.println("普通字符串");
// 直接使用PrintStream输出对象
ps.println(new PrintStreamTest());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

上面程序中先定义了一个节点输出流FileOutputStream,然后程序使用PrintStream包装了该节点输出流,最后使用PrintStream来输出字符串、输出对象…

Printstream的输出功能非常强大,前面程序中一直使用的标准输出System.out的类型就是PrintStream

输出文本内容建议使用PrintStream

由于PrintStream类的输出功能非常强大,通常如果需要输岀文本内容,都应该将输出流包装成PrintStream后进行输出。

从前面的代码可以看出,程序使用处理流非常简单,通常只需要在创建处理流时传入一个节点流作为构造器参数即可,这样创建的处理流就是包装了该节点流的处理流。

只需要关闭最上层的处理流

在使用处理流包装了底层节点流之后,关闭输入输出流资源时,只要关闭最上层的处理流即可关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流

15.3.2 OutputStream和Writer

OutputStreamWriter也非常相似,它们采用如图15.6所示的模型来执行输出,两个流都提供了如下三个方法

OutputStream的write方法

方法 描述
abstract void write(int b) 将指定的字节输出到输出流中,其中c既可以代表字节
void write(byte[] b) 将字节数组中的数据输出到指定输出流中。
void write(byte[] b, int off, int len) 将字节数组中从off位置开始,长度为len的字节输出到输出流中

Writer的write方法

方法 描述
void write(int c) 将指定的字符输出到输出流中,其中c代表字符
void write(char[] cbuf) 将字节字符数组中的数据输出到指定输出流中。
abstract void write(char[] cbuf, int off, int len) 将字符数组中从off位置开始,长度为len的字符输出到输出流中

Writer可以直接写字符串

因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里还包含如下两个方法

方法 描述
void write(String str) Writes a string.
void write(String str, int off, int len) Writes a portion of a string.

OutputStream其他方法

方法 描述
void flush() Flushes this output stream and forces any buffered output bytes to be written out.
void close() Closes this output stream and releases any system resources associated with this stream.

Writer其他方法

方法 描述
abstract void flush() Flushes the stream.
abstract void close() Closes the stream, flushing it first.
Writer append(char c) Appends the specified character to this writer.
Writer append(CharSequence csq) Appends the specified character sequence to this writer.
Writer append(CharSequence csq, int start, int end) Appends a subsequence of the specified character sequence to this writer.

程序 通过字节流复制文件

下面程序使用FileInputStream来执行输入,并使用FileOutputStrean来执行输出,用以实现复制FileOutputStreamTest.java文件的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;

public class FileOutputStreamTest {
public static void main(String[] args) {
try (
// 创建字节 输入流
FileInputStream fis = new FileInputStream("FileOutputStreamTest.java");
// 创建字节 输出流
FileOutputStream fos = new FileOutputStream("newFile.txt")) {
byte[] bbuf = new byte[32];
int hasRead = 0;
// 循环从输入流中取出数据
while ((hasRead = fis.read(bbuf)) > 0) {
// 赌一次写一次,读了多少,就写多少。
fos.write(bbuf, 0, hasRead);
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

运行上面程序,将看到系统当前路径下多了一个文件:newFile.txt,该文件的内容和FileOutputStreamTest.java文件的内容完全相同。

一定要关闭输出流

使用JavaIO流执行输出时,不要忘记关闭输出流,关闭输岀流除可以保证流的物理资源被回收之外,可能还可以将输岀流缓冲区中的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush()方法)。
Java的很多输出流默认都提供了缓冲功能,其实没有必要刻意去记忆哪些流有缓冲功能、哪些流没有,只要正常关闭所有的输出流即可保证程序正常。

程序 使用Writer输出字符串

如果希望直接输出字符串内容,则使用Writer会有更好的效果,如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.*;

public class FileWriterTest {
public static void main(String[] args) {
try (FileWriter fw = new FileWriter("poem.txt")) {
fw.write("锦瑟 - 李商隐\r\n");
fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");
fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");
fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");
fw.write("此情可待成追忆,只是当时已惘然。\r\n");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

运行上面程序,将会在当前目录下输出一个poem.txt文件,文件内容就是程序中输出的内容。

不同平台的换行符

上面程序在输出字符串内容时,字符串内容的最后是\r\n,这是Windows平台的换行符,通讨这种方式就可以让输出内容换行;
如果是Unix/Linux/BSD等平台,则使用\n就作为换行符。

15.3 字节流和字符流

字节流和字符流的操作方式几乎完全一样,区别只是操作的数据单元不同而已:

  • 字节流操作的数据单元是字节,
  • 字符流操作的数据单元是字符。

15.3.1 InputStream和Reader

InputStreamReader是所有输入流的**抽象基类**,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法。

InputStream常用方法

InputStream里包含如下三个方法。

方法 描述
abstract int read() 从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型)。
int read(byte[] b) 从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
int read(byte[] b, int off, int len) 从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数。

Reader常用方法

Reader里包含如下三个常用方法。

方法 描述
int read() 从输入流中读取单个字符,返回所读取的字符数据(字符数据可直接转换为int类型)
int read(char[] cbuf) 从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数。
abstract int read(char[] cbuf, int off, int len) 从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf中时,并不是从数组起点开始,而是从of‘位置开始,返回实际读取的字符数。

对比InputStreamReader所提供的方法,就不难发现这两个基类的功能基本是一样的InputStreamReader都是将输入数据抽象成如图15.5所示的水管,所以程序既可以通过read()方法每次读取一个“水滴”,也可以通过read(char[] cbuf)read(byte[] b)方法来读取多个“水滴”。
当使用数组作为read()方法的参数时,可以理解为使用一个“竹筒”到如图15.5所示的水管中取水,如图15.8所示。
这里有一张图片
read(char[] cbuf)方法中的数组可理解成一个“竹筒”,程序每次调用输入流的read(cha[] cbuf)read(byte[] b)方法,就相当于用“竹筒”从输入流中取岀一筒“水滴”,程序得到“竹筒”里的“水滴”后,转换成相应的数据即可;
程序多次重复这个“取水”过程,直到最后。

如何判断已经读取完毕

程序如何判断取水取到了最后呢?直到read(char[] cbuf)read(byte[] b)方法返回-1,即表明到了输入流的结束点。

FileInputStreamFileReader

正如前面提到的,InputStreamReader都是抽象类,本身不能创建实例,但它们分别有一个用于读取文件的输入流:FileInputStreamFileReader,它们都是节点流——会直接和指定文件关联。

程序示例 读取java文件自身

下面程序示范了使用FileInputStream来读取自身的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class FileInputStreamTest {
public static void main(String[] args) throws IOException {
// 创建字节输入流
FileInputStream fis = new FileInputStream("FileInputStreamTest.java");
// 创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fis.read(bbuf)) > 0) {
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf, 0, hasRead));
}
// 关闭文件输入流,放在finally块里更安全
fis.close();
}
}

上面程序中的while循环就是使用FileInputStream循环“取水”的过程,运行上面程序,将会输出上面程序的源代码。
上面程序创建了一个长度为1024的字节数组来读取该文件,实际上该Java源文件的长度还不到1024字节,也就是说,程序只需要执行一次read()方法即可读取全部内容。
但如果创建较小长度的字节数组,程序运行时在输岀中文注释时就可能出现乱码,这是因为本文件保存时采用的是GBK编码方式,在这种方式下,每个中文字符占2字节,如果read()方法读取时只读到了半个中文字符,这将导致乱码

文件IO资源要显示关闭

上面程序最后使用了fis.close();来关闭该文件输入流,与JDBC编程一样,程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。

Java7后可使用自动关闭资源的try来关闭IO流

Java7改写了所有的IO资源类,它们都实现了AutoCloseable接口,因此都可通过自动关闭资源的try语句来关闭这些IO流。

程序示例:FileReader读取java文件本身

下面程序使用FileReader来读取文件本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;

public class FileReaderTest {
public static void main(String[] args) {
try (
// 创建字符输入流
FileReader fr = new FileReader("FileReaderTest.java")) {
// 创建一个长度为32的“竹筒”
char[] cbuf = new char[32];
// 用于保存实际读取的字符数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fr.read(cbuf)) > 0) {
// 取出“竹筒”中水滴(字符),将字符数组转换成字符串输入!
System.out.print(new String(cbuf, 0, hasRead));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面的FileReaderTest.java程序与前面的FileInputStreamTest.java并没有太大的不同,程序只是将字符数组的长度改为32,这意味着程序需要多次调用read()方法才可以完全读取输入流的全部数据。
程序最后使用了自动关闭资源的try语句来关闭文件输入流,这样可以保证输入流一定会被关闭。

移动记录指针的方法

除此之外,InputStreamReader还支持如下几个方法来移动记录指针。

方法 描述
void mark(int readAheadLimit) 在记录指针当前位置记录一个标记(mark)。
boolean markSupported() 判断此输入流是否支持mark()操作,即是否支持记录标记。
void reset() 将此流的记录指针重新定位到上一次记录标记(mark)的位置。
long skip(long n) 记录指针向前移动n个字节/字符。

InputStream其他方法

方法 描述
int available() Returns an estimate of the number of bytes that can be read(or skipped over) from this input stream without blocking by the next invocation of a method for this input stream.
void close() Closes this input stream and releases any system resources associated with the stream.
byte[] readAllBytes() Reads all remaining bytes from the input stream.
int readNBytes(byte[] b, int off, int len) Reads the requested number of bytes from the input stream into the given byte array.
long transferTo(OutputStream out) Reads all bytes from this input stream and writes the bytes to the given output stream in the order that they are read.

Reader其他方法

方法 描述
abstract void close() Closes the stream and releases any system resources associated with it.
int read(CharBuffer target) Attempts to read characters into the specified character buffer.
boolean ready() Tells whether this stream is ready to be read.

15.2.2 流的概念模型

Java把所有设备里的有序数据抽象成模型,简化了输入/输岀处理,理解了流的概念模型也就了解了Java IO
JavaIO流共涉及40多个类,这40多个类都是从如下4个抽象基类派生的。
-InputStream,Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
-OutputStream,Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

输入流模型

对于InputStreamReader而言,它们把输入设备抽象成一个“水管”,这个水管里的每个“水滴依次排列,如图15.5所示。
这里有一张图片
从图15.5中可以看出,字节流和字符流的处理方式其实非常相似,只是它们处理的输入输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从InputStreamReader里取出一个或多个“水滴”后,记录指针自动向后移动;除此之外,InputStreamReader里都提供一些方法来控制记录指针的移动。

输出流模型

对于OutputStreamWriter而言,它们同样把输出设备抽象成一个“水管”,只是这个水管里没有任何水滴,如图15.6所示。
这里有一张图片
正如图15.6所示,当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置,每当程序向OutputStreamWriter里输出或多个水滴后,记录指针自动向后移动。

处理流的优点

除此之外,Java的处理流模型则体现了Java输入输出流设计的灵活性。处理流的功能主要体现在以下两个方面。

  • 性能的提高:主要以增加缓冲的方式来提高输入输出的效率。
  • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入/输岀大批量的内容,而不是输入输出一个或多个“水滴”。

使用处理流可以统一读写代码

处理流可以“嫁接”在任何已存在的流的基础之上,这就允许Java应用程序采用相同的代码、透明的方式来访问不同的输入输出设备的数据流。图15.7显示了处理流的模型
这里有一张图片
通过使用处理流,Java程序无须理会输入输岀节点是磁盘、网络还是其他的输入/输出设备,程序只要将这些节点流包装成处理流,就可以使用相同的输入输出代码来读写不同的输入输出设备的数据

15.2 理解Java的IO流

JavaIO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为“流”(stream),通过流的方式允许Java程序使用相同的方式来访问不同的输入/输出源
stream是从起源(source)到接收(sink)的有序数据。
Java把所有传统的流类型(类或抽象类)都放在java.io包中,用以实现输入输出功能。
因为Java提供了这种IO流的抽象,所以开发者可以使用一致的IO代码去读写不同的IO流节点。

15.2.1 流的分类

按照不同的分类方式,可以将流分为不同的类型,下面从不同的角度来对流进行分类,它们在概念上可能存在重叠的地方。

1. 输入流和输出流

按照流的流向来分,可以分为输入流和输出流。

  • 输入流:只能从中读取数据,而不能向其写入数据
  • 输出流:只能向其写入数据,而不能从中读取数据

进入内存就是输入

此处的输入、输出涉及一个方向问题,对于如图15.1所示的数据流向,数据从内存到硬盘,通常称为输出流——也就是说,这里的输入、输出都是从程序运行所在内存的角度来划分的。
这里有一张图片
对于如图15.2所示的数据流向,数据从服务器通过网络流向客户端,在这种情况下,Server端的内存负责将数据输出到网络里,因此Server端的程序使用输出流;Client端的内存负责从网络里读取数据,因此Client端的程序应该使用输入流。
这里有一张图片
Java输入流主要由InputStreamReader作为基类,而输出流则主要由OutputStreamWriter作为基类。它们都是一些抽象基类,无法直接创建实例

2. 字节流和字符流

字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同

  • 字节流操作的数据单元是8位的字节,
  • 字符流操作的数据单元是16位的字符

3. 节点流和处理流

按照流的角色来分,可以分为节点流和处理流。

节点流

可以从向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被称为低级流( Low Level stream)。图15.3显示了节点流示意图。
这里有一张图片
从图15.3中可以看出,当使用节点流进行输入输出时,程序直接连接到实际的数据源,和实际的输入/输出节点连接.

处理流

处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流。图15.4显示了处理流示意图。
这里有一张图片

处理流用到装饰器设计模式

实际上,Java使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。因此处理流也被称为包装流