内置序列化技术

本文是我们名为“ 高级Java ”的学院课程的一部分。

本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看

1.简介

本教程的这一部分将专门用于序列化 :将Java对象转换为一种格式的过程,该格式可用于在同一(或其他)环境中进行存储和以后的重构( http://en.wikipedia。 org / wiki / Serialization )。 序列化不仅允许将Java对象保存到持久性存储中或从持久性存储中加载Java对象,而且还是现代分布式系统通信中非常重要的组件。

序列化并不容易,但是有效的序列化则更加困难。 除了Java标准库之外,还有许多可用的序列化技术和框架:其中一些使用紧凑的二进制表示形式,而另一些则将可读性放在首位。 尽管我们将在此过程中提及许多替代方案,但我们的注意力将集中在Java标准库(和最新规范)中: SerializableExternalizable Serializable ,用于XML绑定的Java体系结构( JAXBJSR-222 )和用于Java的Java API。 JSON处理( JSON-PJSR-353 )。

2.可序列化的接口

可以说,Java中将类标记为可用于序列化的最简单方法是实现java.io.Serializable接口。 例如:

public class SerializableExample implements Serializable {
}

序列化运行时与每个可序列化的类关联一个特殊的版本号,称为序列号UID ,该序列号反序列化 (与序列化相反的过程)中使用,以确保为序列化对象加载的类兼容。 如果兼容性受到损害,则将InvalidClassException

可序列化的类可以通过声明名称为serialVersionUID的字段为staticfinal且类型为long的字段来显式引入其自己的串行版本UID 。 例如:

public class SerializableExample implements Serializable {
    private static final long serialVersionUID = 8894f47504319602864L;   
}

但是,如果可序列化的类未明确声明serialVersionUID字段,则序列化运行时将为该类生成一个默认的serialVersionUID字段。 值得一提的是,所有实现Serializable类都强烈建议显式声明serialVersionUID字段,因为默认的serialVersionUID生成很大程度上依赖于内部类的详细信息,并且可能会因Java编译器实现及其版本而有所不同。 这样,为了保证行为的一致性,可序列化的类必须始终声明一个显式的serialVersionUID字段。

一旦该类可序列化(实现Serializable并声明serialVersionUID ),就可以使用例如ObjectOutputStream / ObjectInputStream进行存储和检索:

final Path storage = new File( "object.ser" ).toPath();

try( final ObjectOutputStream out = 
        new ObjectOutputStream( Files.newOutputStream( storage ) ) ) {
    out.writeObject( new SerializableExample() );
}

存储后,可以通过类似的方式进行检索,例如:

try( final ObjectInputStream in = 
        new ObjectInputStream( Files.newInputStream( storage ) ) ) {
    final SerializableExample instance = ( SerializableExample )in.readObject();
    // Some implementation here
}

如我们所见, Serializable接口没有对应该序列化什么以及如何进行序列化提供很多控制( transient关键字将字段标记为不可序列化除外)。 而且,它限制了更改内部类表示形式的灵活性,因为它可能会破坏序列化/反序列化过程。 这就是为什么引入了另一个接口Externalizable原因。

3.可外部化的界面

Serializable接口相反, Externalizable将类应如何序列化和反序列化的职责委托给该类。 它只有两种方法,这是Java标准库中的声明:

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

反过来,每个实现Externalizable接口的类都应提供这两种方法的实现。 让我们看一个例子:

public class ExternalizableExample implements Externalizable {
    private String str;
    private int number;
    private SerializableExample obj;
        
    @Override
    public void readExternal(final ObjectInput in) 
            throws IOException, ClassNotFoundException {
        setStr(in.readUTF());
        setNumber(in.readInt());
        setObj(( SerializableExample )in.readObject());
    }
    
    @Override
    public void writeExternal(final ObjectOutput out) 
            throws IOException {
        out.writeUTF(getStr());
        out.writeInt(getNumber());
        out.writeObject(getObj());
    }
}

与实现Serializable的类相似,可以使用例如ObjectOutputStream / ObjectInputStream存储和检索实现Externalizable的类:

final Path storage = new File( "extobject.ser" ).toPath();
        
final ExternalizableExample instance = new ExternalizableExample();
instance.setStr( "Sample String" );
instance.setNumber( 10 );
instance.setObj( new SerializableExample() );
        
try( final ObjectOutputStream out = 
        new ObjectOutputStream( Files.newOutputStream( storage ) ) ) {
    out.writeObject( instance );
}
        
try( final ObjectInputStream in = 
        new ObjectInputStream( Files.newInputStream( storage ) ) ) {
    final ExternalizableExample obj = ( ExternalizableExample )in.readObject();
    // Some implementation here
}

当使用Serializable接口的简单方法无法正常工作时,使用Externalizable接口可以进行细粒度的序列化/反序列化自定义。

4.有关可序列化接口的更多信息

在上一节中,我们提到了Serializable接口并没有对应该序列化什么以及如何序列化提供很多控制。 实际上,它并不是完全正确的(至少在使用ObjectOutputStream / ObjectInputStream时)。 任何可序列化的类都可以实现一些特殊方法,以控制默认的序列化和反序列化。

private void writeObject(ObjectOutputStream out) throws IOException;

此方法负责为其特定类编写对象的状态,以便相应的readObject方法可以将其恢复(可以通过调用out.defaultWriteObject调用保存对象字段的默认机制)。

private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException;

此方法负责从流中读取并还原对象的状态(可通过调用in.defaultReadObject调用还原对象字段的默认机制)。

private void readObjectNoData() throws ObjectStreamException;

在序列化流未将给定类列为要反序列化的对象的超类的情况下,此方法负责初始化对象的状态。

Object writeReplace() throws ObjectStreamException;

当可序列化的类需要指定将对象写入流时要使用的替代对象时,使用此方法。

Object readResolve() throws ObjectStreamException;

最后,当从流中读取可序列化的类的实例时,可序列化的类需要指定替换时,使用此方法。

一旦知道内在的实现细节和要使用的特殊方法,默认的序列化机制(使用Serializable接口)在Java中就会变得非常麻烦。 您正在编写用于支持序列化的更多代码,更有可能展示出更多的错误和漏洞。

但是,有一种方法可以通过使用名为Serialization Proxy的非常简单的模式来降低这些风险,该模式基于利用writeReplacereadResolve方法。 这种模式的基本思想是引入专用的伴随类进行序列化(通常作为private static内部类),以补充需要序列化的类。 让我们看一下这个例子:

public class SerializationProxyExample implements Serializable {
    private static final long serialVersionUID = 6163321482548364831L;

    private String str;
    private int number;        
    
    public SerializationProxyExample( final String str, final int number) {
        this.setStr(str);
        this.setNumber(number);
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException( "Serialization Proxy is expected" );
    }
    
    private Object writeReplace() {
        return new SerializationProxy( this );
    }
    
    // Setters and getters here
}

对此类的实例进行序列化时,类SerializationProxyExample实现将提供替换对象( SerializationProxy类的实例)。 这意味着SerializationProxyExample类的实例将永远不会直接序列化(和反序列化)。 它还说明了为什么以某种方式进行反序列化尝试时, readObject方法会引发异常。 现在,让我们看一下伴随的SerializationProxy类:

private static class SerializationProxy implements Serializable {
    private static final long serialVersionUID = 8368440585226546959L;

    private String str;
    private int number;
        
    public SerializationProxy( final SerializationProxyExample instance ) {
        this.str = instance.getStr();
        this.number = instance.getNumber();
    }
        
    private Object readResolve() {
        return new SerializationProxyExample(str, number); // Uses public constructor
    }
}

在我们的略微简化的情况下, SerializationProxy类只是复制了所有的领域SerializationProxyExample (但可能比被很多复杂)。 因此,当要反序列化此类的实例时, readResolve调用readResolve方法,并且SerializationProxy提供替换,这次的形式为SerializationProxyExample实例。 因此, SerializationProxy类可作为一个序列化代理SerializationProxyExample类。

5.可序列化和远程方法调用(RMI)

相当长一段时间以来,Java远程方法调用( RMI )是可用于在Java平台上构建分布式应用程序的唯一机制。 RMI提供了所有繁重的工作,并且可以从同一主机或不同物理(或虚拟)主机上的其他JVM透明地调用远程Java对象的方法。 RMI的基础是对象序列化,该对象序列化用于编组(序列化)和解组(反序列化)方法参数。

如今, RMI仍在许多Java应用程序中使用,但由于它的复杂性和通信限制(大多数防火墙都阻止RMI端口),因此越来越少选择RMI 。 要获取有关RMI的更多详细信息,请参考官方文档

6. JAXB

用于XML绑定的Java体系结构,或者只是JAXB ,可能是Java开发人员可以使用的最古老的替代序列化机制。 在下面,它使用XML作为序列化格式,提供了广泛的自定义选项,并包含许多注释,这些注释使JAXB非常吸引人并且易于使用(注释在本教程的第5部分中介绍了如何以及何时使用Enums和注释 )。

让我们看一个用JAXB注释注释的普通旧Java类(POJO)的简化示例:

import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement( name = "example" )
public class JaxbExample {
    @XmlElement(required = true) private String str;
    @XmlElement(required = true) private BigDecimal number;
    
    // Setters and getters here
}

要使用JAXB基础结构将该类的实例序列化为XML格式,唯一需要的是编组器(或序列化器)的实例,例如:

final JAXBContext context = JAXBContext.newInstance( JaxbExample.class );        
final Marshaller marshaller = context.createMarshaller();
     
final JaxbExample example = new JaxbExample();
example.setStr( "Some string" );
example.setNumber( new BigDecimal( 12.33d, MathContext.DECIMAL64 ) );
        
try( final StringWriter writer = new StringWriter() ) {
    marshaller.marshal( example, writer );
}

这是上面示例中JaxbExample类实例的XML表示形式:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<example>
    <str>Some string</str>
    <number>12.33000000000000</number>
</example>

按照相同的原则,可以使用解组器(或反序列化器)的实例将类的实例从XML表示反序列化回Java对象,例如:

final JAXBContext context = JAXBContext.newInstance( JaxbExample.class );
        
final String xml = "" +
    "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" standalone=\\"yes\\"?>" +
    "<example>" +
    "    <str>Some string</str>" +
    "    <number>12.33000000000000</number>" +
    "</example>";
        
final Unmarshaller unmarshaller = context.createUnmarshaller();
try( final StringReader reader = new StringReader( xml ) ) {
    final JaxbExample example = ( JaxbExample )unmarshaller.unmarshal( reader );
    // Some implementaion here
}

正如我们所看到的, JAXB易于使用,并且XML格式在当今仍然很受欢迎。 但是,XML的基本陷阱之一是冗长:很多时候,必要的XML结构元素大大超过了有效的数据有效负载。

7. JSON-P

自2013年以来,借助新引入的JSON处理Java API( JSON-P ),Java开发人员可以将JSON用作序列化格式。

截至目前,JSON-P是不是Java标准库的一部分,虽然有很多讨论,包括原生JSON支持到在即将推出的Java 9释放(语言http://openjdk.java.net/jeps/198 )。 尽管如此,它还是可以作为Java JSON处理参考实现https://jsonp.java.net/ )的一部分获得的。

JAXB相比 ,无需为使该类适合于JSON序列化而添加任何类,例如:

public class JsonExample {
    private String str;
    private BigDecimal number;
    // Setters and getters here
}

序列化不像JAXB那样透明,并且需要为要序列化为JSON的每个类编写一些代码,例如:

final JsonExample example = new JsonExample();
example.setStr( "Some string" );
example.setNumber( new BigDecimal( 12.33d, MathContext.DECIMAL64 ) );
        
try( final StringWriter writer = new StringWriter() ) {
    Json.createWriter(writer).write( 
        Json.createObjectBuilder()
            .add("str", example.getStr() )
            .add("number", example.getNumber() )
            .build()
        );
}

这是上面示例中JsonExample类实例的JSON表示形式:

{
    "str":"Some string",
    "number":12.33000000000000
}

反序列化过程也是如此:

final String json = "{\\"str\\":\\"Some string\\",\\"number\\":12.33000000000000}";  
      
try( final StringReader reader = new StringReader( json ) ) {
    final JsonObject obj = Json.createReader( reader ).readObject();
    final JsonExample example = new JsonExample();
    example.setStr( obj.getString( "str" ) );
    example.setNumber( obj.getJsonNumber( "number" ).bigDecimalValue() );
}

可以说,目前Java中的JSON支持非常基本。 尽管如此,拥有一个很棒的东西,Java社区正在通过引入用于JSON绑定的Java API (JSON-B, JSR-367 )来努力丰富JSON支持。 使用此API,与JSON之间的Java对象序列化和反序列化应该像JAXB一样透明。

8.序列化成本

重要的是要理解,尽管序列化/反序列化在Java中看起来很简单,但它不是免费的,并且取决于数据模型和数据访问模式可能会消耗大量的网络带宽,内存和CPU资源。 不仅如此,尽管如此,Java对可序列化的类提供了某种版本控制(使用序列化的UID,正如我们在“可序列化的接口 ”一节中所看到的),它确实使开发过程变得更加困难,因为开发人员需要自己弄清楚如何管理数据模型的演变。

另外要说明的是,Java序列化在JVM领域之外无法正常工作。 对于使用多种编程语言和运行时构建的现代分布式应用程序,这是一个重要的限制。

这就解释了为什么许多替代的序列化框架和解决方案应运而生,并成为Java生态系统中非常流行的选择。

9.超越Java标准库和规范

在本节中,我们将从Fast-serialization项目( http://ruedigermoeller.github.io/fast-serialization/ )开始,研究无痛且有效的Java序列化的替代解决方案:快速替换Java序列化。 快速序列化的用法与Java标准库提供的用法没有太大区别,但声称它更快,更有效。

另一组框架对此问题有不同的看法。 它们基于结构化数据定义(或协议),并将数据序列化为紧凑的二进制表示形式(甚至可以从定义中生成相应的数据模型)。 除此之外,这些框架远远超出了Java平台,可以用于跨语言/跨平台序列化。 该领域中最知名的Java库是Google协议缓冲区https://developers.google.com/protocol-buffers/),Apache Avrohttp://avro.apache.org/ )和Apache Thrifthttps:/ /thrift.apache.org/ )。

10.下一步是什么

在本部分的教程中,我们讨论了Java语言及其运行时提供的内置序列化技术。 我们已经看到了当今的串行化的重要性,当时几乎所有正在构建的单个应用程序都是大型分布式系统的一部分,并且需要与其其余部分(或与其他外部系统)进行通信。 在本教程的下一部分中,我们将讨论Java中的反射和动态语言支持。

11.下载源代码

您可以在此处下载本课程的源代码: advanced-java-part-10

翻译自: https://www.javacodegeeks.com/2015/09/built-in-serialization-techniques.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值