Java I/O 09 - FileInputStream & FileOutputStream

  关于 java.io.FileInputStream java.io.FileOutputStream 的部分笔记,这两个类完成的是对文件的输入输出的字节流操作。本文演示代码段的执行环境基于JDK版本1.7

概述

  FileInputStream和FileOutputStream用来完成对文件的字节流操作。FileInputStream从文件系统的某个文件中获取数据并读取到内存中,FileOutputStream则将内存中的数据写到某个特定的文件中去。这两个类处理的是诸如图像数据这样的原始字节流,如果需要读取写入类似于字符这样的数据流,需要用FileReader和FileWriter。

继承关系

1
2
3
4
5
6
7
8
9
// FileInputStream
--java.lang.Object
--java.io.InputStream
--java.io.FileInputStream

// FileOutputStream
--java.lang.Object
--java.io.OutputStream
--java.io.FileOutputStream

实现接口

类名 实现接口
FileInputStream Closeable, AutoCloseable
FileOutputStream Closeable, Flushable, AutoCloseable

FileInputStream

主要字段

fd

1
private final FileDescriptor fd;

  当前输入流中维护的文件描述符。用来标识进程中打开的一个文件。

channel

1
private FileChannel channel = null;

  文件管道,用来读、写、映射、操作某个文件。

path

1
private final String path;

  维护当前打开的文件路径。

构造方法

FileInputStream(String name)

1
2
3
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}

  根据一个系统相关的字符串来获取一个文件并创建一个输入流,该输入流会从入参name文件中读取数据到内存中。实际调用的是下面的构造方法public FileInputStream(File file)。

FileInputStream(File file)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
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");
}
fd = new FileDescriptor();
fd.incrementAndGetUseCount();
this.path = name;
open(name);
}

  根据一个入参file创建一个输入流,该输入流将从入参file中获取数据。第3 ~ 6行代码如果存在安全管理器,那么就需要检查是否拥有对该文件进行操作和管理的权限。第7 ~ 12行代码完成一些有效性校验。第13行维护了一个文件描述符,该描述符记录了当前打开的文件。第16行代码则通过调用底层的private native void open(String name)方法来打开特定路径下的文件。

FileInputStream(FileDescriptor fdObj)

1
2
3
4
5
6
7
8
9
10
11
12
13
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;

fd.incrementAndGetUseCount();
}

  通过一个文件描述符来初始化一个文件输入流。因为文件描述符fdObj记录了一个打开的文件,所以输入流中的字段fd与入参fdObj记录的是同样的文件。第2 ~ 8行代码如果存在安全管理器,那么就需要检查是否拥有对该文件进行操作和管理的权限。

部分方法

public int read()

1
2
3
4
5
6
7
8
9
10
public int read() throws IOException {
Object traceContext = IoTrace.fileReadBegin(path);
int b = 0;
try {
b = read0();
} finally {
IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);
}
return b;
}

  从path指定的文件中读取一个字节的内容并返回。第2行和第7行的代码用来完成I/O文件操作的一个跟踪记录,所以这两个方法是配对使用的,用来标记文件操作的开始和结束(具体使用场景和实现还有待考证)。底层调用的是private native int read0(),通过该方法完成从文件中读取数据的操作。

public int read(byte b[], int off, int len)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int read(byte b[], int off, int len) throws IOException {
Object traceContext = IoTrace.fileReadBegin(path);
int bytesRead = 0;
try {
bytesRead = readBytes(b, off, len);
} finally {
IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
}
return bytesRead;
}

public int read(byte b[]) throws IOException {
Object traceContext = IoTrace.fileReadBegin(path);
int bytesRead = 0;
try {
bytesRead = readBytes(b, 0, b.length);
} finally {
IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
}
return bytesRead;
}

  从path指定的文件中读取指定长度的内容到入参数组b中。可能因为已经读到了文件尾部、异常抛出等原因会导致实际返回的b的长度有可能小于入参len。底层调用的是private native int readBytes(byte b[], int off, int len),通过该方法完成从文件中读取数据的操作。

public void close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
fd.decrementAndGetUseCount();
channel.close();
}

int useCount = fd.decrementAndGetUseCount();

