美文网首页
【Java高级】Java IO进阶,最通俗易懂的IO/对象序列化

【Java高级】Java IO进阶,最通俗易懂的IO/对象序列化

作者: 大栗几 | 来源:发表于2020-06-20 16:31 被阅读0次

    本文为原创文章,转载请注明出处
    查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443

    Java的IO模块分为传统的IO和NIO(即:new IO),NIO会在下一篇文章中说,这里详解IO

    IO就是Input和Output,就是输入输出。Java的IO按照操作的对象分为两部分:

    • 一部分是按照字节来操作数据,是由InputStreamOutputStream扩展来的一系列类,扩展的类一般以Stream结尾;
    • 另一部分是按照字符来操作数据(由于字符使用的是Unicode字符,所以:1字符=2字节),扩展自ReaderWriter,扩展的类一般也以ReaderWriter结尾。

    InputStream和OutputStream家族

    我们看InputStream和OutputStream的定义:

    public abstract class InputStream implements Closeable
    public abstract class OutputStream implements Closeable, Flushable
    

    两者都是abstract类,都不能实例化,两者都各有一个方法需要实现:readwrite方法。
    当需要定义自己的数据输入类的时候,只需要继承自InputStream并实现read方法即可:

    public int read()
    

    read方法每次读取一个字节,并转换成int值。
    同理,当需要自定义自己的数据输出类的时候,只需要继承自OutputStream并实现write方法即可:

    public void write(int b)
    

    write方法每次写出一个字节。

    也可以看到,二者都继承了Closeable接口,都需要关闭,OutputStream继承了Flushable接口表示需要刷新,当输出流关闭的时候会自动刷新。

    二者的方法说明如下:

    InputStream的部分方法说明:

    • public abstract int read():读入一个字节,范围:-1~255
    • public 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):写出一个字节b
    • public void write(byte b[]):写出字节数组b
    • public 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家族

    ReaderWriter家族主要是用来处理文本的输入输出,他们将以字符的形式输入输出数据。

    首先来看这两个类的定义:

    public abstract class Reader implements Readable, Closeable
    public abstract class Writer implements Appendable, Closeable, Flushable
    

    两者都是abstract类,继承Reader需要实现readclose方法,继承Writer需要实现writeflushclose方法,其中readwrite方法定义如下:

    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也可以进行markreset等操作。从JDK11开始,这两个类也提供了空的默认读写流。

    他们的类关系图如下所示:

    Reader(基础类)
      |--InputStreamReader(使用InputStream作为输入数据源的字符输入流)
      |--BufferedReader(带缓冲的字符输入流,可以按行读)
          |--LineNumberReader(带行号的BufferedReader)
      |--StringReader(String作为数据源)
      |-...
    
    Writer(基础类)
      |--BufferedWriter(带缓冲区的字符输出流)
      |--OutputStreamWriter(输出到OutputStream的字符输出流)
          |--FileWriter(输出到文件)
      |--PrintWriter(可自主格式化的字符输出流)
      |--StringWriter(用来处理字符串)
      |--...
    

    与上面的类似,这些类之间也是可以组合使用的,甚至可以与InputStreamOutputStream一起组合使用,比如我们想从一个文件读入数据也可以这么写:

    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();
    

    对象的序列化与反序列化

    对于我们很多人经常会用对象的序列化和反序列化来保存一些信息,对象的序列化和反序列化使用的是ObjectInputStreamObjectOutputStream来完成的。我们先来看其各自的定义:

    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中的writeObjectreadObject方法

    对于枚举类型的序列化,需要有一些特殊的序列化方法,用到的时候请自行查阅

    序列化的版本管理

    为了保持序列化的兼容情况,一般在需要被序列化的类中添加一个serialVersionUID静态最终变量,来表示这个类的指纹信息,对象的输入流拒绝序列化不同指纹的对象,所以想要序列化的数据版本兼容,最好自己定义一个serialVersionUID并赋予一个唯一的值,后续这个值就不会再改变了,除非你想标记为不与以前的版本兼容...

    public static final long serialVersionUID = 9273862375L;
    

    上面介绍的IO相关的内容都是阻塞的IO,也就是说,一个InputStream读入不到字符的时候就会等待到一直读到字符为止,这种方式也叫BIO,就是Block IO的意思,后续会介绍非阻塞的IO

    下一节将介绍NIO

    相关文章

      网友评论

          本文标题:【Java高级】Java IO进阶,最通俗易懂的IO/对象序列化

          本文链接:https://www.haomeiwen.com/subject/tgzjxktx.html