IO

File

类图:

File类图

其实观察File里面多数方法的实现,其实是通过委托给属性fs完成的。

FileSystem

在 File中的初始化代码:

1private static final FileSystem fs = DefaultFileSystem.getFileSystem();

在Windows下的类图:

FileSystem类图

那DefaultFileSystem又是个什么东西呢?这东西是从JDK8加入的 ,全部的源码如下:

1class DefaultFileSystem {
2    public static FileSystem getFileSystem() {
3        return new WinNTFileSystem();
4    }
5}

可以想象,在Linux上一定是不同的实现。

文件创建

我们来看看创建文件时到底发生了什么,以代码:

1File file = new File("test");
2System.out.println(file.getAbsolutePath());

File构造器:

1public File(String pathname) {
2    this.path = fs.normalize(pathname);
3    this.prefixLength = fs.prefixLength(this.path);
4}

normalize

这一步是将文件分隔符、路径分隔符转换成系统特定的,比如对于windows平台,就将/转为\。转换的过程不再赘述,来看一下系统特定的分隔符是从哪来的,WinNTFileSystem构造器:

1public WinNTFileSystem() {
2    slash = AccessController.doPrivileged(new GetPropertyAction("file.separator")).charAt(0);
3    semicolon = AccessController.doPrivileged(new GetPropertyAction("path.separator")).charAt(0);
4    altSlash = (this.slash == '\\') ? '/' : '\\';
5}

其实就是System.getProperty。

绝对路径获取

File.getAbsolutePath:

1public String getAbsolutePath() {
2    return fs.resolve(this);
3}

resolve遵从下面的逻辑:

  • 如果给定的路径已经是绝对路径,那么直接返回。
  • 获取系统变量user.dir,将文件指向此目录,而user.dir默认便是java程序执行的路径。

到这里并未涉及到任何系统层面的API操作。

文件/目录判断

其实是一样的套路:

1public boolean isDirectory() {
2    return ((fs.getBooleanAttributes(this) & FileSystem.BA_DIRECTORY != 0);
3}
4            
5public boolean isFile() {
6    return ((fs.getBooleanAttributes(this) & FileSystem.BA_REGULAR) != 0);
7}

getBooleanAttributes为native实现。位于WinNTFileSystem_md.c,核心源码:

 1JNIEXPORT jint JNICALL
 2Java_java_io_WinNTFileSystem_getBooleanAttributes(JNIEnv *env, jobject this, jobject file) {
 3    jint rv = 0;
 4    jint pathlen;
 5    WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
 6    DWORD a = getFinalAttributes(pathbuf);
 7    rv = (java_io_FileSystem_BA_EXISTS
 8        | ((a & FILE_ATTRIBUTE_DIRECTORY)
 9        ? java_io_FileSystem_BA_DIRECTORY : java_io_FileSystem_BA_REGULAR)
10        | ((a & FILE_ATTRIBUTE_HIDDEN) ? java_io_FileSystem_BA_HIDDEN : 0));
11    return rv;
12}

getFinalAttributes即Windows API,其实此函数可以返回更多的属性信息,而源码中的一系列与或操作将很多属性屏蔽了,只保留以下:

  • 是否存在
  • 文件还是目录
  • 是否是隐藏文件/目录

FileSystem.BA_REGULAR等属性定义在抽象类FileSystem中:

1@Native public static final int ACCESS_READ    = 0x04;
2@Native public static final int ACCESS_WRITE   = 0x02;
3@Native public static final int ACCESS_EXECUTE = 0x01;

即:每一个属性用int中的一位来存储。

可读/可写/可执行

File.canRead:

1public boolean canRead() {
2    return fs.checkAccess(this, FileSystem.ACCESS_READ);
3}

写也是一样的。checkAccess为native实现:

 1JNIEXPORT jboolean
 2JNICALL Java_java_io_WinNTFileSystem_checkAccess(JNIEnv *env, jobject this, jobject file, jint access) {
 3    DWORD attr;
 4    WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
 5    // Windows函数
 6    attr = GetFileAttributesW(pathbuf);
 7    attr = getFinalAttributesIfReparsePoint(pathbuf, attr);
 8    switch (access) {
 9    case java_io_FileSystem_ACCESS_READ:
10    case java_io_FileSystem_ACCESS_EXECUTE:
11        return JNI_TRUE;
12    case java_io_FileSystem_ACCESS_WRITE:
13        /* Read-only attribute ignored on directories */
14        if ((attr & FILE_ATTRIBUTE_DIRECTORY) ||
15            (attr & FILE_ATTRIBUTE_READONLY) == 0)
16            return JNI_TRUE;
17        else
18            return JNI_FALSE;
19    default:
20        assert(0);
21        return JNI_FALSE;
22    }
23}

从这里可以看出,对于读和执行权限,只要路径存在且合法,直接返回true,而可写的条件是: 路径是一个目录或非只读文件。

list

此方法用以列出一个目录下的所有子文件(夹)。使用WinNTFileSystem的同名native方法实现,源码较长,在此不再贴出,实现的原理便是利用Windows的FindFirstFileW和FindNextFileW,两个函数的W结尾表示Unicode编码,参考:

FindNextFile function

使用FindFirstFile,FindNextFile遍历一个文件夹

mkdir(s)

mkdir实现:

1public boolean mkdir() {
2    return fs.createDirectory(this);
3}

而mkdirs使用mkdir实现:

1public boolean mkdirs() {
2    File parent = getCanonicalFile().getParentFile();
3    return (parent != null && (parent.mkdirs() || parent.exists()) &&
4            canonFile.mkdir());
5}

createDirectory为native实现,对应Windows CreateDirectoryW函数。

文件删除

native由removeFileOrDirectory函数完成,源码:

 1static int removeFileOrDirectory(const jchar *path) {
 2    /* Returns 0 on success */
 3    DWORD a;
 4    SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL);
 5    a = GetFileAttributesW(path);
 6    if (a == INVALID_FILE_ATTRIBUTES) {
 7        return 1;
 8    } else if (a & FILE_ATTRIBUTE_DIRECTORY) {
 9        //删除目录
10        return !RemoveDirectoryW(path);
11    } else {
12        return !DeleteFileW(path);
13    }
14}

移动/重命名

由Windows函数_wrename实现。

length

用以获取文件的大小,Linux实现由stat函数完成。

FileOutputStream

类图:

FileOutputStream类图

构造器

以File参数构造器为例:

1public FileOutputStream(File file, boolean append) {
2    String name = (file != null ? file.getPath() : null);
3    this.fd = new FileDescriptor();
4    fd.attach(this);
5    this.append = append;
6    this.path = name;
7    open(name, append);
8}

第二个参数为是否追加写,默认false。

open调用了native实现的open0:

1JNIEXPORT void JNICALL Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this, jstring path, jboolean append) {
2    fileOpen(env, this, path, fos_fd,
3        O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
4}

fileOpen最终调用Windows API CreateFileW函数,我们在这里只关注一下参数:

1O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC)

O_WRONLY表示写文件,O_CREAT为创建文件,O_TRUNC 表示若文件存在,则长度被截为0,属性不变,参考:

open()参数宏的意义 O_TRUNC

写文件

实际上,所有的写操作都是通过此方法实现的:

1private native void writeBytes(byte b[], int off, int len, boolean append);

最终的写入由Windows函数WriteFile完成,io_util_md.c,writeInternal函数部分源码:

1static jint writeInternal(FD fd, const void *buf, jint len, jboolean append) {
2    result = WriteFile(h,                /* File handle to write */
3                       buf,              /* pointers to the buffers */
4                       len,              /* number of bytes to write */
5                       &written,         /* receives number of bytes written */
6                       lpOv);            /* overlapped struct */
7    return (jint)written;
8}

通道获取

隔壁 FileChannel。

关闭