if ((useCount <= 0) || !isRunningFinalize()) {
close0();
}
}

  关闭当前输入流并释放所有相关的资源。如果在期间创建了文件管道,那么首先需要把该管道占用的资源释放,同时更新文件描述符的信息。在第15 ~ 17行代码中有两个条件来控制是否执行close方法释放输入流的资源。useCount <= 0表示当前文件描述符已经没有其他对象使用了,所以如果该条件满足的话就可以执行释放操作了。!isRunningFinalize()的值由方法finalize()负责维护。在finalize()方法中,首先向线程本地变量中传入一个true值,然后调用close()方法释放资源。此时,如果文件描述符fd还有其他流在使用,那么就不会执行close0()方法。在finalize()方法的最后将线程本地变量的值变成了false,那么当下次直接调用close方法的时候,不管fd是否还有其他流在使用,都会执行第16行代码,释放所有资源。

protected void finalize()

1
2
3
4
5
6
7
8
9
10
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
runningFinalize.set(Boolean.TRUE);
try {
close();
} finally {
runningFinalize.set(Boolean.FALSE);
}
}
}

  finalize()方法不会释放仍被其他流使用的文件描述符fd,所以第3行代码向线程本地变量中存入了一个true,这样在第5行代码调用close()方法时就不会释放当前资源。最后向线程本地变量中存入了false,所以那当下次直接调用close方法的时候,不管fd是否还有其他流在使用,都会释放所有资源。

public final FileDescriptor getFD()

1
2
3
4
public final FileDescriptor getFD() throws IOException {
if (fd != null) return fd;
throw new IOException();
}

  获取并返回一个文件描述符的引用,否则抛出一个IOException给调用方。

public FileChannel getChannel()

1
2
3
4
5
6
7
8
9
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
fd.incrementAndGetUseCount();
}
return channel;
}
}

  获取一个文件管道。该管道可以用来完成文件复制等一系列操作。每次创建文件管道时需要维护文件描述符的信息。

FileOutputStream

主要字段

fd

1
private final FileDescriptor fd;

  当前输入流中维护的文件描述符。用来标识进程中打开的一个文件。

path

1
private final String path;

  维护当前打开的文件路径。

append

1
private final boolean append;

  写文件的模式,true 为从文件尾部追加写,false为从文件开头写并覆盖当前文件已有内容。

channel

1
private FileChannel channel;

  文件管道,用来读、写、映射、操作某个文件。

构造方法

public FileOutputStream(String name)

1
2
3
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}

  根据一个系统相关的字符串来获取一个文件并创建一个输出流,该输出流会把数据写入到入参指定路径下的文件中。因为在写文件时有追加模式和覆盖模式可供选择,所以该方法默认写文件是覆盖当前已有内容从头开始写文件。实际调用的是下面的构造方法FileOutputStream(File file)。

public FileOutputStream(String name, boolean append)

1
2
3
4
5
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}

  根据一个系统相关的字符串来获取一个文件并创建一个输出流,该输出流会把数据写入到入参指定路径下的文件中。因为在写文件时有追加模式和覆盖模式可供选择,由入参append决定该方法采用哪种模式写文件。实际调用的是下面的构造方法FileOutputStream(File file, boolean append)。

public FileOutputStream(File file)

1
2
3
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}

  根据一个入参file创建一个输出流,该输出流会把数据写到入参file指定的文件中。该方法默认写文件是覆盖当前已有内容从头开始写文件。

public FileOutputStream(File file, boolean append)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
this.append = append;
this.path = name;
fd.incrementAndGetUseCount();
open(name, append);
}

  根据一个入参file创建一个输出流,该输出流会把数据写到入参file指定的文件中。第5 ~ 8行代码如果存在安全管理器,那么就需要检查是否拥有对该文件进行操作和管理的权限。第9 ~ 14行代码完成一些有效性校验。第15行维护了一个文件描述符,该描述符记录了当前打开的文件。第19行代码则通过调用底层的private native void open(String name, boolean append)方法来打开特定路径下的文件。

public FileOutputStream(FileDescriptor fdObj)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public FileOutputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkWrite(fdObj);
}
this.fd = fdObj;
this.path = null;
this.append = false;

fd.incrementAndGetUseCount();
}

  通过一个文件描述符来初始化一个文件输出流。因为文件描述符fdObj记录了一个打开的文件,所以输出流中的字段fd与入参fdObj记录的是同样的文件。第2 ~ 8行代码如果存在安全管理器,那么就需要检查是否拥有对该文件进行操作和管理的权限。

