首页 文章 精选 留言 我的

精选列表

搜索[编译原理],共10000篇文章
优秀的个人博客,低调大师

《AOP 挖掘记》概念介绍及原理初探(一)

AOP 的基本概念 AOP 是 Aspect-Oriented programming 的缩写,中文翻译为面向切面编程,它和 OOP 一样是一种编程思想。 AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。通俗的讲就是将应用程序中的业务逻辑同对其提供支持的通用服务进行分离,AOP 可以说是 OOP 的补充和完善。 使用 OOP 的好处是可以设计出低耦合的系统,使系统更加灵活、更加易于维护,但同时也增加了代码的重复性。比如,我们要打印类中每个方法的出入参。按面向对象的编程思想,就必须在类的每方

优秀的个人博客,低调大师

网络唤醒(WOL)全解指南:原理篇

什么是网络唤醒 网络唤醒(Wake-on-LAN,WOL)是一种计算机局域网唤醒技术,使局域网内处于关机或休眠状态的计算机,将状态转换成引导(Boot Loader)或运行状态。无线唤醒(Wake-on-Wireless-LAN,WoWLAN)作为 WOL 的补充技术,使用无线网卡去唤醒计算机。网络唤醒在一般的局域网环境里使用有限广播地址(255.255.255.255)即可,由于路由器都不转发目的地址为有限广播地址的数据报,因此在复杂网络情况下通常使用子网定向广播地址。在局域网外唤醒局域网内特定计算机,可以使用路由器的 DDNS 与端口转发。 在1996年10月,英特尔和 IBM 成立了 Advanced Manageability Alliance。1997年4月,联盟提出了 WOL 技术。这是 WOL 技术的起源,随后各大厂商纷纷推出了自己的 WOL 技术标准。本文所讨论的 WOL 技术是由 AMD 公司提出的 Magic Packet(幻数据包,魔术包)唤醒方式,这里给出 AMD 关于此技术的白皮书。 幻数据包(Magic Packet) 幻数据包是一个广播帧,包含目标计算机的MAC地址。由于 MAC 地址的唯一性,使数据包可以在网络中被唯一的识别。幻数据包发送通常使用无连接的传输协议,如 UDP ,发送端口为 7 或 9 ,这只是通常做法,没有限制。 WOL 技术被提出了将近20年,绝大多数的现代网卡都支持在超低功耗下监听特定的报文,如 ARP。如果设备网卡接收到一个与自己 MAC 地址相同的幻数据包,则网卡会向计算机的电源或主板发出信号以唤醒计算机。大部分的幻数据包在数据链路层(OSI模型第2层)上发送,当发送时,使用广播地址广播到给定的网络上,不使用IP地址(OSI模型第3层)。当然这是绝大部分情况,幻数据包也可以使用特定的 IP 地址进行发送。 幻数据包最简单的构成是6字节的255(FF FF FF FF FF FF FF),紧接着为目标计算机的48位MAC地址,重复16次,数据包共计102字节。有时数据包内还会紧接着4-6字节的密码信息。这个帧片段可以包含在任何协议中,最常见的是包含在 UDP 中。 例如 MAC 地址为 11 22 33 44 55 66 的目标计算机,幻数据包的格式为: FFFFFFFFFFFF 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 112233445566 [ABABABABABAB(这里为6个字节的密码)] 幻数据包还有一些基本限制条件: 需要知道目标计算机 MAC 地址 不提供送达确认 可能无法在局域网之外工作 需要硬件进行支持 创建幻数据包 项目地址:https://github.com/ZhangGaoxing/wake-on-lan 该项目为 Xamarin 跨平台项目,包含 Xamarin.Android 与 UWP 。支持自动扫描添加局域网设备。 关于 MAC 地址的扫描获取,这里只说一下思路,详细请查阅代码。第一种方式,也是我最开始想到的方式,使用 Ping 来 Ping 整个网段。开了四个线程,1-255大概需要30多秒,稍微有点慢,而且 .NET 的 Ping 类在 Android 上无法限制秒数。第二种方式,百度到的,直接向整个网段发送 UDP 消息,2秒解决战斗。扫描完成后获取 ARP 表就行。 下面给出的是发送幻数据包的方法: public static async void Wake(string broadcast, int port, byte[] mac) { using (UdpClient udp = new UdpClient()) { udp.EnableBroadcast = true; byte[] packet = new byte[6 + 16 * 6]; for (int i = 0; i < 6; i++) { packet[i] = 0xFF; } for (int i = 0; i < 16; i++) { for (int j = 0; j < 6; j++) { packet[6 + i * 6 + j] = mac[j]; } } await udp.SendAsync(packet, packet.Length, broadcast, port); } }

优秀的个人博客,低调大师

unity中实现简单对象池,附教程原理

