1️⃣ 什么是序列化和反序列化
序列化与反序列化是开发过程中不可或缺的一步,简单来说,序列化是将对象转换成字节流的过程,而反序列化的是将字节流恢复成对象的过程。
2️⃣ Java中的序列化与反序列化
Java语言内置了序列化和反序列化,通过Serializable接口实现。
3️⃣ 序列化的使用场景
🔹 对象序列化的应用场景
- 对象的持久化:这允许我们保存对象的状态到本地存储(如文件系统),然后在需要的时候再恢复。
- 远程数据传输:在客户端和服务器之间,或在两个分布式系统节点之间进行对象的传输。
🔹 对象序列化中可能遇到的问题
📌 情况一: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提供了一种机制,使得某些对象的字段可以从序列化操作中被显式地排除。这是通过使用static
和transient
关键字实现的。
🔹 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
字段也会被序列化。
📌示例代码
在以下的例子中,我们有两个类,A
和B
。A
是B
的超类,并且A
实现了Serializable
接口,而B
没有。
class A implements Serializable {
int i = 10;
}
class B extends A {
int j = 20;
}
当我们创建一个B
类的对象并序列化它,由于B
是A
的子类,所以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会调用超类的默认构造函数来创建一个超类对象。这是因为在这种情况下,超类的状态(即其实例变量)不会被序列化。
这有两个重要的后果:
- 超类必须有一个默认的无参构造函数,否则会抛出
InvalidClassException
异常。 - 超类的实例变量不会恢复到序列化时的状态,而是会被初始化为它们的默认值或通过超类的默认构造函数设置的值。
📌 示例:
在以下示例中,A
是超类,它没有实现Serializable
接口。B
是A
的子类,它实现了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
类的对象并随后反序列化时,我们可以观察到:
A
类的默认构造函数被调用,这是因为A
的状态不被序列化。i
的值不会被序列化和恢复,而是被设置为其默认值0
。j
的值会被正常地序列化和恢复。
📌 注意事项:
- 如果超类没有默认的无参构造函数,那么在反序列化时会抛出异常。
- 如果需要序列化超类的状态,那么超类也应该实现
Serializable
接口。
🔹 阻止子类序列化
在Java中,如果你想阻止某个类的实例被序列化,你可以通过实现writeObject()
和readObject()
方法并在这些方法中抛出NotSerializableException
来达到这个目的。
📌 解释:
-
writeObject()
: 当JVM尝试序列化对象时,此方法会被调用。在方法中抛出NotSerializableException
会导致序列化操作失败。 -
readObject()
: 当JVM尝试反序列化对象时,此方法会被调用。在方法中抛出NotSerializableException
会导致反序列化操作失败。
📌 示例:
在下面的示例中,我们有两个类:A
和B
。其中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);