部分方法

public void write(int b)

1
2
3
4
5
6
7
8
9
10
public void write(int b) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
write(b, append);
bytesWritten = 1;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}

  向path指定的文件中以append指定的写入方式写入一个字节的内容。第2行和第8行的代码用来完成I/O文件操作的一个跟踪记录,所以这两个方法是配对使用的,用来标记文件操作的开始和结束(具体使用场景和实现还有待考证)。底层调用的是private native void write(int b, boolean append),通过该方法完成向文件中写入数据的操作。

public void write(byte b[], int off, int len)

1
2
3
4
5
6
7
8
9
10
public void write(byte b[], int off, int len) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
writeBytes(b, off, len, append);
bytesWritten = len;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}

  向path指定的文件中以append指定的写入方式写入数组b中包含的内容。第2行和第8行的代码用来完成I/O文件操作的一个跟踪记录,所以这两个方法是配对使用的,用来标记文件操作的开始和结束(具体使用场景和实现还有待考证)。底层调用的是private native void write(byte b[], int off, int len, boolean append),通过该方法完成向文件中写入数据的操作。

public void close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}

if (channel != null) {
fd.decrementAndGetUseCount();
channel.close();
}

int useCount = fd.decrementAndGetUseCount();

if ((useCount <= 0) || !isRunningFinalize()) {
close0();
}
}

  执行逻辑同FileInputStream.close(),故不予赘述。

protected void finalize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void finalize() throws IOException {
if (fd != null) {
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
runningFinalize.set(Boolean.TRUE);
try {
close();
} finally {
runningFinalize.set(Boolean.FALSE);
}
}
}
}

  执行逻辑同FileInputStream.finalize(),故不予赘述。

public final FileDescriptor getFD()

1
2
3
4
public final FileDescriptor getFD()  throws IOException {
if (fd != null) return fd;
throw new IOException();
}

  获取并返回一个文件描述符的引用,否则抛出一个IOException给调用方。

public FileChannel getChannel()

1
2
3
4
5
6
7
8
9
10
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);

fd.incrementAndGetUseCount();
}
return channel;
}
}

  获取一个文件管道。该管道可以用来完成文件复制等一系列操作。每次创建文件管道时需要维护文件描述符的信息。

涉及基础知识点

  1. FileDescriptor:
      文件描述符,简称fd。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

      在Unix或者Linux系统中,文件描述符标识了内核中一个特定进程正在访问的文件。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,将fd作为参数传送给 read 或 write。在UNIX的传统实现中,文件描述符会被加入到一个内核维护的列表中,该列表中的每个元素指向了一个文件表(file table),这个文件表中维护了当前进程中所有的被打开的文件信息,包括一个指向文件的Inode对象的指针、相关元数据,如当前文件格式、读取模式(read,write,append,read-write……)等信息。file table中的每个元素会指向一个真实文件的物理存储地址。可参考如下图1所示:

    图 - 1

    最左边为维护文件描述符的列表,其中0(标准输入),1(标准输出),2(标准异常输出)为标准的POSIX文件描述符。每个文件描述符会指向File table中的某个文件操作模式,多个文件描述符可以指向同一个文件操作模式。最右边维护了当前进程中打开的所有的真实文件信息,由file table中的某个元素指向其中的某个打开的文件来完成对应的文件操作。

      总结下来,fd只是一个int型的数值,用来标定一个打开的文件,类似于索引的作用。在需要操作文件时,通过fd来指向其标定的那个文件。

参考文献

  1. Wikipedia. 文件描述符 [E]
  2. 木杉. JDK源码阅读-FileDescriptor [E]
  3. WIKIMEDIA COMMONS. File table and inode table [E]
  4. 鸡蛋卷啊卷. FileDescriptor文件描述符与Linux文件系统 [E]
  5. 平林新袖. FileDescriptor [E]
  6. mm_hh. Linux下 文件描述符(fd)与 文件指针(FILE*) [E]
  7. Axtaxt. IO trace generation in java: experimenting with sun.misc.IoTrace [E]




------------- End of this article, thanks! -------------


  版权声明:本文由Nathan R. Lee创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处。
  本文作者为 Nathan R. Lee
  本文标题为 Java I/O 09 - FileInputStream & FileOutputStream
  本文链接为 https://marcuseddie.github.io/2018/java-FileInputStream-FileOutputStream.html