leftso 104 2 2019-09-23 11:39:57

文章位置:左搜> 编程技术> Java编程技术> 正文
Java序列化和反序列化入门使用讲解


Java序列化的作用

Java序列化允许将Java对象写入文件系统以进行永久存储,也可以将其写入网络以传输到其他应用程序。

JAVA实现序列化

Java中的序列化是通过Serializable接口实现的。 Java Serializable接口保证了序列化对象的能力。 此接口也建议我们使用serialVersioUID。

Java序列化中的疑惑

现在,即使您在应用程序类中同时使用了序列化和反序列化,您是否知道也有可能已经破坏您的设计呢? 下面让我们确定类中将来的更改,这些更改将是兼容的更改,而其他更改将证明是不兼容的更改。

Java序列化不兼容的更改

对类的不兼容更改是指不能保持互操作性的那些更改。 下面给出了在演化类时可能发生的不兼容更改(考虑默认的序列化或反序列化):

  1. 删除字段--->如果在类中删除了某个字段,则写入的流将不包含其值。 当较早的类读取流时,该字段的值将设置为默认值,因为流中没有可用的值。 但是,此默认值可能会不利地损害早期版本兼容性的能力。
  2. 将类上移或下移---->这是不允许的,因为流中的数据以错误的顺序显示。
  3. 将非静态字段更改为静态或将非持久化态字段更改为持久化态--->当依赖默认序列化时,此更改等效于从类中删除字段。 该版本的类不会将该数据写入流,因此该类的早期版本将无法读取该数据。 与删除字段时一样,早期版本的字段将被初始化为默认值,这可能导致类以意外方式失败。
  4. 更改原始字段的声明类型--->每个版本的类都使用其声明类型写入数据。 尝试读取该字段的早期版本的类将失败,因为流中的数据类型与该字段的类型不匹配。
  5. 更改writeObject或readObject方法,使其不再写入或读取默认字段数据,或者对其进行更改,以使其尝试写入或读取默认字段数据,而先前版本则不这样做。 默认字段数据必须一致地出现在流中或不出现在流中。
  6. 将类从“可序列化”更改为“可外部化”,反之亦然,这是不兼容的更改,因为流将包含与可用类的实现不兼容的数据。
  7. 将类从非枚举类型更改为枚举类型,反之亦然,因为流将包含与可用类的实现不兼容的数据。
  8. 删除Serializable或Externalizable是一项不兼容的更改,因为在编写时它将不再提供该类的旧版本所需的字段。
  9. 如果该行为会产生与该类的任何旧版本不兼容的对象,则将writeReplace或readResolve方法添加到类是不兼容的。

Java序列化兼容更改

  1. 添加字段–当要重构的类的字段在流中不存在时,该对象中的该字段将被初始化为其类型的默认值。 如果需要特定于类的初始化,则该类可以提供一个readObject方法,该方法可以将字段初始化为非默认值。
  2. 添加类–流将包含流中每个对象的类型层次结构。 将流中的此层次结构与当前类进行比较可以检测到其他类。 由于流中没有用于初始化对象的信息,因此该类的字段将被初始化为默认值。
  3. 删除类–将流中的类层次结构与当前类的层次结构进行比较可以检测到某个类已被删除。 在这种情况下,从该流中读取与该类相对应的字段和对象。 原始字段将被丢弃,但是会创建由删除的类引用的对象,因为它们可能稍后在流中被引用。 当流被垃圾回收或重置时,它们将被垃圾回收
  4. 添加writeObject / readObject方法–如果读取流的版本具有这些方法,则通常希望readObject读取默认序列化写入流中的所需数据。 在读取任何可选数据之前,应先调用defaultReadObject。 通常,writeObject方法将调用defaultWriteObject写入所需的数据,然后再写入可选数据。
  5. 删除writeObject / readObject方法–如果读取流的类没有这些方法,则默认情况下将序列化读取所需的数据,并将丢弃可选数据。
  6. 添加java.io.Serializable –这等效于添加类型。 该类的流中将没有任何值,因此其字段将被初始化为默认值。 对子类化不可序列化类的支持要求该类的超级类型具有no-arg构造函数,并且该类本身将被初始化为默认值。 如果no-arg构造函数不可用,则抛出InvalidClassException。
  7. 更改对字段的访问权限–访问修饰符public,package,protected和private对序列化为字段分配值的能力没有影响。
  8. 将字段从静态更改为非静态或将瞬态更改为非瞬态–当依靠默认序列化来计算可序列化字段时,此更改等效于将字段添加到类中。 新字段将被写入流,但是较早的类将忽略该值,因为序列化不会将值分配给静态或瞬态字段。

