I/O 流是一个逻辑实体,要么产生信息,要么消耗信息。
File
File 类直接处理文件和文件系统,它描述文件自身的性质,获得、处理关联的磁盘文件。
Java 中,目录也作为 File 看待,只不过有一个额外的 list()
方法。构造器如下
File(String dirPath);
File(String dirPath, String fileName);
File(String dirObj, String fileName);
File(URI uriObj);
dirPath 表示文件路径名,fileName 表示文件名或子目录名,dirObj 是一个表示目录的 File 对象,uriObj 是用于描述文件的 URI 对象。
File f1 = new File("/");
File f2 = new File("/", "a.txt");
// 等价于 f2
File f3 = new File(f1, "a.txt");
getName()
方法返回文件名,getParent()
方法返回父目录名,exists()
方法判断文件是否存在。
isFile()
方法判断调用对象是否表示一个文件,isAbsolute()
方法根据文件是否为绝对路径返回 true 或 false。
boolean renameTo(File new)
方法将 new 表示的文件名作为调用者的新名称,如果改名成功,返回 true;否则返回 false。
boolean delete()
方法用于删除调用者表示的文件或空目录,删除成功返回 true;否则返回 false。
File 实现了 Comparable 接口,可以使用 compareTo()
方法。
Path toPath()
方法将调用该方法的 File 对象转换成 Path 对象,Path 接口属于 nio。
目录是包含其他文件和目录的 File 对象,isDirectory()
方法的调用者表示目录时返回 true。String[] list()
方法返回目录中文件名和子目录名构成的数组。
String[] list(FilenameFilter FFObj)
方法根据参数条件返回特定的文件名和子目录名。其中, FFObj 是实现了 FilenameFilter 接口的类对象。FilenameFilter 接口定义了唯一的方法 accept()
,对 list 处理过程中的每一个文件名和目录名调用。形式为 boolean accept(File dir, String name)
,list 处理过程中,每一个文件名和目录名与 name 匹配时返回 true,否则返回 false。
class RetainHtml implements FilenameFilter {
String name;
public RetainHtml(String n) {
name = "." + n;
}
public boolean accept(File dir, String name) {
return name.endsWith(this.name);
}
public static void main(String[] args) {
String dir = "/dir";
var f = new File(dir);
FilenameFilter filter = new RetainHtml("html");
// 获得所有以 .html 结尾的在 /dir 目录下的文件和目录
String[] s = f.list(filter);
for (var e : s) {
System.out.println(e);
}
}
}
listFiles()
方法返回 File 对象表示的目录下,所有文件和目录的 File 对象数组,而不是字符串。带 FilenameFilter 参数的方法根据参数过滤部分文件和目录,带 FileFilter 参数的方法根据路径名是否满足从而决定是否返回对应的 File 对象。FileFilter 定义了唯一方法 boolean accept(File path)
,list 中的每个文件调用一次,文件路径和 path 匹配返回 true;否则返回 false。listFiles()
的形式为
File[] listFiles()
File[] listFiles(FilenameFilter FFObj)
File[] listFiles(FileFilter FObj)
mkdir()
方法创建一个目录,创建成功返回 true。mkdirs()
方法创建目录及其所有的父目录,也就是可以一次创建多个有嵌套关系的目录,创建成功返回 true。
AutoCloseable、Closeable 和 Flushable 接口
AutoCloseable 接口提供了对 try-with-resources 语句的支持,只有实现了 AutoCloseable 接口的类可以被 try-with-resources 语句管理。该接口有唯一的方法 close()
用于关闭调用对象,释放对象持有的资源。该接口被所有打开流的 I/O 类实现,可以防止内存泄漏和其他问题。
Closeable 接口扩展了 AutoCloseable 接口。也定义了 close()
方法,实现了 Closeable 的类可以被关闭。
实现了 Flushable 接口的类对象可以强制将缓冲输出写入流中。该接口定义了 flush()
方法。刷新流会将缓冲区内容输出到流关联的设备。所有向流写入的 I/O 类都实现了该接口。
关闭流的两种方式
JDK7 之前,显式调用 close()
方法关闭流
try {
} catch() {
} finally {
xx.close();
}
JDK7 引入的 try-with-resources 语句块可以自动关闭块中使用的资源。当该块执行完毕时,资源自动释放。JDK9 开始,允许 try 语句中表示资源(例如,流)的变量在 try-with-resources 语句块之前就声明和初始化,此时需要保证该变量是 effectively final,即该变量被初始化之后,值不再改变。
try () {
// process
}
try-with-resources 语句块有三点需要注意
- 可以管理的资源必须是实现了 AutoCloseable 接口的类对象。
- try 语句中定义的资源隐式是 final 的,try 外定义的资源必须是 effectively final。
- 可以管理多个资源,每条语句使用分号分隔。
流类
Java 定义了 4 个流相关的顶层抽象类:InputStream、OutputStream、Reader 和 Writer。其中,InputStream 和 OutputStream 用于字节流,Reader 和 Writer 用于字符流。当处理字符或字符串时使用字符流类,处理字节或其他二进制对象时使用字节流类。
字节流
字节流可以用于任何类型的对象。
InputStream 抽象类被定义为字节输入流。它实现了 AutoCloseable 和 Closeable 接口。
OutputStream 抽象类被定义为字节输出流。它实现了 AutoCloseable、Closeable 和 Flushable 接口。
FileInputStream 允许从文件中读取字节。常见构造器为
// filePath 表示文件的全路径名
FileInputStream(String filePath);
// obj 表示代表文件的 File 对象
FileInputStream(File obj);
class A {
public static void m() {
try (var f = new FileInputStream("file.txt")) {
// 返回当前可读的字节数
int size = f.available();
// 读取一个字节,将该字节表示的整数返回
char ch = (char)f.read();
var b = new byte[5];
// 读取 5 个字节放入 b 中,返回读取的字节数
f.read(b);
// 跳过可读取的 3 个字节
f.skip(3);
//读取 3 个字节从 b 的索引位置 2 开始存储
f.read(b, 2, 3);
}
}
}
FileOutputStream 允许将字节写入到文件中。它实现了 AutoCloseable、Closeable 和 Flushable 接口。构造器为
FileOutputStream(String filePath);
FileOutputStream(File obj);
// append 为 true 表示文件以添加模式打开
FileOutputStream(String filePath, boolean append);
FileOutputStream(File obj, boolean append);
创建 FileOutputStream 时,如果关联的文件不存在,则会自动创建指定文件并打开。
class B {
public static void m() {
String s = "this is a test string for output stream."
var buf = s.getBytes();
try(var f1 = new FileOutputStream("file1.txt");
var f2 = new FileOutputStream("file2.txt");
var f3 = new FileOutputStream("file3.txt")) {
// 将 1 字节内容写入到 f1 表示的文件中
f1.write(buf[1]);
// 将 buf 中所有内容写入到 f2 表示的文件中
f2.write(buf);
// 将 buf 中索引 1 开始的 10 个字节写入到 f3 表示的文件中
f3.write(buf, 1, 10);
}
}
}
ByteArrayInputStream 是使用字节数组作为源的输入流。构造器为
ByteArrayInputStream(byte[] arr);
// 索引 start 开始的 numBytes 个字节
ByteArrayInputStream(byte[] arr, int start, int numBytes);
该类的对象使用 close()
方法无效,但不是错误。
该类实现了 mark()
和 reset()
方法。当没有调用 mark()
方法时,调用 reset()
方法会将下一个待读取的字节位置从当前位置移动到流的第一个字节位置,即流的起始位置。
ByteArrayOutputStream 是将字节数组作为目标的输出流。构造器为
// 缓冲区大小默认为 32 字节
ByteArrayOutputStream();
// 缓冲区大小由 numBytes 指定
ByteArrayOutputStream(int numBytes);
缓冲区由 ByteArrayOutputStream 的 protected 成员属性 buf 引用。缓冲区大小按需自动增加。缓冲区的大小由 ByteArrayOutputStream 的 protected 成员属性 count 存储。
该类的对象使用 close()
方法无效,但不是错误。
class C {
public static void m() {
var f1 = new ByteArrayOutputStream();
var buf = "this is a string".getBytes();
// 将 buf 中的内容写入 f1 的缓冲区
f1.write(buf);
// 将内容转成字符串
var s = f1.toString();
// 将内容转成字节数组
var b = f1.toByteArray();
try (var f2 = new FileOutputStream("out.txt")) {
// 将 f1 中内容写入到 f2 表示的文件中
f1.writeTo(f2);
}
// 清空缓冲区
f1.reset();
}
}
filtered 流是包装了输入、输出流形成的,提供了额外的功能,比如缓冲字符转换(buffering character translation)和原始数据转换(raw data translation)。过滤字节流构造器为
FilterOutputStream(OutputStream os);
FilterInputStream(InputStream is);
缓冲流(buffered stream)扩展了过滤流类,它为流添加了一个内存缓冲区,使每次 I/O 操作不局限于字节,可以提高性能。缓冲字节流是 BufferedInputStream 和 BufferedOutputStream。PushbackInputStream 也实现了缓冲流。
BufferedInputStream 允许“包装”任何类型输入流得到该类型,从而提高性能。构造器为
BufferedInputStream(InputStream is);
BufferedInputStream(InputStream is, int bufSize);
一般认为,缓冲区大小为 8192 字节对于任何 OS 而言,I/O 操作都有性能提升。
该类支持 read()
、skip()
、mark()
和 reset()
方法。
BufferedOutputStream 类似于其他输出流,除了有 flush()
方法将缓冲区数据写入到流中。构造器为
BufferedOutputStream(OutputStream os);
BufferedOutputStream(OutputStream os, int bufSize);
PushbackInputStream 允许从流中读取的字节可以再次被读取。构造器为
// 每次读取只保留 1 字节内容可以再次被读取
PushbackInputStream(InputStream is);
// 创建 numBytes 大小的缓冲区,每次读取保留多个字节内容可以再次被读取
PushbackInputStream(InputStream is, int numBytes);
该类提供了 unread()
方法,形式如下
// 回退一个字节,将 b 的低位字节替代该字节,下一次 read 读取 b 的低位字节
void unread(int b);
// 回退的字节个数与 buf 大小相同,将 buf 的内容替代接下来同样大小的内容,下次读取 buf[0],
// 直到读取完 buf
void unread(byte[] buf);
// 回退 num 个字节,使用 buf 从 start 开始的 num 个字节替代接下来的内容,
// 下次读取 buf[start]
void unread(byte[] buf, int start, int num);
注意:该类的 mark()
和 reset()
方法无效。
SequenceInputStream 可以连接多个流。读取完一个流后,关闭该流,从下一个流开始读取。当该类表示的流关闭时,连接的所有流全部关闭。构造器为
SequenceInputStream(InputStream is1, InputStream is2);
SequenceInputStream(Enumeration<? extends InputStream> enumStream);
PrintStream 提供了所有与输出相关的功能,它实现了 Appendable、AutoCloseable、Closeable 和 Flushable 接口。
部分构造器为
PrintStream(OutputStream os);
PrintStream(OutputStream os, boolean autoFlush);
PrintStream(OutputStream os, boolean autoFlush, String charSet);
os 表示用于接收输出的输出流;autoFlush 为 true 表示当输出换行符时,自动将缓冲区内容写入到输出流,默认为 false;charSet 指定字符编码。
另一部分构造器为
PrintStream(File file);
PrintStream(File file, String charSet);
PrintStream(String fileName);
PrintStream(String fileName, String charSet);
从 File 对象或文件名创建一个 PrintStream,指定的文件自动创建,预先存在的同名文件被销毁。PrintStream 对象直接将输出写入到指定文件。
该类提供了 print()
和 println()
方法。提供的 printf()
方法类似 C/C++ 的,可以精确控制输出数据的格式,形式为
PrintStream printf(String str, Object... args);
PrintStream printf(Locale loc, String str, Object... args);
printf()
方法的工作方式类似于 Formatter 的 format()
方法。
该类也提供了 format()
方法,类似于 printf()
方法,形式为
PrintStream format(String str, Object... args);
PrintStream format(Locale loc, String str, Object... args);
DataOutputStream 和 DataInputStream 用于将原始数据写入到流、从流中读取原始数据,分别实现了 DataOutput 接口和 DataInput 接口。提供了原始值(primitive values)到字节的转换。
DataOutputStream 扩展了 FilterOutputStream,除了 DataOutput 接口,也实现了 AutoCloseable、Closeable 和 Flushable 接口。构造器为
DataOutputStream(OutputStream os);
当该类对象关闭时,指定的 os 流也会关闭。
DataOutput 接口定义的方法可以将原始类型值转换成字节序列写入到流中。部分方法为
final void writeDouble(double value);
final void writeBoolean(boolean value);
final void writeInt(int value);
DataInputStream 扩展了 FilterInputStream,除了 DataInput 接口,也实现了 AutoCloseable 和 Closeable 接口。构造器为
DataInputStream(InputStream is);
当该类对象关闭时,指定的 is 流也会关闭。
DataInput 接口定义的方法从流中读取字节序列将其转换成原始类型值。部分方法如下
final double readDouble();
final boolean readBoolean();
final int readInt();
RandomAccessFile 封装了一个文件,允许随机访问。它与 InputStream 和 OutputStream 无关,实现了 DataInput 和 DataOutput 接口。同时也实现了 AutoCloseable 和 Closeable 接口。构造器如下
RandomAccessFile(File file, String access);
RandomAccessFile(String fileName, String access);
其中,access 表示打开文件的方式。“r” 表示以只读方式打开,“rw” 表示以读写方式打开,“rws” 表示以读写方式打开,同时对文件数据和元数据做出的修改立即写入到存储介质,“rwd” 表示以读写方式打开,同时对文件数据做出的修改立即写入到存储介质。
seek()
方法用于设置文件指针的位置,调用之后,下次读、写从该位置开始。形式为
// newPos 表示从文件开头以字节为单位的偏移量
void seek(long newPos);
void setLength(long len)
方法用于设置文件长度。当设置的文件长度比实际长度大时,增加的部分未定义。
字符流
字符流中,类层次中顶层的类是抽象类 Reader 和 Writer。
Reader 定义字符输入流,实现了 AutoCloseable、Closeable 和 Readable 接口。
Writer 定义了字符输出流,实现了 AutoCloseable、Closeable、Flushable 和 Appendable 接口。
FileReader 从文件读取内容。常见的构造器如下
FileReader(File obj);
// 全路径名
FileReader(String filePath);
public static void {
try(var fr = new FileReader("file.txt")) {
int c;
while ((c = fr.read()) != -1) {
System.out.print((char)c);
}
}
}
FileWriter 向文件中写入内容。构造器为
FileWriter(String filePath);
// append 为 true 表示在文件末尾添加新内容
FileWriter(String filePath, boolean append);
FileWriter(File obj);
FileWriter(File obj, boolean append);
不论文件存不存在,该类会在打开文件前先创建该文件。
CharArrayReader 是需要字符数组作为源的字符输入流。构造器为
CharArrayReader(char[] arr);
CharArrayReader(char[] arr, int start, int numChars);
CharArrayWriter 是使用数组作为目标的字符输出流。构造器为
// 创建默认大小的缓冲区
CharArrayWriter();
// 创建指定大小的缓冲区
CharArrayWriter(int numChars);
缓冲区由该类的 protected 属性 buf 引用,缓冲区中字符个数由 protected 属性 count 存储。缓冲区大小按需自动增加。
close()
方法对该类无效。
BufferedReader 通过缓冲输入提高性能。构造器为
BufferedReader(Reader is);
BufferedReader(Reader is, int bufSize);
关闭该类对象也会自动关闭 is 表示的输入流。
该类实现了 mark()
和 reset()
方法。lines()
方法返回 reader 读取的一系列行的 Stream 引用(这里的 Stream 指 JDK8 引入的流,不是 I/O 流)。
BufferedWriter 通过缓冲减少输出的次数,从而提高性能。构造器为
BufferedWriter(Writer os);
BufferedWriter(Writer os, int bufSize);
PushbackReader 允许一或多个字符返回输入流。构造器为
// 允许 1 个字符回退
PushbackReader(Reader is);
// 指定缓冲区大小
PushbackReader(Reader is, int bufSize);
关闭该类对象也会关闭 is。
unread()
方法返回一或多个字符给输入流,形式为
// 回退一个字符,由 ch 指定
void unread(int ch);
// 回退 buf 数组大小的内容,
void unread(char[] buf);
// 回退 numChar 个从索引 offset 开始的 buf 中的内容
void unread(char[] buf, int offset, int numChar);
PrintWriter 本质上是 PrintStream 面向字符的版本。它实现了 Appendable、AutoCloseable、Closeable 和 Flushable 接口。构造器如下
PrintWriter(OutputStream os);
PrintWriter(OutputStream os, boolean autoFlush);
PrintWriter(Writer os);
PrintWriter(Writer os, boolean autoFlush);
autoFlush 确定当 println()
、printf()
、format()
方法调用时,输出缓冲区是否刷新,为 true 时刷新;为 false 时不刷新。默认为 false。
另一部分构造器如下
PrintWriter(File file);
PrintWriter(File file, String charSet);
PrintWriter(String fileName);
PrintWriter(String fileName, String charSet);
文件在打开前自动创建,预先存在的同名文件会被销毁。JDK11 添加了指定 Charset 参数的构造器。
该类有 print()
和 println()
方法,也有 printf()
和 format()
方法。
Console 类
Console 用于从控制台读取,写入到控制台。它没有提供构造器,使用 System.console()
获得 Console 对象。形式为
static Console console()
当控制台不可用时,返回 null;否则返回对它的引用。
public static void m() {
var con = System.console();
if (con == null) {
return ;
}
String s = con.readLine("Enter a string: ");
con.printf("%s\n", s);
}
序列化
序列化指将一个对象的状态写入到字节流的过程。例如,将一个对象状态写入到文件中存储,之后从该文件中反序列化得到对象。
实现远程方法调用(Remote Method Invocation, RMI)需要序列化的支持。RMI 指一台机器上的对象调用另一台机器上的方法。对象需要先序列化进行传输,到达目标机器后反序列化得到对象。
对象之间的的引用关系构成一张有向图,甚至有些对象是自引用,这些对象在序列化和反序列化时 Java 提供的机制能正确的处理这些关系。当序列化一个对象时,该对象引用了另一个对象,则被引用的对象也会进行序列化。反序列化同理。
实现了 Serializable 接口的类可以被序列化。Serializable 接口是一个标记接口,没有定义任何方法。一个类可以被序列化时,它的所有子类也可以被序列化。
transient 和 static 修饰的变量在序列化时不会被保留。
部分情况下,如果想要自己控制序列化的过程,比如想要使用压缩和加密技术,Externalizable 接口用于这些情况。Externalizable 接口定义了 2 个方法,形式如下
// is 表示读入对象的字节流
void readExternal(ObjectInput is);
// os 表示写入对象的字节流
void writeExternal(ObjectOutput os);
ObjectOutput 接口扩展了 DataOutput、AutoCloseable 接口,用于支持对象序列化。它的 writeObject()
方法用于序列化对象。
ObjectOutputStream 扩展了 OutputStream 类,实现了 ObjectOutput 接口。它用于将一个对象写入流中。构造器为
ObjectOutputStream(OutputStream os);
os 表示已序列化对象写入的输出流。该类对象关闭时,os 自动关闭。
该类有一个名为 PutField 的内部类,用于促进持久化属性的写。
ObjectInput 接口扩展了 DataInput 和 AutoCloseable 接口。用于支持对象序列化。它的 readObject()
方法用于反序列化对象。
ObjectInputStream 扩展了 InputStream 类,实现了 ObjectInput 接口。该类用于从流中读取对象。构造器形式为
ObjectInputStream(InputStream is)
is 表示需读取的已序列化对象所在的输入流。该类对象关闭时,is 自动关闭。
该类有一个名为 GetField 的内部类,用于促进持久化属性的读。
JDK9 开始,ObjectInputStream 类引入了 getObjectInputFilter()
和 setObjectInputFilter()
方法,用于读取输入流时过滤对象。需要使用到 ObjectInputFilter、ObjectInputFilter.FilterInfo、ObjectInputFilter.Config 和 ObjectInputFilter.Status。都是 JDK9 引入的。用于控制反序列。
class Bean implements Serializable {
String s;
int iv;
double dv;
Bean(String s, int iv, double dv) {
this.s = s;
this.iv = iv;
this.dv = dv;
}
public String toString() {
System.out.println(s + ", " + iv + ", " + dv);
}
}
class D {
public static void m() {
try(var oos = new ObjectOutputStream(new FileOutputStream("serial"))) {
var bean = new Bean("abc", 3, 3.14);
oos.writeObject(bean);
}
try(var ois = new ObjectInputStream(new FileInputStream("serial"))) {
Bean b = (Bean)ois.readObject();
}
}
}
对于想要序列化的类,最好定义一个 private static final long 的常量 serialVersionUID。虽然 Java 会自动定义该值,最好还是显式定义。
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.
[2] https://docs.oracle.com/javase/8/docs/api/java/io/PushbackInputStream.html
[3] https://docs.oracle.com/javase/8/docs/api/java/io/File.html