《疯狂java讲义》学习(41):标准序列化机制

2024-04-17 20:48

本文主要是介绍《疯狂java讲义》学习(41):标准序列化机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

标准序列化机制

简单来说,序列化就是将对象转化为字节流,反序列化就是将字节流转化为对象。

基本用法

要让一个类支持序列化,只需要让这个类实现接口java.io.Serializable。Serializable没有定义任何方法,只是一个标记接口。比如,对于前面章节提到的Student类,为支持序列化,可改为:

public class Student implements Serializable {//省略主体代码
}

声明实现了Serializable接口后,保存/读取Student对象就可以使用ObjectOutputStream/ObjectInputStream流了。ObjectOutputStream是OutputStream的子类,但实现了ObjectOutput接口。ObjectOutput是DataOutput的子接口,增加了一个方法:

public void writeObject(Object obj) throws IOException

这个方法能够将对象obj转化为字节,写到流中。ObjectInputStream是InputStream的子类,它实现了ObjectInput接口。ObjectInput是DataInput的子接口,增加了一个方法:

public Object readObject() throws ClassNotFoundException, IOException

这个方法能够从流中读取字节,转化为一个对象。
使用这两个流,保存学生列表的代码就可以变为:

public static void writeStudents(List<Student> students) throws IOException {ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));try {out.writeInt(students.size());for(Student s : students) {out.writeObject(s);}} finally {out.close();}
}

而从文件中读入学生列表的代码可以变为:

public static List<Student> readStudents() throws IOException,ClassNotFoundException {ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));try {int size = in.readInt();List<Student> list = new ArrayList<>(size);for(int i = 0; i < size; i++) {list.add((Student) in.readObject());}return list;} finally {in.close();}
}

实际上,只要List对象也实现了Serializable(ArrayList/LinkedList都实现了),上面代码可以进一步简化,读写只需要一行代码,如下所示:


public static void writeStudents(List<Student> students) throws IOException {ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));try {out.writeObject(students);} finally {out.close();}
}
public static List<Student> readStudents() throws IOException, ClassNotFoundException {ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));try {return (List<Student>) in.readObject();} finally {in.close();}
}

只要将类声明实现Serializable接口,然后就可以使用ObjectOutput-Stream/ObjectInputStream直接读写对象了。我们之前介绍的各种类,如String、Date、Double、ArrayList、LinkedList、HashMap、TreeMap等,都实现了Serializable。

复杂对象

上面例子中的Student对象时非常简单的,如果对象比较复杂呢?比如:

  1. 如果a、b两个对象都引用同一个对象c,序列化后c是保存两份还是一份?在徐丽丽恶化后还能让a、b指向同一个对象吗?答案是,c只会保存一份,反序列化后指向相同对象。
  2. 如果a、b两个对象有循环引用呢?即a引用了b,而b也引用了a。这种情况Java也没问题,可以保持引用关系。

这就是Java序列化机制的神奇之处,它能自动处理引用同一个对象的情况,也能自动处理循环引用的情况。

定制序列化

默认的序列化机制已经很强大了,它可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。
对于有些字段,它的值可能与内存位置有关,比如默认的hashCode()方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有了意义。还有一些字段,可能与当前时间有关,比如表示对象创建时的时间,保存和恢复这个字段就是不正确的。
还有一些情况,如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为什么不适合呢?因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。
比如,我们在容器类中介绍的LinkedList,它的默认序列化就是不适合的。为什么呢?因为LinkedList表示一个List,它的逻辑信息是列表的长度,以及列表中的每个对象,但LinkedList类中的字段表示的是链表的实现细节,如头尾节点指针,对每个节点,还有前驱和后继节点指针等。
那怎么办呢?Java提供了多种定制序列化的机制,主要的有两种:一种是transient关键字,另外一种是实现writeObject和readObject方法。
将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。比如,类LinkedList中,它的字段都声明为了transient,如下所示:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

声明为了transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了,可以实现writeObject/readObject方法来自己保存该字段。
类可以实现writeObject方法,以自定义改了对象的序列化过程,其声明必须为:

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

