Java IO

该系列所有文章均基于java se 11 & jdk 11

1. 使用场景

首先值得注意的是,java.io主要是针对单线程模型I/O操作所设计的,更多更现代的并发IO可以使用java.nio包。因此java.io包中的很多操作都是线程不安全的,当然也可以使用java 的同步机制来进行限制。但是并不是最优的性能。总结如下:

  1. java.io的设计是基于单线程模型。直接在并发环境中使用可能导致数据不一致。
  2. 高并发环境中使用java.io可能会导致比较大的性能损耗。
  3. 适用低并发和简单场景。对于高并发和更加复杂的IO操作,简易使用java.nio

2. 字节输出流

类结构

java.io中和字节输出流相关的类有八个。按照其功能和使用场景可以分为三类:

  1. 基类:java.io.OutputStream 这是所有字节输出流的基类。java.io.FilterOutputStream 只是提供给开发者自定包装类的基类,也是其中几个比较重要包装类的基类。
  2. **具体实现类:**这里也指的是那些能提供具体场景的输出流,也就是可以单独使用或者和包装类配合使用。 java.io.FileOutputStream 向文件中写入字节数据。java.io.PipedOutputStream 向管道中写入字节数据。java.io.ByteArrayOutputStream 向字节数组中写入字节数据,也就是该流可以直接获取相应的字节数组。java.io.PrintStream 可以向文件中写入格式化的数据。
  3. 包装类: 包装类指的是可以通过包装一个OutputStream对象,来提供额外的功能。 java.io.BufferedOutputStream通过缓冲技术,为被包装的流提供高效率的写操作。java.io.PrintStream 向被包装的流提供格式化的输出功能。java.io.ObjectOutputStream 可以向被包装的流中写入Object对象。

具体使用方法

一般首先根据自己的IO需求,选择四个具体实现类中的一个创建一个底层流。然后再根据自己需求决定是否选择使用包装类进行包装。

下面首先是底层流的创建:

  1. 目标设备是文件:java.io.FileOutputStream 或者java.io.PrintStream
  2. 目标设备是管道:java.io.PipedOutputStream
  3. 目标设备是将写入的数据转换为字节数组:java.io.ByteArrayOutputStream

然后根据需要选择包装类进行包装:

  1. 需要更加高效的写操作:BufferedOutputStream
  2. 需要进行格式化的写操作,比如写入的数据是其他类型的基础数据,PrintStream
  3. 如果待写入的数据是对象(Object),那么使用ObjectOutputStream
  4. 如果上述的包装类均不能满足要求,那么可以通过继承拓展java.io.FilterOutputStreamn,通过重写方法来实现自己的要求。

3. 字节输入流

类结构

java.io中字节输入流相关的类有十个,分别是:

  • 基类:java.io.InputStreamjava.io.FilterStream 包装基类,除了java.io.ObjectInputStream和java.io.SequenceInputStream外,其他的包装流均是其子类。同时也是后续包装类拓展的基类。
  • **具体实现类:**能单独使用和底层设备交互的 java.io.FileInputStream 从文件中读取字节数据。java.io.PipedInputStream从管道中读取字节数据。java.io.ByteArrayInputStream 从字节数组中读取字节数据,也就是使用字节数组构造流。
  • 包装类:java.io.ObjectInputStream 可以从被包装的流中直接读取Object对象。java.io.SequenceInputStream 可以将多个字节输入流组合成一个新的流。java.io.BufferedInputStream 使用缓冲技术,为被包装流提供高效的读操作。java.io.DataInputStream 可以直接读取被包装流中的基础数据类型。java.io.PushbackInputStraem 为被包装流提供可插队的读操作。也就是将字节数据插队到下一个读取操作中。

具体使用方式和上述一致,后续不再赘述。

4. 字符输出流

类结构