Java序列化中的serialVersionUID

serialVersionUID是Serializable类的通用版本标识符。 反序列化使用此数字来确保已加载的类与序列化的对象完全对应。 如果找不到匹配项,则抛出InvalidClassException。

  1. 始终将其包含为字段,例如:“ private static final long serialVersionUID = 7526472295622776147L; ”,即使在课程的第一个版本中也要包含此字段,以提醒其重要性。
  2. 除非您有意对类进行更改以使其与旧的序列化对象不兼容,否则请勿在以后的版本中更改此字段的值。 如果需要,请遵循上述给定的准则。

readObject和writeObject方法介绍

  1. 反序列化必须视为任何构造函数:在反序列化结束时验证对象状态-这意味着readObject几乎应始终在Serializable类中实现,以便执行此验证。
  2. 如果构造函数为可变对象字段制作防御性副本,则必须为readObject。

一些序列化最佳实践

  1. 使用javadoc的@serial标记表示可序列化字段。
  2. .ser扩展名通常用于表示序列化对象的文件。
  3. 没有静态或瞬态字段接受默认序列化。
  4. 除非必要,否则可扩展类不应是可序列化的。
  5. 内部类很少(如果有的话)实现Serializable。
  6. 容器类通常应遵循Hashtable的样式,该样式通过存储键和值来实现Serializable,而不是大型哈希表数据结构。

遵循序列化最佳做法的演示

package staticTest;
 
import java.io.Serializable;
import java.text.StringCharacterIterator;
import java.util.*;
import java.io.*;
 
