InputStream
InputStream
就是Java标准库提供的最基本的输入流。它位于java.io
这个包里。java.io
包提供了所有同步IO的功能。
要特别注意的一点是,InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
,签名如下:
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int
值(0~255)。如果已读到末尾,返回-1
表示不能继续读取了。
FileInputStream
是InputStream
的一个子类。顾名思义,FileInputStream
就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream
的所有字节:
public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}
2
3
4
5
6
7
8
9
10
11
12
在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。
InputStream
和OutputStream
都是通过close()
方法来关闭流。关闭流就会释放对应的底层资源。
我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException
异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException
。
仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了IO错误,InputStream
就没法正确地关闭,资源也就没法及时释放。
因此,我们需要用try ... finally
来保证InputStream
在无论是否发生IO错误的时候都能够正确地关闭:
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}
2
3
4
5
6
7
8
9
10
11
12
用try ... finally
来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)
的语法,只需要编写try
语句,让编译器自动为我们关闭资源。推荐的写法如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}
2
3
4
5
6
7
8
实际上,编译器并不会特别地为InputStream
加上自动关闭。
编译器只看try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally
语句并调用close()
方法。
InputStream
和OutputStream
都实现了这个接口,因此,都可以用在try(resource)
中。
# 缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。
InputStream
提供了两个重载方法来支持读取多个字节:
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]
数组作为缓冲区,read()
方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。
read()
方法的返回值不再是字节的int
值,而是返回实际读取了多少个字节。如果返回-1
,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
2
3
4
5
6
7
8
9
10
# 阻塞
在调用InputStream
的read()
方法读取数据时,我们说read()
方法是阻塞(Blocking)的。
它的意思是,对于下面的代码:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
2
3
执行到第二行代码时,必须等read()
方法返回后才能继续。
因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()
方法调用到底要花费多长时间。
# ByteArrayInputStream
用FileInputStream
可以从文件获取输入流,这是InputStream
常用的一个实现类。
此外,ByteArrayInputStream
可以在内存中模拟一个InputStream
:
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
运行结果:
H
e
l
l
o
!
2
3
4
5
6
ByteArrayInputStream
实际上是把一个byte[]
数组在内存中变成一个InputStream
,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream
。
举个例子:我们想从文件中读取所有字节,并转换成char
然后拼成一个字符串,可以这么写:
public class Main {
public static void main(String[] args) throws IOException {
String s;
try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
s = sb.toString();
}
System.out.println(s);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
要测试上面的程序,就真的需要在本地硬盘上放一个真实的文本文件。
如果我们把代码稍微改造一下,提取一个readAsString()
的方法:
public class Main {
public static void main(String[] args) throws IOException {
String s;
try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
s = readAsString(input);
}
System.out.println(s);
}
public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
return sb.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对这个String readAsString(InputStream input)
方法进行测试就相当简单,因为不一定要传入一个真的FileInputStream
:
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
String s = readAsString(input);
System.out.println(s);
}
}
public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
return sb.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行结果:
Hello!
这就是面向抽象编程原则的应用:接受InputStream
抽象类型,而不是具体的FileInputStream
类型,从而使得代码可以处理InputStream
的任意实现类。
# BufferedInputStream
BufferedInputStream也是InputStream的一个实现类,但它是一个缓冲流,就算你使用FileInputStream的效率也不如BufferedInputStream,用法也是一样的,所以我们开发中应该优先使用缓冲流。
# 小结
Java标准库的java.io.InputStream
定义了所有输入流的超类:
FileInputStream
实现了文件流输入;ByteArrayInputStream
在内存中模拟一个字节流输入。
总是使用try(resource)
来保证InputStream
正确关闭。