java.io中字符输出流有九个相关的类,分别是:

  • 基类:java.io.Writerjava.io.FilterWriter 后续包装类拓展的基类。这里的包装类均不是该类的子类。
  • 具体实现类:java.io.PipedWriter 向管道中写入字符数据。java.io.OutputStreamWriter 可以认为是字节输出流和字符输出流的桥接。本质上是将字符数据写入到字节流中。也就是以字节流为底层设备。java.io.FileWriter 向文件中写入字符数据,是OutputStreamWriter的子类。java.io.StringWriter 向字符串中写入字符数据,也就是可以直接将输入的数据转换为String对象。底层使用StringBuffer作为缓冲。java.io.CharArrayWriter 向字符数组中写入字符数据,类似StringWriter。java.io.PrintWriter 向文件或者OutputStream中写入格式化字符数据。
  • 包装类:java.io.BufferedWriter 使用缓冲技术,提供高效的写操作。java.io.PrintWriter 提供格式化字符数据的写入功能。

5. 字符输入流

类结构

java.io中字符输入流相关的类有十个,分别是:

  • 基类:java.io.Readerjava.io.FilterReader 开发者通过继承该类拓展自定义的包装类。该类有子类java.io.PushbackReader。
  • 具体实现类:java.io.PipedReader 从管道中读取字符数据。java.io.InputStreamReader 从InputStream中读取字符数据。java.io.FileReader 从文件中读取字符数据,是InputStreamReader的子类。java.io.StringReader 从字符串中读取数据,也就是使用String构造流。java.io.CharArrayReader 从字符数组中读取数据,类似StringReader。
  • 包装类:java.io.BufferedReaderjava.io.LineNumberReader 是BufferedReader的子类,可以追踪行号。java.io.PushbackReader 为被包装的类提供可插队的功能。

对象持久化

Java Specification -> Serialization

对象图

概念:对象图是一组对象以及他们之间的关系形成的一个有向图。

作用:对象图用于描述程序运行时内存中的对象和对象之间的相互关系。

  1. 对象:对象是程序中的基本单元,它们代表数据和行为的组合。对象可以是实例化的类、数据结构、变量等。
  2. 关系:对象之间的关系是对象图的核心。这些关系包括引用、依赖、继承、关联等,它们定义了对象之间的交互方式和依赖关系。
  3. 有向图:对象图是一个有向图,其中节点表示对象,边表示对象之间的关系。有向边的方向表示关系的方向,例如,一个对象引用另一个对象。
  4. 图的遍历:遍历对象图允许您访问和操作图中的对象。这可以通过深度优先遍历、广度优先遍历或其他遍历算法来实现。
  5. 内存中的表示:对象图实际上是在程序内存中存在的,它们在程序执行期间动态变化,根据对象之间的交互而变化。
  6. 持久化:对象图可以持久化到磁盘、数据库或其他媒介,以便在程序的不同运行之间或不同系统之间传输和恢复。
  7. 应用:对象图在编程和软件设计中有广泛的应用,特别是在面向对象编程、数据库设计、图数据库、可视化工具等领域。

序列化和反序列化的本质就是讲非线性的对象图结构转化为线性表示的字节流,或者从字节流中重建对象图。

对象持久化(序列化)

java.io.Serializable

接口声明:public interface Serializable

功能

根本的功能首先就是配合java.io.ObjectOutputStraem进行对象的持久化。该机制首先提供了一套默认的机制来做对象的持久化,其次也支持两种自定义的两种操作:writeObjectwriteReplace

下面我们将从一下几个方面来进行介绍:

  1. 如何配合java.io.ObjectOutputStream进行对象的持久化?
  2. 默认的序列化机制是怎样的?
  3. 自定义的操作规定了什么样的功能以及需要在什么时候自定义?

<u>场景:父类A没有实现接口,其子类B实现了接口,现在需要对B进行序列化和反序列化。</u>

首先这里要求父类A必须有一个子类B能访问到的无参数的构造方法(public或者protected的权限修饰符)。不然在运行时会报错。

实际在序列化和反序列化的时候,父类A的存储和恢复都是由子类负责进行的。反序列化期间,A类对象的初始化是由B类对象调用其无参数构造器完成的。初始化之后可以可以直接从流中对各个域进行赋值。

