用 Java 的 IO 流进行读写文件操作

时间:2022-11-17 14:15:57

前言

在计算机领域里 IO,有时也写作 ​​I/O​​,是​​Input / Output​​的缩写,也就是输入和输出。这里的输入和输出是指不同系统之间的数据输入和输出,比如读写文件数据,读写网络数据等等。

本文内容大纲如下:

用 Java 的 IO 流进行读写文件操作

Java 有哪些IO框架

Java 中有三代 IO 框架,分别是第一代的同步阻塞 IO (也叫 BIO, Blocking IO),第二代的NIO ,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。第三代 NIO2 有的地方也叫 AIO,即Async IO,进一步支持了异步IO。

这些 IO 框架都是针对文件的,网络通信同样属于 IO 行为,但是被 Java 单独放在了 ​​java.net​​ 包下,不在这里说的 IO 体系内。

这个教程中我们来学习 Java IO 体系中最简单和易于理解的同步阻塞 IO,后面有了这里的知识积累后再去进一步学习 NIO 和 AIO。

BIO 简介

同步阻塞 IO 即 BIO(blocking IO),指的主要是传统的 ​​java.io​​ 包,它基于流模型实现。​​java.io​​ 包提供了我们最熟知的一些 IO 功能,比如 File 对象提供的文件和目录操作,还有一大块就是通过输入输出流读写文件等。

BIO 交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在完成之前,线程会一直阻塞在那里。多个 IO 调用的执行顺序是线性顺序。不过 BIO 的优点是代码比较简单、直观,虽然不适合在高并发场景下使用,但足够应对普通场景,同时也更容易学习和掌握。

IO 流

IO 流是 Java IO 中的核心概念。流是在概念上表示无穷无尽的数据流。IO 流连接到数据源或数据的目的地,连接到数据源的叫输入流,连接到数据目的地的叫输出流。 Java 程序不能直接从数据源读取和向数据源写入,只能借助 IO 流从输入流中读取数据,向输出流中写入数据。

Java IO 中的流可以是基于字节的(读取和写入字节)也可以基于字符的(读取和写入字符),所以分为字节流和字符流,两类流根据流的方向都可以再细分出输入流和输出流。

  • 字节流
  • 输入字节流:InputStream
  • 输出字节流:OutputStream
  • 字符流
  • 输入字符流:Reader
  • 输出字符流:Writer

用 Java 的 IO 流进行读写文件操作

这里有一点可能容易让人迷惑的是,IO中的输入和输出指的是相对于程序的输入和输出,程序向外输出内容,会向输出流里写入,虽然写入操作看似是输入,但相对于程序本身而言它是在向外输出内容。所以程序写的是​​OutputStream​​ 读的是​​InputStream​​。

字节流

字节流主要操作字节数据或二进制对象。 字节流有两个核心抽象类:InputStream 和 OutputStream。所有的字节流类都继承自这两个抽象类。 ##

用 Java 的 IO 流进行读写文件操作

字符流

字符流操作的是字符,字符流有两个核心类:Reader 类和 Writer 。所有的字符流类都继承自这两个抽象类。

用 Java 的 IO 流进行读写文件操作

字节流、字符流怎么选择

字节流和字符流都有 ​​read()​​、​​write()​​、​​flush()​​、​​close()​​ 这样的方法,这决定了它们的操作方式近似。

  • 字节流的数据是字节(二进制对象)。主要核心类是 ​​InputStream​​ 类和 ​​OutputStream​​ 类。
  • 字符流的数据是字符,主要核心类是 ​​Reader​​ 类和 ​​Writer​​ 类。

所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。

所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。

字节流到字符流的转换可以使用 InputStreamReader 和 OutputStreamWriter。使用 InputStreamReader 可以将输入字节流转化为输入字符流,使用OutputStreamWriter可以将输出字节流转化为输出字符流。

下面我们通过一个读写文本文件的程序来演示一下字节流到字符流的转换以及 Java IO 的基本操作。

实践--Java IO 读写文本文件

Java IO 中的类非常多,对应的方法也很多,一一罗列会导致内容过于枯燥,所以我们写两个用 IO 流写文件和读文件的例子,来展示下怎么使用 IO 流读写文件。

下面主要使用的是 ​​FileOutputStream​​ / ​​FileInputStream​​ 把 ​​IO​​ 流绑定到 ​​File​​ 对象上,然后将这两个字节流通过​​OutputStreamReader​​ / ​​InputStreamReader​​ 转换为字符流,并设置字符编码,最后再用 ​​PrintWriter​​ / ​​BufferedReader​​ 给字节流增加缓冲更能,让程序能更方便地以行为单位操作 ​​IO​​ 流。

理解和掌握了这两个基本的用法后,其他 IO 流的使用也就不是什么难事儿了。

写文件示例程序

我们先来个写文件的示例小程序,在这个程序里面除了用到了 Java 文件、字节输出流等相关的知识外,还会用到我们前面在 Java 异常通关指南里讲过的帮助我们自动回收已打开资源的 ​​try-with-resource​​形式的异常处理,Java 交互式获取命令行输入的 ​​Scanner​​工具等。算是对我们专栏以前知识的一个实践应用和复习。

如果你对这些知识还有点生疏或者忘记了,也不用先着急回看,在这个示例程序的注释里会把这些知识点进行相关提示,下面也有对程序每个重要部分的详细解释,我们先来看例子。

这个例子里我们运行程序后,Java 程序会在命令行界面等待用户的输入,先让用户从命令行界面输入想要保存内容的文件的名字,再让用户输入内容。内容支持多行输入,直到遇到空行,程序会认为输入完毕,然后 Java 用用户指定的名字在项目目录下创建一个文件,最后把程序读取到的所有内容输入,写到文件里去。

package com.learnfile;

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

public class WriteToFilesAppMain {

private static final Scanner in = new Scanner(System.in);

public static void main(String[] args) throws IOException {
File targetFile = createFile();

writeToFile(targetFile);

System.out.println("程序执行结束");

}

private static void writeToFile(File targetFile) throws IOException {
// 使用 try with resource 自动回收打开的资源
try (
// 创建一个outputstream 建立一个从程序到文件的byte数据传输流
FileOutputStream fos = new FileOutputStream(targetFile);
// 创建一个可以使用outputstream的Writer,并制定字符集,这样程序就能一个一个字符地写入
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
// 使用PrintWriter, 可以方便的写入一行字符
PrintWriter pw = new PrintWriter(osw);
) {
System.out.println("输入的内容会实时写入文件,如果输入空行则结束");
while (true) {
String lineToWrite = in.nextLine().trim();
System.out.println("输入内容为:" + lineToWrite);
if (lineToWrite.trim().isBlank()) {
System.out.println("输入结束");
break;
} else {
pw.println(lineToWrite);
// 真正用的时候不要写一行就flush() 这里只是演示
pw.flush();
}
}
// 平时用的时候放在外面 flush
// pw.flush();
} catch (Exception ex) {
ex.printStackTrace();
}
}

private static File createFile() throws IOException {
System.out.println("请输入文件名:");
String fileName = in.nextLine().trim();
File f = new File("." + File.separator + fileName +".txt");
if (f.isFile()) {
System.out.println("目标文件存在,删除:" + f.delete());
}
System.out.println(f.createNewFile());
return f;
}
}

复制代码

