Java基础之IO(1)-InputStream

概述

之前介绍过一篇IO总览的文章,概述性的讲解了现有的一些Java IO。从这一篇开始,详细讲解Java IO的各个模块,今天首先讲一下InputStream。

InputStream是一个字节输入流,什么是字节流呢?我们知道在java中有一个叫byte的基本类型,就是同一个。字节是一个8位表示的二进制。很多人会误认为输入流就是打开一个磁盘文件然后从里边读数据,但是输入流并不局限于文件,应该说IO并不代表和磁盘交互。所以读取磁盘文件仅仅是输入流中的一种FileInputStream,在输入流中还有很多其他的输入流。接下会介绍几种,虽然有些不常用。在介绍之前先看一下在JDK中,InputStream的继承关系:

ALt

AutoCloseable

这里的AutoCloseable是jdk1.7引入的一种语法糖,可配合jdk1.7出现的try-with-resources新语法特性一起使用,用于在使用完资源后自动关闭。Closeable是表示这是一个可关闭的数据源或目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AutoCloseableDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("/tmp/test")){
int i;
while ((i=fis.read()) != -1){
System.out.println(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

节点流

什么是节点流呢?节点流指可以从一个数据源读写数据,这个数据源可以是磁盘文件,内存或者网络。根据这个定义可以将上图中的流FileInputStream、PipedInputStream、ByteArrayInputStream划分为节点流。最熟悉的可能就是FileInputStream,因为平时读取磁盘文件应该用得最多,先介绍FileInputStream。

FileInputStream

看一下该类的构造函数

1
2
3
4
5
6
//参数:文件路径
public FileInputStream(String name) throws FileNotFoundException;
//参数:一个File对象
public FileInputStream(File file) throws FileNotFoundException;
//一个文件描述符
public FileInputStream(FileDescriptor fdObj);

通过jdk api文档可以看到FileInputStream有三个构造函数,第一个和第二个比较常用,第三个是一个已存在的文件系统的真实连接。

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
56
57
58
59
60
61
62
63
public class FileInputStreamDemo {
/**
* 文件路径参数创建
* @throws IOException
*/
public void FilePathDemo() throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream("/tmp/test");
int i;
while ((i=fis.read()) != -1){
System.out.println(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
}

/**
* 以File对象方式创建
*/
public void FileDemo() throws IOException {
File file = new File("/tmp/test");
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int i;
while ((i=fis.read()) != -1){
System.out.println(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
}

/**
* 以文件描述符参数创建
*/
public void FileDescriptorDemo() throws IOException {
FileInputStream fis = new FileInputStream(FileDescriptor.in);
System.out.println(fis.read());
fis.close();
}

public static void main(String[] args) {
FileInputStreamDemo demo = new FileInputStreamDemo();
try {
demo.FilePathDemo();
demo.FileDemo();
demo.FileDescriptorDemo();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ByteArrayInputStream

ByteArrayInputStream比较少见,ByteArrayInputStream内部维护着一个缓冲区,从该缓冲区中读取字节,内部计数器跟踪read要读取的下一个字节。看一下构造函数:

1
2
3
ByteArrayInputStream(byte[] buf);

ByteArrayInputStream(byte[] buf, int offset, int length)

可以看到ByteArrayInputStream以byte数组为参数构造ByteArrayInputStream对象。ByteArrayInputStream和FileInputStream的区别在于,FileInputStream以磁盘文件为数据源,ByteArrayInputStream以内存数据为数据源。ByteArrayInputStream的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ByteArrayInputStreamDemo {
public static void main(String[] args) {
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes, 2, 2);
int i;
while ((i = byteArrayInputStream.read()) != -1){
System.out.println(i);
}
try {
byteArrayInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

结果:

1
2
3
4
5
6
Connected to the target VM, address: '127.0.0.1:38135', transport: 'socket'
3
4
Disconnected from the target VM, address: '127.0.0.1:38135', transport: 'socket'

Process finished with exit code 0

从结果中看到虽然bytes有5个数据,但是只能读取2个数据,因为在构造时传入了offset和length表示从offset开始的length个数据。至于ByteArrayInputStream的内部实现细节,在以后的进阶文章中再详细论述。

PipedInputStream

PipedInputStream是一种很特殊的输入流,因为它单独存在没有什么意义,它需要和PipedOutputStream一起使用,数据的流向是从PipedOutputStream流向PipedInputStream的缓存区,然后PipedInputStream的read方法从PipedInputStream的缓冲区中读取数据。如下图

Alt

接下来看一下使用PipedInputStream来实现一个简单线程通信例子,当然线程通信有其他方式来实现,但是如果两个线程之间需要传递字节数组时,PipedInputStream还是不错的选择。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class PipedInputStreamDemo {
public static void main(String[] args) {
PipedInputStream pipedInputStream = new PipedInputStream();
PipedOutputStream pipedOutputStream = new PipedOutputStream();

try {
pipedOutputStream.connect(pipedInputStream);
} catch (IOException e) {
e.printStackTrace();
}

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ReadWorker(pipedInputStream));
executor.execute(new WirteWorker(pipedOutputStream));

try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}

class ReadWorker implements Runnable {
private PipedInputStream pipedInputStream;

public ReadWorker(PipedInputStream pipedInputStream){
this.pipedInputStream = pipedInputStream;
}

@Override
public void run() {
byte[] bytes = new byte[1024];
try {
while (pipedInputStream.read(bytes) != -1){
System.out.println(new String(bytes, Charset.forName("UTF-8")));
}
} catch (IOException e){

} finally {
try {
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

class WirteWorker implements Runnable {
private PipedOutputStream pipedOutputStream;

public WirteWorker(PipedOutputStream pipedOutputStream){
this.pipedOutputStream = pipedOutputStream;
}
@Override
public void run() {
String str = "hello world!";
try {
pipedOutputStream.write(str.getBytes(Charset.forName("UTF-8")));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

处理流

什么又是处理流呢?处理流是连接在已存在的流(节点流或者处理流)之上,通过对数据的处理提供更强大的读写功能。在InputStream中有如下几种处理流,BufferedInputStream、DataInputStream、ObjectInputStream、SequenceInputStream。接下来简单介绍一下几种流。

BufferedInputStream

BufferedInputStream是一个处理流,它的作用是增强InputStream,为其提供缓冲区的功能。这里可能会有疑问,BufferedInputStream实现的方法和InputStream的方法差不多,它存在的意义是什么?

这里要从Java IO读取数据的过程说起,仅仅简单的介绍一下,InputStream读取数据源的数据需要操作系统支持,也就是说Java的线程发出读取数据(read方法)的请求,操作系统接收到这个请求,然后从数据源读取一个byte(read方法,不是read(byte[])),然后将数据从内核空间复制到用户空间,java线程拿到要的数据,返回。可以看到,如果调用read()方法,那么读取10个字节,就要进行10个这样的流程,是相当低效的,当然可以用read(byte[]),但是有时需要read()的场景,所以就出现了InputStream的增强类BufferedInputStream,该BufferedInputStream提供了缓存区的功能。

举个例子,以前你要把一堆砖从一个地方搬到另一个地方整理好,你力气小一次只能搬一块,当然别人可能力气大,一次可以搬两块,这样你搬10块砖要来回跑10次,那现在为了增强你的能力,给你提供一个小车,你一次可以运50块,这个时候你只需要一次就可以运50块砖,而你要整理好这些砖,只需要从小车上一次一块拿下来放好,就像从缓冲区里读取数据一样。通过提供缓冲区来增强IO的能力,减小系统开销。

简单使用

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
public void readDemo(String path){
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(path);
//可以在构造时传入参数指定缓冲区的大小
bis = new BufferedInputStream(fis, 2048);
int data;
while ((data = bis.read()) != -1){
System.out.println(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void readBytesDemo(String path){
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(path);
bis = new BufferedInputStream(fis);
byte[] bytes = new byte[1024];
while (bis.read(bytes) != -1){
System.out.println(bytes);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
BufferedInputStreamDemo demo = new BufferedInputStreamDemo();
demo.readDemo("/tmp/test");
demo.readBytesDemo("/tmp/test");
}
DataInputStream

DataInputStream提供了从字节输入流中读取Java基本类型的能力,比如,如果要想从输入流中读取一个int类型的数据,需要读取4个字节然后自己组装成一个int,而DataInputStream通过提供一个InputStream的装饰器,增加了自动包装的功能。

简单使用

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
public class DataInputStreamDemo {
public static void main(String[] args) {
FileInputStream fis = null;
DataInputStream dis = null;
FileOutputStream fos = null;
DataOutputStream dos = null;

try {
fos = new FileOutputStream("/tmp/test1");

dos = new DataOutputStream(fos);

dos.writeChars("hello world");
dos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dos != null){
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

try {
fis = new FileInputStream("/tmp/test1");
dis = new DataInputStream(fis);
while(dis.available() > 0) {
System.out.println(dis.readChar());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dis != null){
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

打开文件test1看到的结果,如果以字节流读取出来,将两两字节组合转换为char便可以还原。DataInputStream提供的就是这种功能,此外还有readInt()、readLong()、readFloat()等,比较特殊一点的readUTF(),readUTF是转化成utf-8编码格式的,用readUTF()读,那么你写的时候也要用writeUTF()写,writeUTF在写时会在开头写两个字节表示写入的数据大小。readUTF时会首先读取这两个字节,来设置读取的字节数。

1
^@h^@e^@l^@l^@o^@ ^@w^@o^@r^@l^@d

读取结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Connected to the target VM, address: '127.0.0.1:44515', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:44515', transport: 'socket'
h
e
l
l
o

w
o
r
l
d

Process finished with exit code 0
ObjectInputStream

ObjectInputStream是java提供的反序列化的功能,ObjectOutputStream提供序列化输出的功能,能将java对象序列化之后输出到指定的输出流中。被持久化的对象需要实现Serializable、Externalizable接口。

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
56
57
58
59
60
61
62
63
64
65
66
67
public class ObjectOutputStreamDemo {
public static void main(String[] args) {
FileOutputStream fileOutputStream;
ObjectOutputStream objectOutputStream = null;
{
try {
fileOutputStream = new FileOutputStream("/tmp/person");
objectOutputStream = new ObjectOutputStream(fileOutputStream);
Person person = new Person();
person.setName("mark");
person.setAge(20);
objectOutputStream.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (objectOutputStream != null) {
objectOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

FileInputStream fileInputStream;
ObjectInputStream objectInputStream = null;
try {
fileInputStream = new FileInputStream("/tmp/person");
objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();
System.out.println("Person name:" + person.getName() + " age:" + person.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (objectInputStream != null) {
objectInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

class Person implements Serializable {
private String name;

private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
SequenceInputStream

SequenceInputStream的作用是将多个输入流合并为一个SequenceInputStream输入流,然后从第一个输入流开始读取数据,接着第二个,直到最后一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SequenceInputStreamDemo {
public static void main(String[] args) {
try {
FileInputStream is1 = new FileInputStream("/tmp/test1");
FileInputStream is2 = new FileInputStream("/tmp/test2");

SequenceInputStream sis = new SequenceInputStream(is1, is2);
int i;
while ((i = sis.read()) != -1){
System.out.println(i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}