客户端的调用

只是作为可序列化类型的一个标识,没有任何的域或者方法。Java不会对任何没有实现该接口的类型对象进行序列化或者反序列化,值得注意的是该接口的声明和其他的继承实现机制一样是可以继承的。

<client>
ObjectOutputStream out = new ObjectOutputStream(outputStream);
out.writeObject(obj);

关于客户端进行序列化的相关方法:

  • void writeObject(Object obj) 直接将对象写入流中,而且对于对象的中的引用则使用句柄的方法进行引用指定,不会真正将对象存储在该对象的序列流中。例如某个对象重复写入,后续的对象则是直接使用第一个的句柄进行指定。
  • void writeUnshared(Object obj) 和上述方法不同的是,他对于对象中的引用不适用句柄进行指定,而且对于相同的对象可以重复写入。
序列化机制

通过out.writeObject或者out.writeUnshared的调用,其代码内部逻辑会判断类中是否定义了writeObject方法,如果定义了那么直接调用Serializable中的writeObject方法,如果没有定义则直接调用内部默认的defaultWriteFields方法。下面主要介绍当调用out.writeObject方法的时候,对象序列化机制是如何的。

  1. 如果该子类重写了对象输出流,那么直接执行重写的代码writeObjectOverride方法,然后writeObject返回。
  2. 如果块数据缓冲(block-data buffer)中有数据,那么刷新缓冲,然后重置缓冲。
  3. 如果对象是null,那么将null写入流中,writeObject方法返回。
  4. 如果该对象被替换了(也就是建立了从原对象到替换对象之间的映射记录),那么将替换之后对象的handler写入流中。writeObject方法返回。
  5. 如果该对象之前已经被写入了流中,那么直接将之前的handler写入流中。writeObject方法返回。
  6. 如果对象是Class类的,那么将相应的ObjectStreamClass对象写入流中,并为该对象分配一个handle。writeObject方法返回。
  7. 如果对象是ObjectStreamClass类型的,首先给该对象指定一个句柄,然后按照某种类描述格式写入流中。在Java 2 SDK之后,如果该对象所对应类不是动态代理类,那么调用writeClassDescriptor将该对象写入流中。之后就是将对应类的注解写入流中:如果对应的类是动态代理类,就调用annotateProxyClass方法。否则调用annotateClass方法。writeObject方法返回。
  8. 使用被序列化类或者流来处理对象的替换:如果该对象不是枚举类型并且该类中声明了writeReplace方法,那么调用该方法。该方法返回的是该对象的替换对象。如果流中是允许进行替换的(enableReplaceObject),那么就会调用对象流中的replaceObject方法来将对象替换成新的对象。如果原对象在上述两个过程中被替换成新对象了,那么就建立起原对象到新对象之间映射的记录,之后使用新对象重复执行步骤3~7。如果writeObject在步骤 3~7中没有返回,那么则从直接跳到步骤9。
  9. 如果对象是java.lang.String类型的,先将字符串的内容写入流中,最后将字符串的长度写入对象流中。并且对字符串分配一个handle,writeObject方法返回。具体的字符串写入格式参照Stream Elements。
  10. 如果对象是一个数组对象(Array),调用writeClassDesc写入数组的类型。然后为数组对象指定一个handle,之后分别写入数组的长度和数组的内容。writeObject方法返回。
  11. 如果对象是枚举常量,首先写入ObjectStreamClass对象,并且ObjectStreamClass只会在流中出现一次,后续的使用引用进行指定。之后使用Step 10中写入String的方法,将枚举常量name()方法返回的内容写入流中,写入的方式也是共享的,后续相同的内容使用引用进行指定。writeObject方法返回。
  12. 对于一般的对象,首先将对象类型的ObjectStreamClass对象共享地写入流,然后为该对象指定一个handle。
  13. 将对象的内容写入流中:如果对象是可序列化(实现了java.io.Serializable接口)的,那么直接定位到继承体系中可序列化的基类。针对于这个基类和其派生类,将他们的域都写入流中(保证所有可以被继承到的域被写入流)。如果被序列化的类中没有writeObject(out),那么自动调用defaultWriteObject来将可序列化的域写入流。如果被序列化的类中有writeObject(out),那么就调用该方法。如果对象实现了java.io.Externalizable接口,那么直接调用对象中重写的writeExternal方法。如果对象既没有实现java.io.Serializable也没有实现java.io.Externalizable接口,那么抛出NotSerializableException。
