Administrator
发布于 2025-03-23 / 0 阅读
0
0

JAVA IO-202509120159

JAVA IO-202509120159

JAVA IO

IO分类:

字节流

字符流

字节流和字符流的区别

字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,

如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。)

字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是

特殊的二进制文件,使用了某种编码,人可以阅读)。

字节是给计算机看的,字符是给人看的。

字节流

字符流

设计模式

JAVA IO 使用装饰者模式来实现。

装饰者模式:如下图,InputStream 在这里是一个组件(Component),那么FileInp

utStream 则为具体组件(ConcreteComponent),而FilterInputStream 则是一个装

饰者,它组合了一个组件,这样装饰者就可以装饰组件或者具体组件了。BufferedInpu

tStream 是装饰者的具体实现,真正完成了装饰(比如这里BufferedInputStream是添加

了缓冲功能)。

实现一个具有缓冲功能的字节流对象时,只需在FileInputStream上面套一层BufferedInput

Stream就行了。

FileInputStream fileInputStream = new FileInputStream(filePath);

字节输入流InputStream

InputStream抽象类

BufferedInputStream bufferedInputStream = new

BufferedInputStream(fileInputStream);

/**

  • 主要方法
  • */

    // 从输入流中读取数据的下一字节,byte值在0到255之间作为int返回;返回-1表示到达流的

    末尾

    public abstract int read();

    // 从输入流中读取一定数量的字节到byte[]中。如果b长度为0,则不读取并返回0;如果流位

    于末尾,返回-1;否则返回读取到的字节数量。

    public int read(byte[] b);

    // 上面的read(byte[] b)调用的就是此方法,从off位置读取最多len到字节数组中。

    // 读取前调用Objects.checkFromIndexSize(off, len, b.length)方法判断是否超出

    范围。

    public int read(byte[] b, int off, int len)

    // 从输入流中读取所有剩余字节,调用的是readNBytes(Integer.MAX_VALUE)方法

    public byte[] readAllBytes()

    // 从输入流中读取最多指定数量的字节

    public byte[] readNBytes(int len)

    // 将请求的字节数从输入流读取到给定的字节数组

    public int readNBytes(byte[] b, int off, int len)

    // 跳过指定个数的字节不读取, 返回实际跳过的字节数

    public long skip(long n)

    // 返回可读的字节数

    public int available()

    // 关闭此输入流并释放与流关联的任何系统资源

    public void close()

    // 读取输入流中的所有字节到outputStream中

    public long transferTo(OutputStream out)

    /**

  • 部分方法解析
  • */


    // 从off位置读取最多len到字节数组中

    public int read(byte[] b, int off, int len) throws IOException {

    Objects.checkFromIndexSize(off, len, b.length); // 校验是否超出范围

    if (len == 0) {

    return 0; // 读取长度为0,直接返

    }

    int c = read(); // 预先读取下一次字节

    if (c == -1) {

    return -1; // -1表示已达流末尾,

    直接返回

    }

    b[off] = (byte)c; // int转byte,存储

    int i = 1;

    try {

    for (; i < len ; i++) { // for循环读取,直至到

    达流末尾

    c = read();

    if (c == -1) {

    break;

    }

    b[off + i] = (byte)c;

    }

    } catch (IOException ee) {

    }

    return i;

    }

    // 要分配数组的最大大小

    private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    // 默认缓冲区大小

    private static final int DEFAULT_BUFFER_SIZE = 16384;

    // 从输入流中读取最多指定数量的字节

    public byte[] readNBytes(int len) throws IOException {

    if (len < 0) { // 长度不能小于0

    throw new IllegalArgumentException("len < 0");

    }

    List bufs = null; // 分段字节数据集

    byte[] result = null;

    int total = 0;

    int remaining = len;

    int n;

    do {

    // 循环读取,每次最多读取默认缓冲区大小的

    byte[] buf = new byte[Math.min(remaining,

    DEFAULT_BUFFER_SIZE)];

    int nread = 0;

    // 循环,每次把单个字节数据读满,或者读到流末尾

    while ((n = read(buf, nread,

    Math.min(buf.length - nread, remaining))) > 0) {

    nread += n;

    remaining -= n;

    }

    // 判断有没有超过最大缓冲区数据量

    if (nread > 0) { // 单个字节数据已读取字节数

    大于0

    if (MAX_BUFFER_SIZE - total < nread) { // 超出则直接抛异

    throw new OutOfMemoryError("Required array size too

    large");

    }

    if (nread < buf.length) { // 单个缓冲字节数组没装满,

    复制到新数组

    buf = Arrays.copyOfRange(buf, 0, nread);

    }

    total += nread; // 累加合计已读

    if (result == null) { // 第一次读取

    result = buf;

    } else {

    if (bufs == null) { // 第二次读取

    bufs = new ArrayList<>();

    bufs.add(result);

    }

    bufs.add(buf); // 后续读取。。。

    }

    }

    // if the last call to read returned -1 or the number of bytes

    // requested have been read then break

    FileInputStream

    FileInputStream继承了InputStream,主要体现在重写了read()等方法,这些方法直接通过调用native本

    地方法进行文件流内容的读取。

    } while (n >= 0 && remaining > 0); // 最后read()没有返回-1

    并且有剩余

    if (bufs == null) { // 说明只读了一次,处理后直

    接返回

    if (result == null) {

    return new byte[0];

    }

    return result.length == total ?

    result : Arrays.copyOf(result, total);

    }

    result = new byte[total]; // 数据处理,组合到result

    int offset = 0;

    remaining = total;

    for (byte[] b : bufs) {

    int count = Math.min(b.length, remaining);

    System.arraycopy(b, 0, result, offset, count);

    offset += count;

    remaining -= count;

    }

    return result;

    public class FileInputStream extends InputStream {

    private static final int DEFAULT_BUFFER_SIZE = 8192;

    private final FileDescriptor fd; // 文件描述符

    /**

  • The path of the referenced file
  • (null if the stream is created with a file descriptor)
  • */

    private final String path;

    private volatile FileChannel channel;

    private final Object closeLock = new Object();

    private volatile boolean closed;

    字节输出流OutputStream

    常见类使用

    File相关

    }

    /**

  • 主要方法
  • */

    // 写入一个字节,可以看到这里的参数是一个 int 类型,对应上面的读方法,int 类型的

    32 位,只有低 8 位才写入,高 24 位将舍弃。

    public abstract void write(int b)

    // 将数组中的所有字节写入,实际调用的是write(byte b[], int off, int len)方法。

    public void write(byte b[])

    // 将 byte 数组从 off 位置开始,len 长度的字节写入

    public void write(byte b[], int off, int len)

    // 强制刷新,将缓冲中的数据写入; 默认是空实现,供子类覆盖

    // 如果输出流的实现缓冲了以前写入的任何字节,则会立即将此类字节写入其预期目标

    public void flush()

    // 关闭输出流,流被关闭后就不能再输出数据了; 默认是空实现,供子类覆盖

    public void close()

    /**

  • File类可以用于表示文件或者目录的信息
  • 下面为递归的列出一个目录下所有文件
  • */

    public static void listAllFiles(File dir) {

    if (dir == null || !dir.exists()) {

    return;

    }

    if (dir.isFile()) {

    System.out.println(dir.getName());

    return;

    }

    for (File file : Objects.requireNonNull(dir.listFiles())) {

    listAllFiles(file);

    }

    }

    字节流相关

    逐行输出文本内容

    Serializable序列化

    /**

  • 复制文件
  • */

    public static void copyFile(String src, String dist) throws

    IOException {

    FileInputStream in = new FileInputStream(src);

    FileOutputStream out = new FileOutputStream(dist);

    byte[] buffer = new byte[20 * 1024];

    // read() 最多读取 buffer.length 个字节

    // 返回的是实际读取的个数

    // 返回 -1 的时候表示读到 eof,即文件尾

    while (in.read(buffer, 0, buffer.length) != -1) {

    out.write(buffer);

    }

    in.close();

    out.close();

    }

    public static void readFileContent(String filePath) throws IOException

    {

    FileReader fileReader = new FileReader(filePath);

    BufferedReader bufferedReader = new BufferedReader(fileReader);

    String line;

    while ((line = bufferedReader.readLine()) != null) {

    System.out.println(line);

    }

    // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象

    // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方

    // 因此只要一个 close() 调用即可

    bufferedReader.close();

    }

    序列化的类需要实现 Serializable 接口,如果不去实现它的话而进行序列化,会抛出异常java.

    io.NotSerializableException 。

    BIO

    阻塞式同步IO

    阻塞IO 和非阻塞IO 

    这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,

    那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准

    备好了)

    同步IO 和非同步IO 

    这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没

    有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好

    让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

    BIO通信方式

    多线程BIO处理

    让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模

    型:

    --javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    上面的代码中,socket.accept()/socket.read()/socket.write()都是同步阻塞的(不知道能不能

    读,能不能写,就算后面知道了不能读写,也不能进行有效的中断),当一个连接在处理IO的时

    候,系统是阻塞的,所以使用多线程,让CPU去处理更多的事情。但创建线程存在开销,多线程

    的上下文切换也有资源消耗。面对大量连接会导致系统负载过载,此时需要一个更高效的IO模

    型。

    NIO

    同步非阻塞IO

    流与块

    I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

    面向流的 I/O 一次处理一个字节数据: 一个输入流产生一个字节数据,一个输出流消费一个字节数

    {

    ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//

    线程池

    ServerSocket serverSocket = new ServerSocket();

    serverSocket.bind(8088);

    while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来

    Socket socket = serverSocket.accept();

    executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程

    }

    class ConnectIOnHandler extends Thread{

    private Socket socket;

    public ConnectIOnHandler(Socket socket){

    this.socket = socket;

    }

    public void run(){

    while(!Thread.currentThread.isInturrupted()&&!socket.isClosed())

    {死循环处理读写事件

    String someThing = socket.read()....//读取数据

    if(someThing!=null){

    ......//处理数据

    socket.write()....//写数据

    }

    }

    }

    }

    据。

    面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。

    通道和缓冲区

    通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据

    通道的类型有:

    FileChannel: 从文件中读写数据;

    DatagramChannel: 通过 UDP 读写网络中数据;

    SocketChannel: 通过 TCP 读写网络中数据;

    ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一

    个 SocketChannel。

    发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都

    要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

    缓冲区的类型有:

    ByteBuffer

    CharBuffer

    ShortBuffer

    IntBuffer

    LongBuffer

    FloatBuffer

    DoubleBuffe

    选择器

    NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询

    的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

    通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入

    阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

    应用层不再通过阻塞模式或者非阻塞模式直接询问操作系统“事件有没有发生”,而是由Selector代

    其询问。

    // 1.创建选择器

    Selector selector = Selector.open();

    // 2.将通道注册到选择器上

    ServerSocketChannel ssChannel = ServerSocketChannel.open();

    ssChannel.configureBlocking(false);// 关闭阻塞

    ssChannel.register(selector, SelectionKey.OP_ACCEPT);

    NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进

    行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操

    作,没有必要开启多线程。

    应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel

    配置非阻塞也没有意义。普通文件IO读写永远能立即知道结果(要么成功要么失败),而网络I

    O可能卡在半路。

    套接字 Channel通过继承AbstractSelectableChannel熟悉实现通道可以注册到选择器。

    AIO

    异步IO则是采用“订阅-通知”模式: 即应用程序向操作系统注册IO监听,然后继续做自己的事情。当

    操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数:


    本文整理自实践经验,如有问题欢迎交流讨论。


    评论