Unity对象池的创建与使用 本文提供全流程,中文翻译。 Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 —— 高分辨率用户请根据需求调整网页缩放比例) Chinar —— 心分享、心创新!助力快速完成 Unity 对象池的创建与使用为新手节省宝贵的时间,避免采坑! Chinar 教程效果: 1 MonoSingleton —— 单例基类 2 ObjectPool —— 对象池 3 PoolTest —— 测试对象池的使用 支持 May Be —— 搞开发,总有一天要做的事! 全文高清图片,点击即可放大观看 (很多人竟然不知道) 1 MonoSingleton —— 单例基类 任何继承自 MonoSingleton 泛型基类的脚本/类 都是单例类 using UnityEngine; /// <summary> /// 泛型单例基类 —— 任何继承自该类的类,都是单例类 /// </summary> /// <typeparam name="T">泛型</typeparam> public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T> { private static T instance; public static T Instance { get { if (instance == null) { instance = FindObjectOfType(typeof(T)) as T; if (instance == null) instance = new GameObject("Chinar Single of " + typeof(T).ToString(), typeof(T)).GetComponent<T>(); } return instance; } } private void Awake() { if (instance == null) instance = this as T; } private void OnApplicationQuit() { instance = null; } } 2 ObjectPool —— 对象池 新建一个脚本 ObjectPool 继承自泛型基类 MonoSingleton《ObjectPool》 是以 ObjectPool 就会是一个单例类 using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 对象池(重复调用/使用的游戏物体) 子弹,技能,导弹,敌人 /// </summary> public class ObjectPool : MonoSingleton<ObjectPool> { //字段 池 技能预设物(因为一个技能可能有多个预制件) 技能的复用性 private Dictionary<string, List<GameObject>> cache = new Dictionary<string, List<GameObject>>(); int i = 0; //标记 0 /// <summary> /// 创建显示对象 /// </summary> /// <returns>The object.</returns> /// <param name="key">对象名称</param> /// <param name="go">对象的预制件</param> /// <param name="position">对象的新位置</param> /// <param name="quaternion">对象的角度</param> public GameObject CreateObject(string key, GameObject go, Vector3 position, Quaternion quaternion) { GameObject tempgo = cache.ContainsKey(key) ? cache[key].Find(p => !p.activeSelf) : null; //返回池中未激活的对象,所有都被激活就返回空,赋值给临时对象 if (tempgo != null) //如果临时对象不为空 { tempgo.transform.position = position; //设置位置 tempgo.transform.rotation = quaternion; //旋转信息 } else //否则,就是空了。(也就是没能从池子里取出对象) { tempgo = Instantiate(go, position, quaternion); //那就根据传入的预设物,生成一个新物体 print("实例化物体数量:" + i++); if (!cache.ContainsKey(key)) //池中没有键 { cache.Add(key, new List<GameObject>()); //新建一个 列表 } cache[key].Add(tempgo); //给字典中的列表加入/add 临时物体,如果有键就直接添加了 } tempgo.SetActive(true); //并启用临时物体 return tempgo; //返回 } /// <summary> /// 直接回收 /// </summary> /// <param name="go">Go.</param> public void CollectObject(GameObject go) { go.SetActive(false); } /// <summary> /// 延迟回收 /// </summary> /// <param name="go">Go.</param> /// <param name="delay">Delay.</param> public void CollectObject(GameObject go, float delay) { StartCoroutine(Collect(go, delay)); } private IEnumerator Collect(GameObject go, float delay) { yield return new WaitForSeconds(delay); CollectObject(go); } /// <summary> /// 释放资源 /// </summary> /// <returns>The clear.</returns> /// <param name="key">Key.</param> public void Clear(string key) { if (cache.ContainsKey(key)) { //Destroy当中所有的对象 for (int i = 0; i < cache[key].Count; i++) { Destroy(cache[key][i]); } //清除键当中的所有值 //cache[key].Clear(); //清除这个键(键值一起清除) cache.Remove(key); } } /// <summary> /// 释放所有对象池 /// </summary> public void ClearAll() { var list = new List<string>(cache.Keys); for (int i = 0; i < list.Count; i++) { Clear(list[i]); } } } 3 PoolTest —— 测试对象池的使用 新建一个脚本 PoolTest 用来测试对象池的使用 using UnityEngine; /// <summary> /// 测试对象池的调用 /// </summary> public class PoolTest : MonoBehaviour { GameObject go; //临时对象 GameObject item; //临时对象池对象 void Start() { go = Resources.Load<GameObject>("Cube"); //在Resources文件目录下做一个Cube预设物 } void Update() { if (Input.GetKey(KeyCode.Space)) //按下空格创建一个Cube { item = ObjectPool.Instance.CreateObject("Object1", go, Vector3.zero, new Quaternion(0, 0, 0, 0)); item.transform.position += new Vector3(0, 0, 1); } if (Input.GetMouseButtonDown(0)) //按下左键执行回收对象 { if (item != null) { ObjectPool.Instance.CollectObject(item, 0.2f); } } if (Input.GetMouseButtonDown(1)) //按下右键执行清除已经生成的对象 { ObjectPool.Instance.Clear("Object1"); } } } 支持 May Be —— 搞开发,总有一天要做的事! 拥有自己的服务器,无需再找攻略! Chinar 提供一站式教程,闭眼式创建!为新手节省宝贵时间,避免采坑! 先点击领取 —— 阿里全产品优惠券 (享受最低优惠) 1 —— 云服务器超全购买流程 (新手必备!) 2 —— 阿里ECS云服务器自定义配置 - 购买教程(新手必备!) 3—— Windows 服务器配置、运行、建站一条龙 ! 4 —— Linux 服务器配置、运行、建站一条龙 ! Chinar END 本博客为非营利性个人原创,除部分有明确署名的作品外,所刊登的所有作品的著作权均为本人所拥有,本人保留所有法定权利。违者必究 对于需要复制、转载、链接和传播博客文章或内容的,请及时和本博主进行联系,留言,Email: ichinar@icloud.com 对于经本博主明确授权和许可使用文章及内容的,使用时请注明文章或内容出处并注明网址

优秀的个人博客,低调大师

Docker镜像进阶:了解其背后的技术原理

