IO流(1)--文件流及其原理

文件流的基本类有四种:

  • FileInputStream/FileOutputStream
  • FileReader/FileWriter

一、File对象

文件流是一种节点流,它沟通程序与文件之间的数据传输。在Java中,文件被抽象为File。

我们通过File的构造器创建File对象,最常用的是通过文件路径字符串进行创建。

public class Main{
    public static void main(String[] args){
        // 将一个已经存在的,或者不存在的文件或者目录封装成file对象
        File f = new File("/home/ubuntu/test/a.txt");
        File dir = new File("/home/ubuntu/test");
    }
}        

File类提供了很多对于文件或目录的操作。

  1. 获取文件的信息。文件名称,路径,文件大小,修改时间等等。
  2. 文件的创建和删除,目录的创建
  3. 文件设置权限(读,写,执行)
  4. ...

二、FileInputStream/FileOutputStream

FileInputStream和FileOutputStream是作用于文件的字节流。其实例连接了程序内存与文件对象,在构造流对象的时候需要指定文件对象。

// FileInputStream.java
public class FileInputStream extends InputStream{
    // 传入文件名作为参数
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

    // 传入文件作为参数
    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.attach(this);
        // 设置path
        path = name;
        // 打开文件
        open(name);
    }
    // 传入文件描述符
    public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;

        /*
         * FileDescriptor is being shared by streams.
         * Register this stream with FileDescriptor tracker.
         */
        fd.attach(this);
    }

}

从源码中我们可以看到,FileInputStream在构造的时候需要传入一个文件对象,同时你可能还注意到,在构造器中我们还实例化了一个FileDescriptor,甚至在重载的构造器里有直接传入一个FileDescriptor对象,这个对象有什么作用暂且不说,我们接着看一下FileInputStream的读数据操作。

//FileInputStream.java
// 打开文件
private void open(String name) throws FileNotFoundException {
    open0(name);
}
private native void open0(String name) throws FileNotFoundException;

// 读字节
public int read() throws IOException {
    return read0();
}
private native int read0() throws IOException;

// 读字节数组
public int read(byte b[]) throws IOException {
    return readBytes(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
    return readBytes(b, off, len);
}
private native int readBytes(byte b[], int off, int len) throws IOException;

// 忽略部分字节
public long skip(long n) throws IOException {
    return skip0(n);
}
private native long skip0(long n) throws IOException;

可以看到,读数据等操作都由本地方法实现。我们可以分析一下,FileInputStream在文件与程序之间建立了连接,实现了文件的读字节操作。在构造FileInputStream的实例对象时候,我们传入了文件,那么具体的操作怎么去执行呢,FileDescriptor就派上了用场。

根据定义,FileDescriptor也称作文件描述符,内核通过文件描述符来访问文件,文件描述符通常为非负整数的索引,它指向内核为每个进程所维护的该进程打开文件的记录表。通俗来说,文件描述符就是文件的索引,有了这个索引,内核才能找到文件,也就才能把“流”连接起来,对于文件的操作也是基于这个索引展开的。

还有一点比较有意思,POSIX定义了3个符号常量:

  • 标准输入的文件描述符 0
  • 标准输出的文件描述符 1
  • 标准错误的文件描述符 2

而在FileDescriptor类中,也定义了三个常量in、out、err。根据注释,这三个变量就是System.out、System.in、System.err所对应的三个文件描述符。

public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);

总的来说,为了构建基本流,我们需要:

  • 程序内存端。
  • 节点端,如文件对象。
  • 文件描述符对象,用于开放节点(如开放文件、开放套接字、或者某个字节的源/目的地)
  • 节点流对象,用于连接程序内存与文件对象,连接内存自不用说,连接文件对象则是通过文件描述符来完成。

FIleOutputStream与FileInputStream也是类似的,只是将读操作变为写操作。

三、FileReader/FileWriter

FileReader/FileWriter是作用于文件的字符流。它们分别继承自转换流InputStreamReader/OutputStreamWriter。在构造流对象时同样需要传入文件对象。此处就以FileWriter为例。

FileWriter继承自OutputStreamWriter,只是定义了几个构造器。在构造器中,FileWriter调用了父类构造器,并传入FileOutputStream对象作为参数。由此可见FileWriter的写文件操作底层还是通过FileOutputStream完成。

public class FileWriter extends OutputStreamWriter {

    // 传入文件名
    public FileWriter(String fileName) throws IOException {
        super(new FileOutputStream(fileName));
    }

    // 传入文件名,并指定追加模式
    public FileWriter(String fileName, boolean append) throws IOException {
        super(new FileOutputStream(fileName, append));
    }

    // 传入文件对象
    public FileWriter(File file) throws IOException {
        super(new FileOutputStream(file));
    }

    // 传入文件对象,指定是否追加
    public FileWriter(File file, boolean append) throws IOException {
        super(new FileOutputStream(file, append));
    }

    // 传入文件描述符
    public FileWriter(FileDescriptor fd) {
        super(new FileOutputStream(fd));
    }

}

那么写操作的功能都在OutputStreamWriter中实现

// OutputStreamWriter.java
// 在构造器中会初始化一个StreamEncoder的实例对象se,对文件的操作就是通过se的方法来完成。
// 构造se对象时将FileWriter传入的FileOutputStream对象作为参数,因此我猜想写文件的操作过程在se中,首先对字符进行编码,然后调用FileOutputStream进行写入操作。 // 写字符 public void write(int c) throws IOException { se.write(c); } // 写字符数组 public void write(char cbuf[], int off, int len) throws IOException { se.write(cbuf, off, len); } // 写字符串 public void write(String str, int off, int len) throws IOException { se.write(str, off, len); } // 刷写 public void flush() throws IOException { se.flush(); } // 关闭流 public void close() throws IOException { se.close(); }

FileReader与FileWriter的原理也是类似的。这里就不一一赘述了。

四、总结

文件的操作流主要就是这四个,我们可以通过源码窥见出,FileInputStream/FileOutputStream是对文件进行字节的读写。FileReader/FileWriter是字符流,它们通过中间的编码解码器操作,将字符转换成字节或者将字节转换成字符,最终对文件的操作还是落在FileInputStream/FileOutputStream这两个字节流上。

上一篇:大二下学期学习进度(十五)


下一篇:java – 为什么FileWriter没有创建新文件? FileNotFoundException