这个示例程序里我们需要重点关注以下几个方面的知识点

  • 上面例程里使用了​​Scanner​​,以命令行交互的方式让我们能输入程序将要创建文件的名称和要往文件里写入的内容。
  • Java 的 ​​IO​​流在使用完成后需要统一调用​​close()​​方法把流关闭掉。​​IO​​流的关闭会让程序释放出它们占用的内存资源,而且字符流操作时使用了缓冲区,并在关闭字符流时会强制将缓冲区内容输出,如果不关闭流,则缓冲区的内容是无法输出的。
  • 如果想在不关闭流时,就将缓冲区的内容输出到文件,可以调用流的​​flush()​​方法强制清空流使用的缓冲区。
  • 上面的示例程序里我们使用了​​try-with-resource​​形式的异常处理,把资源的关闭交给了​​Java​​-- 在资源被使用完成后或者程序出现异常终止执行时都会由 Java 自动关闭在​​try-with-resource​​中打开的流资源。
  • 把字符串内容写入到文件的程序中,我们首先使用了 ​​FileOutputStream​​ 把目标文件绑定到字节输出流,再用 ​​OutputStreamWriter​​ 创建一个可以使用​​OutputStream​​的​​Writer​​,并指定其字符集为​​UTF_8​​,这样程序就能一个字符一个字符地写入文件啦。
  • 接下来程序在​​OutputStreamWriter​​ 字符流的基础上创建了 ​​PrintWriter​​,使用 ​​PrintWriter​​ 可以让程序方便地写入字符串,并且也可以通过它的 ​​println​​ 方法来自动处理换行符。

用 Java 程序完成文件的写入操作后,我们再来看看,给定一个文件,怎么用 Java 程序读取器中的内容。

读文本示例程序

我们先在要执行程序的目录下,添加一个测试用的名为 ​​file.txt​​ 的文本文件,如果是用​​Intelij IDEA​​ 这样的 IDE 工具执行程序的话,可以在项目根目录下添加这个文件。

创建好文件后,在文件里随便输入几行内容用于测试:

aaa
一二三
bbb
四五六
ccc
七八九
ddd
锟斤拷
烫烫屯屯
复制代码

接下来我们用 Java 程序读取这个文件里的内容,这次我们则是会用到 IO 输入流相关的几个类: ​​FileInputStream​​, ​​InputStreamReader​​, ​​BufferedReader​​等;他们的具体功能作用我写在了程序的注释里

package com.learnfile;

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

public class ReadStringFromFileAppMain {
private static final String SOURCE_FILE_NAME = "file1.txt";

public static void main(String[] args) {

File sourceFile = new File("." + File.separator + SOURCE_FILE_NAME);

ReadLineFromFile(sourceFile);
}
private static void ReadLineFromFile(File sourceFile) {
try (
// 建立从文件到程序的数据输入(input)流
FileInputStream fis = new FileInputStream(sourceFile);
// 用 InputStreamReader 包装 byte流,并指定字符编码,让它能够将读出的byte转为字符
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
// 增加缓冲功能,输入输出效率更高,并且可以一次读一行
BufferedReader reader = new BufferedReader(isr)
) {
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line.trim().toUpperCase());
}
// 还可以从reader里获取 Stream,然后通过流操作+Lambda的形式完成
// reader.lines().map(String::trim).map(String::toUpperCase).forEach(System.out::println);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
复制代码

上面这个示例程序会用 Java 的 IO 输入流从文件中每次读取一行,并把行内容应用 ​​toUpperCase()​​ 方法后输出到终端,程序里打开的流资源的方式仍然是交给​​try-with-resource​​机制自动处理,这里不再赘述。

和输出流一样,在读取文件内容的程序中我们也做了字节流到字符流的转换,先用 ​​FileInputStream​​ 把文件绑定到了字节流上,然后通过 ​​InputStreamReader​​ 把字节流转换为字符流,在这个过程中也是设置了字符编码,最后又用 ​​BufferedReader​​ 为字符流增加缓冲功能,这样读取的效率会更高一些,通过它程序可以一次读取文件的一整行。

通过 ​​BufferedReader​​ 的 ​​readLine()​​遍历或者 ​​lines()​​ 获取​​Stream​​ 后进行流处理,都可以用 ​​BufferedReader​​ 完成文件内容的遍历读取,上面在程序代码和注释里我们演示了两种遍历 ​​BufferedReader​​ 的方法。

总结

这篇文章我们介绍了 Java IO流里面的各种流,比较难记,常用的怎么把文件转成文件流,文件流转成字节和字符流以及设计字符的编码都给大家介绍了,把文章中提供的两个例子看明白差不多就算入门了。