序列化系列(1)java序列化技术

简介: 这篇文章开始讲java对象的序列化,这是因为近期自己的项目当中,大量使用了序列化技术,这里面有java提供的序列化技术,也有一些序列化框架;所以,下定决心把java的序列化技术整理一下,以供参考。这是序列化系列的第一篇文章,所以主要是描述java提供的序列化技术。后续系列再分别讲使用框架实现序列化。按照惯例,先给出这篇文章的大致脉络首先,描述了序列化技术的使用场景和序列化的几种方案。接着,讲java提供的序列化技术然后,就是需要注意的几个问题,比如transient关键字、序列化ID的作用、深度克隆等等最后,对java提供的系列化技术的一个总结

一、认识序列化


1、从网络通信认识序列化


为了很好的理解序列化,先不讲概念,而是先从网络通信谈起,我们知道现在的网络通信技术基本上都是基于TCP/IP来实现的。假设我们有两台电脑,这两台电脑之间写好了java程序,一个是send端,一个是receive端,要实现他们的通信,其底层是怎么实现的呢?请看下面这张图。

v2-c8d825bd76ad20e505c81b87b5b31101_1440w.jpg从上面这张图我们可以看到,两个进程进行通信时候,想要发送数据,要先要把数据发送到TCP缓冲区,然后形成报文再发送出去,同样的道理,接收端也是一样。我们可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。同样的,当两个Java进程进行通信时,也可以使用序列化技术实现对象之间的传递。


为了理解起来方便,再来看一张图。

v2-1ef6f4de3c21559461f391fd8f00a2e6_1440w.jpg

从这张图也可以清晰的看出,发送数据之前要序列化,接受数据要反序列化。到了这,我们再来看序列化的概念就比较好理解了,一句话:Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;


2、序列化的使用场景


这个使用场景应该算是最重要的一环了,因为我们学习序列化就是为了使用他,现在把他们归纳一下:


(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;

(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;

(3)通过序列化在进程间传递对象;


3、序列化有什么好处呢?


其实好处是根据使用场景来的;


(1)实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上

(2)利用序列化实现远程通信,即在网络上传送对象的字节序列。


4、序列化技术都有哪些?


文字总是看着很枯燥,还是看图吧。

v2-a6f37efa519f097204971bd47e81ccfb_1440w.jpg

看起来很多呀,不过后续的课程中,我会一个一个的讲,或者是挑主要的,而且里面我也没有全用过,大概会六七种吧。这篇文章也主要看第一个java的序列化机制。


二、java序列化机制


从上面的图中我们也已经看到了,java序列化主要有两个接口,这两个接口的实现方式,我都会给出,但是重点在于serialize接口的实现方式。在这一部分中,先给出序列化基本的代码实现,在下一部分当中再来看序列化有哪些需要注意的问题。OK,现在开始代码实现java的序列化机制。


1、使用Serializable接口实现序列化(重点,要牢记,第三部分会多次使用)


首先我们定义一个对象类User


public class User implements Serializable {
    //序列化ID
    private static final long serialVersionUID = 1L;
    private int age;
    private String name;
    //getter和setter方法、
    //toString方法
}

接下来,在Test类中去实现序列化和反序列化。

public class Test {
    public static void main(String[] args) throws Exception, IOException {
        //SerializeUser();
        DeSerializeUser();
    }
    //序列化方法
    private static void SerializeUser() throws FileNotFoundException, IOException {
        User user = new User();
        user.setName("Java的架构师技术栈");
        user.setAge(24);
        //序列化对象到文件中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("G://Test/template"));
        oos.writeObject(user);
        oos.close();
        System.out.println("序列化对象成功");
    }
    //反序列化方法
    private static void DeSerializeUser() throws FileNotFoundException, IOException{
        File file = new File("G://Test/template");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        System.out.println("反序列化对象成功"+newUser.toString());
    }
}

当我们运行序列化方法时候,就可以看到,我们把数据存在了G://Test/template。

同时当我们运行反序列化方法的时候,就可以看到,反序列化成功,结果就不贴出来了,比较简单。


2、使用Externalizable接口实现序列化


首先,定义一个User1类

public class User1 implements Externalizable{
    private int age;
    private String name;
    //getter、setter
    //toString方法
    public User1() {}
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    }
}

然后就是Test1类,和上面的Test一样,就不再贴出来了。在这里主要看Externalizable和Serializable接口的区别。下面对其进行归纳一下。


(1)Externalizable继承自Serializable接口

(2)需要我们重写writeExternal()与readExternal()方法

(3)实现Externalizable接口的类必须要提供一个public的无参的构造器。


因此,我们可以对writeExternal()与readExternal()方法重新更改一下;

@Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String)in.readObject();
        age = in.readInt();
    }

到这,java序列化机制的基本使用就讲完了,从上面可以看出,使用起来还是非常简单的。不过,仅仅会基本的使用还不行,想要面试的时候更加的装13,还需要进一步深化。因此下面一部分主要就是对序列化机制的深入分析。还需要说一点,对于Serializable接口实现的序列化方式一定要牢记,因为下面要多次使用


三、深入分析java序列化机制


1、serialVersionUID的作用