可以在这个方法中,调用ObjectOutputStream的方法向流中写入对象的数据。比如,LinkedList使用如下代码序列化列表的逻辑数据:

private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException {s.defaultWriteObject();//写元素个数s.writeInt(size);//循环写每个元素for(Node<E> x = first; x ! = null; x = x.next)s.writeObject(x.item);
}

需要注意的是代码:

s.defaultWriteObject();

这一行是必须的,它会调用默认的序列化机制,默认机制会保存所有没声明为transient的字段,即使类中的所有字段都是transient,也应该写这一行,因为Java的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化之所以能够神奇的重要原因。
与writeObject对应的是readObject方法,通常它自定义反序列化过程,其声明必须为:

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException

在这个方法中,调用ObjectInputStream的方法从流中读入数据,然后初始化类中的成员变量。比如,LinkedList的反序列化代码为:

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();//读元素个数int size = s.readInt();//循环读入每个元素for (int i = 0; i < size; i++)linkLast((E)s.readObject());
}

注意代码:

s.defaultReadObject();

这一行代码也是必需的。
除了自定义writeObject/readObject方法,还有一些自定义序列化过程的机制:Exter-nalizable接口、readResolve方法和writeReplace()方法,这些机制用的相对较少,我们就不介绍了。

序列化的基本原理

稍微总结一下。

  1. 如果类的字段表示的就是类的逻辑信息,如上面的Student类,那就可以使用默认序列化机制,只要声明实现Serializable接口即可。
  2. 否则的话,如LinkedList,那就可以使用transient关键字,实现writeObject和read-Object自定义序列化过程。
  3. Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。

序列化到底是如何发生的呢?关键在ObjectOutputStream的writeObject和ObjectInput-Stream的readObject方法内。它们的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇,我们简单介绍其基本逻辑。
writeObject的基本逻辑是:

  1. 如果对象没有实现Serializable,抛出异常NotSerializableException。
  2. 每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题。
  3. 如果对象实现了writeObject方法,调用它的自定义方法。
  4. 默认是利用反射机制(反射在第21章介绍),遍历对象结构图,对每个没有标记为transient的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型,即完整类名、字段名、字段值等。

readObject的基本逻辑是:

  1. 不调用任何构造方法;
  2. 它自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制;
  3. 在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出ClassNotFoundException。

序列化特点分析

序列化的主要用途有两个:一个是对象持久化;另一个是跨网络的数据交换、远程过程调用。Java标准的序列化机制有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便地进行定制,处理版本问题等,但它也有一些局限性。

  1. Java序列化格式是一种私有格式,是一种Java特有的技术,不能被其他语言识别,不能实现跨语言的数据交换。
  2. Java在序列化字节中保存了很多描述信息,使得序列化格式比较大。
  3. Java的默认序列化使用反射分析遍历对象结构,性能比较低。
  4. Java的序列化格式是二进制的,不方便查看和修改。

由于这些局限性,实践中往往会使用一些替代方案。在跨语言的数据交换格式中,ⅩML/JSON是被广泛采用的文本格式,各种语言都有对它们的支持,文件格式清晰易读。有很多查看和编辑工具,它们的不足之处是性能和序列化大小,在性能和大小敏感的领域,往往会采用更为精简高效的二进制方式,如ProtoBuf、Thrift、MessagePack等。

Java编程练习

使用I/O流生成Word文件

在日常生活中,我们使用最多的就是Word文件,很多我们日常的文稿都需要使用Word文件来进行编辑和保存。那么我们能不能使用Java程序来实现Word文件的生成操作呢?答案是肯定的。本实例就是一个使用Java程序生成Word文件的例子。

1.

新建项目GenerateWord,并在其中创建一个GenerateWord.java文件。在该类中引入iText包来生成Word文件,并在该文件中生成文字和表格:

package GenerateWord;import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.rtf.RtfWriter2;import java.awt.*;
import java.io.FileOutputStream;
import java.io.IOException;public class GenerateWord {                               // 用iText生成word文件public void createDocFile(String file) throws DocumentException,IOException {Document document = new Document(PageSize.A4); // 设置纸张大小// 建立一个书写器(writer)与document对象关联,通过书写器(writer)可以将文档写入到磁盘中RtfWriter2.getInstance(document, new FileOutputStream(file));document.open();BaseFont macintosh = BaseFont.createFont ("C:\\wINDOwS\\Fonts\\STFANGSO.TTF",BaseFont.IDENTITY_H, BaseFont.EMBEDDED);                  // 设置中文字体Font ThemeFont = new Font(macintosh, 18, Font.BOLD);// 标题字体风格ThemeFont.setColor(Color.RED);              // 设置标题字体的颜色Font bodyFont = new Font(macintosh, 14, Font.NORMAL);// 正文字体风格bodyFont.setColor(56, 94, 15);              // 设置正文字体的颜色Paragraph theme = new Paragraph("主标题");  // 创建主题的Phrases对象theme.setAlignment(Element.ALIGN_CENTER);// 将设置好的字体添加到主题的短句上document.add(theme);                   // 将Phrases添加到document文档中String bodyText = "这是一个word文档";       // 设置word正文中的内容Paragraph context = new Paragraph(bodyText);    // 创建正文的Phrases对象context.setAlignment(Element.ALIGN_LEFT);   // 正文格式左对齐context.setSpacingBefore(3);                // 离上一段落(标题)空的行数context.setFirstLineIndent(20);             // 设置第一行空的列数document.add(context);                 // 将Phrases添加到document文档中// 利用类FontFactory结合Font和Color可以设置各种各样字体样式Paragraph line = new Paragraph("下画线的实现", FontFactory.getFont(FontFactory.HELVETICA_BOLDOBLIQUE, 18, Font.UNDERLINE,new Color(0, 0, 255)));document.add(line);// 创建Table表格Table table = new Table(5);        // 创建该表格的列数,在本程序中设为5列int width[] = { 25, 25, 25, 25, 25 };       // 每列的单元格的宽度;table.setWidths(width);                     // 设置每列所占比例table.setWidth(90);                         // 占页面宽度90%table.setAlignment(Element.ALIGN_CENTER);// 设置该表格中的元素水平方向居中显示table.setAlignment(Element.ALIGN_MIDDLE);// 设置表格中的元素垂直方向纵向居中显示table.setAutoFillEmptyCells(true);                    // 自动填满table.setBorderWidth(1);                              // 边框宽度table.setBorderColor(new Color(160, 32, 240));        // 边框颜色table.setPadding(2);  // 单元格内部的空白距离,相当于html中的cellpadding属性table.setSpacing(3);          //单元格之间的间距,相当于html中的cellspacingtable.setBorder(2);           // 边框的宽度// 设置表头Cell haderCell = new Cell("用iText创建的表格-表头");  // 创建单元格haderCell.setBackgroundColor(Color.pink);       // 设置此单元格的背景色haderCell.setHeader(true);                      // 设置为表头haderCell.setColspan(5);                        // 合并列的列数haderCell.setHorizontalAlignment(haderCell.ALIGN_CENTER);// 水平显示的位置table.addCell(haderCell);                       // 将单元格添加到表格中table.endHeaders();                             // 结束表头的设置Font fontChinese = new Font(macintosh, 15, Font.NORMAL, Color.blue);// 设置字体风格Cell cell = new Cell(new Phrase("这是一个3行1列合并的表格", fontChinese));// 创建单元格cell.setVerticalAlignment(Element.ALIGN_TOP);cell.setBorderColor(new Color(255, 215, 0));cell.setRowspan(3);                         // 设置合并的行数// 添加单元格table.addCell(cell);for (int i = 0; i < 12; i++) {table.addCell(new Cell(i+""));}// 创建一个合并5列的单元格Cell cell3 = new Cell(new Phrase("一行5列合并的表格", fontChinese));cell3.setColspan(5);cell3.setVerticalAlignment(Element.ALIGN_CENTER);table.addCell(cell3);document.add(table);document.close();}public static void main(String[] args) {GenerateWord word = new GenerateWord();String file = "D:/word.doc";try {word.createDocFile(file);} catch (DocumentException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}
}

