Java-序列化和反序列化

本文介绍了Java中序列化和反序列化的基本概念、使用场景,以及可能出现的问题和解决方案,包括serialVersionUID的重要性和如何处理对象结构变化。同时,讨论了序列化时对象的引用存储规则和不能被序列化的字段,如static和transient修饰的成员,以及序列化在继承关系中的处理。最后,提到了Jackson库在JSON序列化和反序列化中的应用。
摘要由CSDN通过智能技术生成

1️⃣ 什么是序列化和反序列化

序列化与反序列化是开发过程中不可或缺的一步,简单来说,序列化是将对象转换成字节流的过程,而反序列化的是将字节流恢复成对象的过程。
在这里插入图片描述


2️⃣ Java中的序列化与反序列化

Java语言内置了序列化和反序列化,通过Serializable接口实现。


3️⃣ 序列化的使用场景


🔹 对象序列化的应用场景

  1. 对象的持久化:这允许我们保存对象的状态到本地存储(如文件系统),然后在需要的时候再恢复。
  2. 远程数据传输:在客户端和服务器之间,或在两个分布式系统节点之间进行对象的传输。

🔹 对象序列化中可能遇到的问题

📌 情况一:serialVersionUID不一致

  • 问题:如果两个系统之间的类的serialVersionUID不一致,那么反序列化时会抛出java.io.InvalidClassException
  • 解决:确保序列化和反序列化的类具有相同的serialVersionUID。

📌 情况二:A端增加字段,B端不变

  • 结果:反序列化时,B端会忽略A端新增的字段,因此A端新增的字段的值会丢失。

📌 情况三:B端减少字段,A端不变

  • 结果:反序列化时,由于B端的字段比A端少,因此A端多出的字段的值会丢失。

📌 情况四:B端增加字段,A端不变

  • 结果:B端新增的字段在反序列化时会被赋予其默认值(例如,int类型的默认值为0)。

4️⃣ 序列化兼容性(serialVersionUID最好设置成1L)

序列化是Java提供的一个能够将对象转化为字节流的机制。这样的处理在存储和网络传输中非常有用。但是,当涉及到对象的结构变更时,序列化可能会遇到问题。

🔹 何为 serialVersionUID

每个可序列化的类都有一个唯一的标识符,称为 serialVersionUID。这个标识符在对象序列化时被写入到序列化的输出流中。当对象被反序列化时,JVM会检查输入流中的 serialVersionUID 是否与目标类的 serialVersionUID 一致。如果一致,对象被反序列化;如果不一致,抛出 InvalidClassException 异常。

🔹 自动生成的 serialVersionUID 的风险

默认情况下,如果没有为类显式地定义 serialVersionUID,JVM会基于类的详情(包括:名称、所有的公共和私有成员、方法等)自动生成一个。但是,这种自动生成的方式很容易因为类的任何小的结构变化而改变,从而导致序列化的版本不一致的问题。

例如,你的类已经被序列化到文件中。后来,你为该类添加了一个新的字段。此时,如果你尝试从文件中反序列化该对象,会因为 serialVersionUID 的不匹配而失败。

🔹 固定 serialVersionUID 的好处

为了避免上述问题,建议为你的类显式地定义一个固定的 serialVersionUID。例如:

private static final long serialVersionUID = 1L;

这样即使类结构发生了变化,只要你不改变这个值,对象仍然可以被反序列化。但是这也意味着你需要确保新的类结构仍然与老的序列化对象兼容。

🔹 关于 serialVersionUID = 1L 是否会重复?

虽然 1L 看起来很简单,可能会有重复的担忧,但实际上JVM在比较 serialVersionUID 时不仅仅是比较这个数字。JVM还会考虑类的名称等其他信息。因此,只要你的类名是唯一的,即使多个类都使用 serialVersionUID = 1L,它们之间也不会发生冲突。

🔹 具体示例

示例:将Account对象保存到文件中,然后在Account类中添加address字段,再从文件中读取之前保存的内容。

public class Account implements Serializable {
	private int age;
	private long birthday;
	private String name;
}
// 将Account对象保存到文件中
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(account);
oos.flush();

// 修改Account对象的结构
public class Account implements Serializable {