FileOutputStream.close:

 1public void close() throws IOException {
 2  	//设置状态
 3	synchronized (closeLock) {
 4		if (closed) {
 5			return;
 6		}
 7		closed = true;
 8	}
 9  	//关闭通道
10	if (channel != null) {
11		channel.close();
12	}
13	fd.closeAll(new Closeable() {
14		public void close() throws IOException {
15		   close0();
16	   }
17	});
18}

close0为native实现,关闭其对应的文件句柄。

RandomAccessFile

老规矩,类图:

RandomAccessFile类图

可见,这货与File, 输入输出流半毛钱的关系都没有。

模式

支持的读写模式整理如下表:

Mode 意义
r 只读
rw 读写
rws 任何对文件内容或元信息的写会被立即同步刷新至磁盘
rwd 对文件内容的写会被立即同步刷新至磁盘

可以看出,rws和rwd两个模式的作用和FileChannel的force方法是一样的。

构造器

简略版源码:

1public RandomAccessFile(File file, String mode) {
2    String name = (file != null ? file.getPath() : null);
3    int imode = -1;
4    fd = new FileDescriptor();
5    fd.attach(this);
6    path = name;
7    open(name, imode);
8}

open方法调用了native方法open0,此方法的实现位于src\share\native\java\io\RandomAccessFile.c:

 1JNIEXPORT void JNICALL
 2Java_java_io_RandomAccessFile_open0(JNIEnv *env,
 3                                    jobject this, jstring path, jint mode) {
 4    int flags = 0;
 5    if (mode & java_io_RandomAccessFile_O_RDONLY)
 6        flags = O_RDONLY;
 7    else if (mode & java_io_RandomAccessFile_O_RDWR) {
 8        flags = O_RDWR | O_CREAT;
 9        if (mode & java_io_RandomAccessFile_O_SYNC)
10            flags |= O_SYNC;
11        else if (mode & java_io_RandomAccessFile_O_DSYNC)
12            flags |= O_DSYNC;
13    }
14    fileOpen(env, this, path, raf_fd, flags);
15}

结合FileOutputStream的open方法便可以发现,两者的open操作其实使用了相同的系统级实现,只不过RandomAccessFile支持更多的参数

seek

seek允许我们自己设定当前文件的指针(偏移)。注意,系统允许设置的偏移大于文件的真实长度,但这并不改变文件的大小,只有当在新的偏移写入数据之后才会改变。

由native方法seek0调用Windows函数SetFilePointerEx实现,与之比对,FileChannel的position方法采用的是SetFilePointer函数,两者的区别是SetFilePointer将新的文件指针存放在两个long中,而SetFilePointerEX只需要一个long,至于两者为什么要采用不同的实现,不得而知。

skipBytes

用seek方法实现。

length

获取文件的大小,不同于File的length方法,此处使用seek来实现,RandomAccessFile.c相关源码(简略):

1JNIEXPORT jlong JNICALL
2Java_java_io_RandomAccessFile_length(JNIEnv *env, jobject this) {
3    FD fd;
4    jlong cur = jlong_zero;
5    jlong end = jlong_zero;
6    fd = GET_FD(this, raf_fd);
7    end = IO_Lseek(fd, 0L, SEEK_END);
8    return end;
9}

IO_Lseek对应linux上的lseek,windows上的SetFilePointer,关于为什么可以利用seek取得文件大小,参考:

Linux 通过lseek()来实现文件大小的设置

setLength

此方法可以实现文件裁剪的效果,和FileChannel的truncate方法效果相同。native实现:

 1JNIEXPORT void JNICALL
 2Java_java_io_RandomAccessFile_setLength(JNIEnv *env, jobject this, jlong newLength) {
 3    FD fd;
 4    jlong cur;
 5    fd = GET_FD(this, raf_fd);
 6    if ((cur = IO_Lseek(fd, 0L, SEEK_CUR)) == -1) goto fail;
 7    //here
 8    if (IO_SetLength(fd, newLength) == -1) goto fail;
 9    //重设指针
10    if (cur > newLength) {
11        if (IO_Lseek(fd, 0L, SEEK_END) == -1) goto fail;
12    } else {
13        if (IO_Lseek(fd, cur, SEEK_SET) == -1) goto fail;
14    }
15    return;
16}

IO_SetLength在Windows上的真正函数是SetFilePointer,这和FileChannel的truncate是一样的,在Linux上是ftruncate函数。

读方法

虽然RandomAccessFile和InputStream在继承上没有关系,但很多API是一样的。

读取一个字节

方法声明:

1private native int read0();

注意返回值是int,即范围为0-255,。如果我们将byte值-1写入到文件,再读出来就成了255,如要转为-1强转为byte就行了。

底层对应Windows的ReadFile,Linux的read函数。

readFully

1public final void readFully(byte b[]) throws IOException {
2    readFully(b, 0, b.length);
3}

顾名思义,此方法会在所有要求的字节读完之前阻塞,其实就是替我们做了循环判断的过程:

1 public final void readFully(byte b[], int off, int len) {
2    int n = 0;
3    do {
4        int count = this.read(b, off + n, len - n);
5        if (count < 0)
6            throw new EOFException();
7        n += count;
8    } while (n < len);
9}

"语法糖"读

指各种readInt,readChar等方法,其实就是读指定的字节然后给你拼起来。

浮点数读取/写入

我们以float为例:

1public final void writeFloat(float v) {
2    writeInt(Float.floatToIntBits(v));
3}
4public final float readFloat() throws IOException {
5    return Float.intBitsToFloat(readInt());
6}

从根本上来说,写入/读取到的只不过是一组二进制数字,关键在于我们如何解读它,所以写入/读取浮点数的关键在于如何进行浮点数和int值得转换(两者的二进制形式是一样的),上面的两个native Float方法巧妙地利用了C语言的共用体实现这一转换:

1JNIEXPORT jint JNICALL
2Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v) {
3    union {
4        int i;
5        float f;
6    } u;
7    u.f = (float)v;
8    return (jint)u.i;
9}

readLine

源码:

 1public final String readLine() throws IOException {
 2    StringBuffer input = new StringBuffer();
 3    int c = -1;
 4    boolean eol = false;
 5    while (!eol) {
 6        switch (c = read()) {
 7        case -1:
 8        case '\n':
 9            eol = true;
10            break;
11        case '\r':
12            eol = true;
13            long cur = getFilePointer();
14            if ((read()) != '\n') {
15                seek(cur);
16            }
17            break;
18        default:
19            input.append((char)c);
20            break;
21        }
22    }
23    if ((c == -1) && (input.length() == 0)) {
24        return null;
25    }
26    return input.toString();
27}

可以看出,方法将byte转为了char,但是这样有一个问题,char是由byte转换而来,所以这里只支持ASCII字符,如果真的想要读一行不应使用此方法,而应该使用字符流。

readUnsignedByte

不是很理解这个方法的用意,源码:

1public final int readUnsignedByte() throws IOException {
2    int ch = this.read();
3    if (ch < 0)
4        throw new EOFException();
5    return ch;
6}

奇怪的地方在于这里还是只读了一个字节,之后转为int,ch怎么可能是负值?这样的话和read方法又有什么区别?

readUTF

应和writeUTF配合食用。writeUTF用于写入一个UTF-8编码的字符串,注意前两个字节代表后面有多少个字节(字符串)。所以一个10字节的字符串需要12字节存储。

感觉此方法应该是用在字符串、int等多种类型混合存储的场景下。

过滤流

或者说是包装流,类图(只列出常用的):

FilterInputStream类图

DataInputStream与RandomAccessFile的API基本一致(实现了同一个接口),InflaterInputStream用于zip压缩包的读取。

字符流

"字符"是一个文化上的概念,字符流基于字节流,只不过是按照特定的编码规则进行解码罢了。我们以Reader为例,看一下其类图即可:

Reader类图