1.概述

FileInputStream即文件输入流,FileOutputStream即文件输出流,内部采用二进制字节数组的数据传输形式,实现本地文件的读写。这俩个对象是java.io包下最基本的对象,java大量采用装饰者模式、适配器模式对这俩个类的功能进行增强和扩展,衍生出了其他众多字节流、字符流对象,因此了解这俩对象对学习IO流非常重要。

2.FileInputStream

2.1 构造器

FileInputStream对象提供了3个构造方法,String参数就不提了,还剩下File和FileDescriptor参数构造器,最终都是调用操作系统函数拿到一个文件描述符:

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
public FileInputStream(File file) throws FileNotFoundException {

// 提取文件路径
String name = (file != null ? file.getPath() : null);

// 获取jvm安全管理器,进行安全检查
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}

// 文件路径不能为空
if (name == null) {
throw new NullPointerException();
}

// 文件是否有效
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}

// 创建一个文件描述符,这时候描述符号为-1,也就是说符号是无效的
fd = new FileDescriptor();

// 将自身注册到文件描述符对象,方便后续的关闭操作
fd.attach(this);

// 记录文件的路径
path = name;

// 内部会通过name获得操作系统的文件描述符值,并赋值给内部对象FileDescriptor的fd属性
open(name);
}

public FileInputStream(FileDescriptor fdObj) {

// 获取jvm安全管理器,进行安全检查
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}

// 保存文件描述符引用
fd = fdObj;

// 已经得到文件描述符对象,路径就不需要了
path = null;

// 将自身注册到文件描述符对象,方便后续的关闭操作
fd.attach(this);
}

2.2 成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对应的文件描述符对象,在构造器中会初始化
private final FileDescriptor fd;

// 如果使用File构造器,那么还会存储文件的路径
private final String path;

// 文件描述符的通道,用于nio
private FileChannel channel = null;

// 一个流对象可能会被多个线程调用,这个创建一个对象用于synchronized关键字的锁
private final Object closeLock = new Object();

// 流关闭状态,使用volatile保证可见性
private volatile boolean closed = false;

2.3 native方法

FileInputStream类内部的native方法,底层都是对操作系统的IO函数进行调用,而这些IO函数都是围绕某个文件描述符操作的,虽然java层面没有作为参数传递,但native方法内部会使用this获取到文件描述符,然后作为参数调用系统IO函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 调用操作系统open函数,通过路径定位到具体的文件,并将文件描述符赋值给内部FileDescriptor 
private native void open0(String name) throws FileNotFoundException;

// 调用操作系统read函数,读取一个字节数据
private native int read0() throws IOException;

// 调用操作系统read函数,可以指定读取的起始位置off,读取的长度len,并将读取的结果放入字节数组b中,返回值代表读到的字节数量
private native int readBytes(byte b[], int off, int len) throws IOException;

// 调用操作系统skip函数,移动输入流的当前指针,也就是说在读取时可以忽略开头的一部分字节数据
public native long skip(long n) throws IOException;

// 调用操作系统skip函数,移动输入流的当前指针,也就是说在读取时可以忽略开头的一部分字节数据
public native int available() throws IOException;

// 调用操作系统close函数,关闭系统的文件描述符
private native void close0() throws IOException;
关于native方法的具体逻辑,可以下载openjdk源码查看。

2.4 普通方法

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

// 调用内部native open0
private void open(String name) throws FileNotFoundException {
open0(name);
}

// 调用内部native read
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}

// 调用内部native read
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}

// 关闭当前流对象
public void close() throws IOException {

// 断流是否已经被关闭,防止多个线程操作一个流对象,进行加锁后再判断
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}

// 如果流生成了通道,需要关闭
if (channel != null) {
channel.close();
}

// 利用close0(),将关闭操作系统文件描述符的逻辑封装为一个Closeable对象,交给fd去关闭
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}

// 获取流对象对应的文件描述符对象,主要用于刷盘、判断是否有效
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}

// 获取或创建文件描述符的通道,主要用于nio操作
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}

// 防止应用程序没有关闭流,在GC回收此对象时再次关闭
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
close();
}
}

2.5 简单使用

对于一个稍大的文件来说,通过单字节的形式挨个读取,应用程序对系统内核的调用、内核对设备管理器的调用都太频繁,所以都会采用缓冲区批量读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception{

// 创建流
FileInputStream fis = new FileInputStream("you file path");

// 文件内容容器
StringBuilder result = new StringBuilder();

// 缓冲区
byte[] bytes = new byte[1024];

// 批量读取
int length = 0;
while((length = fis.read(bytes)) != -1){
result.append(new String(bytes, 0, length));
}

// 打印结果
System.out.println(result.toString());
}

3.FileOutputStream

3.1 内部源码

FileOutputStream的构造器和成员变量和FileInputStream几乎一样,仅仅多了一个append概念,主要用于区分写入的形式是将原来的内容覆盖,还是在后面追加。方法也几乎一样,只不过read变成了write。

3.2 简单使用

1
2
3
4
public static void main(String[] args) throws Exception{
FileOutputStream fos = new FileOutputStream("you file path");
fos.write("hello world".getBytes());
}

评论