	private int age;
	private long birthday;
	private String name;
	private String address;
	
	public Account(int age, String name) {
	    this.age = age;
	    this.name = name;
	}
}   

// 读取Account的内容
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Account account2 = (Account)ois.readObject();

由于在保存Account对象后修改了Account的结构,会导致serialVersionUID的值发生变化,在读文件(反序列化)的时候就会出错。所以为了更好的兼容性,在序列化的时候,最好将serialVersionUID的值设置为固定的。

public class Account implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private int age;
    private long birthday;
    private String name;
}

5️⃣ 序列化的存储规则

Java在序列化时,为了节省磁盘空间或网络带宽,对于相同的对象只保存其引用而不是整个对象。但这种策略在某些场景下可能导致意想不到的结果。


🔹 为什么保存的是引用?

如果有一个对象网状结构,其中多个对象互相引用,完全序列化这种结构会导致大量冗余。为解决这一问题,Java只序列化对象的第一次实例,之后的相同对象只保存其引用。

🔹 示例

public class Account implements Serializable {
    private String userName;

    // Constructors, getters and setters...
}

// 将account对象保存两次,第二次保存时修改其用户名
Account account = new Account();
account.setUserName("Freeman");

File file = new File("account.ser");
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(account);

account.setUserName("Tom");
oos.writeObject(account);
oos.close();

// 读取两次保存的account对象
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Account account2 = (Account) ois.readObject();
Account account3 = (Account) ois.readObject();
ois.close();

System.out.println("account2.userName=" + account2.getUserName());  
System.out.println("account3.userName=" + account3.getUserName());  
System.out.println("account2==account3 -> " + (account2 == account3));

输出:

account2.userName=Freeman  
account3.userName=Freeman 
account2==account3 -> true

这里,尽管我们修改了account的用户名并再次保存,但由于Java序列化保存的是引用,两次序列化的对象都指向同一个状态。

🔹 解决方法

要保存对象的不同状态,可以考虑克隆对象然后序列化。这样每次都有一个新的对象实例,其状态可以独立地被保存。

🔹 总结

Java序列化的这一特性在大多数情况下很有用,但在特定场景下可能导致问题。了解这一特性并根据需要进行处理是关键。如果需要保存对象的多个状态,确保每次都序列化一个新的对象实例。

对于网上这个疑问,我的解释就是如上的,不存在对象不一致的问题

在这里插入图片描述


6️⃣ 不能被序列化的字段

Java提供了一种机制,使得某些对象的字段可以从序列化操作中被显式地排除。这是通过使用statictransient关键字实现的。


🔹 static修饰的字段

  • static关键字用于修饰类级别的变量和方法。由于static修饰的变量是属于类而不是对象的,所以它们不会被序列化。

  • 序列化过程是对象的序列化,并且static字段的值对于类的所有实例都是相同的,因此它们不需要被序列化。

public class Account implements Serializable {
    private String userName;
    private static String bankName = "ABC Bank"; // This will not be serialized
    // Constructors, getters and setters...
}

在上面的例子中,bankName是一个静态字段,因此它不会被序列化。

🔹 transient修饰的字段

  • transient关键字在Java中用于声明一个对象的某些字段不应该被序列化。

  • 当对象被序列化时,被transient修饰的字段的值不会被保存到序列化的输出中。

  • 反序列化后,这些字段的值会被设置为其默认值(例如,对于对象是null,对于int是0)。

public class Account implements Serializable {
    private String userName;
    private transient String password; // This will not be serialized
    // Constructors, getters and setters...
}

在上面的例子中,password字段被标记为transient,因此它不会被序列化。这通常用于安全或敏感信息,以确保这些信息不会被无意地传播或存储。


7️⃣ 序列化的继承


🔹 当超类实现可序列化的接口而子类没有

如果一个类的超类实现了Serializable接口,该类的所有子类都将自动实现序列化。这意味着,当你序列化一个子类对象时,其超类的非静态、非transient字段也会被序列化。

📌示例代码

在以下的例子中,我们有两个类,ABAB的超类,并且A实现了Serializable接口,而B没有。

class A implements Serializable {
   int i = 10;
}

class B extends A {
   int j = 20;
}

