本文为原创文章,转载请注明出处
查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443
Java的IO模块分为传统的IO和NIO(即:new IO),NIO会在下一篇文章中说,这里详解IO
IO就是Input和Output,就是输入输出。Java的IO按照操作的对象分为两部分:
- 一部分是按照字节来操作数据,是由
InputStream
和OutputStream
扩展来的一系列类,扩展的类一般以Stream
结尾;- 另一部分是按照字符来操作数据(由于字符使用的是Unicode字符,所以:1字符=2字节),扩展自
Reader
和Writer
,扩展的类一般也以Reader
和Writer
结尾。
InputStream和OutputStream家族
我们看InputStream和OutputStream的定义:
public abstract class InputStream implements Closeable
public abstract class OutputStream implements Closeable, Flushable
两者都是abstract
类,都不能实例化,两者都各有一个方法需要实现:read
和write
方法。
当需要定义自己的数据输入类的时候,只需要继承自InputStream
并实现read
方法即可:
public int read()
read
方法每次读取一个字节,并转换成int值。
同理,当需要自定义自己的数据输出类的时候,只需要继承自OutputStream
并实现write
方法即可:
public void write(int b)
write
方法每次写出一个字节。
也可以看到,二者都继承了Closeable
接口,都需要关闭,OutputStream
继承了Flushable
接口表示需要刷新,当输出流关闭的时候会自动刷新。
二者的方法说明如下:
InputStream的部分方法说明:
public abstract int read()
:读入一个字节,范围:-1~255public int read(byte b[])
:读入b.length
个字节到b
中,返回读入字节的数量(其他的read不再介绍)public byte[] readAllBytes()
:读入所有可用的字节public byte[] readNBytes(int len)
:读入长度位len
的字节,其他readNBytes不再介绍public long skip(long n)
:跳过多少个字节不读public void skipNBytes(long n)
:没有跳过足够的字节会异常(文件末尾等)public int available()
:返回当前可用字节数public void close()
:关闭public synchronized void mark(int readlimit)
:为输入流的某一位置打标记,有些输入流不支持打标记public boolean markSupported()
:检测是否支持打标记public synchronized void reset()
:重新回到上一次mark打标记的地方开始读入public long transferTo(OutputStream out)
:将一个输入流转换为输出流
OutputStream的部分方法说明:
public abstract void write(int b)
:写出一个字节bpublic void write(byte b[])
:写出字节数组bpublic void flush()
:冲刷输出流,一般用来将数据回写public void close()
:关闭输出流,一般关闭之前要flush或者自动flush
从JDK11开始,这两个类分别提供了public static InputStream nullInputStream()
和public static OutputStream nullOutputStream()
来产生空的输入输出流,一般产生和处理废弃数据使用。
由这两个基础的输入输出流类,衍生出了一大批输入输出流的类,他们分别有不同的功能:
InputStream(基础类)
|--FileInputStream(处理文件输入)
|--ObjectInputStream(对象反序列化使用)
|--ByteArrayInputStream(字节数组输入流,获取字节数组使用)
|--FilterInputStream(过滤字节输入流,对字节输入做了更多操作,主要用子类)
|--BufferedInputStream(字节输入缓冲流,处理字节输入)
|--DataInputStream(处理基本数据类型、String类型等读入,二进制形式存储)
|--...
OutputStream(基础类)
|--FileOutputStream(输出到文件)
|--ObjectOutputStream(对象序列化使用)
|--ByteArrayOutputStream(字节数组输出流,输出字节数组)
|--FilterOutputStream(过滤字节输出流,对字节输出做了一定处理)
|--BufferedOutputStream(字节缓冲输出流)
|--DataOutputStream(数据类型输出流,输出基本的数据类型、String等,二进制形式存储)
|--...
所有的类都实现了Closeable
接口,在每次使用完之后都需要关闭。
组合使用输入输出流
在实际使用过程中,一般使用组合流的较多,比如对于一个文本文件,里面存储的是一些数字,那么我们可能使用FileInputStream
来读入文件,而将之与DataInputStream
组合来达到读取数字的目的:
DataInputStream input = new DataInputStream(new FileInputStream("data.txt"));
int a = input.readInt();
同样,输出流也可以进行类似的组合。
Reader和Writer家族
Reader
和Writer
家族主要是用来处理文本的输入输出,他们将以字符的形式输入输出数据。
首先来看这两个类的定义:
public abstract class Reader implements Readable, Closeable
public abstract class Writer implements Appendable, Closeable, Flushable
两者都是abstract
类,继承Reader
需要实现read
和close
方法,继承Writer
需要实现write
、flush
和close
方法,其中read
和write
方法定义如下:
public abstract int read(char cbuf[], int off, int len) throws IOException;
public abstract void write(char cbuf[], int off, int len) throws IOException;
可以看到,这里实际上是按照char
的数据类型读写的。具体的还有很多其他的衍生方法,这里不再介绍。
与上面类似,Reader
也可以进行mark
、reset
等操作。从JDK11开始,这两个类也提供了空的默认读写流。
他们的类关系图如下所示:
Reader(基础类)
|--InputStreamReader(使用InputStream作为输入数据源的字符输入流)
|--BufferedReader(带缓冲的字符输入流,可以按行读)
|--LineNumberReader(带行号的BufferedReader)
|--StringReader(String作为数据源)
|-...
Writer(基础类)
|--BufferedWriter(带缓冲区的字符输出流)
|--OutputStreamWriter(输出到OutputStream的字符输出流)
|--FileWriter(输出到文件)
|--PrintWriter(可自主格式化的字符输出流)
|--StringWriter(用来处理字符串)
|--...
与上面的类似,这些类之间也是可以组合使用的,甚至可以与InputStream
和OutputStream
一起组合使用,比如我们想从一个文件读入数据也可以这么写:
InputStreamReader reader = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);
int val = reader.read();
Java8之后,可以使用BufferedReader
直接产生一个流,对于大文件可以使用流的方法来进行读入:
try (Stream<String> stream = new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"))).lines()) {
stream.forEach(System.out::println);
}
对于文本输出,可以直接使用PrintWriter
来进行输出:
PrintWriter writer = new PrintWriter("data.txt", StandardCharsets.UTF_8);
writer.println("hello");
writer.close();
对象的序列化与反序列化
对于我们很多人经常会用对象的序列化和反序列化来保存一些信息,对象的序列化和反序列化使用的是ObjectInputStream
和ObjectOutputStream
来完成的。我们先来看其各自的定义:
public void writeObject(Object obj) throws IOException; // 来源于ObjectOutputStream,用于保存对象
public Object readObject() throws ClassNotFoundException, IOException; // 来源于ObjectInputStream,用于读入对象
在正式开始介绍之前,请自行运行如下代码:
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
Entity e1 = new Entity(1, "this is e1");
Entity e2 = new Entity(2, "this is e2");
Entity e3 = new Entity(3, "this is e3");
e1.next = e3;
e2.next = e3;
serialize(e1, "D:\\e1.dat");
serialize(e2, "D:\\e2.dat");
e1 = unserialize("D:\\e1.dat");
e2 = unserialize("D:\\e2.dat");
System.out.println("e1:val=" + e1.val + " desc:" + e1.desc);
System.out.println("e2:val=" + e2.val + " desc:" + e2.desc);
System.out.println("e1.next:val=" + e1.next.val + " desc:" + e1.next.desc);
System.out.println("e1.next==e2.next: " + (e1.next == e2.next));
}
// 序列化
public static void serialize(Entity e, String file) throws Exception {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file));
outputStream.writeObject(e);
}
// 反序列化
public static Entity unserialize(String file) throws Exception {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
return (Entity) inputStream.readObject();
}
private static class Entity implements Serializable {
int val;
transient String desc;
Entity next;
Entity() {
}
Entity(int val, String desc) {
this.val = val;
this.desc = desc;
}
}
}
预期输出:
e1:val=1 desc:null
e2:val=2 desc:null
e1.next:val=3 desc:null
e1.next==e2.next: false
当我们调用序列化的代码时,序列化的流程图如下图所示:
这里要注意几个事情:
- 一个对象中包含了其他对象,其他对象也会被序列化
- 已经被序列化的对象只会保存其序列化的序列号,而不会重复序列化(这样能保证反序列化后指向的是同一个对象,上面的例子两个对象是分别序列化的,所以不适用这条)
- 序列化的每个对象在该文件中都有一个唯一的序列号(自动生成,不是
serialVersionUID
)- 被
transient
关键字标记和static
的变量不会被序列化- 可以自定义自己的序列化和反序列化方法,可以在方法中对
trainsient
关键字标记的字段等进行序列化,例如HashMap
中的writeObject
和readObject
方法
对于枚举类型的序列化,需要有一些特殊的序列化方法,用到的时候请自行查阅
序列化的版本管理
为了保持序列化的兼容情况,一般在需要被序列化的类中添加一个serialVersionUID
静态最终变量,来表示这个类的指纹信息,对象的输入流拒绝序列化不同指纹的对象,所以想要序列化的数据版本兼容,最好自己定义一个serialVersionUID
并赋予一个唯一的值,后续这个值就不会再改变了,除非你想标记为不与以前的版本兼容...
public static final long serialVersionUID = 9273862375L;
上面介绍的IO相关的内容都是阻塞的IO,也就是说,一个InputStream
读入不到字符的时候就会等待到一直读到字符为止,这种方式也叫BIO,就是Block IO的意思,后续会介绍非阻塞的IO
下一节将介绍NIO