自定义序列化规则

自定义的方式主要有两个方面:1)在被序列化类中声明相应的方法。2)自定义对象输出流,重写相关的方法。

但是用户端主要的通过第一种方式进行自定义。下面主要介绍在被序列化类中声明相关的方法:

private void writeObject(java.io.ObjectOutputStream out) throws IOException

该方法负责为特定的类写入当前对象的状态。以便后续能通过readObject方法恢复该对象。 默认的写法的策略可以调用out.defaultWriteObject方法。该实现方法不会写入其父类或者子类的状态。状态写入的方式就是对于那些Object类型的域直接调用writeObject方法,对于基础数据类型则调用DataOutput中的方法写入流即可。该方法在实现的时候一般可以调用defaultWriteObject方法或者putFields,writeFields方法。其中putFields方法回返回一个PutField对象进行域的写入操作。writeFields方法则会将该类中的可序列化域进行持久化。并且defaultWriteObject只能在该方法内部进行调用,否则抛出异常。

private Object writeReplace() throws ObjectStreamException

返回该对象序列化时候的替代对象,也就是当序列化该对象的时候,实质上是序列化的该方法的返回对象。在对象流准备将对象写入之前调用该方法即可。

java.io.Externalizable

java.io.Serializable接口不同的是,实现该接口类的对象将对象的序列化和反序列化全权委托给public void readExternal(ObjectInput in)public void writeExternal(ObjectOutput out)。在实现这两个方法的时候,如有有需要父类的状态存储和解析一并在其中实现。在实际的序列化过程中,将直接调用readExternalwriteExternal方法。同时为了保证反序列化的正常进行,类需要有一个无参数构造方法。也就是在实际的反序列化过程中先使用公共的无参数构造方法创建对象,然后再调用readExternal方法。同时该接口也支持writeReplace方法。

反序列化

java.io.Serializable

功能

ObjectInputStream实现了对象的反序列化功能,并且维护包含多个已经反序列化对象的序列化流状态。它提供了一系列的方法ObjectInput/DataInput用于从序列化流中读取数据(基础数据类型/对象)。同时该流管理者流中对象的相互引用关系。

一个死锁条件:输入流的构造方法中需要传入一个InputStream,但是如果该参数中没有相应的ObjectOutputStraem进行输出那么将导致思索。因为输入流的构造中将堵塞读取流中的头部信息(readStreamHeader)。

