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

本文为原创文章,转载请注明出处
查看[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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341