10、FileInputStream和RandomAccessFile的源码分析和使用方法详细分析(windows操作系统,JDK8)

一、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):