在计算机领域里 IO,有时也写作 I/O
,是Input / Output
的缩写,也就是输入和输出。这里的输入和输出是指不同系统之间的数据输入和输出,比如读写文件数据,读写网络数据等等。
本文内容大纲如下:
Java 中有三代 IO 框架,分别是第一代的同步阻塞 IO (也叫 BIO, Blocking IO),第二代的NIO ,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。第三代 NIO2 有的地方也叫 AIO,即Async IO,进一步支持了异步IO。
这些 IO 框架都是针对文件的,网络通信同样属于 IO 行为,但是被 Java 单独放在了 java.net 包下,不在这里说的 IO 体系内。
这个教程中我们来学习 Java IO 体系中最简单和易于理解的同步阻塞 IO,后面有了这里的知识积累后再去进一步学习 NIO 和 AIO。
同步阻塞 IO 即 BIO(blocking IO),指的主要是传统的 java.io 包,它基于流模型实现。java.io 包提供了我们最熟知的一些 IO 功能,比如 File 对象提供的文件和目录操作,还有一大块就是通过输入输出流读写文件等。
BIO 交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在完成之前,线程会一直阻塞在那里。多个 IO 调用的执行顺序是线性顺序。不过 BIO 的优点是代码比较简单、直观,虽然不适合在高并发场景下使用,但足够应对普通场景,同时也更容易学习和掌握。
IO 流是 Java IO 中的核心概念。流是在概念上表示无穷无尽的数据流。IO 流连接到数据源或数据的目的地,连接到数据源的叫输入流,连接到数据目的地的叫输出流。 Java 程序不能直接从数据源读取和向数据源写入,只能借助 IO 流从输入流中读取数据,向输出流中写入数据。
Java IO 中的流可以是基于字节的(读取和写入字节)也可以基于字符的(读取和写入字符),所以分为字节流和字符流,两类流根据流的方向都可以再细分出输入流和输出流。
这里有一点可能容易让人迷惑的是,IO中的输入和输出指的是相对于程序的输入和输出,程序向外输出内容,会向输出流里写入,虽然写入操作看似是输入,但相对于程序本身而言它是在向外输出内容。所以程序写的是OutputStream
读的是InputStream
。
字节流主要操作字节数据或二进制对象。 字节流有两个核心抽象类:InputStream 和 OutputStream。所有的字节流类都继承自这两个抽象类。 ##
字符流操作的是字符,字符流有两个核心类:Reader 类和 Writer 。所有的字符流类都继承自这两个抽象类。
字节流和字符流都有 read()
、write()
、flush()
、close()
这样的方法,这决定了它们的操作方式近似。
InputStream
类和 OutputStream
类。Reader
类和 Writer
类。所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。
所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。
字节流到字符流的转换可以使用 InputStreamReader 和 OutputStreamWriter。使用 InputStreamReader 可以将输入字节流转化为输入字符流,使用OutputStreamWriter可以将输出字节流转化为输出字符流。
下面我们通过一个读写文本文件的程序来演示一下字节流到字符流的转换以及 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
,以命令行交互的方式让我们能输入程序将要创建文件的名称和要往文件里写入的内容。IO
流在使用完成后需要统一调用close()
方法把流关闭掉。IO
流的关闭会让程序释放出它们占用的内存资源,而且字符流操作时使用了缓冲区,并在关闭字符流时会强制将缓冲区内容输出,如果不关闭流,则缓冲区的内容是无法输出的。flush()
方法强制清空流使用的缓冲区。try-with-resource
形式的异常处理,把资源的关闭交给了Java
-- 在资源被使用完成后或者程序出现异常终止执行时都会由 Java 自动关闭在try-with-resource
中打开的流资源。FileOutputStream
把目标文件绑定到字节输出流,再用 OutputStreamWriter
创建一个可以使用OutputStream
的Writer
,并指定其字符集为UTF_8
,这样程序就能一个字符一个字符地写入文件啦。OutputStreamWriter
字符流的基础上创建了 PrintWriter
,使用 PrintWriter
可以让程序方便地写入字符串,并且也可以通过它的 println
方法来自动处理换行符。用 Java 程序完成文件的写入操作后,我们再来看看,给定一个文件,怎么用 Java 程序读取器中的内容。