什么是 docker 镜像 docker 镜像是一个只读的 docker 容器模板,含有启动 docker 容器所需的文件系统结构及其内容,因此是启动一个 docker 容器的基础。docker 镜像的文件内容以及一些运行 docker 容器的配置文件组成了 docker 容器的静态文件系统运行环境:rootfs。可以这么理解,docker 镜像是 docker 容器的静态视角,docker 容器是 docker 镜像的运行状态。我们可以通过下图来理解 docker daemon、docker 镜像以及 docker 容器三者的关系(此图来自互联网): 从上图中我们可以看到,当由 ubuntu:14.04 镜像启动容器时,ubuntu:14.04 镜像的镜像层内容将作为容器的 rootfs;而 ubuntu:14.04 镜像的 json 文件,会由 docker daemon 解析,并提取出其中的容器执行入口 CMD 信息,以及容器进程的环境变量 ENV 信息,最终初始化容器进程。当然,容器进程的执行入口来源于镜像提供的 rootfs。 rootfs rootfs 是 docker 容器在启动时内部进程可见的文件系统,即 docker 容器的根目录。rootfs 通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类 Unix 操作系统中的目录系统,如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及运行 docker 容器所需的配置文件、工具等。在传统的 Linux 操作系统内核启动时,首先挂载一个只读的 rootfs,当系统检测其完整性之后,再将其切换为读写模式。而在 docker 架构中,当 docker daemon 为 docker 容器挂载 rootfs 时,沿用了 Linux 内核启动时的做法,即将 rootfs 设为只读模式。在挂载完毕之后,利用联合挂载(union mount)技术在已有的只读 rootfs 上再挂载一个读写层。这样,可读写的层处于 docker 容器文件系统的最顶层,其下可能联合挂载了多个只读的层,只有在 docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的旧版本文件。 Docker 镜像的主要特点 为了更好的理解 docker 镜像的结构,下面介绍一下 docker 镜像设计上的关键技术。 分层docker 镜像是采用分层的方式构建的,每个镜像都由一系列的 "镜像层" 组成。分层结构是 docker 镜像如此轻量的重要原因。当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版本所隐藏。当使用 docker commit 提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。分层达到了在不的容器同镜像之间共享镜像层的效果。 写时复制docker 镜像使用了写时复制(copy-on-write)的策略,在多个容器之间共享镜像,每个容器在启动的时候并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点,再在上面覆盖一个可读写的容器层。在未更改文件内容时,所有容器共享同一份数据,只有在 docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间。 内容寻址在 docker 1.10 版本后,docker 镜像改动较大,其中最重要的特性便是引入了内容寻址存储(content-addressable storage) 的机制,根据文件的内容来索引镜像和镜像层。与之前版本对每个镜像层随机生成一个 UUID 不同,新模型对镜像层的内容计算校验和,生成一个内容哈希值,并以此哈希值代替之前的 UUID 作为镜像层的唯一标识。该机制主要提高了镜像的安全性,并在 pull、push、load 和 save 操作后检测数据的完整性。另外,基于内容哈希来索引镜像层,在一定程度上减少了 ID 的冲突并且增强了镜像层的共享。对于来自不同构建的镜像层,主要拥有相同的内容哈希,也能被不同的镜像共享。 联合挂载通俗地讲,联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。实现这种联合挂载技术的文件系统通常被称为联合文件系统(union filesystem)。以下图所示的运行 Ubuntu:14.04 镜像后的容器中的 aufs 文件系统为例: 由于初始挂载时读写层为空,所以从用户的角度看,该容器的文件系统与底层的 rootfs 没有差别;然而从内核的角度看,则是显式区分开来的两个层次。当需要修改镜像内的某个文件时,只对处于最上方的读写层进行了变动,不复写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版本文件所隐藏,当 docker commit 这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。联合挂载是用于将多个镜像层的文件系统挂载到一个挂载点来实现一个统一文件系统视图的途径,是下层存储驱动(aufs、overlay等) 实现分层合并的方式。所以严格来说,联合挂载并不是 docker 镜像的必需技术,比如在使用 device mapper 存储驱动时,其实是使用了快照技术来达到分层的效果。 Docker 镜像的存储组织方式 综合考虑镜像的层级结构,以及 volume、init-layer、可读写层这些概念,一个完整的、在运行的容器的所有文件系统结构可以用下图来描述: 从图中我们不难看到,除了 echo hello 进程所在的 cgroups 和 namespace 环境之外,容器文件系统其实是一个相对独立的组织。可读写部分(read-write layer 以及 volumes)、init-layer、只读层(read-only layer) 这 3 部分结构共同组成了一个容器所需的下层文件系统,它们通过联合挂载的方式巧妙地表现为一层,使得容器进程对这些层的存在一无所知。 Docker 镜像中的关键概念 registry我们知道,每个 docker 容器都要依赖 docker 镜像。那么当我们第一次使用 docker run 命令启动一个容器时,是从哪里获取所需的镜像呢?答案是,如果是第一次基于某个镜像启动容器,且宿主机上并不存在所需的镜像,那么 docker 将从 registry 中下载该镜像并保存到宿主机。如果宿主机上存在该镜像,则直接使用宿主机上的镜像完成容器的启动。那么 registry 是什么呢?registry 用以保存 docker 镜像,其中还包括镜像层次结构和关于镜像的元数据。可以将 registry 简单的想象成类似于 Git 仓库之类的实体。用户可以在自己的数据中心搭建私有的 registry,也可以使用 docker 官方的公用 registry 服务,即 Docker Hub。它是由 Docker 公司维护的一个公共镜像库。Docker Hub 中有两种类型的仓库,即用户仓库(user repository) 与顶层仓库(top-level repository)。用户仓库由普通的 Docker Hub 用户创建,顶层仓库则由 Docker 公司负责维护,提供官方版本镜像。理论上,顶层仓库中的镜像经过 Docker 公司验证,被认为是架构良好且安全的。 repositoryrepository 由具有某个功能的 docker 镜像的所有迭代版本构成的镜像组。Registry 由一系列经过命名的 repository 组成,repository 通过命名规范对用户仓库和顶层仓库进行组织。所谓的顶层仓库,其其名称只包含仓库名,如: 而用户仓库的表示类似下面: 可以看出,用户仓库的名称多了 "用户名/" 部分。比较容易让人困惑的地方在于,我们经常把 mysql 视为镜像的名称,其实 mysql 是 repository 的名称。repository 是一个镜像的集合,其中包含了多个不同版本的镜像,这些镜像之间使用标签进行版本区分,如 mysql:5.6、mysql:5.7 等,它们均属于 mysql 这个 repository。简单来说,registry 是 repository 的集合,repository 是镜像的集合。 manifestmanifest(描述文件)主要存在于 registry 中作为 docker 镜像的元数据文件,在 pull、push、save 和 load 过程中作为镜像结构和基础信息的描述文件。在镜像被 pull 或者 load 到 docker 宿主机时,manifest 被转化为本地的镜像配置文件 config。在我们拉取镜像时显示的摘要(Digest): 就是对镜像的 manifest 内容计算 sha256sum 得到的。 image 和 layerdocker 内部的 image 概念是用来存储一组镜像相关的元数据信息,主要包括镜像的架构(如 amd64)、镜像默认配置信息、构建镜像的容器配置信息、包含所有镜像层信息的 rootfs。docker 利用 rootfs 中的 diff_id 计算出内容寻址的索引(chainID) 来获取 layer 相关信息,进而获取每一个镜像层的文件内容。layer(镜像层) 是 docker 用来管理镜像层的一个中间概念。我们前面提到,镜像是由镜像层组成的,而单个镜像层可能被多个镜像共享,所以 docker 将 layer 与 image 的概念分离。docker 镜像管理中的 layer 主要存放了镜像层的 diff_id、size、cache-id 和 parent 等内容,实际的文件内容则是由存储驱动来管理,并可以通过 cache-id 在本地索引到。 DockerfileDockerfile 是通过 docker build 命令构建 docker 镜像时用到的配置文件。它允许用户使用基本的 DSL 语法来定义 docker 镜像,其中的每一条指令描述一个构建镜像的步骤。更多关于 Dockerfile 信息请参考笔者的文章《Docker 基础 : Dockerfile》。 原文发布时间为:2018-05-29 本文作者:sparkdev 本文来自云栖社区合作伙伴“IT168”,了解相关信息可以关注“IT168”。

