反序列化对象列表发生异常_谈谈 Java 对象序列化与反序列化。

↑ 点击上面 “时代Java”关注我们, 关注新技术,学习新知识!

节选自《Java 精简入门教程》

进入下面链接或点击文章末尾的 “阅读原文” 查看全部教程

https://nowjava.com/book/java-simplify

什么是对象序列化?

序列化 过程中,会以一种特殊的二进制格式存储一个对象及其元数据(比如对象的类名称及其属性名称)的状态。将对象存储为此格式 — 序列化 它 — 可保留在需要时重新构成(或 反序列化 )该对象所必需的全部信息。

对象序列化有两种主要用例:

  • 对象持久化 — 将对象的状态存储在一种永久的持久性机制中,比如数据库

  • 对象远程传输 — 将对象发送至另一个计算机或系统

5a58954df98792c33559a89e5936b434.png

java.io.Serializable

实现序列化的第一步是让对象能够使用该机制。希望能够序列化的每个对象都必须实现一个名为 java.io.Serializable 的接口:

import java.io.Serializable;public class Person implements Serializable {  // etc...}

在此示例中,Serializable 接口将 Person 类(和 Person 的每个子类的对象)向运行时标记为 serializable

如果 Java 运行时尝试序列化您的对象,无法序列化的对象的每个属性会导致它抛出一个 NotSerializableException。可以使用 transient 关键字管理此行为,告诉运行时不要尝试序列化一些属性。在这种情况下,您应该负责确保恢复这些属性(在必要时),以便您的对象能正常运行。

序列化一个对象

现在我们通过一个示例,尝试将刚学到的 Java I/O 知识与现在学习的序列化知识结合起来。

假设您创建并填充一个包含 Employee 对象的 List,然后希望将该 List 序列化为一个 OutputStream,在本例中是序列化为一个文件。该过程如清单 12 所示。

清单 12. 序列化一个对象
public class HumanResourcesApplication {  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();  public static ListcreateEmployees() {    List ret = new ArrayList();    Employee e = new Employee("Jon Smith", 45, 175, 75, "BLUE", Gender.MALE,       "123-45-9999", "0001", BigDecimal.valueOf(100000.0));    ret.add(e);    //    e = new Employee("Jon Jones", 40, 185, 85, "BROWN", Gender.MALE, "223-45-9999",       "0002", BigDecimal.valueOf(110000.0));    ret.add(e);    //    e = new Employee("Mary Smith", 35, 155, 55, "GREEN", Gender.FEMALE, "323-45-9999",       "0003", BigDecimal.valueOf(120000.0));    ret.add(e);    //    e = new Employee("Chris Johnson", 38, 165, 65, "HAZEL", Gender.UNKNOWN,       "423-45-9999", "0004", BigDecimal.valueOf(90000.0));    ret.add(e);    // Return list of Employees    return ret;  }  public boolean serializeToDisk(String filename, List employees) {    final String METHOD_NAME = "serializeToDisk(String filename, List employees)";    boolean ret = false;// default: failed    File file = new File(filename);    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) {      log.info("Writing " + employees.size() + " employees to disk (using Serializable)...");      outputStream.writeObject(employees);      ret = true;      log.info("Done.");    } catch (IOException e) {      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " +       file.getName() + ", message = " + e.getLocalizedMessage(), e);    }    return ret;  }

第一步是创建这些对象,在 createEmployees() 中使用 Employee 的特殊化构造函数和一些属性值来完成该工作。接下来创建一个 OutputStream (在本例中为 FileOutputStream),然后在该流上调用 writeObject()writeObject() 是一个方法,它使用 Java 序列化将一个对象序列化为流。

在此示例中,您将 List 对象(以及它包含的 Employee 对象)存储在一个文件中,但同样的技术可用于任何类型的序列化。

要成功运行清单 12 中的代码,您可以使用 JUnit 测试,如下所示:

public class HumanResourcesApplicationTest {  private HumanResourcesApplication classUnderTest;  private List testData;  @Before  public void setUp() {    classUnderTest = new HumanResourcesApplication();    testData = HumanResourcesApplication.createEmployees();  }  @Test  public void testSerializeToDisk() {    String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser";    boolean status = classUnderTest.serializeToDisk(filename, testData);    assertTrue(status);  }}
反序列化对象

序列化对象的唯一目的就是为了能够重新构成或反序列化它。清单 13 读取刚序列化的文件并对其内容反序列化,然后恢复 Employee 对象的 List 的状态。

清单 13. 反序列化对象
public class HumanResourcesApplication {  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();  @SuppressWarnings("unchecked")  public List deserializeFromDisk(String filename) {    final String METHOD_NAME = "deserializeFromDisk(String filename)";    List ret = new ArrayList<>();    File file = new File(filename);    int numberOfEmployees = 0;    try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {      List employees = (List)inputStream.readObject();      log.info("Deserialized List says it contains " + employees.size() +         " objects...");      for (Employee employee : employees) {        log.info("Read Employee:" + employee.toString());        numberOfEmployees++;      }      ret = employees;      log.info("Read " + numberOfEmployees + " employees from disk.");    } catch (FileNotFoundException e) {      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " +         file.getName() + ", message = " + e.getLocalizedMessage(), e);    } catch (IOException e) {      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred,       message = " + e.getLocalizedMessage(), e);    } catch (ClassNotFoundException e) {      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "ClassNotFoundException,         message = " + e.getLocalizedMessage(), e);    }    return ret;  }}

同样地,要成功运行清单 13 中的代码,可以使用一个类似这样的 JUnit 测试:

public class HumanResourcesApplicationTest {  private HumanResourcesApplication classUnderTest;  private List testData;  @Before  public void setUp() {    classUnderTest = new HumanResourcesApplication();  }  @Test  public void testDeserializeFromDisk() {    String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser";    int expectedNumberOfObjects = testData.size();    classUnderTest.serializeToDisk(filename, testData);    List employees = classUnderTest.deserializeFromDisk(filename);    assertEquals(expectedNumberOfObjects, employees.size());  }}

对于大多数应用程序的用途,将对象标记为 serializable 是执行序列化工作时唯一需要担心的问题。需要明确地序列化和反序列化对象时,可使用清单 12 和清单 13 中所示的技术。但是,随着应用程序对象不断演变,以及在它们之中添加和删除属性,序列化会变得更加复杂。

serialVersionUID

回想中间件和远程对象通信的发展初期,开发人员主要负责控制其对象的”连接格式”,随着技术开始演变,这引发了大量头疼的问题。

假设您向一个对象添加了一个属性,重新编译了它,然后将该代码重新分发到一个应用程序集群中的每个计算机上。该对象存储在一个具有某种代码版本的计算机中,但其他可能具有不同代码版本的计算机访问该对象。这些计算机尝试反序列化该对象时,常常会发生糟糕的事情。

Java 序列化元数据 — 所包含的二进制序列化格式的信息 — 很复杂,而且解决了困扰早期中间件开发人员的许多问题。但它并非能解决所有问题。

Java 序列化使用一个名为 serialVersionUID 的特性来帮助您处理序列化场景中的不同对象版本问题。不需要在对象上声明此特性;默认情况下,Java 平台会使用一种算法并根据类的属性、它的类名称以及在庞大的本地集群中的位置来计算值。在大多数情况下,该算法都能正常运行。但是,如果添加或删除一个属性,这个动态生成的值就会发生变化,而且 Java 运行时会抛出 InvalidClassException

要想避免这种情况,可养成明确声明 serialVersionUID 的习惯:

import java.io.Serializable;  public class Person implements Serializable {  private static final long serialVersionUID = 20100515;  // etc...  }

我建议您为 serialVersionUID 版本号使用某种模式(我在前面的示例中使用了当前日期)。而且应将 serialVersionUID 声明为 private static finallong 类型。

您可能想知道何时应更改此特性。简单的答案是,只要对类执行了不兼容的更改(这通常意味着删除了一个属性),就应该更改它。如果在一台计算机上拥有该对象的一个已删除了某个属性的版本,而且将该对象远程传输至一台计算机,其中包含的对象版本需要该属性,此时就会发生怪异的事情。这时就可以使用 Java 平台的内置 serialVersionUID 进行检查。

作为一条经验规则,任何时候添加或删除一个类特性(也就是属性和方法),都需要更改它的 serialVersionUID。在连接的另一端获得一个 InvalidClassException,比由不兼容的类更改导致应用程序错误要更好一些。

--

知识分享,时代前行!

~~ 时代Java

还有更多好文章……

请查看历史文章和官网,

↓有分享,有收获~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值