当我们创建一个B类的对象并序列化它,由于BA的子类,所以A类的i字段和B类的j字段都会被序列化。

public class TestSerialization {
   public static void main(String[] args) throws IOException, ClassNotFoundException {
      B obj = new B();
      FileOutputStream fos = new FileOutputStream("abc.ser");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      oos.writeObject(obj);
      
      FileInputStream fis = new FileInputStream("abc.ser");
      ObjectInputStream ois = new ObjectInputStream(fis);
      B obj1 = (B)ois.readObject();
      
      System.out.println("value of i is : " + obj1.i + " & value of j is : " + obj1.j);
   }
}

📌输出:

value of i is : 10 & value of j is : 20

📌结论:

子类对象的序列化会包括超类的字段。如果超类已经实现了Serializable接口,那么子类不需要显式地实现它,除非子类需要自定义序列化和反序列化的行为。


🔹 当超类未实现可序列化的接口而子类实现时

当超类没有实现Serializable接口,但子类实现了,那么在序列化子类对象时,JVM会调用超类的默认构造函数来创建一个超类对象。这是因为在这种情况下,超类的状态(即其实例变量)不会被序列化。

这有两个重要的后果:

  1. 超类必须有一个默认的无参构造函数,否则会抛出InvalidClassException异常。
  2. 超类的实例变量不会恢复到序列化时的状态,而是会被初始化为它们的默认值或通过超类的默认构造函数设置的值。

📌 示例:

在以下示例中,A是超类,它没有实现Serializable接口。BA的子类,它实现了Serializable接口。

class A {
   int i;
   A() {
      System.out.println("父类的默认构造函数。");
   }
   A(int i) {
      this.i = i;
   }
}

class B extends A implements Serializable {
   int j;
   B(int i, int j) {
      super(i);
      this.j = j;
   }
}

在序列化B类的对象并随后反序列化时,我们可以观察到:

  1. A类的默认构造函数被调用,这是因为A的状态不被序列化。
  2. i的值不会被序列化和恢复,而是被设置为其默认值0
  3. j的值会被正常地序列化和恢复。

📌 注意事项:

  1. 如果超类没有默认的无参构造函数,那么在反序列化时会抛出异常。
  2. 如果需要序列化超类的状态,那么超类也应该实现Serializable接口。

🔹 阻止子类序列化

在Java中,如果你想阻止某个类的实例被序列化,你可以通过实现writeObject()readObject()方法并在这些方法中抛出NotSerializableException来达到这个目的。

📌 解释:

  • writeObject(): 当JVM尝试序列化对象时,此方法会被调用。在方法中抛出NotSerializableException会导致序列化操作失败。

  • readObject(): 当JVM尝试反序列化对象时,此方法会被调用。在方法中抛出NotSerializableException会导致反序列化操作失败。

📌 示例:

在下面的示例中,我们有两个类:AB。其中A实现了Serializable接口,而B继承自A。尽管B继承自A,但我们想防止B的实例被序列化。为此,我们在B中实现了writeObject()readObject()方法,并在这两个方法中都抛出了NotSerializableException

class A implements Serializable {
   int i = 10;
}

class B extends A {
   int j = 20;
   
   private void writeObject(ObjectOutputStream out) throws IOException {
      throw new NotSerializableException("B cannot be serialized");
   }
   
   private void readObject(ObjectInputStream in) throws IOException {
      throw new NotSerializableException("B cannot be deserialized");
   }
}