public final class UserDetails implements Serializable {
 
/**
* 此构造函数需要所有字段
*
* @param aFirstName
* contains only letters, spaces, and apostrophes.
* @param aLastName
* contains only letters, spaces, and apostrophes.
* @param aAccountNumber
* is non-negative.
* @param aDateOpened
* has a non-negative number of milliseconds.
*/
public UserDetails(String aFirstName, String aLastName, int aAccountNumber,
                        Date aDateOpened)
{
  super();
  setFirstName(aFirstName);
  setLastName(aLastName);
  setAccountNumber(aAccountNumber);
  setDateOpened(aDateOpened);
  // there is no need here to call verifyUserDetails.
}
 
// 默认构造函数
public UserDetails() {
  this("FirstName", "LastName", 0, new Date(System.currentTimeMillis()));
}
 
public final String getFirstName() {
  return fFirstName;
}
 
public final String getLastName() {
  return fLastName;
}
 
public final int getAccountNumber() {
  return fAccountNumber;
}
 
/**
* 返回该字段的防御性副本,因此没有人可以更改此字段。
* 
*/
public final Date getDateOpened() {
  return new Date(fDateOpened.getTime());
}
 
/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setFirstName(String aNewFirstName) {
  verifyNameProperty(aNewFirstName);
  fFirstName = aNewFirstName;
}
 
/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setLastName(String aNewLastName) {
  verifyNameProperty(aNewLastName);
  fLastName = aNewLastName;
}
 
/**
* Validate before setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setAccountNumber(int aNewAccountNumber) {
  validateAccountNumber(aNewAccountNumber);
  fAccountNumber = aNewAccountNumber;
}
 
public final void setDateOpened(Date aNewDate) {
  // make a defensive copy of the mutable date object
  Date newDate = new Date(aNewDate.getTime());
  validateAccountOpenDate(newDate);
  fDateOpened = newDate;
}
 
/**
* The client's first name.
*
* @serial
*/
private String fFirstName;
 
/**
* The client's last name.
*
* @serial
*/
private String fLastName;
 
/**
* The client's account number.
*
* @serial
*/
private int fAccountNumber;
 
/**
* The date the account was opened.
*
* @serial
*/
private Date fDateOpened;
 
/**
* Determines if a de-serialized file is compatible with this class.
* Included here as a reminder of its importance.
*/
private static final long serialVersionUID = 7526471155622776147L;
 
/**
* Verify that all fields of this object take permissible values
*
* @throws IllegalArgumentException
* if any field takes an unpermitted value.
*/
private void verifyUserDetails() {
  validateAccountNumber(fAccountNumber);
  verifyNameProperty(fFirstName);
  verifyNameProperty(fLastName);
  validateAccountOpenDate(fDateOpened);
}
 
/**
* Ensure names contain only letters, spaces, and apostrophes.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void verifyNameProperty(String aName) {
boolean nameHasContent = (aName != null) && (!aName.equals(""));
  if (!nameHasContent) {
    throw new IllegalArgumentException(
    "Names must be non-null and non-empty.");
  }
 
StringCharacterIterator iterator = new StringCharacterIterator(aName);
char character = iterator.current();
  while (character != StringCharacterIterator.DONE) {
    boolean isValidChar = (Character.isLetter(character)
    || Character.isSpaceChar(character) || character == ''');
    if (isValidChar) {
      // do nothing
    } else {
      String message = "Names can contain only letters, spaces, and apostrophes.";
      throw new IllegalArgumentException(message);
    }
    character = iterator.next();
  }
}
 
/**
* AccountNumber must be non-negative.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountNumber(int aAccountNumber) {
  if (aAccountNumber < 0) {
    String message = "Account Number must be greater than or equal to 0.";
    throw new IllegalArgumentException(message);
  }
}
 
/**
* DateOpened must be after 1970.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountOpenDate(Date aDateOpened) {
  if (aDateOpened.getTime() < 0) {
    throw new IllegalArgumentException(
      "Date Opened must be after 1970.");
  }
}
 
/**
* Always treat deserialization as a full-blown constructor, by validating
* the final state of the de-serialized object.
*/
private void readObject(ObjectInputStream aInputStream)
throws ClassNotFoundException, IOException {
  // always perform the default deserialization first
  aInputStream.defaultReadObject();
 
  // make defensive copy of the mutable Date field
  fDateOpened = new Date(fDateOpened.getTime());
 
  // ensure that object state has not been corrupted or tampered with
  // malicious code
  verifyUserDetails();
}
 
/**
* This is the default implementation of writeObject. Customise if
* necessary.
*/
private void writeObject(ObjectOutputStream aOutputStream)
throws IOException {
  // perform the default serialization for all non-transient, non-static
  // fields
  aOutputStream.defaultWriteObject();
}
}
现在让我们看看如何在Java中进行序列化和反序列化。

Java序列化和反序列化示例

package serializationTest;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Calendar;
import java.util.Date;
public class TestUserDetails {
  public static void main(String[] args) {
    // Create new UserDetails object
    UserDetails myDetails = new UserDetails("Lokesh", "Gupta", 102825,
    new Date(Calendar.getInstance().getTimeInMillis()));
 
    // Serialization code
    try {
      FileOutputStream fileOut = new FileOutputStream("userDetails.ser");
      ObjectOutputStream out = new ObjectOutputStream(fileOut);
      out.writeObject(myDetails);
      out.close();
      fileOut.close();
    } catch (IOException i) {
      i.printStackTrace();
    }
 
    // deserialization code
    @SuppressWarnings("unused")
    UserDetails deserializedUserDetails = null;
    try {
      FileInputStream fileIn = new FileInputStream("userDetails.ser");
      ObjectInputStream in = new ObjectInputStream(fileIn);
      deserializedUserDetails = (UserDetails) in.readObject();
      in.close();
      fileIn.close();
 
      // verify the object state
      System.out.println(deserializedUserDetails.getFirstName());
      System.out.println(deserializedUserDetails.getLastName());
      System.out.println(deserializedUserDetails.getAccountNumber());
      System.out.println(deserializedUserDetails.getDateOpened());
    } catch (IOException ioe) {
      ioe.printStackTrace();
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
    }
  }
}
执行示例,得到结果如下:
Lokesh
Gupta
102825
Wed Nov 21 15:06:34 GMT+05:30 2012