序列化机制
  1. 如果序列化流已经被重写了,那么直接调用readObjectOverride方法并且返回。
  2. 如果流中有block data record,那么直接抛出异常BlockDataException
  3. 如果流中的对象是null,那么返回null
  4. 如果流中的对象是已经被序列化的,那么通过相应的handle返回该对象。
  5. 如果流中对象是Class对象,那么先读取其ObjectStreamClass对象,并将该类描述对象和他的handle添加到已知对象中。在当前的JVM中寻找到相应的Class对象,然后返回。
  6. 如果流中的对象是ObjectStraemClass对象,那么通过相应的格式读取对象。注意具体的调用过程和对象类型是否为动态代理类。
  7. 如果对象是String对象,将对象和相应的handle添加到已知对象中。按照相应的格式读取即可。
  8. 如果对象是数组,则先读取其ObjectStreamClass对象和数组的长度,之后为数组分配控件,并将对象和相应的handle添加到已知对象中。最后逐个读取元素。
  9. 如果对象是枚举类型,那么读取其ObjectStreamClass对象和相应的name属性即可。通过调用java.lang.Enum.valueOf方法得到相应的引用即可(参数ObjectStreamClassname)。并将对象和相应的handle添加到已知对象中。直接跳到Step 12。
  10. 对于其他类型的对象,先直接读取其ObjectStreamClass对象,然后在当前JVM空间中检索到相应的本地类。
  11. 为对应类的对象分配控件,并将对象和相应的handle添加到已知对象中。 对于实现了java.io.Serializable的对象,首先调用继承级别中最高的非序列化类的无参数构造方法。并且使用默认值为其域赋值。然后对于域中的数据通过调用readObject方法,如果没有定义则调用defaultReadObject方法。值得注意的是在反序列化过程可序列化类型的域中的初始化器和构造器并不会被调用。如果流中类的版本和本地的类版本不一致,那么直接按照域名进行匹配,如果没有相应的数据则使用默认值即可。对于实现了java.io.Externalizable的对象,首先调用类的无参数构造方法构造出对象,然后调用readExternal方法即可。
  12. 处理对象的替换: 如果对象的类型不是枚举类型,并且定义了readResolve方法,则使用其返回的对象替换当前的对象。然后在ObjectInputStream内部,调用resolveObject方法来检查和替换对象。替换之后,在流中建立相应的对象关系。
自定义反序列化规则

public interface ObjectInputValidation

方法public void validateObject,当这个对象图完成反序列化的时候将调用该方法。如果对象无效无法使用,那么应该抛出ObjectInvalidException。直接在被序列化类中实现该接口即可。

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

在实现的时候该方法只是负责重建自己的域,不用负责重建父类的域。父类的域可以直接在父类中定义该方法。

private void readObjectNoData() throws ObjectStreamException

在处理流中类和本地类版本不一致的时候域的初始化。

public void readExternal(ObjectInput in) throws IOException

该方法全权负责实现java.io.Externalizable对象(包含父类)的重建。

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

被序列化类中声明的方法可以用于反序列化过程中对象替换。

java.io.Externalizable

例子

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    // 实现自定义的反序列化逻辑
    // 对相关的域进行赋值。
    name = (String) in.readObject();
    age = in.readInt();
}

java.io.ObjectInputFilter

接口声明

@functionalInterface
public interface ObjectInputFilter

方法:ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo)

验证反序列化流中的类,数组长度,对象引用数量,深度,和流的大小等ObjectInputFilter.FilterInfo中的信息是否合法。返回值有三种类型:

  1. ObjectInputFilter.StatusALLOWED
  2. REJECTED
  3. UNDECIDED

该方法在一个类被反序列化之后进行调用。

ObjectInputStream::setObjectFilter为特定的流设置过滤器。ObjectInputFilter.Config设置进程级的过滤器。

java.io.ObjectInputFilter.Config

static ObjectInputFilter createFilter(String pattern) 创建Filter。统配规则:

  1. "="表示设置限制,如"max-depth=value"等。 1,各条规则之间使用";"隔开。
  2. 如果模式以"!"开头,代表拒绝后续模式。
  3. 如果模式包括"/",那么"/"之前的内容被认为是模块名。
  4. 如果模式以".**"结尾,那么匹配该包中所有的类和子包。
  5. 如果模式以".*"结尾,那么匹配该包中所有的类。
  6. 如果模式以"*"结尾,那么匹配所有以模式为前缀的类。
  7. 如果模式就是其全限定类名,那么匹配。
  8. 过滤器首先检查限制表达式,然后匹配类名/包名。

static ObjectInputFilter getSerialFilter() 返回进程级过滤器对象。

static void setSerialFilter(ObjectInputFilter filter) 设置进程级的过滤器对象。

#离职学习##求offer#
Java SE 文章被收录于专栏

基于Java SE 11 &amp; JDK11 的学习笔记。

全部评论

相关推荐

点赞 4 评论
分享
牛客网
牛客企业服务