一、FileInputStream的源码分析和使用方法详细分析
FileInputStream 是 Java IO 体系中文件读取的基础类,通过封装操作系统的文件操作,提供了简单易用的字节流读取接口。其设计融合了模板方法模式(统一接口)、适配器模式(屏蔽系统差异)和代理模式(资源生命周期管理),是面向对象设计原则的典型实践
FileInputStream 适用于二进制文件读取:如图片、音频、视频等非文本文件(字符文件建议使用 FileReader)。FileInputStream 的在操作文件时,需要与FileDescriptor(文件操作符)相关联,关于FileDescriptor(文件操作符),可以查看我的另一篇博客:5、FileDescriptor的源码和使用注意事项(windows操作系统,JDK8)
FileInputStream.class::getChannel() 函数可以将当前的FileInputStream对象与NIO中的FileChannel相关联,从而获得更高效的文件操作(如内存映射、分散/聚集读取)。
使用完FileInputStream之后,必须显式调用 close() 函数释放文件句柄(或使用 try-with-resources 自动关闭),避免资源泄漏。FileInputStream不是线程安全的,在多线程下使用FileInputStream时,需要注意线程安全的问题。
FileInputStream的UML关系图,如下所示:
FileInputStream.class的源码如下
import java.nio.channels.FileChannel;
import sun.nio.ch.FileChannelImpl;
public
class FileInputStream extends InputStream
{
//文件描述符
private final FileDescriptor fd;
//在构造函数中,通过File对象获取文件的路径
private final String path;
//NIO中的FileChannel,可以更高效的对文件进行操作,在getChannel() 函数中将当前的FileInputStream对象与FileChannel相关联
private FileChannel channel = null;
//在close()函数中(关闭当前这个FileInputStream的函数),用于线程同步的锁对象
private final Object closeLock = new Object();
private volatile boolean closed = false;//当前这个FileInputStream是否关闭的标记
//构造函数,入参是文件的路径名,比如要传入windows操作系统中D盘下的nio_data.txt文件路径时,入参为"D:\nio_data.txt"
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);//通过文件的路径名构造的FileInputStream对象时,最终都要将文件的路径名构造为File对象
}
//构造函数,通过File对象构造FileInputStream对象
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();//文件的路径名==null时,抛出一个NullPointerException
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");//File对象的isInvalid()函数返回true时,抛出一个FileNotFoundException
}
fd = new FileDescriptor();
fd.attach(this);// 将当前这个FileInputStream与文件描述符绑定(用于资源回收)
path = name;
open(name);// 最终调用本地函数(native修饰)open0()打开文件
}
//构造函数,通过FileDescriptor对象构造FileInputStream对象
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与文件描述符绑定(用于资源回收)
}
//native修饰的本地函数,当前这个FileInputStream对象打开1个指定文件并为了之后的read()函数、readBytes()函数、skip()函数做准备
private native void open0(String name) throws FileNotFoundException;
private void open(String name) throws FileNotFoundException {
open0(name);
}
public int read() throws IOException {
return read0();
}
//native修饰的本地函数,当前这个FileInputStream对象从1个指定文件中每次读取1个字节;
//如果没有读取到这个文件的末尾,则返回读取到的这1个字节的ASCII编码,如果已经读取到了这个文件的末尾,则返回-1
private native int read0() throws IOException;
//native修饰的本地函数,当前这个FileInputStream对象从1个指定文件中每次读取len个字节到byte b[]数组的[off,off+len)索引位置;
//如果没有读取到这个文件的末尾,则返回已经读取到的byte b[]数组中的累计字节数量(所有累计读取到的字节都是通过ASCII编码的),如果已经读取到了这个文件的末尾,则返回-1
private native int readBytes(byte b[], int off, int len) 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);
}
public long skip(long n) throws IOException {
return skip0(n);
}
//native修饰的本地函数,通过这个函数,当前这个FileInputStream对象可以从1个指定文件跳过n个字节再进行后续操作(比如通过read()函数读取)
private native long skip0(long n) throws IOException;
public int available() throws IOException {
return available0();
}
//native修饰的本地函数,返回与当前这个FileInputStream对象关联的1个指定文件还可以进行后续操作(比如read()函数和skip()函数)的字节数量
private native int available0() throws IOException;
public void close() throws IOException {
synchronized (closeLock) {// 通过synchronized 防止多线程重复关闭
if (closed) {
return;
}
closed = true;//第一个执行到这里的线程先设置boolean closed = true,防止后面执行close() 函数的线程还要继续执行后续的代码片段
}
//如果NIO中的FileChannel对象与这个FileInputStream对象相关联,同时关闭NIO中FileChannel对象
if (channel != null) {
channel.close();
}
// 释放文件描述符FileDescriptor对象关联的所有资源
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();//调用本地函数关闭文件
}
});
}
//获取文件描述符FileDescriptor对象
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}
//将当前这个FileInputStream对象与NIO中的FileChannel相关联
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
private static native void initIDs();
private native void close0() throws IOException;
static {
initIDs();
}
//重写了Object.class的finalize()函数,这个函数会在对象销毁时由JVM自动调用
//不建议当前这个FileInputStream对象销毁时通过finalize()函数调用close()函数来关闭当前这个FileInputStream对象,建议在使用完FileInputStream对象时手动调用close()函数
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
}
1.1、FileInputStream.class的skip()函数和available()函数
关于read()函数和read(byte b[])函数的使用方式,可以查看我的另一篇博客:1、Java的IO概览(一)
我的windows操作系统的D盘根目录下有nio-data.txt文件,如下所示:
通过skip()函数可以从这个nio-data.txt文件跳过n个字节再进行后续操作,通过available()函数可以返回与这个FileInputStream对象关联的nio-data.txt文件还可以进行后续操作的字节数量,如下所示:
package com.xxx.bio;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileInputStreamTest {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream("D:\\nio-data.txt");
System.out.println(is.available());
is.skip(5);
System.out.println(is.available());
}
}
程序运行结果,如下所示:
1.2、模板方法模式
InputStream 作为抽象基类,定义了 read()、skip()、available() 等抽象方法, FileInputStream、需实现这些方法以提供具体功能。例如:
// InputStream 中的抽象方法
public abstract int read() throws IOException;
// FileInputStream 中的具体实现
public int read() throws IOException {
return read0(); // 调用本地方法实现
}
通过模板方法模式,统一了字节流读取的接口,子类只需关注具体读取逻辑,提高了代码的可扩展性。
1.3、适配器模式(Adapter Pattern)
文件读取的底层操作依赖操作系统(如 Linux 的 read() 系统调用),FileInputStream 通过 native 方法将这些系统调用封装为 Java 接口(如 read()、close()),使得上层代码无需关心具体操作系统的差异。
private native int read0() throws IOException; // 适配系统级读取操作
private native void close0() throws IOException; // 适配系统级关闭操作
通过适配器模式,屏蔽了底层系统的复杂性,提供了跨平台的统一文件读取接口。
1.4、代理模式(Proxy Pattern)
FileDescriptor 是系统级文件句柄的代理对象,FileInputStream 通过 fd.attach(this) 将自身与描述符绑定。当流关闭时,描述符会触发资源释放(如 close0())。这种设计使得多个流可以共享同一个描述符(如通过 getFD() 获取),但最终由最后一个关联的流负责关闭。
fd.attach(this); // 将流与描述符绑定
fd.closeAll(/* 关闭回调 */); // 所有关联的流关闭后释放资源
通过代理模式,实现了文件句柄的生命周期管理,避免资源泄漏。
二、RandomAccessFile的源码分析和使用方法详细分析
RandomAccessFile 是 Java IO 体系中文件读取的基础类,用于在文件中的任何位置进行读写操作。RandomAccessFile实现了DataOutput.interface和DataInput.interface两个接口,拥有读取和写入java基本数据类型(byte,short,int,long,double,float,boolean,char) 和UTF-8字符串方法,有效地与IO流继承体系中其他部分实现了分离,由于不支持装饰者设计模式,所以不能与OutputStream和InputStream的子类结合起来使用。RandomAccessFile之所以说对文件随机访问,其原理是将文件看成是一个大型的字节数组,然后通过游标(cursor)或者移动文件指针(可以理解为数组中索引对数组中任意位置字节读取或者写入),从而做到对文件的随机访问,RandomAccessFile构造方法中会传入对应的读写模式,共有4种。如下所示:
| 读写模式 | 解释 |
|---|---|
| "r" | 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 |
| "rw" | 打开以便读取和写入。 |
| "rws" | 打开以便读取和写入。相对于 "rw","rws" 还要求对"文件的内容"或"元数据"的每个更新都同步写入到基础存储设备。 |
| "rwd" | 打开以便读取和写入,相对于 "rw","rwd" 还要求对"文件的内容"的每个更新都同步写入到基础存储设备。 |
当操作的文件是存储在本地的基础存储设备上时(如硬盘, NandFlash等),"rw" 、"rws" 、"rwd"之间才有区别,区别如下:
①、当模式是 "rws" 并且 操作的是基础存储设备上的文件;那么,每次"更改文件内容(如执行write()函数)" 或 "修改文件元数据(如文件的mtime)"时,都会将这些改变同步到基础存储设备上;
②、当模式是 "rwd" 并且操作的是基础存储设备上的文件;那么,每次"更改文件内容(如执行write()函数)"时,都会将这些改变同步到基础存储设备上;
③、当模式是 "rw" 并且 操作的是基础存储设备上的文件;那么,关闭文件时,会将"文件内容的修改"同步到基础存储设备上。至于,"更改文件内容"时,是否会立即同步,取决于系统底层实现。
RandomAccessFile的UML关系图,如下所示:
RandomAccessFile.class的源码如下
package java.io;
import java.nio.channels.FileChannel;
import sun.nio.ch.FileChannelImpl;
public class RandomAccessFile implements DataOutput, DataInput, Closeable {
//文件描述符
private FileDescriptor fd;
//NIO中的FileChannel,可以更高效的对文件进行操作,在getChannel() 函数中将当前的FileInputStream对象与FileChannel相关联
private FileChannel channel = null;
private boolean rw;
//在构造函数中,通过File对象获取的文件路径保存在该变量中
private final String path;
//在close()函数中(关闭当前这个RandomAccessFile 对象的函数),用于线程同步的锁对象
private Object closeLock = new Object();
private volatile boolean closed = false;//当前这个RandomAccessFile 是否关闭的标记
//只读模式,不具备写权限,如果文件不存在不会创建文件。
private static final int O_RDONLY = 1;
//读写模式,具备读写权限,如果文件不存在会创建文件,该模式下数据改变时不会立马写入底层存储设备。
private static final int O_RDWR = 2;
//同步的读写模式,具备读写模式的所有特性,当文件内容或元数据改变时,会立马同步写入到底层存储设备中。
private static final int O_SYNC = 4;
//同步的读写模式,具备读写模式的所有特性,当文件内容改变时,会立马同步写入到底层存储设备中。
private static final int O_DSYNC = 8;
public RandomAccessFile(String name, String mode)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, mode);
}
public RandomAccessFile(File file, String mode)
throws FileNotFoundException
{
// 定义了一个String类型变量name用于接收操作文件的路径名,如果文件为null,则name赋值为null。
String name = (file != null ? file.getPath() : null);
int imode = -1;
if (mode.equals("r"))
imode = O_RDONLY;//情况1:字符串model=r时imode=1
else if (mode.startsWith("rw")) {
imode = O_RDWR;//情况2:字符串model以rw开头时(可以为rws或者rwd,也可以为rw+任何字符串),imode=2
rw = true;
if (mode.length() > 2) {
if (mode.equals("rws"))
imode |= O_SYNC;//情况3:字符串model=rws时imode=6
else if (mode.equals("rwd"))
imode |= O_DSYNC;//情况4:字符串model=rwd时imode=10
else
imode = -1;//model不属于情况1、2、3、4的其余情况,imode=-1
}
}
if (imode < 0)//model不属于情况1、2、3、4的其余情况时,抛出一个IllegalArgumentException
throw new IllegalArgumentException("Illegal mode \"" + mode
+ "\" must be one of "
+ "\"r\", \"rw\", \"rws\","
+ " or \"rwd\"");
//获得java的安全管理器,根据rw的状态监测文件的读写权限。
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
if (rw) {
security.checkWrite(name);
}
}
if (name == null) {
throw new NullPointerException();//文件的路径名==null时,抛出一个NullPointerException
}
if (file.isInvalid()) {
//File对象的isInvalid()函数返回true时,抛出一个FileNotFoundException
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);// 将当前这个RandomAccessFile与文件描述符绑定(用于资源回收)
path = name;
open(name, imode);// 最终调用本地函数(native修饰)open0()打开文件
}
//获取文件描述符FileDescriptor对象
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}
//将当前这个RandomAccessFile对象与NIO中的FileChannel相关联
public final FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, rw, this);
}
return channel;
}
}
//native修饰的本地函数,当前这个RandomAccessFile对象用指定的读写模式打开1个指定文件并为了之后的read()函数、readBytes()函数、seek()函数做准备
private native void open0(String name, int mode)
throws FileNotFoundException;
private void open(String name, int mode)
throws FileNotFoundException {
open0(name, mode);
}
public int read() throws IOException {
return read0();
}
//native修饰的本地函数,当前这个RandomAccessFile对象从1个指定文件中每次读取1个字节;
//如果没有读取到这个文件的末尾,则返回读取到的这1个字节的ASCII编码,如果已经读取到了这个文件的末尾,则返回-1
private native int read0() throws IOException;
//native修饰的本地函数,当前这个RandomAccessFile对象从1个指定文件中每次读取len个字节到byte b[]数组的[off,off+len)索引位置;
//如果没有读取到这个文件的末尾,则返回已经读取到的byte b[]数组中的累计字节数量(所有累计读取到的字节都是通过ASCII编码的),如果已经读取到了这个文件的末尾,则返回-1
private native int readBytes(byte b[], int off, int len) throws IOException;
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
public final void readFully(byte b[]) throws IOException {
readFully(b, 0, b.length);
}
//从打开的文件中读取len个字节到指定的byte[]数组b中,这len个字节被放到byte[]数组b的[off,off+len)索引位置。
public final void readFully(byte b[], int off, int len) throws IOException {
int n = 0;//累计读取到byte[]数组b中的字节数量
do {
//count=-1时,表示读取到了文件的末尾。
//count>0时,表示从本次打开的文件中读取了(len - n)个字节到byte[]数组b的[off + n,off + len)索引位置。
int count = this.read(b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;//累加读到的字节总数量
} while (n < len);//当累计读到的字节总数量>=len时,跳出循环
}
//当前这个RandomAccessFile对象可以从1个指定文件(大型的字节数组)中跳过n个字节(n<文件的字节总数量)再进行后续操作(比如通过read()函数读取)
public int skipBytes(int n) throws IOException {
long pos;//将文件看成是一个大型的字节数组时,pos表示当前操作该数组时,所处的索引位置
long len;//将文件看成是一个大型的字节数组时,len表示数组的长度
long newpos;//将文件看成是一个大型的字节数组时,newpos表示要跳跃(重新指向)到的索引位置
if (n <= 0) {
return 0;
}
pos = getFilePointer();//获取RandomAccessFile对象正在操作的文件(大型的字节数组)的索引位置
len = length();//获取文件(大型的字节数组)的长度
newpos = pos + n;//计算要跳跃(重新指向)到的索引位置
if (newpos > len) {
newpos = len;//如果索引越界了,将索引置为文件(大型的字节数组)的最后一个索引位置
}
seek(newpos);//通过native修饰的函数,将文件(大型的字节数组)的的索引置为newpos
/* return the actual number of bytes skipped */
return (int) (newpos - pos);//返回实际跳跃的字节数量
}
public void write(int b) throws IOException {
write0(b);
}
//native修饰的本地函数,当前这个RandomAccessFile对象向1个指定文件中写入1个字节
private native void write0(int b) throws IOException;
//native修饰的本地函数,当前这个RandomAccessFile对象向1个指定文件中每次写入byte b[]数组中[off,off+len)索引位置的字节
private native void writeBytes(byte b[], int off, int len) throws IOException;
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
writeBytes(b, off, len);
}
//native修饰的本地函数,将文件看成是一个大型的字节数组时,该函数返回当前操作(读或者写)到该数组的索引位置
public native long getFilePointer() throws IOException;
public void seek(long pos) throws IOException {
if (pos < 0) {
throw new IOException("Negative seek offset");
} else {
seek0(pos);
}
}
//native修饰的本地函数,将文件看成是一个大型的字节数组时,该函数可以跳跃到该数组的指定索引位置
private native void seek0(long pos) throws IOException;
//native修饰的本地函数,将文件看成是一个大型的字节数组时,该函数可以获得这个数组的长度
public native long length() throws IOException;
//native修饰的本地函数,将文件看成是一个大型的字节数组时,该函数可以设置这个数组的长度
public native void setLength(long newLength) throws IOException;
public void close() throws IOException {
synchronized (closeLock) {// 通过synchronized 防止多线程重复关闭
if (closed) {
return;
}
closed = true;//第一个执行到这里的线程先设置boolean closed = true,防止后面执行close() 函数的线程还要继续执行后续的代码片段
}
//如果NIO中的FileChannel对象与这个FileInputStream对象相关联,同时关闭NIO中FileChannel对象
if (channel != null) {
channel.close();
}
// 释放文件描述符FileDescriptor对象关联的所有资源
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();//调用本地函数关闭文件
}
});
}
//读取1个boolean类型数据
public final boolean readBoolean() throws IOException {
int ch = this.read();//boolean类型占一个字节,调用1次native修饰的read0()函数即可读取1个字节
if (ch < 0)
throw new EOFException();
return (ch != 0);
}
//读取1个byte类型数据
public final byte readByte() throws IOException {
int ch = this.read();//byte类型占1个字节,调用1次native修饰的read0()函数即可读取1个字节
if (ch < 0)
throw new EOFException();
return (byte)(ch);
}
//读取1个无符号的byte类型数据
public final int readUnsignedByte() throws IOException {
int ch = this.read();//无符号的byte类型占1个字节,调用1次native修饰的read0()函数即可
if (ch < 0)
throw new EOFException();
return ch;
}
//读取1个short类型数据
public final short readShort() throws IOException {
int ch1 = this.read();//short类型占2个字节,占16位(bit),第1次调用native修饰的read0()函数读取的是高8位(bit)表示的1个字节
int ch2 = this.read();//第2次调用native修饰的read0()函数读取的是低8位(bit)表示的1个字节
if ((ch1 | ch2) < 0)
throw new EOFException();
return (short)((ch1 << 8) + (ch2 << 0));//高8位(bit)和低8位(bit)相加,组合成2字节(占16bit位)的short类型数据
}
//读取1个无符号的short类型数据
public final int readUnsignedShort() throws IOException {
int ch1 = this.read();//无符号的short类型占2个字节,占16位(bit),第1次调用native修饰的read0()函数读取的是高8位(bit)表示的1个字节
int ch2 = this.read();//第2次调用native修饰的read0()函数读取的是低8位(bit)表示的1个字节
if ((ch1 | ch2) < 0)
throw new EOFException();
return (ch1 << 8) + (ch2 << 0);//高8位(bit)和低8位(bit)相加,组合成2字节(占16bit位)的无符号short类型数据
}
//读取1个char类型数据
public final char readChar() throws IOException {
int ch1 = this.read();//char类型占2个字节,占16位(bit),第1次调用native修饰的read0()函数读取的是高8位(bit)表示的1个字节
int ch2 = this.read();//第2次调用native修饰的read0()函数读取的是低8位(bit)表示的1个字节
if ((ch1 | ch2) < 0)
throw new EOFException();
return (char)((ch1 << 8) + (ch2 << 0));//高8位(bit)和低8位(bit)相加,组合成2字节(占16bit位)的char类型数据
}
//读取1个int类型数据
public final int readInt() throws IOException {
int ch1 = this.read();//int类型占4个字节,占32位(bit),第1次调用native修饰的read0()函数读取的是第1个8位(bit)表示的第1个字节
int ch2 = this.read();//第2次调用native修饰的read0()函数读取的是第2个8位(bit)表示的第2个字节
int ch3 = this.read();//第3次调用native修饰的read0()函数读取的是第3个8位(bit)表示的第3个字节
int ch4 = this.read();//第4次调用native修饰的read0()函数读取的是第4个8位(bit)表示的第4个字节
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));//4个字节相加,组合成了4字节(占32bit位)的int类型数据
}
//读取1个long类型数据,long类型占8个字节,占64位(bit)
public final long readLong() throws IOException {
//先读4个字节作为高32位(bit),再读4个字节作为低32位(bit),高32位(bit)和低32位(bit)相加,组合成了8字节(占64bit位)的long类型数据
return ((long)(readInt()) << 32) + (readInt() & 0xFFFFFFFFL);
}
//读取1个float类型数据
public final float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
//读取1个double类型数据
public final double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
//一次读取一行数据,并将读取到的数据返回。
public final String readLine() throws IOException {
//创建了一个StringBuffer对象,用于接收读取的数据。
StringBuffer input = new StringBuffer();
//声明了一个int型变量c,用于接收读取的数据,声明了一个boolean型变量eol,表明是否读取到了换行符。
int c = -1;
boolean eol = false;
//通过一个循环来不断读取数据。
while (!eol) {
switch (c = read()) {
case -1://返回-1表示文件已经读取完毕。
case '\n':
eol = true;//返回'\n',表示读到换行符,此时将eol置为true,跳出循环。
break;
case '\r':
eol = true;//返回'\r',将eol置为true,因为不同平台换行符不同,向后读取看是否有'\n'(windows操作系统的换行符为"\r\n"),如果没有,则将操作文件(大型的字节数组)的指针(索引)重置为'\r'的下一个位置,然后跳出循环。
long cur = getFilePointer();
if ((read()) != '\n') {
seek(cur);
}
break;
default:
input.append((char)c);//每次操作向StringBuffer input中添加读取到的字节。
break;
}
}
//如果没有读取到任何数据,则返回null。
if ((c == -1) && (input.length() == 0)) {
return null;
}
//执行到此处时,表示读取到了当前行的数据,最终将input转化成String类型然后返回。
return input.toString();
}
public final String readUTF() throws IOException {
return DataInputStream.readUTF(this);
}
//向RandomAccessFile对象正在操作的文件中写入1个boolean类型数据
public final void writeBoolean(boolean v) throws IOException {
write(v ? 1 : 0);
//written++;
}
//向RandomAccessFile对象正在操作的文件中写入1个byte类型数据
public final void writeByte(int v) throws IOException {
write(v);
//written++;
}
//向RandomAccessFile对象正在操作的文件中写入1个short类型数据
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
public final void writeShort(int v) throws IOException {
write((v >>> 8) & 0xFF);//先向文件中写入short类型(占2个字节,共16位)的高8位(bit)
write((v >>> 0) & 0xFF);//再向文件中写入short类型(占2个字节,共16位)的低8位(bit)
//written += 2;
}
//向RandomAccessFile对象正在操作的文件中写入1个char类型数据
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
public final void writeChar(int v) throws IOException {
//先向文件中写入char类型(占2个字节,共16位)的高8位(bit)
write((v >>> 8) & 0xFF);
//再向文件中写入char类型(占2个字节,共16位)的低8位(bit)
write((v >>> 0) & 0xFF);
//written += 2;
}
//向RandomAccessFile对象正在操作的文件中写入1个int类型数据
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
public final void writeInt(int v) throws IOException {
//先向文件中写入int类型(占4个字节,共32位)的第1个8位bit(从左到右)
write((v >>> 24) & 0xFF);
//再向文件中写入int类型(占4个字节,共32位)的第2个8位bit(从左到右)
write((v >>> 16) & 0xFF);
//再向文件中写入int类型(占4个字节,共32位)的第3个8位bit(从左到右)
write((v >>> 8) & 0xFF);
//最后向文件中写入int类型(占4个字节,共32位)的第4个8位bit(从左到右)
write((v >>> 0) & 0xFF);
//written += 4;
}
//向RandomAccessFile对象正在操作的文件中写入1个long类型数据
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
public final void writeLong(long v) throws IOException {
//先向文件中写入long类型(占8个字节,共64位)的第1个8位bit(从左到右)
write((int)(v >>> 56) & 0xFF);
//再向文件中写入long类型的第2个8位bit(从左到右)
write((int)(v >>> 48) & 0xFF);
//再向文件中写入long类型的第3个8位bit(从左到右)
write((int)(v >>> 40) & 0xFF);
//再向文件中写入long类型的第4个8位bit(从左到右)
write((int)(v >>> 32) & 0xFF);
//再向文件中写入long类型的第5个8位bit(从左到右)
write((int)(v >>> 24) & 0xFF);
//再向文件中写入long类型的第6个8位bit(从左到右)
write((int)(v >>> 16) & 0xFF);
//再向文件中写入long类型的第7个8位bit(从左到右)
write((int)(v >>> 8) & 0xFF);
//最后向文件中写入long类型的第8个8位bit(从左到右)
write((int)(v >>> 0) & 0xFF);
//written += 8;
}
//向RandomAccessFile对象正在操作的文件中写入1个float类型数据
public final void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
//向RandomAccessFile对象正在操作的文件中写入1个double类型数据
public final void writeDouble(double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
//向RandomAccessFile对象正在操作的文件中写入1个字符串
public final void writeChars(String s) throws IOException {
int clen = s.length();//获取这个字符串的长度
int blen = 2*clen;//因为Java中的1个字符占2个字节,所以字节数组的长度是字符串长度的2倍
byte[] b = new byte[blen];
char[] c = new char[clen];
s.getChars(0, clen, c, 0);//先将字符串写入字符数组中
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
for (int i = 0, j = 0; i < clen; i++) {
//所以先向字节数组中写入每个字符(char类型)的高8位bit,再向字节数组中写入每个字符(char类型)的低8位bit
b[j++] = (byte)(c[i] >>> 8);
b[j++] = (byte)(c[i] >>> 0);
}
writeBytes(b, 0, blen);//将转换好的字节数组通过writeBytes()函数写入文件中
}
public final void writeUTF(String str) throws IOException {
DataOutputStream.writeUTF(str, this);
}
private static native void initIDs();
private native void close0() throws IOException;
static {
initIDs();
}
}
2.1、大端字节序(Big-Endian)存储在RandomAccessFile 中的写入问题(以writeInt()函数为例)
writeInt(int v) 函数将一个 int 类型的整数(共 4 个字节)按顺序写入文件。由于底层的 write() 函数每次只能写入 1 个字节,因此需要将 int 的 4 个字节逐个提取并写入。源码如下所示:
//向RandomAccessFile对象正在操作的文件中写入1个int类型数据
//由于Java 默认采用大端字节序(Big-Endian)存储,即最高有效字节(MSB)在前,最低有效字节(LSB)在后。
public final void writeInt(int v) throws IOException {
//先向文件中写入int类型(占4个字节,共32位)的第1个8位bit(从左到右)
write((v >>> 24) & 0xFF);
//再向文件中写入int类型(占4个字节,共32位)的第2个8位bit(从左到右)
write((v >>> 16) & 0xFF);
//再向文件中写入int类型(占4个字节,共32位)的第3个8位bit(从左到右)
write((v >>> 8) & 0xFF);
//最后向文件中写入int类型(占4个字节,共32位)的第4个8位bit(从左到右)
write((v >>> 0) & 0xFF);
//written += 4;
}
①、比如要写入一个int类型的数据1,666,688,888,用二进制表示为0110 0011 0101 0111 1010 0111 0111 1000,当需要写入从左往右的第1个字节0110 0011时,会执行
write((v >>> 24) & 0xFF);
整个过程,,如下所示:
step1:先右移24位
0110 0011 0101 0111 1010 0111 0111 1000 >>> 24
结果为(>>> 是无符号右移,高位补 0,避免符号扩展干扰):
0000 0000 0000 0000 0000 0000 0110 0011
step2:将剩余的8位与0xFF(1111,1111)进行&运算
0000 0000 0000 0000 0000 0000 0110 0011
& 1111 1111
结果为:
0110 0011
最终将0110 0011(占1个字节)写入RandomAccessFile对象正在操作的文件中。
②、然后写入从左往右的第2个字节0101 0111时,会执行
write((v >>> 16) & 0xFF);
整个过程,,如下所示:
step1:先右移16位
0110 0011 0101 0111 1010 0111 0111 1000 >>> 16
结果为(>>> 是无符号右移,高位补 0,避免符号扩展干扰):
0000 0000 0000 0000 0110 0011 0101 0111
step2:将剩余的16位与0xFF(1111,1111)进行&运算
0000 0000 0000 0000 0110 0011 0101 0111
& 1111 1111
结果为:
0101 0111
最终将0101 0111(占1个字节)写入RandomAccessFile对象正在操作的文件中。
③、然后写入从左往右的第3个字节1010 0111时,会执行
write((v >>> 8) & 0xFF);
整个过程,,如下所示:
step1:先右移8位
0110 0011 0101 0111 1010 0111 0111 1000 >>> 8
结果为:
0000 0000 0110 0011 0101 0111 1010 0111
step2:将剩余的24位与0xFF(1111,1111)进行&运算
0000 0000 0110 0011 0101 0111 1010 0111
& 1111 1111
结果为:
1010 0111
最终将1010 0111(占1个字节)写入RandomAccessFile对象正在操作的文件中。
④、最后写入从左往右的第4个字节0111 1000时,会执行
write((v >>> 0) & 0xFF);
整个过程,,如下所示:
step1:先右移0位,其实就是原值不变,直接拿来参与运算
0110 0011 0101 0111 1010 0111 0111 1000 >>> 0
结果为(>>> 是无符号右移,高位补 0,避免符号扩展干扰):
0110 0011 0101 0111 1010 0111 0111 1000
step2:原值与0xFF(1111,1111)进行&运算
0110 0011 0101 0111 1010 0111 0111 1000
& 1111 1111
结果为:
0111 1000
最终将0111 1000(占1个字节)写入RandomAccessFile对象正在操作的文件中。
其余函数,比如writeByte()函数、writeShort()函数、writeChar()函数...与writeInt()函数的原理相同,只是最终写入的字节数量可能会有所不同,但都是从左到右先依次写入高位字节。
2.2、大端字节序(Big-Endian)存储在RandomAccessFile 中的读的问题(以readInt()函数为例)
readInt() 函数会将1个 int 类型的整数(共 4 个字节)从RandomAccessFile 操作的文件中读取出 来。由于底层的 read() 函数每次只能读取 1 个字节,因此需要将 int 的 4 个字节先逐个读取出来,再将4次调用read() 函数读到的结果移项后再拼接起来。源码如下所示:
//读取1个int类型数据
public final int readInt() throws IOException {
int ch1 = this.read();//int类型占4个字节,占32位(bit),第1次调用native修饰的read0()函数读取的是第1个8位(bit)表示的第1个字节
int ch2 = this.read();//第2次调用native修饰的read0()函数读取的是第2个8位(bit)表示的第2个字节
int ch3 = this.read();//第3次调用native修饰的read0()函数读取的是第3个8位(bit)表示的第3个字节
int ch4 = this.read();//第4次调用native修饰的read0()函数读取的是第4个8位(bit)表示的第4个字节
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));//4个字节相加,组合成了4字节(占32bit位)的int类型数据
}
最后一步
(ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)
是将4次调用read() 函数读到的结果按照大端字节序(Big-Endian)拼接起来,整个过程如下(与writeInt()函数的执行原理相反):
①、比如读取的1个int类型的数据是1,666,688,888,用二进制表示为0110 0011 0101 0111 1010 0111 0111 1000,将这个数读取到内存后,ch1、ch2、ch3、ch4分别为:
ch1:0110 0011
ch2:0101 0111
ch3:1010 0111
ch4:0111 1000
最后一步的拼接前的移项过程为:
ch1 << 24:0110 0011 0000 0000 0000 0000 0000 0000
ch2 << 16:0000 0000 0101 0111 0000 0000 0000 0000
ch3 << 8: 0000 0000 0000 0000 1010 0111 0000 0000
ch4 << 0: 0000 0000 0000 0000 0000 0000 0111 1000
拼接过程为:
0110 0011 0000 0000 0000 0000 0000 0000
+0000 0000 0101 0111 0000 0000 0000 0000
+0000 0000 0000 0000 1010 0111 0000 0000
+0000 0000 0000 0000 0000 0000 0111 1000
结果为:
0110 0011 0101 0111 1010 0111 0111 1000
2.3、RandomAccessFile的使用示例
在使用RandomAccessFile时读写文件时,可以使用多线程读写一个文件以提高整个文件的读写效率,如下所示:
package com.xxx.bio;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class RandomAccessFileIOTest {
public static void main(String[] args) {
final File source = new File("F:\\realMe手机文件备份\\照片视频\\VID20260217183653.mp4");
final File target = new File("D:\\copy.mp4");
int threadNum = (int) Math
.ceil(Math.ceil((double) source.length() / 1024 / 1024 / 20));
//只用线程池开启5个核心线程做复制,ArrayBlockingQueue<>(10)队列的长度为10
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 100, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadFactory() {
private final AtomicInteger threadCount = new AtomicInteger(1);
private final String namePrefix = "MyPool-thread-";
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + threadCount.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
});
for (int i = 0; i < threadNum; i++) {
threadPoolExecutor.execute(new MyRunnable(i, source, target));
}
threadPoolExecutor.shutdown();
}
}
class MyRunnable implements Runnable {
private int num;
private File source;
private File target;
MyRunnable(int num, File source, File target) {
this.num = num;
this.source = source;
this.target = target;
}
@Override
public void run() {
try (RandomAccessFile sourceFile = new RandomAccessFile(source, "rw");
RandomAccessFile targetFile = new RandomAccessFile(target, "rw");) {
System.out.println(Thread.currentThread().getName() + "线程启动");
sourceFile.seek(num * 1024 * 1024 * 20);
targetFile.seek(num * 1024 * 1024 * 20);
byte[] buffer = null;
if ((sourceFile.length() - sourceFile.getFilePointer()) < 1024 * 1024 * 20) {
buffer = new byte[(int) (sourceFile.length() - sourceFile
.getFilePointer())];
} else {
buffer = new byte[1024 * 1024 * 20];
}
sourceFile.read(buffer);
targetFile.write(buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程复制结束");
}
}
我的windows操作系统的F:\realMe手机文件备份\照片视频\路径下有一个VID20260217183653.mp4文件,上述代码通过多线程将这个文件复制到D盘根目录(D:\)下的copy.mp4文件中,执行结果如下:
三、对JDK原生的RandomAccessFile进行扩展------增加缓冲区
从RandomAccessFile的源码中可以看出,RandomAccessFile类在进行读写操作时,都是直接与底层介质进行数据传递的,即使是读写一个字节的数据,也必须进行一次I/O操作,这样就大大降低了其工作的效率,我们可以通过内置一个数据缓存区来提升读写效率,RandomAccessFile也同样可以这样操作,我们可以完全重构一个属于自己的带缓存的BufferedRandomAccessFile 类,如下所示:
package com.xxx.bio;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
public class BufferedRandomAccessFile extends RandomAccessFile {
private static final int Default_Buffer_Size = 1024 * 8;
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
private static final long BuffMask_ = ~(((long) Default_Buffer_Size) - 1L);
//表示缓存区是否有未flush的数据。
private boolean hasDatas;
//表示是否进行同步操作,将缓存内容flush。
private boolean syncNeeded_;
//当前操作文件的索引位置(包括在缓存区中)。
private long cPos = 0L;
//磁盘上操作文件的索引位置(存储介质中)。
private long diskPos_ = 0L;
private long lo_, hi_ = 0L;
private long maxHi_ = (long)Default_Buffer_Size;
//是否到了文件结束部分。
private boolean isEOF;
//内置的一个数组缓存区,默认大小是8k。
private byte[] buffer;
public BufferedRandomAccessFile(File file, String mode) throws IOException {
this(file, mode, Default_Buffer_Size);
}
public BufferedRandomAccessFile(String name, String mode)
throws IOException {
this(name, mode, Default_Buffer_Size);
}
public BufferedRandomAccessFile(File file, String mode, int size)
throws IOException {
super(file, mode);
init(size);
}
public BufferedRandomAccessFile(String name, String mode, int size)
throws FileNotFoundException {
super(name, mode);
init(size);
}
//对内置缓存区进行初始化
private void init(int size) {
if (size < Default_Buffer_Size) {
size = Default_Buffer_Size;
} else if (size > MAX_BUFFER_SIZE) {
size = MAX_BUFFER_SIZE;
}
buffer = new byte[size];
}
//将缓存区中的数据同步写出到存储介质中。
public void sync() throws IOException {
if (syncNeeded_) {
//将内置缓存区中的数据写入
flush();
//将文件通道内未写入磁盘的数据强制写入到磁盘中,传入的参数表示是否将文件元信息写入到磁盘之上。
getChannel().force(true);
syncNeeded_ = false;
}
}
// close前将缓存区刷新一次防止缓存区中有未写入的数据,然后将缓存区置为null,调用父类的close方法释放资源。
public void close() throws IOException {
this.flush();
this.buffer = null;
super.close();
}
//将缓存区中内容写入存储介质中
public void flush() throws IOException {
this.flushBuffer();
}
//将缓存中内容写入存储介质之中
private void flushBuffer() throws IOException {
if (hasDatas) {
if (diskPos_ != lo_)
super.seek(lo_);
int len = (int) (cPos - lo_);
super.write(buffer, 0, len);
diskPos_ = cPos;
hasDatas = false;
}
}
//向缓存区中填充数据。返回实际填充了多少字节的数据。
private int fillBuffer() throws IOException {
int nextChar = 0;
int nChars = buffer.length;
//通过一个循环,向缓存区中填充数据,直至将缓存区填满或者文件读到末尾。
while (nChars > 0) {
int n = super.read(buffer, nextChar, nChars);
if (n < 0)
break;
nextChar += n;
nChars -= n;
}
if ((nextChar < 0) && (isEOF = (nextChar < buffer.length))) {
//将为缓存区中未填充到的部分全用-1初始化。
Arrays.fill(buffer, nextChar, buffer.length, (byte) 0xff);
}
diskPos_ += nextChar;
return nextChar;
}
//跳过指定的字节数
public void seek(long pos) throws IOException {
if (pos >= hi_ || pos < lo_) {
flushBuffer();
lo_ = pos & BuffMask_;
maxHi_ = lo_ + (long) buffer.length;
if (diskPos_ != lo_) {
super.seek(lo_);
diskPos_ = lo_;
}
int n = fillBuffer();
hi_ = lo_ + (long) n;
} else {
if (pos < cPos) {
flushBuffer();
}
}
cPos = pos;
}
public long getFilePointer() {
return cPos;
}
public long length() throws IOException {
return Math.max(cPos, super.length());
}
public int read() throws IOException {
if (cPos >= hi_) {
if (isEOF)
return -1;
seek(cPos);
if (cPos == hi_)
return -1;
}
byte res = buffer[(int) (cPos - lo_)];
cPos++;
return ((int) res) & 0xFF;
}
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public int read(byte[] b, int off, int len) throws IOException {
if (cPos >= hi_) {
if (isEOF)
return -1;
seek(cPos);
if (cPos == hi_)
return -1;
}
len = Math.min(len, (int) (hi_ - cPos));
int buffOff = (int) (cPos - lo_);
System.arraycopy(buffer, buffOff, b, off, len);
cPos += len;
return len;
}
public void write(int b) throws IOException {
if (cPos >= hi_) {
if (isEOF && hi_ < maxHi_) {
hi_++;
} else {
seek(cPos);
if (cPos == hi_) {
hi_++;
}
}
}
buffer[(int) (cPos - lo_)] = (byte) b;
cPos++;
hasDatas = true;
syncNeeded_ = true;
}
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
public void write(byte[] b, int off, int len) throws IOException {
while (len > 0) {
int n = writeAtMost(b, off, len);
off += n;
len -= n;
hasDatas = true;
syncNeeded_ = true;
}
}
private int writeAtMost(byte[] b, int off, int len) throws IOException {
if (cPos >= hi_) {
if (isEOF && hi_ < maxHi_) {
hi_ = maxHi_;
} else {
seek(cPos);
if (cPos == hi_) {
hi_ = maxHi_;
}
}
}
len = Math.min(len, (int) (hi_ - cPos));
int buffOff = (int) (cPos - lo_);
System.arraycopy(b, off, buffer, buffOff, len);
cPos += len;
return len;
}
}
3.1、自定义的BufferedRandomAccessFile 性能对比
package com.xxx.bio;
import java.io.*;
public class BufferedRandomAccessFilefTest {
public static void main(String[] args) {
long startTime;
long endTime;
File source = new File("F:\\realMe手机文件备份\\照片视频\\VID20260217183653.mp4");
File target = new File("D:\\copy.mp4");
byte[] buffer = new byte[1024];
startTime = System.currentTimeMillis();
int len;
try (RandomAccessFile sourceFile = new RandomAccessFile(source, "rw");
RandomAccessFile targetFile = new RandomAccessFile(target, "rw")) {
while ((len = sourceFile.read(buffer)) != -1) {
targetFile.write(buffer, 0, len);
}
endTime = System.currentTimeMillis();
System.out.println("RandomAccessFile拷贝耗时" + (endTime - startTime)
+ "ms");
} catch (Exception e) {
}
startTime = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(target));) {
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
endTime = System.currentTimeMillis();
System.out.println("BufferedInputStream/BuffedOutputStream拷贝耗时"
+ (endTime - startTime) + "ms");
} catch (Exception e) {
}
startTime = System.currentTimeMillis();
try (BufferedRandomAccessFile sourceFile = new BufferedRandomAccessFile(
source, "rw");
BufferedRandomAccessFile targetFile = new BufferedRandomAccessFile(
target, "rw")) {
while ((len = sourceFile.read(buffer)) != -1) {
targetFile.write(buffer, 0, len);
}
endTime = System.currentTimeMillis();
System.out.println("BufferedRandomAccessFile拷贝耗时" + (endTime - startTime)
+ "ms");
} catch (Exception e) {
}
}
}
上述代码的执行结果如下(拷贝文件的大小为151,808,193 byte):