类图:
其实观察File里面多数方法的实现,其实是通过委托给属性fs完成的。
在 File中的初始化代码:
private static final FileSystem fs = DefaultFileSystem.getFileSystem();
在Windows下的类图:
那DefaultFileSystem又是个什么东西呢?这东西是从JDK8加入的 ,全部的源码如下:
class DefaultFileSystem {
public static FileSystem getFileSystem() {
return new WinNTFileSystem();
}
}
可以想象,在Linux上一定是不同的实现。
我们来看看创建文件时到底发生了什么,以代码:
File file = new File("test");
System.out.println(file.getAbsolutePath());
File构造器:
public File(String pathname) {
this.path = fs.normalize(pathname);
this.prefixLength = fs.prefixLength(this.path);
}
这一步是将文件分隔符、路径分隔符转换成系统特定的,比如对于windows平台,就将/转为\。转换的过程不再赘述,来看一下系统特定的分隔符是从哪来的,WinNTFileSystem构造器:
public WinNTFileSystem() {
slash = AccessController.doPrivileged(new GetPropertyAction("file.separator")).charAt(0);
semicolon = AccessController.doPrivileged(new GetPropertyAction("path.separator")).charAt(0);
altSlash = (this.slash == '\\') ? '/' : '\\';
}
其实就是System.getProperty。
File.getAbsolutePath:
public String getAbsolutePath() {
return fs.resolve(this);
}
resolve遵从下面的逻辑:
- 如果给定的路径已经是绝对路径,那么直接返回。
- 获取系统变量user.dir,将文件指向此目录,而user.dir默认便是java程序执行的路径。
到这里并未涉及到任何系统层面的API操作。
其实是一样的套路:
public boolean isDirectory() {
return ((fs.getBooleanAttributes(this) & FileSystem.BA_DIRECTORY != 0);
}
public boolean isFile() {
return ((fs.getBooleanAttributes(this) & FileSystem.BA_REGULAR) != 0);
}
getBooleanAttributes为native实现。位于WinNTFileSystem_md.c,核心源码:
JNIEXPORT jint JNICALL
Java_java_io_WinNTFileSystem_getBooleanAttributes(JNIEnv *env, jobject this, jobject file) {
jint rv = 0;
jint pathlen;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
DWORD a = getFinalAttributes(pathbuf);
rv = (java_io_FileSystem_BA_EXISTS
| ((a & FILE_ATTRIBUTE_DIRECTORY)
? java_io_FileSystem_BA_DIRECTORY : java_io_FileSystem_BA_REGULAR)
| ((a & FILE_ATTRIBUTE_HIDDEN) ? java_io_FileSystem_BA_HIDDEN : 0));
return rv;
}
getFinalAttributes即Windows API,其实此函数可以返回更多的属性信息,而源码中的一系列与或操作将很多属性屏蔽了,只保留以下:
- 是否存在
- 文件还是目录
- 是否是隐藏文件/目录
FileSystem.BA_REGULAR等属性定义在抽象类FileSystem中:
@Native public static final int ACCESS_READ = 0x04;
@Native public static final int ACCESS_WRITE = 0x02;
@Native public static final int ACCESS_EXECUTE = 0x01;
即:每一个属性用int中的一位来存储。
File.canRead:
public boolean canRead() {
return fs.checkAccess(this, FileSystem.ACCESS_READ);
}
写也是一样的。checkAccess为native实现:
JNIEXPORT jboolean
JNICALL Java_java_io_WinNTFileSystem_checkAccess(JNIEnv *env, jobject this, jobject file, jint access) {
DWORD attr;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
// Windows函数
attr = GetFileAttributesW(pathbuf);
attr = getFinalAttributesIfReparsePoint(pathbuf, attr);
switch (access) {
case java_io_FileSystem_ACCESS_READ:
case java_io_FileSystem_ACCESS_EXECUTE:
return JNI_TRUE;
case java_io_FileSystem_ACCESS_WRITE:
/* Read-only attribute ignored on directories */
if ((attr & FILE_ATTRIBUTE_DIRECTORY) ||
(attr & FILE_ATTRIBUTE_READONLY) == 0)
return JNI_TRUE;
else
return JNI_FALSE;
default:
assert(0);
return JNI_FALSE;
}
}
从这里可以看出,对于读和执行权限,只要路径存在且合法,直接返回true,而可写的条件是: 路径是一个目录或非只读文件。
此方法用以列出一个目录下的所有子文件(夹)。使用WinNTFileSystem的同名native方法实现,源码较长,在此不再贴出,实现的原理便是利用Windows的FindFirstFileW和FindNextFileW,两个函数的W结尾表示Unicode编码,参考:
使用FindFirstFile,FindNextFile遍历一个文件夹
mkdir实现:
public boolean mkdir() {
return fs.createDirectory(this);
}
而mkdirs使用mkdir实现:
public boolean mkdirs() {
File parent = getCanonicalFile().getParentFile();
return (parent != null && (parent.mkdirs() || parent.exists()) &&
canonFile.mkdir());
}
createDirectory为native实现,对应Windows CreateDirectoryW函数。
native由removeFileOrDirectory函数完成,源码:
static int removeFileOrDirectory(const jchar *path) {
/* Returns 0 on success */
DWORD a;
SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL);
a = GetFileAttributesW(path);
if (a == INVALID_FILE_ATTRIBUTES) {
return 1;
} else if (a & FILE_ATTRIBUTE_DIRECTORY) {
//删除目录
return !RemoveDirectoryW(path);
} else {
return !DeleteFileW(path);
}
}
由Windows函数_wrename实现。
用以获取文件的大小,Linux实现由stat函数完成。
类图:
以File参数构造器为例:
public FileOutputStream(File file, boolean append) {
String name = (file != null ? file.getPath() : null);
this.fd = new FileDescriptor();
fd.attach(this);
this.append = append;
this.path = name;
open(name, append);
}
第二个参数为是否追加写,默认false。
open调用了native实现的open0:
JNIEXPORT void JNICALL Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this, jstring path, jboolean append) {
fileOpen(env, this, path, fos_fd,
O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}
fileOpen最终调用Windows API CreateFileW函数,我们在这里只关注一下参数:
O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC)
O_WRONLY表示写文件,O_CREAT为创建文件,O_TRUNC 表示若文件存在,则长度被截为0,属性不变,参考:
实际上,所有的写操作都是通过此方法实现的:
private native void writeBytes(byte b[], int off, int len, boolean append);
最终的写入由Windows函数WriteFile完成,io_util_md.c,writeInternal函数部分源码:
static jint writeInternal(FD fd, const void *buf, jint len, jboolean append) {
result = WriteFile(h, /* File handle to write */
buf, /* pointers to the buffers */
len, /* number of bytes to write */
&written, /* receives number of bytes written */
lpOv); /* overlapped struct */
return (jint)written;
}
隔壁 FileChannel。
FileOutputStream.close:
public void close() throws IOException {
//设置状态
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
//关闭通道
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
close0为native实现,关闭其对应的文件句柄。
老规矩,类图:
可见,这货与File, 输入输出流半毛钱的关系都没有。
支持的读写模式整理如下表:
Mode | 意义 |
---|---|
r | 只读 |
rw | 读写 |
rws | 任何对文件内容或元信息的写会被立即同步刷新至磁盘 |
rwd | 对文件内容的写会被立即同步刷新至磁盘 |
可以看出,rws和rwd两个模式的作用和FileChannel的force方法是一样的。
简略版源码:
public RandomAccessFile(File file, String mode) {
String name = (file != null ? file.getPath() : null);
int imode = -1;
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name, imode);
}
open方法调用了native方法open0,此方法的实现位于src\share\native\java\io\RandomAccessFile.c:
JNIEXPORT void JNICALL
Java_java_io_RandomAccessFile_open0(JNIEnv *env,
jobject this, jstring path, jint mode) {
int flags = 0;
if (mode & java_io_RandomAccessFile_O_RDONLY)
flags = O_RDONLY;
else if (mode & java_io_RandomAccessFile_O_RDWR) {
flags = O_RDWR | O_CREAT;
if (mode & java_io_RandomAccessFile_O_SYNC)
flags |= O_SYNC;
else if (mode & java_io_RandomAccessFile_O_DSYNC)
flags |= O_DSYNC;
}
fileOpen(env, this, path, raf_fd, flags);
}
结合FileOutputStream的open方法便可以发现,两者的open操作其实使用了相同的系统级实现,只不过RandomAccessFile支持更多的参数。
seek允许我们自己设定当前文件的指针(偏移)。注意,系统允许设置的偏移大于文件的真实长度,但这并不改变文件的大小,只有当在新的偏移写入数据之后才会改变。
由native方法seek0调用Windows函数SetFilePointerEx实现,与之比对,FileChannel的position方法采用的是SetFilePointer函数,两者的区别是SetFilePointer将新的文件指针存放在两个long中,而SetFilePointerEX只需要一个long,至于两者为什么要采用不同的实现,不得而知。
用seek方法实现。
获取文件的大小,不同于File的length方法,此处使用seek来实现,RandomAccessFile.c相关源码(简略):
JNIEXPORT jlong JNICALL
Java_java_io_RandomAccessFile_length(JNIEnv *env, jobject this) {
FD fd;
jlong cur = jlong_zero;
jlong end = jlong_zero;
fd = GET_FD(this, raf_fd);
end = IO_Lseek(fd, 0L, SEEK_END);
return end;
}
IO_Lseek对应linux上的lseek,windows上的SetFilePointer,关于为什么可以利用seek取得文件大小,参考:
此方法可以实现文件裁剪的效果,和FileChannel的truncate方法效果相同。native实现:
JNIEXPORT void JNICALL
Java_java_io_RandomAccessFile_setLength(JNIEnv *env, jobject this, jlong newLength) {
FD fd;
jlong cur;
fd = GET_FD(this, raf_fd);
if ((cur = IO_Lseek(fd, 0L, SEEK_CUR)) == -1) goto fail;
//here
if (IO_SetLength(fd, newLength) == -1) goto fail;
//重设指针
if (cur > newLength) {
if (IO_Lseek(fd, 0L, SEEK_END) == -1) goto fail;
} else {
if (IO_Lseek(fd, cur, SEEK_SET) == -1) goto fail;
}
return;
}
IO_SetLength在Windows上的真正函数是SetFilePointer,这和FileChannel的truncate是一样的,在Linux上是ftruncate函数。
虽然RandomAccessFile和InputStream在继承上没有关系,但很多API是一样的。
方法声明:
private native int read0();
注意返回值是int,即范围为0-255,。如果我们将byte值-1写入到文件,再读出来就成了255,如要转为-1强转为byte就行了。
底层对应Windows的ReadFile,Linux的read函数。
public final void readFully(byte b[]) throws IOException {
readFully(b, 0, b.length);
}
顾名思义,此方法会在所有要求的字节读完之前阻塞,其实就是替我们做了循环判断的过程:
public final void readFully(byte b[], int off, int len) {
int n = 0;
do {
int count = this.read(b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
} while (n < len);
}
指各种readInt,readChar等方法,其实就是读指定的字节然后给你拼起来。
我们以float为例:
public final void writeFloat(float v) {
writeInt(Float.floatToIntBits(v));
}
public final float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
从根本上来说,写入/读取到的只不过是一组二进制数字,关键在于我们如何解读它,所以写入/读取浮点数的关键在于如何进行浮点数和int值得转换(两者的二进制形式是一样的),上面的两个native Float方法巧妙地利用了C语言的共用体实现这一转换:
JNIEXPORT jint JNICALL
Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v) {
union {
int i;
float f;
} u;
u.f = (float)v;
return (jint)u.i;
}
源码:
public final String readLine() throws IOException {
StringBuffer input = new StringBuffer();
int c = -1;
boolean eol = false;
while (!eol) {
switch (c = read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
long cur = getFilePointer();
if ((read()) != '\n') {
seek(cur);
}
break;
default:
input.append((char)c);
break;
}
}
if ((c == -1) && (input.length() == 0)) {
return null;
}
return input.toString();
}
可以看出,方法将byte转为了char,但是这样有一个问题,char是由byte转换而来,所以这里只支持ASCII字符,如果真的想要读一行不应使用此方法,而应该使用字符流。
不是很理解这个方法的用意,源码:
public final int readUnsignedByte() throws IOException {
int ch = this.read();
if (ch < 0)
throw new EOFException();
return ch;
}
奇怪的地方在于这里还是只读了一个字节,之后转为int,ch怎么可能是负值?这样的话和read方法又有什么区别?
应和writeUTF配合食用。writeUTF用于写入一个UTF-8编码的字符串,注意前两个字节代表后面有多少个字节(字符串)。所以一个10字节的字符串需要12字节存储。
感觉此方法应该是用在字符串、int等多种类型混合存储的场景下。
或者说是包装流,类图(只列出常用的):
DataInputStream与RandomAccessFile的API基本一致(实现了同一个接口),InflaterInputStream用于zip压缩包的读取。
"字符"是一个文化上的概念,字符流基于字节流,只不过是按照特定的编码规则进行解码罢了。我们以Reader为例,看一下其类图即可: