IO
IO是指Input/Output,即输入和输出,以内存为中心:
- Input指从外部读入数据到内存
- Output指把数据从内存输出到外部
InputStream / OutputStream
IO流以byte
(字节)为最小单位,因此也称为字节流。例如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据:
Reader / Writer
如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char
来读写显然更方便,这种流称为字符流。
Java提供了Reader
和Writer
表示字符流,字符流传输的最小数据单位是char
。
同步和异步
同步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
对象,需要传入文件路径:
import java.io.*;
public class Main {
public static void main(String[] args) {
File f = new File("C:\\Windows\\notepad.exe");
System.out.println(f);
}
}
构造File对象时,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径,例如:
File f = new File("C:\\Windows\\notepad.exe");
注意Windows平台使用\
作为路径分隔符,在Java字符串中需要用\\
表示一个\
。Linux平台使用/
作为路径分隔符:
File f = new File("/usr/bin/javac");
传入相对路径时,相对路径前面加上当前目录就是绝对路径:
// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
可以用.
表示当前目录,..
表示上级目录。
File对象有3种形式表示的路径,一种是getPath()
,返回构造方法传入的路径,一种是getAbsolutePath()
,返回绝对路径,一种是getCanonicalPath
,它和绝对路径类似,但是返回的是规范路径。
什么是规范路径?
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
}
绝对路径可以表示成C:\Windows\System32\..\notepad.exe
,而规范路径就是把.
和..
转换成标准的绝对路径后的路径:C:\Windows\notepad.exe
。
因为Windows和Linux的路径分隔符不同,File对象有一个静态变量用于表示当前平台的系统分隔符:
System.out.println(File.separator); // 根据当前平台打印"\"或"/"
文件和目录
File
对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File
对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File
对象,并不会导致任何磁盘操作。只有当调用File
对象的某些方法的时候,才进行磁盘操作
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("C:\\Windows");
File f2 = new File("C:\\Windows\\notepad.exe");
File f3 = new File("C:\\Windows\\nothing");
System.out.println(f1.isFile());
System.out.println(f1.isDirectory());
System.out.println(f2.isFile());
System.out.println(f2.isDirectory());
System.out.println(f3.isFile());
System.out.println(f3.isDirectory());
}
}
用File
对象获取到一个文件时,还可以进一步判断文件的权限和大小:
boolean canRead()
:是否可读;boolean canWrite()
:是否可写;boolean canExecute()
:是否可执行;long length()
:文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录。
创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()
创建一个新文件,用delete()
删除该文件:
File对象提供了createTempFile()
来创建一个临时文件,以及deleteOnExit()
在JVM退出时自动删除该文件
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}
遍历文件和目录
当Filebeat对象表示一个目录时,可以使用list()
和listFiles()
列出目录下的文件和子目录名。listFiles()
提供了一系列重载方法,可以过滤不想要的文件和目录:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}
static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path
Java标准库还提供了一个Path
对象,它位于java.nio.file
包。Path
对象和File
对象类似,但操作更加简单:
import java.io.*;
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
System.out.println(" " + p);
}
}
}
InputStream
InputStream
就是Java标准库提供的最基本的输入流。它位于java.io
这个包里。java.io
包提供了所有同步IO的功能。
要特别注意的一点是,InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
,签名如下:
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int
值(0-255)。如果已读到末尾,返回-1
表示不能继续读取了
FileInputStream
是InputStream
的一个子类。即从文件流中读取数据,下面的代码演示了如何完整地读取一个FileInputStream
的所有字节:
public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}
// 使用了异常捕获
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}
缓冲
很多流支持一次性读取多个字节到缓冲区,对于文件和网络来说,利用缓冲去一次性读取多个字节效率往往要高很多。
InputStream
提供了两个重置方法来支持读取多字节:
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]
数组作为缓冲区,read()
方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()
方法的返回值不再是字节的int
值,而是返回实际读取了多少个字节。如果返回-1
,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
阻塞
在调用InputStream
的read()
方法读取数据时,我们说read()
方法是阻塞(Blocking)的。它的意思是,对于下面的代码:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
执行到第二行代码时,必须等read()
方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()
方法调用到底要花费多长时间。
InputStream实现类
用FileInputStream
可以从文件获取输入流,这是InputStream
常用的一个实现类。此外,ByteArrayInputStream
可以在内存中模拟一个InputStream
:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}
ByteArrayInputStream
实际上是把一个byte[]
数组在内存中变成一个InputStream
,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream
。
OutputStream
OutputStream
是java标准库提供的最基本的输出流,OutputStream
也是抽象类,它是所有输出流的超类,最重要的方法是void write(int b)
,签名如下:
public abstract void write(int b) throws IOException;
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int
参数,但只会写入一个字节,即只写入int
最低8位表示字节的部分(相当于b & 0xff
)。
和InputStream
类似,OutputStream
也提供了close()
方法关闭输出流,以便释放系统资源。要特别注意:OutputStream
还提供了一个flush()
方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()
方法,因为缓冲区写满了OutputStream
会自动调用它,并且,在调用close()
方法关闭OutputStream
之前,也会自动调用flush()
方法。
但是,在某些情况下,我们必须手动调用flush()
方法。举个栗子:
小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStream
的write()
方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么回事?
原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。
解决办法就是每输入一句话后,立刻调用flush()
,不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。
实际上,InputStream
也有缓冲区。例如,从FileInputStream
读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()
读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read()
,则会触发操作系统的下一次读取并再次填满缓冲区。
FileOutputStream
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}
上述代码,没有考虑到遇到异常时,资源无法关闭的情况,在实际生产项目中,需要考虑做异常捕获关闭资源
阻塞
OutputStream
的write()
方法也是阻塞的。
OutputStream实现类
用FileOutputStream
可以从文件获取输出流,这是OutputStream
常用的一个实现类。此外,ByteArrayOutputStream
可以在内存中模拟一个OutputStream
(使用不多):
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("Hello ".getBytes("UTF-8"));
output.write("world!".getBytes("UTF-8"));
data = output.toByteArray();
}
System.out.println(new String(data, "UTF-8"));
}
}
同时操作多个AutoCloseable
资源时,在try(resource) { ... }
语句中可以同时写出多个资源,用;
隔开
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是?
}
Filter模式
java的IO标准库提供的InputStream
根据来源可以包括:
FileInputStream
: 从文件读取数据,是最终数据源ServletInputStream
:从HTTP请求读取数据,是最终数据源;Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream
分为两大类:
一类是直接提供数据的基础InputStream
,例如:
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
一类是提供额外附加功能的InputStream
,例如:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
当我们需要给一个“基础”InputStream
附加各种功能时,我们先确定这个能提供数据源的InputStream
,因为我们需要的数据总得来自某个地方,例如,FileInputStream
,数据来源自文件:
InputStream file = new FileInputStream("test.gz");
紧接着,我们希望FileInputStream
能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream
包装这个InputStream
,得到的包装类型是
BufferedInputStream
,但它仍然被视为一个InputStream
:
InputStream buffered = new BufferedInputStream(file);
最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream
:
InputStream gzip = new GZIPInputStream(buffered);
无论我们包装多少次,得到的对象始终是InputStream
读取ClassPath资源
很多Java程序启动的时候,都需要读取配置文件。例如,从一个.properties
文件中读取配置:
String conf = "C:\\conf\\default.properties";
try (InputStream input = new FileInputStream(conf)) {
// TODO:
}
这段代码要正常执行,必须在C盘创建conf
目录,然后在目录里创建default.properties
文件。但是,在Linux系统上,路径和Windows的又不一样。
因此,从磁盘的固定目录读取配置文件,不是一个好的办法。
从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties
文件放到classpath中,就不需关系实际存放路径
在classpath中的资源文件,路径总是以/
开头,我们先获取当前的Class
对象,然后调用getResourceAsStream()
就可以直接从classpath读取任意的资源文件:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
// TODO:
}
调用getResourceAsStream()
需要特别注意的一点是,如果资源文件不存在,它将返回null
。因此,我们需要检查返回的InputStream
是否为null
,如果为null
,表示资源文件在classpath中没有找到:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// TODO:
}
}
如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));
这样读取配置文件,应用程序启动就更加灵活。
序列化
序列化是指把一个java对象编程二进制内容,本质上就是一个byte[]
数组,序列化后可以把byte[]
保持到文件中,或者把byte[]
通过网络传输到远程,这样就相当于把java对象存储到文件或者通过网络传输了
一个java对象要序列化,必须实现``java.io.Serializable`接口,它的定义如下:
public interface Serializable {
}
Serializable
接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
序列化
把一个java对象变成byte[]
数组,需要使用ObjectOutputStream
。它负责把一个java对象写入一个字节流
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
ObjectOutputStream
既可以写入基本类型,如int
,boolean
,也可以写入String
(以UTF-8编码),还可以写入实现了Serializable
接口的Object
。
因为写入Object
时需要大量的类型信息,所以写入的内容很大
反序列化
和ObjectOutputStream
相反,ObjectInputStream
负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
除了能读取基本类型和String
类型外,调用readObject()
可以直接返回一个Object
对象。要把它变成一个特定类型,必须强制转型。
readObject()
可能抛出的异常有:
ClassNotFoundException
:没有找到对应的Class;InvalidClassException
:Class不匹配。
对于ClassNotFoundException
,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person
对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person
类,所以无法反序列化。
对于InvalidClassException
,这种情况常见于序列化的Person
对象定义了一个int
类型的age
字段,但是反序列化时,Person
类定义的age
字段被改成了long
类型,所以导致class不兼容。
为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID
静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID
的值,这样就能自动阻止不匹配的class版本:
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
要特别注意反序列化的几个重要特点:
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
安全性
因为Java的序列化机制可以导致一个实例能直接从byte[]
数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]
数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
Reader
Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取:
InputStream | Reader |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
读取字节(-1,0~255):int read() |
读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) |
读到字符数组:int read(char[] c) |
java.io.Reader
是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
这个方法读取字符流的下一个字符,并返回字符表示的int
,范围是0
~65535
。如果已读到末尾,返回-1
。
FileReader
FileReader
是Reader
的一个子类,它可以打开文件并获取Reader
。下面的代码演示了如何完整地读取一个FileReader
的所有字符:
public void readFile() throws IOException {
// 创建一个FileReader对象:
Reader reader = new FileReader("src/readme.txt"); // 字符编码是???
for (;;) {
int n = reader.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 打印char
}
reader.close(); // 关闭流
}
如果我们读取一个纯ASCII编码的文本文件,上述代码工作是没有问题的。但如果文件中包含中文,就会出现乱码,因为FileReader
默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK
,打开一个UTF-8
编码的文本文件就会出现乱码。
要避免乱码问题,我们需要在创建FileReader
时指定编码:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
和InputStream
类似,Reader
也是一种资源,需要保证出错的时候也能正确关闭,所以我们需要用try (resource)
来保证Reader
在无论有没有IO错误的时候都能够正确地关闭:
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
// TODO
}
Reader
还提供了一次性读取若干字符并填充到char[]
数组的方法:
public int read(char[] c) throws IOException
它返回实际读入的字符个数,最大不超过char[]
数组的长度。返回-1
表示流结束。
利用这个方法,我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
CharArrayReader
CharArrayReader
可以在内存中模拟一个Reader
,它的作用实际上是把一个char[]
数组变成一个Reader
,这和ByteArrayInputStream
非常类似:
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
StringReader
StringReader
可以直接把String
作为数据源,它和CharArrayReader
几乎一样:
try (Reader reader = new StringReader("Hello")) {
}
InputStreamReader
Reader
和InputStream
有什么关系?
除了特殊的CharArrayReader
和StringReader
,普通的Reader
实际上是基于InputStream
构造的,因为Reader
需要从InputStream
中读入字节流(byte
),然后,根据编码设置,再转换为char
就可以实现字符流。如果我们查看FileReader
的源码,它在内部实际上持有一个FileInputStream
。
既然Reader
本质上是一个基于InputStream
的byte
到char
的转换器,那么,如果我们已经有一个InputStream
,想把它转换为Reader
,是完全可行的。InputStreamReader
就是这样一个转换器,它可以把任何InputStream
转换为Reader
。示例代码如下:
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
构造InputStreamReader
时,我们需要传入InputStream
,还需要指定编码,就可以得到一个Reader
对象。上述代码可以通过try (resource)
更简洁地改写如下:
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}
上述代码实际上就是FileReader
的一种实现方式。
使用try (resource)
结构时,当我们关闭Reader
时,它会在内部自动调用InputStream
的close()
方法,所以,只需要关闭最外层的Reader
对象即可。
使用InputStreamReader,可以把一个InputStream转换成一个Reader。
Writer
Reader
是带编码转换器的InputStream
,它把byte
转换为char
,而Writer
就是带编码转换器的OutputStream
,它把char
转换为byte
并输出。
Writer
和OutputStream
的区别如下:
OutputStream | Writer |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
写入字节(0~255):void write(int b) |
写入字符(0~65535):void write(int c) |
写入字节数组:void write(byte[] b) |
写入字符数组:void write(char[] c) |
无对应方法 | 写入String:void write(String s) |
Writer
是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):
void write(int c)
; - 写入字符数组的所有字符:
void write(char[] c)
; - 写入String表示的所有字符:
void write(String s)
。
FileWriter
FileWriter
就是向文件中写入字符流的Writer
。它的使用方法和FileReader
类似:
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
CharArrayWriter
CharArrayWriter
可以在内存中创建一个Writer
,它的作用实际上是构造一个缓冲区,可以写入char
,最后得到写入的char[]
数组,这和ByteArrayOutputStream
非常类似:
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
StringWriter
StringWriter
也是一个基于内存的Writer
,它和CharArrayWriter
类似。实际上,StringWriter
在内部维护了一个StringBuffer
,并对外提供了Writer
接口。
OutputStreamWriter
除了CharArrayWriter
和StringWriter
外,普通的Writer实际上是基于OutputStream
构造的,它接收char
,然后在内部自动转换成一个或多个byte
,并写入OutputStream
。因此,OutputStreamWriter
就是一个将任意的OutputStream
转换为Writer
的转换器:
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}
上述代码实际上就是FileWriter
的一种实现方式。这和上一节的InputStreamReader
是一样的。
PrintStream和PrintWriter
PrintStream
是一种FilterOutputStream
,它在OutputStream
的接口上,额外提供了一些写入各种数据类型的方法:
- 写入
int
:print(int)
- 写入
boolean
:print(boolean)
- 写入
String
:print(String)
- 写入
Object
:print(Object)
,实际上相当于print(object.toString())
以及对应的一组println()
方法,它会自动加上换行符。
我们经常使用的System.out.println()
实际上就是使用PrintStream
打印各种数据。其中,System.out
是系统默认提供的PrintStream
,表示标准输出:
System.out.print(12345); // 输出12345
System.out.print(new Object()); // 输出类似java.lang.Object@3c7a835a
System.out.println("Hello"); // 输出Hello并换行
System.err
是系统默认提供的标准错误输出。
PrintStream
和OutputStream
相比,除了添加了一组print()
/println()
方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException
,这样我们在编写代码的时候,就不必捕获IOException
。
PrintWriter
PrintStream
最终输出的总是byte数据,而PrintWriter
则是扩展了Writer
接口,它的print()
/println()
方法最终输出的是char
数据。两者的使用方法几乎是一模一样的:
import java.io.*;
public class Main {
public static void main(String[] args) {
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("Hello");
pw.println(12345);
pw.println(true);
}
System.out.println(buffer.toString());
}
}
使用Files
Files
工具类,能极大地方便我们读取文件。虽然Files
是java.nio
包里面的类,但他俩封装了很多读写文件的简单方法,例如,我们要把一个文件的全部内容读取为一个byte[]
,可以这么写:
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
如果是文本文件,可以把一个文件的全部内容读取为String
:
// 默认使用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
工具类还有copy()
、delete()
、exists()
、move()
等快捷方法操作文件和目录。
最后需要特别注意的是,Files
提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。