Java IO

IO

IO 是指 Input/Output,即输入和输出。以内存为中心:

  • Input 只从外部读入数据到内存。例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output 指把数据从内存输出到外部。例如,把数据从内存写入文件,把数据从内存输出到网络等等。

为什么要把数据读到内存才能处理数据?
因为代码是在内存中运行的,数据也必须读到内存。

InputStream/OutputStream

IO 流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为 IO 流。

IO 流以 byte为最小单位,因此也称为 字节流。例如,我们要从磁盘读入一个文件,包含 6 个字节,就相当于读入了 6 个字节的数据。
在 Java 中,InputStream代表输入字节流,OutputStream代表输出字节流,这是最基本的两种 IO 流。

Reader/Writer

如果我们需要读写的是字符,并且字符不全是单字节表示的 ASCII 字符,那么,按照 char来读写显然更方便,这种流称为 字符流。
Java 提供了 Reader和 Writer表示字符流,字符流传输的最小数据单位是 char。
究竟使用 Reader 还是 InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用 InputStream,如果数据源是文本,使用 Reader 更方便一些。

同步和异步

同步 IO 是指,读写 IO 时代码必须等待数据返回后才继续执行后续代码。

  • 优点是代码编写简单
  • 缺点是 CPU 执行效率低
    异步 IO 是指,读写 IO 时仅发出请求,然后立即执行后续代码。
  • 优点是 CPU 执行效率高
  • 缺点是代码编写复杂

Java 标准库的包 java.io提供了同步 IO,而 java.nio提供了异步 IO。
上面讨论的 InputStream,OutputStream,Reader和 Writer都是同步 IO 的抽象类,对应的具体实现类,以文件为例,有 FileInputStream,FileOutputStream,FileReader 和 FileWriter。

File

在计算机系统中,文件是非常重要的存储方式。
Java 标准库 java.io提供了 File对象来操纵文件和目录。
构造一个 File 对象

File f = new File("index.html");

System.out.println(f);

文件和目录

File 对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个 File 对象,即使传入的文件或目录不存在,代码也不会出错。因为构造一个 File 对象,并不会导致任何磁盘操作。只有当我们调用 File 对象的某些方法时,才真正进行磁盘操作。

关于 isFile() 或 isDirectory() 方法始终返回 false问题,检查文件路径是否正确,读取路径与实际文件路径不一致,导致读取不到,因此返回 false。

InputStream

InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。
FileInputStream是 InputStream的一个子类。从文件流中读取数据。

package com.chen.file;

import java.io.*;

public class TestFile {
    public static void main(String[] args) throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("index.txt");
            while (true) {
                int n = input.read();
                if (n == -1) {
                    break;
                }
                System.out.println(n);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (input != null) {
                input.close();
            }
        }
    }
}

注意:在计算机中,类似文件,网络端口这些资源,都是由操作系统统一管理。应用程序在运行过程中,如果打开了一个文件进行读写,完成后要及时关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其它应用程序的运行。
InputStream和 OutputStream都是通过 close()方法来关闭流。关闭流就会释放对应的底层资源。

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b)读取若干字节并填充到 byte[]数组,返回读取的字节数。
  • int read(byte[] b,int off,int len):指定 byte[]数组的偏移量和最大填充数。

利用上述方法一次读取多个字节时,需要先定义一个 byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区,但不会超过缓冲区的大小。read()方法的返回值不再是字节的 int值,而是返回实际读取了多少字节。如果返回 -1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:

package com.chen.file;

import java.io.*;

public class TestFile {
    public static void main(String[] args) throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("index.txt");
            //定义 1000 字节的一个缓冲区
            byte[] buffer = new byte[1000];
            int n;
            while (true) {
                n = input.read(buffer);
                if (n == -1) {
                    break;
                }
                System.out.println(n);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (input != null) {
                input.close();
            }
        }
    }
}

阻塞

在调用 InputStream的 read()方法读取数据时,我们说 read()方法是阻塞的。它的意思是,对于下面的代码:

int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;

执行到第二行代码时,必须等 read()方法返回后才能继续。因为读取 IO 流相比执行普通代码,速度会慢很多,因此,无法确定 read()方法调用到底要花费多长时间。

OutputStream

OutputStream是 Java 标准库提供的最基本的输出流。
OutputStream也提供了 close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个 flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有 flush()?
因为向磁盘,网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立即写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个 byte[]数组),等到缓冲区满了,再一次性写入文件或者网络。
对于很多 IO 设备来说,一次写一个字节和一次写 1000 个字节,花费的时间几乎是完全一样的,所有 OutputStream有个 flush()方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个 flush()方法,因为缓冲区写满了会自动调用它,并且,在关闭资源之前,也会调用一次。

Reader

Reader是 Java 的 IO 库提供的另一个输入流接口。和 InputStream的区别是,InputStream是一个字节流,即以 byte为单位读取,而 Reader是一个字符流,即以 char为单位读取。
java.io.Reader是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
这个方法读取字符流的下一个字符,并返回字符表示的 int,范围是 0 ~ 65535。如果已读到末尾,返回 -1。

FileReader
FileReader是 Reader的一个子类,它可以打开文件并获取 Reader。
package com.chen.file;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class TestFile {
    public static void main(String[] args) throws IOException {
        Reader reader = new FileReader("./index.txt", StandardCharsets.UTF_8);//避免乱码
        for (;;) {
            int n = reader.read();
            if (n == -1) {
                break;
            }
            System.out.println((char)n); //打印 char
        }

        reader.close();

    }
}

Writer

Reader是带编码转换器的 InputStream,它把 byte转换为 char,而 Writer就是带编码转换器的 OutputStream,它把 char转换为 byte并输出。
Writer是所有字符输出流的超类,它提供的方法主要有:

  • 写入一个字符(0~65535):void write(int c);
  • 写入字符数组的所有字符:void write(char[] c);
  • 写入 String 表示的所有字符:void write(String s);
    FileWriter
    FileWriter就是向文件中写入字符流的 Writer。它的使用方法和 FileReader类似:
package com.chen.file;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class TestFile {
    public static void main(String[] args) throws IOException {
        try (Writer writer = new FileWriter("index.txt", StandardCharsets.UTF_8)) {
            writer.write('H'); //写入单个字符
            writer.write('\n');
            writer.write("Hello\n".toCharArray()); //写入 char[]
            writer.write("hello 你好,世界"); //写入 String
        }

    }
}

使用 Files

从 Java 7 开始,提供了 Files这个工具类,能极大地方便我们读写文件。

byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));
// 写入二进制文件:
byte[] data = ...
Files.write(Path.of("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Path.of("/path/to/file.txt"), lines);

特别注意:Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个 G 的大文件。读写大文件仍然需要使用文件流,每次只读写一部分文件内容。

参考文章

posted @   codeChen1838  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示