什么是IO流


IO流( Input Output Stream ),即输入输出流。

流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。

既然是流,那么当然就可以有多种传输方式,比如: 根据传输方式划分

根据传输方式划分

IO流根据传输方式,可划分为:

字节流

字节(Byte),指的是计算机的存储单位,计算机的文件都是以二进制的方式进行存储的,而字节可以表示它的存储计量。

既然字节可以表示二进制,那么字节流就是可以处理任意的计算机文件,比如:

  • 图片
  • 视频
  • 音乐

字符流

字符(word),指的是一个虚拟单位,表示一个字,如:

那么字符合字节对比就是:

与字节流不同的是,字节流可以处理任意计算机文件,但字符却只能处理单个或多个字,并且字节流也是可以处理字符的

IO的主要组成

主要为:

InputStream类
  • int read():读取数据
  • int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字节
  • int available():返回可读的字节数
  • void close():关闭流,释放资源
OutputStream类
  • void write(int b): 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲)
  • void write(byte b[], int off, int len): 将数组 b 中的从 off 位置开始,长度为 len 的字节写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流
Reader类
  • int read():读取单个字符
  • int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字符
  • int ready():是否可以读了
  • void close():关闭流,释放资源
Writer类
  • void write(int c): 写入一个字符
  • void write( char cbuf[], int off, int len): 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

根据操作对象划分

IO,是输入输出流,根据传输方式划分后,进一步就是根据操作对象划分

文件

文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。

FileInputStream的例子:

static void testFileInputStream() {
    int b; // 记录字节
    // 创建 InputStream 对象
    try {
        FileInputStream is = new FileInputStream("in.txt");
        while ((b = is.read()) != -1) System.out.print((char) b);
        System.out.println();
        is.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  
public static void main(String[] args) {
    testFileInputStream();
}

输出结果:

abc@06b526d36e0d:~/workspace$ java IO_test.java 
this is a test text.

FileReader的例子:

static void testFileReader() {
    int b; // 记录存储的字符(和字节不是一个概念!)
    try {
        FileReader fr = new FileReader("in.txt");
        while ((b = fr.read()) != -1)
            System.out.print((char) b);
        System.out.println();
        fr.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    // testFileInputStream();
    testFileReader();
}

输出结果:

abc@06b526d36e0d:~/workspace$ java IO_test.java 
this is a test text.

FileOutputStream的例子:

static void testFileOutputStream() {
    try {
        FileOutputStream fo = new FileOutputStream("out.txt");
        fo.write("this is a out test text.".getBytes()); // 需要获取字符串的字节数
        fo.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void main(String[] args) {
    // testFileInputStream();
    // testFileReader();
    testFileOutputStream();
}

FileWriter的例子:

static void testFileWriter() {
    try {
        FileWriter fo = new FileWriter("out.txt");
        fo.write("this is a out test text.".toCharArray()); // 需要获取字符串的字符数组
        fo.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    // testFileInputStream();
    // testFileReader();
    // testFileOutputStream();
    testFileWriter();
}

FileOutputStreamFileWriter 的例子就能明显的看出,字节流与字符流的差别(write写入的区别)

文件流还可以用于创建、删除、重命名文件等操作。FileOutputStream 和 FileWriter 构造函数的第二个参数可以指定是否追加数据到文件末尾。

示例代码:

// 创建文件
File file = new File("test.txt");
if (file.createNewFile()) {
    System.out.println("文件创建成功");
} else {
    System.out.println("文件已存在");
}

// 删除文件
if (file.delete()) {
    System.out.println("文件删除成功");
} else {
    System.out.println("文件删除失败");
}

// 重命名文件
File oldFile = new File("old.txt");
File newFile = new File("new.txt");
if (oldFile.renameTo(newFile)) {
    System.out.println("文件重命名成功");
} else {
    System.out.println("文件重命名失败");
}

数组(内存)

通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流

数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。

ByteArrayInputStream的例子:

static void testByteArrayInputStream() {
    try {
        // 创建 ByteArrayInputStream 对象
        InputStream is = new BufferedInputStream(
                new ByteArrayInputStream(
                        "this is a ByteArray test text.".getBytes(StandardCharsets.UTF_8)));

        // 定义一个字节数组用于存储读取到的数据
        byte[] flush = new byte[1024];

        // 定义一个变量存储每次读取的字节数
        int len = 0;

        // 循环读取字节数组中的数据,并输出到控制台
        while (-1 != (len = is.read(flush))) {
            System.out.println(new String(flush, 0, len));
        }

        is.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}


public static void main(String[] args) {
    testByteArrayInputStream();
}
关于ByteArrayInputStream外面套一层BufferedInputStream流:
提升读取性能
  • 缓冲机制作用BufferedInputStream内部设有缓冲区。当把ByteArrayInputStream作为其参数构建实例后,BufferedInputStream会按一定的块大小从ByteArrayInputStream对应的字节数组中预读取数据并存入缓冲区。后续再从流中读取数据时,优先从缓冲区获取,只有缓冲区数据耗尽才会再次从ByteArrayInputStream里读取填充缓冲区。这样避免了频繁地逐字节从字节数组(也就是ByteArrayInputStream所关联的数据源)读取,在大量数据读取场景下,能显著加快读取速度。例如,要多次读取一个较大字节数组中的内容,若直接使用ByteArrayInputStream每次读取可能效率低,而通过BufferedInputStream包装后利用其缓冲机制,整体读取性能会改善很多。
适配通用流处理逻辑
  • 统一接口便利:在很多程序里,需要处理多种不同类型的输入流,像文件输入流、网络输入流等。BufferedInputStream提供了一种相对统一的、带缓冲功能的读取数据的方式。将ByteArrayInputStream包装进BufferedInputStream,能让代码按照一致的逻辑去处理基于字节数组的数据以及其他来源的数据。比如在一个方法中参数接收的是InputStream类型,不管实际传入的是字节数组对应的流还是别的类型输入流,都可以先包装成BufferedInputStream来统一后续的数据读取操作,便于代码复用和维护,使程序在面对不同流数据源时逻辑更加清晰简洁。
便于功能拓展与组合
  • 功能叠加优势:通过先将ByteArrayInputStream包装成BufferedInputStream,后续可以更方便地在此基础上进一步和其他输入流相关类进行组合,实现更多功能拓展。例如,可以接着将这个包装后的流再传递给DataInputStream,进而实现按照基本数据类型(如intfloat等)来读取字节数组里的数据,构建起更复杂、功能更丰富的流处理链路,满足多样化的业务需求。
提供更好的流控制特性
  • 额外控制能力BufferedInputStream自身具备一些额外的控制流读取的特性,比如可以通过markreset方法来标记流中的某个位置并能回退到该位置重新读取数据。当以ByteArrayInputStream为基础进行包装后,就能在字节数组数据读取过程中利用这些特性,实现更灵活的流操作逻辑,像多次解析字节数组中某个特定区间的数据等情况就会很有用。

ByteArrayOutputStream的例子:

static void testByteArrayOutputStream() {
    try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        // 定义一个字节数组,存储写入内存中的数据
        byte[] info = "this is byteArray test text.".getBytes();

        // 向内存缓存区写入数据
        bos.write(info, 0, info.length);

        // 读取内存缓存中的字节数组
        byte[] dest = bos.toByteArray();
        for (byte b : dest)
            System.out.println((char) b);

        bos.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    testByteArrayOutputStream();
}

管道

Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。

一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。

例子如下:

static void testPipedStream() throws IOException {
    // 创建 PipedOutputStream 和 PipedInputStream 对象
    final PipedOutputStream pipedOutputStream = new PipedOutputStream();
    final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

    // 创建线程,向 PipedOutputStream 写入数据
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                pipedOutputStream.write("this is Piped test text.".getBytes(StandardCharsets.UTF_8));
                pipedOutputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    // 创建线程,向 PipedInputStram 读取数据
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                int b;
                while (-1 != (b = pipedInputStream.read()))
                    System.out.print((char) b);
                System.out.println();
                pipedInputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    thread1.start();
    thread2.start();
}

public static void main(String[] args) throws IOException {
    testPipedStream();
}

基本数据类型

基本数据类型输入输出流水一个字节六个,该流主要增加了【可以读写基本数据类型】这个特性。

简单示例:

static void testDataInputStream() throws IOException {
    // 创建一个 DataOutputStream 对象,从文件中写入数据
    DataOutputStream dos = new DataOutputStream(new FileOutputStream("das.txt"));

    // 创建一个 DataInputStream 对象,从文件中读取数据
    DataInputStream dis = new DataInputStream(new FileInputStream("das.txt"));

    // 写入boolean类型
    dos.writeBoolean(false);

    // 读取一个字节,将其转换为 boolean 类型
    boolean bb = dis.readBoolean();
    System.out.println("读取到的布尔类型: " + bb);

    // 写入 int 类型
    dos.writeInt(123);

    // 读取四个字节,将其转换为 int 类型
    int i = dis.readInt();
    System.out.println("读取到的int类型: " + i);

    dis.close();
    dos.close();
}

public static void main(String[] args) throws IOException {
    testDataInputStream();
}

打开文件可以看到神♂秘♂编♂码♂:

使用 ObjectInputStream 或 ObjectOutputStream 可以读写对象:

static void testObjectStreamObject() {
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("das.txt"));

        Person person = new Person(18, "zxb", 1);

        oos.writeObject(person);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("das.txt"));

        Person p = (Person) ois.readObject();
        System.out.println(p.getName());
        System.out.println(p.getAge());

        ois.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void main(String[] args) throws IOException {
    testObjectStreamObject();
}

注: Person 对象需要实现序列化

缓冲

缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高

以下是一个使用 BufferedInputStream 读取文件的示例代码:

static void testBufferedInputStream() throws UnsupportedEncodingException, IOException {
    // 创建一个 BufferedInputStream 对象,从文件中读取数据
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("in.txt"));

    // 创建一个字节数组, 作为缓存区
    // byte[] buffer = new byte[1024];
    byte[] buffer = new byte[8];

    // 读取文件中的数据, 将其存储到缓存区中
    int bytesRead;
    while (-1 != (bytesRead = bis.read(buffer))) {
        // 输出缓存数据
        System.out.println(new String(buffer, 0, bytesRead));
    }
    bis.close();
}

public static void main(String[] args) throws IOException {
    testBufferedInputStream();
}

输出结果:

this is 
a test t
ext.

BufferedOutputStream 的使用示例:

static void testBufferedOutputStream() throws IOException {
    // 创建一个 BufferedOutputStream 对象
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("in.txt"));

    // 创建一个字节数组, 作为缓冲区
    byte[] buffer = new byte[8];

    // 将数据写入到文件中
    String data = "this is a buffered text.";
    buffer = data.getBytes();
    bos.write(buffer);

    // 刷新缓存区
    bos.flush();

    // 关闭 BufferedOutputStream
    bos.close();
}

public static void main(String[] args) throws IOException {
    testBufferedOutputStream();
}

代码解释:

上述代码中,首先创建了一个 BufferedOutputStream 对象,用于将数据写入到文件中。然后创建了一个字节数组作为缓存区,将数据写入到缓存区中。写入数据的过程是通过 write() 方法实现的,将字节数组作为参数传递给 write() 方法即可。
最后,通过 flush() 方法将缓存区中的数据写入到文件中。在写入数据时,由于使用了 BufferedOutputStream,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。

BufferedReader 读取文件的示例代码:

static void testBufferedReader() throws IOException {
    // 创建一个 BufferedReader 对象
    BufferedReader br = new BufferedReader(new FileReader("in.txt"));

    // 读取文件中的数据, 存储到字符串中
    String line;
    while ((line = br.readLine()) != null) System.out.println(line);

    // 关闭
    br.close();
}

public static void main(String[] args) throws IOException {
    testBufferedReader();
}

BufferedWriter 写入文件的示例代码:

static void testBufferedWriter() throws IOException {
    BufferedWriter bw = new BufferedWriter(new FileWriter("in.txt"));

    bw.write("this is a buffered writer text.");

    bw.flush();
    bw.close();

}

public static void main(String[] args) throws IOException {
    testBufferedWriter();
}

代码解释:

上述代码中,首先创建了一个 BufferedWriter 对象,用于将数据写入到文件中。然后使用 write() 方法将数据写入到缓存区中,写入数据的过程和使用 FileWriter 类似。需要注意的是,使用 BufferedWriter 写入数据时,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中
最后,通过 flush() 方法将缓存区中的数据写入到文件中,并通过 close() 方法关闭 BufferedWriter,释放资源。

打印

Java 的打印流是一组用于打印输出数据的类,包括 PrintStream 和 PrintWriter 两个类。

System.out.println("test!");

PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 print()/println() 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。

StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
    pw.println("test");
}
System.out.println(buffer.toString());

对象序列化

序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。

// 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据
ByteArrayOutputStream buffer = new ByteArrayOutputStream();

// 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
    
    // 使用 writeUTF() 方法将字符串 "test" 写入到缓冲区中
    output.writeUTF("test");
}

// 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组,并输出到控制台
System.out.println(Arrays.toString(buffer.toByteArray()));

与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。

try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
        new File("Person.txt")))) {
    String s = input.readUTF();
}

这段代码主要使用了 Java 的 ByteArrayOutputStream 和 ObjectOutputStream 类,将字符串 "test" 写入到一个字节数组缓冲区中,并将缓冲区中的数据转换成字节数组输出到控制台。

具体的执行过程如下:

  • 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据。
  • 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联。
  • 使用 writeUTF() 方法将字符串 "test" 写入到缓冲区中。
  • 当 try-with-resources 语句执行完毕时,会自动调用 output 的 close() 方法关闭输出流,释放资源。
  • 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组。
  • 使用 Arrays.toString() 方法将字节数组转换成字符串,并输出到控制台。

转换

InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。

// 创建一个 InputStreamReader 对象 isr,使用 FileInputStream 对象读取文件 demo.txt 的内容并将其转换为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));

// 创建一个字符数组 cha,用于存储读取的字符数据,其中 1024 表示数组的长度
char[] cha = new char[1024];

// 使用 read() 方法读取 isr 中的数据,并将读取的字符数据存储到 cha 数组中,返回值 len 表示读取的字符数
int len = isr.read(cha);

// 将 cha 数组中从下标 0 开始、长度为 len 的部分转换成字符串,并输出到控制台
System.out.println(new String(cha, 0, len));

// 关闭 InputStreamReader 对象 isr,释放资源
isr.close();

这段代码主要使用了 Java 的 InputStreamReader 和 FileInputStream 类,从文件 demo.txt 中读取数据并将其转换为字符流,然后将读取的字符数据存储到一个字符数组中,并输出转换成字符串后的结果到控制台。

OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。

// 创建一个 File 对象 f,表示文件 test.txt
File f = new File("test.txt");

// 创建一个 OutputStreamWriter 对象 out,使用 FileOutputStream 对象将数据写入到文件 f 中,并将字节流转换成字符流
Writer out = new OutputStreamWriter(new FileOutputStream(f));

// 使用 write() 方法将字符串 "test!!" 写入到文件 f 中
out.write("test!!");

// 关闭 Writer 对象 out,释放资源
out.close();

使用转换流可以方便地在字节流和字符流之间进行转换。在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作。