优秀的个人博客,低调大师

Fiori里花瓣的动画效果实现原理

Fiori里的busy dialog有两种表现形式,一种是下图里的花朵形状,由5个不断旋转的花瓣组成。另一种是下图的3/4个圆环不断旋转的效果。 关于前者的效果,可以看我制作的这个视频。这个视频是手动将下图setTimeout的300毫秒延时改成30秒之后的效果,意思是30秒之后这个花瓣效果才消失。 1. 根据调试器里调用栈的观察,每当url发生变化时,会触发sap.ui.controller.doHashChange,该方法则会调用openLoadingScreen.这个方法里会创建BusyDialog的实例并调用open方法。Busy Dialog的实现位置: /sap/bc/ui5_ui5/ui2/ushell/resources/sap/m/BusyDialog.js 2. 动画效果的渲染实现位置: /sap/bc/ui5_ui5/ui2/ushell/resources/sap/ui/core/Popup.js. 第52行的renderFioriFlower方法: 从97行的循环能看出在循环体内创建了5个div element,实际上就是UI Fiori动画效果里的5个花瓣。 3.Fiori动画效果显示之后,什么时候消失? 使用setTimeout将LoadingDialog的关闭动作加入到执行队列里。如果300毫秒之后(第188行的参数)执行队列里没有其他待执行的事件,则关闭该Dialog。 要获取更多Jerry的原创技术文章,请关注公众号"汪子熙"或者扫描下面二维码:

优秀的个人博客,低调大师

Java对象序列化底层原理源码解析

