java 序列化机制和自定义序列化

简介: java 序列化机制和自定义序列化

对象的序列化:指将一个java对象写入到IO流中,与此对应的是,对象的反序列化则是从IO流中恢复该对象.


如果要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。


使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无需实现任何方法。


一旦某个类实现了Serializable接口,该类的对象就是可以序列化的.


package cn.lvkang.com.gradletest;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
public class SerTest implements Serializable {
    private int age = 25;
    private String name ="张三";
    private SerTest() {
        System.out.println(this.age+"-----------"+this.name);
    }
    public static void main(String[] args) {
        //对对象进行序列号
        try {
            ObjectOutputStream fos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            SerTest ser = new SerTest();
            fos.writeObject(ser);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //从文件中读取该对象,成为反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
             SerTest serTest = (SerTest) ois.readObject();
            System.out.println(serTest.age);
            System.out.println(serTest.name);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


必须指出的是反序列化 的仅仅是java对象的数据,而不是java类,因此采用的反序列化恢复java对象时,必须提供该Java对象所属类的class文件,否则会引发ClassNotFoundException异常


还有一点,SerTest只有一个构造器,而且构造器中只有一个打印语句。而在反序列化时,并没有看到程序调用该构造器,这表明反序列化机制无需通过构造器来初始化java对象。


对象引用的序列化


如果某个类的成员变量不是基本类型或者String类型,而是一个引用类型,那么这个引用的类必须是可序列化的,否则拥有该引用变量的类也是不可序列化的.


如下:


class Person implements Serializable {
    String name;
    SerTest test;
    public Person(String name, SerTest test) {
        this.name = name;
        this.test = test;
    }
}


Person 持有SerTest的引用,只有SerTest是可序列化的,Person才可以被序列化,


假设有如下情景:

SerTest test = new SerTest();
        Person p1 = new Person("张三",test);
        Person p2 = new Person("李四",test);


这里产生了一个问题,在序列化p1的时候,系统会将p1对象所引用的test对象一起序列化,如果程序在序列化p2的时候,系统一样会序列p2,并且序列化test。从而引起p1和p2使用的不是同一个对象,显然这个就违背了java序列化的初衷。所以java采用了一种特殊的算法。算法内容如下:


所有保存在磁盘中的对象都有一个序列化的编号。

当程序视图序列化一个对象时,程序将先简称该对象是否被序列化过,只有该对象从未被序列化过,系统才会将对象转换成字节序列并输出

如果这个对象已经被序列化过,程序将只是输出一个序列化编号,而不是重新序列化该对象。

根据上面的算法,可以知道 在程序在序列化p2的时候,发现test已经被序列化过了,所以程序不会对test进行序列化,3而是输出一个序列化编号。


自定义序列化:


在一些特殊的情况下,如果一个类中包含某种特殊的信息,如银行账户信息时,这是不希望将该实例变量值进行序列化,或者这个类的某个变量是不可被序列化的,因此不希望对该实例遍历进行递归序列化。


当某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果这个类的实例变量引用到其他类的对象,则被引用的对象也会被序列化,如果被引用的对象的实例变量也引用了其他类,在被引用的对象也会被实例化,这种情况称之为递归序列化。


通过在实例变量前面使用 transient关键字修饰, 可以指定java序列化时无需理会该实例变量.如下所示


public class SerTest implements Serializable {
    private transient int age = 25;
    private String name ="张三";
    private SerTest() {
        System.out.println(this.age+"-----------"+this.name);
    }
    public static void main(String[] args) {
        //对对象进行序列号
       try {
            ObjectOutputStream fos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            SerTest ser = new SerTest();
            fos.writeObject(ser);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //从文件中读取该对象,成为反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
             SerTest serTest = (SerTest) ois.readObject();
            System.out.println(serTest.age);
            System.out.println(serTest.name);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


添加了transient 之后,该类的age属性将不会被序列化,同样的在进行反序列化的时候age是没有值的也就是为0。


对敏感的字段进行加密:


在序列化的过程中,虚拟机是试图调用对象里的writeObject和readOjbect方法,如果没有这样的方法,则默认调用的是ObjectOutputStream的defaultWriteObject 方法以及 ObjectInputStream的 defaultReadObject方法。用户自定义的writeObject和readObject方法可以允许用户控制序列化的过程。比如可以在序列化的过程中动态改变序列化的数组,基于这个原理,可以在实际应用中使用,可以对敏感的字段进行加密的工作。


public class SerTest implements Serializable {
    private static ObjectOutputStream fos;
    private static ObjectInputStream ois;
    //敏感字段 年龄和 姓名
    private  int age = 28;
    private String name = "张三";
    private SerTest() {
    }
    //对象在序列化的时候调用
    private void writeObject(ObjectOutputStream fos) throws IOException {
        System.out.println("--------------加密中--------------");
        System.out.println("原来的姓名:"+this.name);
        System.out.println("原来的年龄:"+this.age);
        StringBuffer buffer = new StringBuffer(this.name);
        //对姓名进行反转
        fos.writeObject(buffer.reverse());
        //对年龄进行加密
        fos.writeInt(this.age*10*(5-2));
    }
    //对象在反序列化的时候调用
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("--------------加密后--------------");
        StringBuffer buffer = (StringBuffer) ois.readObject();
        this.name = buffer.toString();
        System.out.println("加密后的姓名:"+this.name);
        this.age = ois.readInt();
        System.out.println("加密后的年龄:"+this.age);
    }
    public static void main(String[] args) {
        try {
            SerTest test = new SerTest();
            fos = new ObjectOutputStream(new FileOutputStream("obj.obj"));
            fos.writeObject(test);//进行序列化
            fos.close();
            ois = new ObjectInputStream(new FileInputStream("obj.obj"));
            //进行反序列化,并且进行敏感字段的解密
            SerTest t = (SerTest) ois.readObject();
            System.out.println("---------------解密中-------------------");
            StringBuffer buffer = new StringBuffer(t.name);
            System.out.println("解密后的姓名:"+buffer.reverse().toString());
            System.out.println("解密后的年龄:"+(t.age/10/(5-2)));
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}


在程序中使用了自定义序列化,在序列化的时候ObjectOutputStream /ObjectInputStream 会根据你传入的对象进行反射。判断你是否写了writeObject/readObject 方法。如果写了,就会调用你写的,否则就会调用默认的方法.


在序列化的时候对对象里面的数据进行了加密,然后取出来的时候又进行了解密。这就是对象的自定义序列化


静态变量序列化:


序列化保存的是对象的状态,而不是类的状态。静态变量属于类的状态,所以序列化的时候不会保存静态常量。


writeReplace 方法:


还有一种更彻底的自定义机制,他甚至可以自序列化的时候将该对象转为其他的对象。writeReplace将由序列化机制调用,只有该方法存在,就会被调用。如下所示:


public class SerTest implements Serializable {
    private static ObjectOutputStream fos;
    private static ObjectInputStream ois;
    //敏感字段 年龄和 姓名
    private  int age = 28;
    private String name = "张三";
    private SerTest() {
    }
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("序列化中..............");
        ArrayList<Object> list = new ArrayList<>();
        list.add(new Person("李四"));
        return list;
    }
    public static void main(String[] args) {
        try {
            SerTest test = new SerTest();
            fos = new ObjectOutputStream(new FileOutputStream("obj.obj"));
            fos.writeObject(test);//进行序列化
            fos.close();
            ois = new ObjectInputStream(new FileInputStream("obj.obj"));
            ArrayList t = (ArrayList) ois.readObject();
            System.out.println(t.get(0).toString());
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
class Person implements Serializable {
    String name;
    public Person(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}


打印结果如下


序列化中..............

李四


系统在序列化某个对象之前,会先调用该对象的WriteReplace方法。如果该方法返回的是另一个对象,系统则会调用另一个对象的writeReplace方法—直到该方法不在返回另一个对象为止。程序最后将调用该对象的writeObject()方法来保存该对象的状态。


由打印的结果可以看出,在序列化的时候看似序列化的SerTest,实际上序列化的是ArrayList。在ArrayList中添加了一个Person对象,该对象也实现了序列化接口。但是并没有实现writeReplace方法。所以最后将会调用writeObject保存该对象的状态。


注意:上面集合里面存的是person对象,并没有writeReplace方法,所以最后将调用该对象的writeObject()方法来保存该对象的状态。但是如果保存的是当前类的对象(SerTest类的对象),就会造成递归,然后程序就会直接挂了。 因为SerTest对象有writeReplace方法。程序会一直的调用这个方法。最后程序会直接挂掉。如下所示:


修改writeReplace方法


private Object writeReplace() throws ObjectStreamException {
        System.out.println("序列化中..............");
        ArrayList<Object> list = new ArrayList<>();
//        list.add(new Person("李四"));
        list.add(new SerTest());
        return list;
    }


打印结果如下:


......
序列化中..............
序列化中..............
Exception in thread "main" java.lang.StackOverflowError
  at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
  at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
  at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
  at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
  at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
  at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
  at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
  ......


直接报 堆栈异常。


ReadResolve方法:


该方法和writeReplace方法对应。他可以实现保护性的复制整个对象,该方法会紧跟着readObject()之后调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会被立即丢弃。


Externalizable 序列化机制,这种序列化方式完全由程序员决定存储和恢复对象的数据。要想使用Externalizable,必须实现这个接口。


该接口定义了两个方法。


writeExternal():


需要序列化的类实现writeExternal()方法来保存对象的状态.该方法调用的是DataOutput(他是ObjectOutput的父接口)的方法来保存基本类型的实例变量的值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值。


readExternal:

需要序列化的类实现readExternal方法来实现反序列化。该方法调用DataInput(他是ObjectInput的父接口)的方法来恢复基本类型的实例变量的值,调用ObjectInput的readObject()方法来恢复引用类型的实例变量值。


实际上,采用实现Externalizable接口的方式 和前面说的自定义序列化十分像是,只是这个强制实现了自定义序列化。如下所示:


public class ExterTest implements Externalizable {
    private static ObjectOutputStream out;
    private static ObjectInputStream in;
    private String name ="王五";
    private int age = 35;
    public ExterTest(){
        System.out.println("我是构造器");
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(this.age);
    }
    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
    public static void main(String[] args){
        ExterTest test = new ExterTest();
        try {
            out = new ObjectOutputStream(new FileOutputStream("test.obj"));
            out.writeObject(test);
            out.close();
            in = new ObjectInputStream(new FileInputStream("test.obj"));
            ExterTest t = (ExterTest) in.readObject();
            System.out.println(t.name+"-------"+t.age);
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}


上面程序实现了Externalizable接口,也实现了两个方法,这两个方法除了名字和readOjbect()/writeObject()不同外,其他方法体都一样.


如果程序需要序列化显示Externalizable接口的对象,一样调用OabjectOutputStream的writeObject()方法即可。


需要说明的是:当使用这个方式反序列化时,程序首先会使用public的无参构造器创建实例。然后在执行readExternal()方法进行反序列化,因此实现Externalizable的接口必须提供public的无参构造器.


关于对象的序列化,还有一下几点需要注意:


对象的类名,实力变量(包括基本数据类型,数组,对其他对象的引用)都会被序列化。方法、类变量、transient实例变量都不会被序列化。

实现Serializable,接口的类如果需要让某个实例变量不被序列化,这个在该实例变量前加transient修饰符,而不是static关键字。

保证序列化对象的实例变量类型也是可以被序列化的,否则需要使用transient关键字来修饰该实例变量。不然该类是不可被序列化的。

反序列化的时候北徐有序列化对象的class文件。

当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取.


版本:


根据前面所说的,反序列化是必须有该对象的class文件,现在问题是随着项目的升级,系统的class文件也会跟着升级,java如果保证两个class的兼容性?


java序列化机制允许为序列化的类提供一个private static final 的serialVersionUID值,该类的变量用于表示java类的序列化版本。也就是说,当一个类升级后,只要该类变量的值没有修改,序列化机制也会把他们当作为同一个序列化版本。


最好在每个要序列化的类中加入private static final long serialVersionUID这个类变量的值,值自己定义。这样该类被修改了,该对象也能被序列化.


如果不显示的定义该值,该类变量的值将会有jvm进行计算,而修改后的类的值往往和没修改的值不同,从而容易造成对象反序列化的时候因为版本问题而导致的无法被序列化。


如有错误,还请指出.谢谢!


相关文章
|
5月前
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
238 32
AQS:Java 中悲观锁的底层实现机制
|
5月前
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
159 0
|
3月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
185 4
|
3月前
|
XML 人工智能 Java
java通过自定义TraceId实现简单的链路追踪
本文介绍了如何在Spring Boot项目中通过SLF4J的MDC实现日志上下文traceId追踪。内容涵盖依赖配置、拦截器实现、网关与服务间调用的traceId传递、多线程环境下的上下文同步,以及logback日志格式配置。适用于小型微服务架构的链路追踪,便于排查复杂调用场景中的问题。
123 0
|
5月前
|
Java 区块链 网络架构
酷阿鲸森林农场:Java 区块链系统中的 P2P 区块同步与节点自动加入机制
本文介绍了基于 Java 的去中心化区块链电商系统设计与实现,重点探讨了 P2P 网络在酷阿鲸森林农场项目中的应用。通过节点自动发现、区块广播同步及链校验功能,系统实现了无需中心服务器的点对点网络架构。文章详细解析了核心代码逻辑,包括 P2P 服务端监听、客户端广播新区块及节点列表自动获取等环节,并提出了消息签名验证、WebSocket 替代 Socket 等优化方向。该系统不仅适用于农业电商,还可扩展至教育、物流等领域,构建可信数据链条。
|
5月前
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
104 0
|
7月前
|
缓存 Dubbo Java
理解的Java中SPI机制
本文深入解析了JDK提供的Java SPI(Service Provider Interface)机制,这是一种基于接口编程、策略模式与配置文件组合实现的动态加载机制,核心在于解耦。文章通过具体示例介绍了SPI的使用方法,包括定义接口、创建配置文件及加载实现类的过程,并分析了其原理与优缺点。SPI适用于框架扩展或替换场景,如JDBC驱动加载、SLF4J日志实现等,但存在加载效率低和线程安全问题。
277 7
理解的Java中SPI机制
|
6月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
470 1
|
7月前
|
缓存 运维 Java
Java静态代码块深度剖析:机制、特性与最佳实践
在Java中,静态代码块(或称静态初始化块)是指类中定义的一个或多个`static { ... }`结构。其主要功能在于初始化类级别的数据,例如静态变量的初始化或执行仅需运行一次的初始化逻辑。
233 4
|
10月前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
274 34