public class TestSerialization {
   public static void main(String[] args) {
      B obj = new B();
      try {
         FileOutputStream fos = new FileOutputStream("abc.ser");
         ObjectOutputStream oos = new ObjectOutputStream(fos);
         oos.writeObject(obj);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

在上述代码中,当我们尝试序列化B的对象时,会抛出NotSerializableException,这正是我们期望的行为。

📌 注意:

  • 这种方法对于阻止类的序列化非常有效。

  • writeObject()readObject()必须声明为private,这样它们只能被JVM调用,不能被其他类调用。


8️⃣ 序列化框架

Java下常见的Json类库有Gson、Json-lib和Jackson等,Jackson相对来说比较高效,在项目中主要使用Jackson进行JSON和Java对象转换,下面给出一些Jackson的JSON操作方法。


🔹 jackson类库的JSON操作方法:ObjectMapper

Jackson是Java世界中最流行的JSON解析和生成库之一。它提供了一套简单、流畅的API来处理JSON数据。其中,ObjectMapper是Jackson库中最核心的一个类,用于处理Java对象与JSON数据之间的转换。

以下是关于ObjectMapper的一些基本使用方法:

📌 1. 创建ObjectMapper实例

ObjectMapper objectMapper = new ObjectMapper();

📌 2. Java对象转JSON

Person person = new Person("John", 30);
// Java对象转JSON字符串
String jsonString = objectMapper.writeValueAsString(person);
System.out.println(jsonString);

📌 3. JSON转Java对象

String jsonString = "{\"name\":\"John\",\"age\":30}";
// JSON字符串转Java对象
Person person = objectMapper.readValue(jsonString, Person.class);
System.out.println(person.getName());

📌 4. Java对象保存到文件

File file = new File("person.json");
objectMapper.writeValue(file, person);

📌 5. 从文件读取JSON并转为Java对象

Person personFromFile = objectMapper.readValue(file, Person.class);
System.out.println(personFromFile.getName());

📌 6. 其他配置

ObjectMapper还提供了一系列的配置选项,如:

  • 控制是否格式化输出的JSON
  • 控制是否允许未知的属性
  • 控制日期的格式化
  • …等等

例如,要生成格式化(pretty-printed)的JSON:

String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(person);
System.out.println(prettyJson);

在实际应用中,通常会根据具体需求,为ObjectMapper设置一些特定的配置,以适应不同的场景。

总之,ObjectMapper是处理JSON数据的强大工具,它的API既简单又灵活,可以满足绝大多数的JSON处理需求。


🔹 java对象转json【json序列化】

//JSON序列化和反序列化使用的User类 
public class User {  
    private String name;  
    private Integer age;  
    private Date birthday;  
    private String email;
}
import java.io.IOException;  
import java.text.ParseException;  
import java.text.SimpleDateFormat;  
  
import com.fasterxml.jackson.databind.ObjectMapper;  
  
public class JacksonDemo {
        User user = new User();  
        user.setName("小民");   
        user.setEmail("xiaomin@sina.com");  
        user.setAge(20);  
          
        SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd");  
        user.setBirthday(dateformat.parse("1996-10-01"));  
        ObjectMapper mapper = new ObjectMapper();  
          
        //User类转JSON  : 序列化为JSON字符串序列
        //输出结果:{"name":"小民","age":20,"birthday":844099200000,"email":"xiaomin@sina.com"}  
        String json = mapper.writeValueAsString(user);  
        System.out.println(json);  
          
        //Java集合转JSON  
        //输出结果:[{"name":"小民","age":20,"birthday":844099200000,"email":"xiaomin@sina.com"}]  
        List<User> users = new ArrayList<User>();  
        users.add(user);  
        String jsonlist = mapper.writeValueAsString(users);  
        System.out.println(jsonlist);  
       }

    }  

🔹 json转Java类【json反序列化】

import java.io.IOException;  
import java.text.ParseException;  
import com.fasterxml.jackson.databind.ObjectMapper;  
  
public class JacksonDemo {  
    public static void main(String[] args) throws ParseException, IOException {  
        String json = "{\"name\":\"小民\",\"age\":20,\"birthday\":844099200000,\"email\":\"xiaomin@sina.com\"}";  
        /** 
         * ObjectMapper支持从byte[]、File、InputStream、字符串等数据的JSON反序列化。 
         */  
        ObjectMapper mapper = new ObjectMapper();  
        User user = mapper.readValue(json, User.class);  
        System.out.println(user);  
    }  
} 

🔹 字符串数组序列化和反序列化

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper objectMapper = new ObjectMapper();
String[] originStrArray = {"a","b","b"};
// json 序列化为json格式的字符串序列
String  str = objectMapper.writeValueAsString(originStrArray);
System.out.println(str);

// 反序列化为字符串数组
String[] originStrArray  = objectMapper.readValue(strArray, String[].class);
System.out.println(originStrArray.length);

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值