一句话:其目的是序列化对象版本控制,有关各版本反序列化时是否兼容。如果在新版本中这个值修改了,新版本就不兼容旧版本,反序列化时会抛出InvalidClassException异常。如果修改较小,比如仅仅是增加了一个属性,我们希望向下兼容,老版本的数据都能保留,那就不用修改;如果我们删除了一个属性,或者更改了类的继承关系,必然不兼容旧数据,这时就应该手动更新版本号,即SerialVersionUid。


serialVersionUID有两种显示的生成方式: 一是默认的1L,比如:private static final long serialVersionUID = 1L; 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL;

现在我们去验证一下序列化版本不一致的情况:


首先我们在User里面设置一下serialVersionUID=123456L。

public class User implements Serializable {
    //序列化ID
    private static final long serialVersionUID = 123456L;
    。。。。。
}

然后再Test里面开始序列化。


接着我们更改serialVersionUID = 123456789L。然后再反序列化,就可以看到如下的错误了。同时也验证了序列化和反序列化需要版本一致的问题。

v2-28d0ead45893b8fe11a579b337773568_1440w.jpg

2、静态变量的序列化


首先需要说一下,静态变量不会被序列化。因为静态变量在全局区,本来流里面就没有写入静态变量,我打印静态变量当然会去全局区查找,当我write read 同时使用时,内存中的静态变量变了,所以打印出来的也变了。眼见为实,代码验证一下;


下面这个例子,主要是在对静态变量序列化之后,然后更改静态变量age的值,再重新反序列化输出一下。结果会出现两种情况:


(1)反序列输出的静态变量值没有变化:说明静态变量被序列化了。

(2)反序列输出的静态变量值变化了:说明静态变量没有被序列化了。


第一步:把User里面的年龄属性改成static静态类型

第二步:现在改一下Test类

//序列化
    private static void SerializeUser() throws 
    FileNotFoundException, IOException, ClassNotFoundException {
        User user = new User();
        user.setName("Java的架构师技术栈");
        //初始化之前静态变量age年龄是24.
        user.setAge(24);
        ObjectOutputStream oos = 
        new ObjectOutputStream(new FileOutputStream("G://Test/template"));
        oos.writeObject(user);
        oos.close();
        //现在把年龄改成18
        user.setAge(18);
        ObjectInputStream oin = 
        new ObjectInputStream(new FileInputStream( "G://Test/template"));
        User modifyUser = (User) oin.readObject();
        oin.close();
        //再读取,通过t.staticVar打印新的值
        System.out.println("静态变量age:"+modifyUser.getAge());
    }

然后看一下输出结果

v2-2cb04d33bd3c77dd90dad6d06ed49d13_1440w.jpg

3、Transient 关键字作用


Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。


这个很好理解也很简单。下面代码来看一下他的作用


第一步:把User里面的年龄属性改成Transient 修饰。private transient int age;

第二步:Test不变,进行序列化和反序列化。

第三步:看结果。反序列化之后的输出age应该为0;

v2-28f09a4857f521bc9e2c5b5438eb33d8_1440w.jpg

4、使用序列化实现深度克隆


对象的克隆也叫作对象的拷贝,拷贝有浅拷贝和深拷贝之分。


  • 浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
  • 深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝。


浅拷贝存在对象属性拷贝不彻底问题。因此在这里我们的侧重点不是克隆问题,我们的关注点更在于深拷贝。现在抛弃之前的User。我们重新定义一个类Person。


第一步:定义一个CloneUtils

public class CloneUtils {
    public static <T extends Serializable> T clone(T obj){
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();
            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

第二步:定义一个person类

public class Person implements Serializable {
    private static final long serialVersionUID = 123456L;
    private String name;
    //带参构造器
    //getter、setter
    //toString方法
}

第三步:在Test2类中实现深度克隆

public class Test2 {
    public static void main(String[] args) {
        Person person1 =  new Person("张三");  
        Person person2 =  CloneUtils.clone(person1);
        person2.setName("李四");
        Person person3 =  CloneUtils.clone(person1);
        person3.setName("王五");
        System.out.println("person1 "+person1.getName());
        System.out.println("person1 "+person2.getName());
        System.out.println("person1 "+person3.getName());
    }
}
//相应的输出是:张三、李四、王五


四、总结


好了,到这里我们对java序列化机制已经有了基本的了解了,从一开始的网络机制,到最后的深拷贝。总之,java提供的序列化机制还是比较常用的,也是比较简单的。但是并不是说,平时开发的时候这一种就可以满足了,还有一些问题java提供的序列化机制是不能满足我们的要求的。比如说跨语言之间的序列化。因为在当前的开发中,可能需要C语言和java进行通信。就比如我现在的项目需要无人机和服务端通信。无人机上的ros系统,另外的开发人员就是通过C++开发的,但是后端我使用java实现的,这就涉及到了这个问题。

相关文章
|
10月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
439 1
|
10月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
407 1
|
11月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
842 1
|
11月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
987 0
|
11月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
1043 44
|
12月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
1444 5
|
12月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
361 4
|
12月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
1862 1
|
12月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
488 3
|
12月前
|
存储 人工智能 算法
Java 大视界 -- Java 大数据在智能医疗影像数据压缩与传输优化中的技术应用(227)
本文探讨 Java 大数据在智能医疗影像压缩与传输中的关键技术应用,分析其如何解决医疗影像数据存储、传输与压缩三大难题,并结合实际案例展示技术落地效果。