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关系图,如下所示:

clipboard

  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文件,如下所示:

clipboard

通过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());
   }
}

程序运行结果,如下所示:

clipboard

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关系图,如下所示:

clipboard

  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个字节
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