运行程序,生成文件如下:
在这里插入图片描述

使用I/O流生成Excel文件

Excel文件在我们的生活中也起到了非常广泛的作用,比如财务报表、工资清单等。本实例我们将为大家展示如何使用Java的输入/输出实现Excel文件的生成操作。

1.

新建项目GenerateExcel,并在其中创建一个GenerateExcel.java文件。在该类中首先引入org.apache.poi包,然后通过其中的hssf类来实现Excel文件的生成:

package GenerateExcel;import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;import java.io.*;public class GenerateExcel {                              //创建Excel文件// 新建一个Excel文件,里面添加5行5列的内容,另外添加两个合并大单元格public void createExcel(String fileName) {File file = new File(fileName);                 // 创建excel文件对象FileOutputStream fOut = null;try {HSSFWorkbook workbook = new HSSFWorkbook();// 创建一个新的HSSFworkbook对象HSSFSheet sheet = workbook.createSheet("myFirstExcel");// 创建一个Excel的工作表HSSFFont font = workbook.createFont();      // 创建字体,红色、粗体font.setColor(HSSFFont.COLOR_RED);font.setBold(true);HSSFFont font1 = workbook.createFont();font1.setColor(HSSFFont.COLOR_NORMAL);      // 创建字体,黑色、非粗体font1.setBold(false);HSSFCellStyle cellStyle = workbook.createCellStyle();// 创建单元格的格式,如居中、左对齐等cellStyle.setAlignment(HorizontalAlignment.CENTER);// 水平方向上居中对齐// 垂直方向上居中对齐cellStyle.setFont(font);           // 设置字体HSSFCellStyle cellStyle1 = workbook.createCellStyle();cellStyle1.setAlignment(HorizontalAlignment.LEFT);cellStyle1.setFont(font1);// 下面将建立一个4行3列的表。第一行为表头int rowNum = 0;                    // 行标int colNum = 0;                    // 列标// 建立表头信息HSSFRow row = sheet.createRow((short) rowNum); // 在索引0的位置创建行HSSFCell cell = null;                           // 单元格for (colNum = 0; colNum < 5; colNum++) {cell = row.createCell((short) colNum);// 在当前行的colNum列上创建单元格cell.setCellType(Cell.CELL_TYPE_STRING);// 定义编码方式,为了支持中文,这里使用了ENCODING_UTF_16cell.setCellStyle(cellStyle); // 为单元格设置格式cell.setCellValue("表头-第" + (colNum + 1) + "列");// 添加内容至单元格}rowNum++;for (; rowNum < 5; rowNum++) {row = sheet.createRow((short) rowNum);      // 新建第rowNum行for (colNum = 0; colNum < 5; colNum++) {cell = row.createCell((short) colNum);// 在当前行的colNum位置创建单元格cell.setCellStyle(cellStyle1);cell.setCellValue("表体-第" + rowNum + "行第" + (colNum + 1)+ "列");}}rowNum = 5;                                // 合并单元格for (; rowNum < 9; rowNum++) {row = sheet.createRow((short) rowNum);for (colNum = 0; colNum < 5; colNum++) {cell = row.createCell((short) colNum);     // 在当前行的colNum位置创建单元格}}rowNum = 5;                   // 建立第一个大单元格,高度为2,宽度为2colNum = 0;CellRangeAddress region = new CellRangeAddress(rowNum, (rowNum + 1), (short) colNum,(short) (colNum + 4));sheet.addMergedRegion(region);cell = sheet.getRow(rowNum).getCell((short) colNum);// 获得第一个大单元格cell.setCellStyle(cellStyle);cell.setCellValue("合并行单元格");rowNum = 7;                   // 建立第二个大单元格,高度为2,宽度为3for (colNum = 0; colNum < 5; colNum++) {region = new CellRangeAddress(rowNum, (rowNum + 1), (short) colNum,(short) (colNum));sheet.addMergedRegion(region);cell = sheet.getRow(rowNum).getCell((short) colNum);// 获得第二个大单元格cell.setCellStyle(cellStyle);cell.setCellValue("合并列单元格");}fOut = new FileOutputStream(file);     // 新建一输出文件流workbook.write(fOut);// 将创建的内容写到指定的Excel文件中fOut.flush();fOut.close();                          // 操作结束,关闭文件System.out.println("Excel文件创建成功!\nExcel文件的存放路径为:"+ file.getAbsolutePath());} catch (Exception e) {System.out.println("Excel文件" + file.getAbsolutePath()+ "创建失败\n其原因为:" + e);} finally {if (fOut != null) {try {fOut.close();} catch (IOException e1) {}}}}public static void main(String[] args) throws Exception {GenerateExcel excel = new GenerateExcel();String fileName = "D://Excel.xls";         // 指定生成Excel文件名称excel.createExcel(fileName);}
}

运行程序,生成文件内容如下:
在这里插入图片描述

这篇关于《疯狂java讲义》学习(41):标准序列化机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/912811

相关文章

关于跨域无效的问题及解决(java后端方案)

《关于跨域无效的问题及解决(java后端方案)》:本文主要介绍关于跨域无效的问题及解决(java后端方案),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录通用后端跨域方法1、@CrossOrigin 注解2、springboot2.0 实现WebMvcConfig

Java SWT库详解与安装指南(最新推荐)

《JavaSWT库详解与安装指南(最新推荐)》:本文主要介绍JavaSWT库详解与安装指南,在本章中,我们介绍了如何下载、安装SWTJAR包,并详述了在Eclipse以及命令行环境中配置Java... 目录1. Java SWT类库概述2. SWT与AWT和Swing的区别2.1 历史背景与设计理念2.1.

使用SpringBoot整合Sharding Sphere实现数据脱敏的示例

《使用SpringBoot整合ShardingSphere实现数据脱敏的示例》ApacheShardingSphere数据脱敏模块,通过SQL拦截与改写实现敏感信息加密存储,解决手动处理繁琐及系统改... 目录痛点一:痛点二:脱敏配置Quick Start——Spring 显示配置:1.引入依赖2.创建脱敏

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

SpringBoot 中 CommandLineRunner的作用示例详解

《SpringBoot中CommandLineRunner的作用示例详解》SpringBoot提供的一种简单的实现方案就是添加一个model并实现CommandLineRunner接口,实现功能的... 目录1、CommandLineRunnerSpringBoot中CommandLineRunner的作用

Java死锁问题解决方案及示例详解

《Java死锁问题解决方案及示例详解》死锁是指两个或多个线程因争夺资源而相互等待,导致所有线程都无法继续执行的一种状态,本文给大家详细介绍了Java死锁问题解决方案详解及实践样例,需要的朋友可以参考下... 目录1、简述死锁的四个必要条件:2、死锁示例代码3、如何检测死锁?3.1 使用 jstack3.2

Go语言中Recover机制的使用

《Go语言中Recover机制的使用》Go语言的recover机制通过defer函数捕获panic,实现异常恢复与程序稳定性,具有一定的参考价值,感兴趣的可以了解一下... 目录引言Recover 的基本概念基本代码示例简单的 Recover 示例嵌套函数中的 Recover项目场景中的应用Web 服务器中

Java日期类详解(最新推荐)

《Java日期类详解(最新推荐)》早期版本主要使用java.util.Date、java.util.Calendar等类,Java8及以后引入了新的日期和时间API(JSR310),包含在ja... 目录旧的日期时间API新的日期时间 API(Java 8+)获取时间戳时间计算与其他日期时间类型的转换Dur

java对接海康摄像头的完整步骤记录

《java对接海康摄像头的完整步骤记录》在Java中调用海康威视摄像头通常需要使用海康威视提供的SDK,下面这篇文章主要给大家介绍了关于java对接海康摄像头的完整步骤,文中通过代码介绍的非常详细,需... 目录一、开发环境准备二、实现Java调用设备接口(一)加载动态链接库(二)结构体、接口重定义1.类型