What Java序列化是指把Java对象保存为二进制字节码的过程,Java反序列化是指把二进制码重新转换成Java对象的过程。 那么为什么需要序列化呢? 第一种情况是:一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。 第二种情况是:需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。 How 本部分以序列化到文件为例讲解Java序列化的基本用法。 package test; import java.io.*; /** * @author v_shishusheng * @date 2018/2/7 */ public class SerializableTest { public static void main(String[] args) throws Exception { FileOutputStream fos = new FileOutputStream("temp.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); TestObject testObject = new TestObject(); oos.writeObject(testObject); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("temp.out"); ObjectInputStream ois = new ObjectInputStream(fis); TestObject deTest = (TestObject) ois.readObject(); System.out.println(deTest.testValue); System.out.println(deTest.parentValue); System.out.println(deTest.innerObject.innerValue); } } class Parent implements Serializable { private static final long serialVersionUID = -4963266899668807475L; public int parentValue = 100; } class InnerObject implements Serializable { private static final long serialVersionUID = 5704957411985783570L; public int innerValue = 200; } class TestObject extends Parent implements Serializable { private static final long serialVersionUID = -3186721026267206914L; public int testValue = 300; public InnerObject innerObject = new InnerObject(); } 程序执行完用sublime打开temp.out文件,可以看到 aced 0005 7372 0017 636f 6d2e 7373 732e 7465 7374 2e54 6573 744f 626a 6563 74d3 c67e 1c4f 132a fe02 0002 4900 0974 6573 7456 616c 7565 4c00 0b69 6e6e 6572 4f62 6a65 6374 7400 1a4c 636f 6d2f 7373 732f 7465 7374 2f49 6e6e 6572 4f62 6a65 6374 3b78 7200 1363 6f6d 2e73 7373 2e74 6573 742e 5061 7265 6e74 bb1e ef0d 1fc9 50cd 0200 0149 000b 7061 7265 6e74 5661 6c75 6578 7000 0000 6400 0001 2c73 7200 1863 6f6d 2e73 7373 2e74 6573 742e 496e 6e65 724f 626a 6563 744f 2c14 8a40 24fb 1202 0001 4900 0a69 6e6e 6572 5661 6c75 6578 7000 0000 c8 Why 调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?temp.out文件中的二进制分别代表什么意思? 1. ObjectStreamClass类 方文档对这个类的介绍如下 Serialization’s descriptor for classes. It contains the name and serialVersionUID of the class. The ObjectStreamClass for a specific class loaded in this Java VM can be found/created using the lookup method. 可以看到是类的序列化描述符,这个类可以描述需要被序列化的类的元数据,包括被序列化的类的名字以及序列号。可以通过lookup()方法来查找/创建在这个JVM中加载的特定的ObjectStreamClass对象。 2.序列化:writeObject() 调用wroteObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,构造函数如下: public ObjectOutputStream(OutputStream out) throws IOException { verifySubclass(); // bout表示底层的字节数据容器 bout = new BlockDataOutputStream(out); handles = new HandleTable(10, (float) 3.00); subs = new ReplaceTable(10, (float) 3.00); enableOverride = false; writeStreamHeader(); // 写入文件头 bout.setBlockDataMode(true); // flush数据 if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack(); } else { debugInfoStack = null; } } 构造函数中首先会把bout绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,该方法实现如下: protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION); } 在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号,定义为 /** * Magic number that is written to the stream header. */ final static short STREAM_MAGIC = (short)0xaced; /** * Version number that is written to the stream header. */ final static short STREAM_VERSION = 5; 接下来会调用writeObject()方法进行序列化,实现如下: public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return; } try { // 调用writeObject0()方法序列化 writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } } 正常情况下会调用writeObject0()进行序列化操作,该方法实现如下: private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false); depth++; try { // handle previously written and non-replaceable objects int h; if ((obj = subs.lookup(obj)) == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } // check for replacement object Object orig = obj; // 获取要序列化的对象的Class对象 Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // REMIND: skip this check for strings/arrays? Class<?> repCl; // 创建描述cl的ObjectStreamClass对象 desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } obj = rep; } // if object replaced, run through original checks a second time if (obj != orig) { subs.assign(orig, obj); if (obj == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } } // 根据实际的类型进行不同的写入操作 // remaining cases if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { // 被序列化对象实现了Serializable接口 writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } } 从代码里面可以看到,程序会 生成一个描述被序列化对象类的类元信息的ObjectStreamClass对象 根据传入的需要序列化的对象的实际类型进行不同的序列化操作。从代码里面可以很明显的看到, 对于String类型、数组类型和Enum可以直接进行序列化 如果被序列化对象实现了Serializable对象,则会调用writeOrdinaryObject()方法进行序列化 这里可以解释一个问题:Serializbale接口是个空的接口,并没有定义任何方法,为什么需要序列化的接口只要实现Serializbale接口就能够进行序列化。 答案是:Serializable接口这是一个标识,告诉程序所有实现了”我”的对象都需要进行序列化。 因此,序列化过程接下来会执行到writeOrdinaryObject()这个方法中,该方法实现如下: private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { if (extendedDebugInfo) { debugInfoStack.push( (depth == 1 ? "root " : "") + "object (class \"" + obj.getClass().getName() + "\", " + obj.toString() + ")"); } try { desc.checkSerialize(); // 写入Object标志位 bout.writeByte(TC_OBJECT); // 写入类元数据 writeClassDesc(desc, false); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); // 写入被序列化的对象的实例数据 } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } 在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object /** * new Object. */ final static byte TC_OBJECT = (byte)0x73; 接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,writeClassDesc()方法实现如下: private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException { int handle; if (desc == null) { // 如果desc为null writeNull(); } else if (!unshared && (handle = handles.lookup(desc)) != -1) { writeHandle(handle); } else if (desc.isProxy()) { writeProxyDesc(desc, unshared); } else { writeNonProxyDesc(desc, unshared); } } 在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法 private void writeNull() throws IOException { // TC_NULL = (byte)0x70; // 表示对一个Object引用的描述的结束 bout.writeByte(TC_NULL); } 如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法,该方法实现如下: private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException { // TC_CLASSDESC = (byte)0x72; // 表示一个新的Class描述符 bout.writeByte(TC_CLASSDESC); handles.assign(unshared ? null : desc); if (protocol == PROTOCOL_VERSION_1) { // do not invoke class descriptor write hook with old protocol desc.writeNonProxy(this); } else { writeClassDescriptor(desc); } Class cl = desc.forClass(); bout.setBlockDataMode(true); if (cl != null && isCustomSubclass()) { ReflectUtil.checkPackageAccess(cl); } annotateClass(cl); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false); } 在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,writeNonProxy()实现如下: void writeNonProxy(ObjectOutputStream out) throws IOException { out.writeUTF(name); // 写入类的名字 out.writeLong(getSerialVersionUID()); // 写入类的序列号 byte flags = 0; // 获取类的标识 if (externalizable) { flags |= ObjectStreamConstants.SC_EXTERNALIZABLE; int protocol = out.getProtocolVersion(); if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) { flags |= ObjectStreamConstants.SC_BLOCK_DATA; } } else if (serializable) { flags |= ObjectStreamConstants.SC_SERIALIZABLE; } if (hasWriteObjectData) { flags |= ObjectStreamConstants.SC_WRITE_METHOD; } if (isEnum) { flags |= ObjectStreamConstants.SC_ENUM; } out.writeByte(flags); // 写入类的flag out.writeShort(fields.length); // 写入对象的字段的个数 for (int i = 0; i < fields.length; i++) { ObjectStreamField f = fields[i]; out.writeByte(f.getTypeCode()); out.writeUTF(f.getName()); if (!f.isPrimitive()) { // 如果不是原始类型,即是对象或者Interface // 则会写入表示对象或者类的类型字符串 out.writeTypeString(f.getTypeString()); } } } writeNonProxy()方法中会按照以下几个过程来写入数据: 调用writeUTF()方法写入对象所属类的名字,对于本例中name = com.sss.test.对于writeUTF()这个方法,在写入实际的数据之前会先写入name的字节数,代码如下: void writeUTF(String s, long utflen) throws IOException { if (utflen > 0xFFFFL) { throw new UTFDataFormatException(); } // 写入两个字节的s的长度 writeShort((int) utflen); if (utflen == (long) s.length()) { writeBytes(s); } else { writeUTFBody(s); } } 接下来会调用writeLong()方法写入类的序列号UID,UID是通过getSerialVersionUID()方法来获取。 接着会判断被序列化的对象所属类的flag,并写入底层字节容器中(占用两个字节)。类的flag分为以下几类: final static byte SC_EXTERNALIZABLE = 0×04;表示该类为Externalizable类,即实现了Externalizable接口。 final static byte SC_SERIALIZABLE = 0×02;表示该类实现了Serializable接口。 final static byte SC_WRITE_METHOD = 0×01;表示该类实现了Serializable接口且自定义了writeObject()方法。 final static byte SC_ENUM = 0×10;表示该类是个Enum类型。 对于本例中flag = 0×02表示只是Serializable类型。 依次写入被序列化对象的字段的元数据。 <1> 首先会写入被序列化对象的字段的个数,占用两个字节。本例中为2,因为TestObject类中只有两个字段,一个是int类型的testValue,一个是InnerObject类型的innerValue。 <2> 依次写入每个字段的元数据。每个单独的字段由ObjectStreamField类来表示。 1.写入字段的类型码,占一个字节。 类型码的映射关系如下 类型码的映射关系 2.调用writeUTF()方法写入每个字段的名字。注意,writeUTF()方法会先写入名字占用的字节数。 3.如果被写入的字段不是基本类型,则会接着调用writeTypeString()方法写入代表对象或者类的类型字符串,该方法需要一个参数,表示对应的类或者接口的字符串,最终调用的还是writeString()方法,实现如下 private void writeString(String str, boolean unshared) throws IOException { handles.assign(unshared ? null : str); long utflen = bout.getUTFLength(str); if (utflen <= 0xFFFF) { // final static byte TC_STRING = (byte)0x74; // 表示接下来的字节表示一个字符串 bout.writeByte(TC_STRING); bout.writeUTF(str, utflen); } else { bout.writeByte(TC_LONGSTRING); bout.writeLongUTF(str, utflen); } } 在这个方法中会先写入一个标志位TC_STRING表示接下来的数据是一个字符串,接着会调用writeUTF()写入字符串。 执行完上面的过程之后,程序流程重新回到writeNonProxyDesc()方法中 private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException { // 其他省略代码 // TC_ENDBLOCKDATA = (byte)0x78; // 表示对一个object的描述块的结束 bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false); // 尾递归调用,写入父类的类元数据 } 接下来会写入一个字节的标志位TC_ENDBLOCKDATA表示对一个object的描述块的结束。 然后会调用writeClassDesc()方法,传入父类的ObjectStreamClass对象,写入父类的类元数据。 需要注意的是writeClassDesc()这个方法是个递归调用,调用结束返回的条件是没有了父类,即传入的ObjectStreamClass对象为null,这个时候会写入一个字节的标识位TC_NULL. 在递归调用完成写入类的类元数据之后,程序执行流程回到wriyeOrdinaryObject()方法中, private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { // 其他省略代码 try { desc.checkSerialize(); // 其他省略代码 if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); // 写入被序列化的对象的实例数据 } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } 从上面的分析中我们可以知道,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据。 接下来会调用writeSerialData()方法写入被序列化的对象的字段的数据,方法实现如下: private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { // 获取表示被序列化对象的数据的布局的ClassDataSlot数组,父类在前 ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { // 如果被序列化对象自己实现了writeObject()方法,则执行if块里的代码 // 一些省略代码 } else { // 调用默认的方法写入实例数据 defaultWriteFields(obj, slotDesc); } } } 在这个方法中首先会调用getClassDataSlot()方法获取被序列化对象的数据的布局,关于这个方法官方文档中说明如下: /** * Returns array of ClassDataSlot instances representing the data layout * (including superclass data) for serialized objects described by this * class descriptor. ClassDataSlots are ordered by inheritance with those * containing "higher" superclasses appearing first. The final * ClassDataSlot contains a reference to this descriptor. */ ClassDataSlot[] getClassDataLayout() throws InvalidClassException; 需要注意的是这个方法会把从父类继承的数据一并返回,并且表示从父类继承的数据的ClassDataSlot对象在数组的最前面。 对于没有自定义writeObject()方法的对象来说,接下来会调用defaultWriteFields()方法写入数据,该方法实现如下: private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { // 其他一些省略代码 int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte[primDataSize]; } // 获取对应类中的基本数据类型的数据并保存在primVals字节数组中 desc.getPrimFieldValues(obj, primVals); // 把基本数据类型的数据写入底层字节容器中 bout.write(primVals, 0, primDataSize, false); // 获取对应类的所有的字段对象 ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; // 把对应类的Object类型(非原始类型)的对象保存到objVals数组中 desc.getObjFieldValues(obj, objVals); for (int i = 0; i < objVals.length; i++) { // 一些省略的代码 try { // 对所有Object类型的字段递归调用writeObject0()方法写入对应的数据 writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } } 可以看到,在这个方法中会做下面几件事情: <1> 获取对应类的基本类型的字段的数据,并写入到底层的字节容器中。 <2> 获取对应类的Object类型(非基本类型)的字段成员,递归调用writeObject0()方法写入相应的数据。 从上面对写入数据的分析可以知道,写入数据是是按照先父类后子类的顺序来写的。 至此,Java序列化过程分析完毕,总结一下,在本例中序列化过程如下: 现在可以来分析下第二步中写入的temp.out文件的内容了。 aced Stream Magic 0005 序列化版本号 73 标志位:TC_OBJECT,表示接下来是个新的Object 72 标志位:TC_CLASSDESC,表示接下来是对Class的描述 0020 类名的长度为32 636f 6d2e 6265 6175 7479 626f 7373 2e73 com.beautyboss.s 6c6f 6765 6e2e 5465 7374 4f62 6a65 6374 logen.TestObject d3c6 7e1c 4f13 2afe 序列号 02 flag,可序列化 00 02 TestObject的字段的个数,为2 49 TypeCode,I,表示int类型 0009 字段名长度,占9个字节 7465 7374 5661 6c75 65 字段名:testValue 4c TypeCode:L,表示是个Class或者Interface 000b 字段名长度,占11个字节 696e 6e65 724f 626a 6563 74 字段名:innerObject 74 标志位:TC_STRING,表示后面的数据是个字符串 0023 类名长度,占35个字节 4c63 6f6d 2f62 6561 7574 7962 6f73 732f Lcom/beautyboss/ 736c 6f67 656e 2f49 6e6e 6572 4f62 6a65 slogen/InnerObje 6374 3b ct; 78 标志位:TC_ENDBLOCKDATA,对象的数据块描述的结束 接下来开始写入数据,从父类Parent开始 0000 0064 parentValue的值:100 0000 012c testValue的值:300 接下来是写入InnerObject的类元信息 73 标志位,TC_OBJECT:表示接下来是个新的Object 72 标志位,TC_CLASSDESC:表示接下来是对Class的描述 0021 类名的长度,为33 636f 6d2e 6265 6175 7479 626f 7373 com.beautyboss 2e73 6c6f 6765 6e2e 496e 6e65 724f .slogen.InnerO 626a 6563 74 bject 4f2c 148a 4024 fb12 序列号 02 flag,表示可序列化 0001 字段个数,1个 49 TypeCode,I,表示int类型 00 0a 字段名长度,10个字节 69 6e6e 6572 5661 6c75 65 innerValue 78 标志位:TC_ENDBLOCKDATA,对象的数据块描述的结束 70 标志位:TC_NULL,Null object reference. 0000 00c8 innervalue的值:200 3. 反序列化:readObject() 反序列化过程就是按照前面介绍的序列化算法来解析二进制数据。 有一个需要注意的问题就是,如果子类实现了Serializable接口,但是父类没有实现Serializable接口,这个时候进行反序列化会发生什么情况? 答:如果父类有默认构造函数的话,即使没有实现Serializable接口也不会有问题,反序列化的时候会调用默认构造函数进行初始化,否则的话反序列化的时候会抛出.InvalidClassException:异常,异常原因为no valid constructor。 Other 1. static和transient字段不能被序列化。 序列化的时候所有的数据都是来自于ObejctStreamClass对象,在生成ObjectStreamClass的构造函数中会调用fields = getSerialFields(cl);这句代码来获取需要被序列化的字段,getSerialFields()方法实际上是调用getDefaultSerialFields()方法的,getDefaultSerialFields()实现如下: private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) { Field[] clFields = cl.getDeclaredFields(); ArrayList<ObjectStreamField> list = new ArrayList<>(); int mask = Modifier.STATIC | Modifier.TRANSIENT; for (int i = 0; i < clFields.length; i++) { if ((clFields[i].getModifiers() & mask) == 0) { // 如果字段既不是static也不是transient的才会被加入到需要被序列化字段列表中去 list.add(new ObjectStreamField(clFields[i], false, true)); } } int size = list.size(); return (size == 0) ? NO_FIELDS : list.toArray(new ObjectStreamField[size]); } 从上面的代码中可以很明显的看到,在计算需要被序列化的字段的时候会把被static和transient修饰的字段给过滤掉。 在进行反序列化的时候会给默认值。 2. 如何实现自定义序列化和反序列化? 只需要被序列化的对象所属的类定义了void writeObject(ObjectOutputStream oos)和void readObject(ObjectInputStream ois)方法即可,Java序列化和反序列化的时候会调用这两个方法,那么这个功能是怎么实现的呢? 在ObjectClassStream类的构造函数中有下面几行代码: cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class }, Void.TYPE); readObjectMethod = getPrivateMethod(cl, "readObject", new Class<?>[] { ObjectInputStream.class }, Void.TYPE); readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = (writeObjectMethod != null); cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class }, Void.TYPE); readObjectMethod = getPrivateMethod(cl, "readObject", new Class<?>[] { ObjectInputStream.class }, Void.TYPE); readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = (writeObjectMethod != null); cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class }, Void.TYPE); readObjectMethod = getPrivateMethod(cl, "readObject", new Class<?>[] { ObjectInputStream.class }, Void.TYPE); readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = (writeObjectMethod != null); getPrivateMethod()方法实现如下: private static Method getPrivateMethod(Class<?> cl, String name, Class<?>[] argTypes, Class<?> returnType) { try { Method meth = cl.getDeclaredMethod(name, argTypes); meth.setAccessible(true); int mods = meth.getModifiers(); return ((meth.getReturnType() == returnType) && ((mods & Modifier.STATIC) == 0) && ((mods & Modifier.PRIVATE) != 0)) ? meth : null; } catch (NoSuchMethodException ex) { return null; } } 可以看到在ObejctStreamClass的构造函数中会查找被序列化类中有没有定义为void writeObject(ObjectOutputStream oos) 的函数,如果找到的话,则会把找到的方法赋值给writeObjectMethod这个变量,如果没有找到的话则为null。 在调用writeSerialData()方法写入序列化数据的时候有 private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { // 其他一些省略代码 try { curContext = new SerialCallbackContext(obj, slotDesc); bout.setBlockDataMode(true); // 在这里调用用户自定义的方法 slotDesc.invokeWriteObject(obj, this); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } finally { curContext.setUsed(); curContext = oldContext; if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut; } else { defaultWriteFields(obj, slotDesc); } } } 首先会调用hasWriteObjectMethod()方法判断有没有自定义的writeObject(),代码如下 boolean hasWriteObjectMethod() { return (writeObjectMethod != null); } hasWriteObjectMethod()这个方法仅仅是判断writeObjectMethod是不是等于null,而上面说了,如果用户自定义了void writeObject(ObjectOutputStream oos)这么个方法,则writeObjectMethod不为null,在if()代码块中会调用slotDesc.invokeWriteObject(obj, this);方法,该方法中会调用用户自定义的writeObject()方法。 Java编程思想相关知识点 当程序运行时,有关对象的信息就存储在了内存当中,但是当程序终止时,对象将不再继续存在。我们需要一种储存对象信息的方法,使我们的程序关闭之后他还继续存在,当我们再次打开程序时,可以轻易的还原当时的状态。这就是对象序列化的目的。 java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并且能够在以后将这个字节序列完全恢复为原来的对象,甚至可以通过网络传播。 这意味着序列化机制自动弥补了不同OS之间的差异. 如此,java实现了“轻量级持久性”,为啥是轻量级,因为在java中我们还不能直接通过一个类似public这样的关键字直接使一个对象序列化,并让系统自动维护其他细节问题。因此我们只能在程序中显示地序列化与反序列化 对象序列化的概念加入到语言中是为了支持两种主要特性: java的远程方法调用(RMI),它使存活于其他计算机上的对象使用起来就像存活于本机上一样。当远程对象发送消息时,需要通过对象序列化来传输参数和返回值。 对于Java Bean来说,对象序列化是必须的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动的时候进行后期恢复,这种具体工作就是由对象序列化完成的。 使用——对象实现Serializable接口(仅仅是一个标记接口,没有任何方法)。 序列化一个对象: 1. 创建某些OutputStream对象 2. 将其封装在一个ObjectOutputStream对象内 3. 只需调用writeObject()即可将对象序列化 注:也可以为一个String调用writeObject();也可以用与DataOutputStream相同的方法写入所有基本数据类型(它们具有同样的接口) 反序列化 将一个InputStream封装在ObjectInputStream内,然后调用readObject()。最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们 对象序列化不仅保存了对象的“全景图”,而且能够追踪对象内所包含的所有引用,并保存这些对象;接着又能对对象内包含的每个这样的引用进行追踪;以此类推。这种情况有时被称为“对象网”。 例子: ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"); out.writeObject(w); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"); String s = (String)in.readObject(); 在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器。 整个对象都是通过InputStream中取得数据恢复而来的。 寻找类:必须保证java虚拟机能够找到相关的.class文件。找不到就会得到一个ClassNotFOundExcption的异常。 序列化的控制——通过实现Externalizable接口——代替实现Serializable接口——来对序列化过程进行控制。 1. Externalizable接口继承了Serializable接口,增加了两个方法,writeExternal()和readExternal(),这两个方法会在序列化和反序列化还原的过程中被自动调用。 2. Externalizable对象,在还原的时候所有普通的默认构造器都会被调用(包括在字段定义时的初始化)(只有这样才能使Externalizable对象产生正确的行为),然后调用readExternal(). 3. 如果我们从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。 4. 为了正常运行,我们不仅需要在writeExternal()方法中将来自对象的重要信息写入,还必须在readExternal()中恢复数据 防止对象的敏感部分被序列化,两种方式: 1. 将类实现Externalizable,在writeExternal()内部只对所需部分进行显示的序列化 2. 实现Serializable,用transient(瞬时)关键字(只能和Serializable一起使用)逐个字段的关闭序列化,他的意思:不用麻烦你保存或恢复数据——我自己会处理。 Externalizable的替代方法 1. 实现Serializable接口,并添加名为writeObject()和readObject()的方法,这样一旦对象被序列化或者被反序列化还原,就会自动的分别调用writeObject()和readObject()的方法(它们不是接口的一部分,接口的所有东西都是public的)。只要提供这两个方法,就会使用它们而不是默认的序列化机制。 2. 这两个方法必须具有准确的方法特征签名,但是这两个方法并不在这个类中的其他方法中调用,而是在ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法 [图片上传失败...(image-c92672-1517928660769)] 3. 技巧:在你的writeObject()和readObject()内部调用defaultWriteObject()和defaultReadObject来选择执行默认的writeObject()和readObject();如果打算使用默认机制写入对象的非transient部分,那么必须调用defaultwriteObject()和defaultReadObject(),且作为writeObject()和readObject()的第一个操作。 使用“持久性” 1. 只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制出的对象。当然,我们可以在写出第一个对象和写出最后一个对象期间改变这些对象的状态,但是这是我们自己的事;无论对象在被序列化时处于什么状态(无论它们和其他对象有什么样的连接关系),我们都可以被写出。 2. Class是Serializable的,因此只需要直接对Class对象序列化,就可以很容易的保存static字段,任何情况下,这都是一种明智的做法。但是必须自己动手去实现序列化static的值。 使用serializeStaticState()和deserializeStaticState()两个static方法,它们是作为存储和读取过程的一部分被显示的调用的 3. 安全问题:序列化会将private数据保存下来,对于你关心的安全问题,应将其标记为transient。但是这之后,你还必须设计一种安全的保存信息的方法,以便在执行恢复时可以复位那些private变量。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册