一文讲透Java序列化

 

本文目录java

  • 1、序列化是什么
  • 2、为何须要序列化
  • 3、序列化怎么用
  • 4、序列化深度探秘
    • 4.1 为何必须实现Serializable接口
    • 4.2 被序列化对象的字段是引用时该怎么办 
    • 4.3 同一个对象会被序列化屡次吗
    • 4.4 只想序列化对象的部分字段该怎么办
    • 4.5 被序列化对象具备继承关系该怎么办
  • 5、serialVersionUID的做用及自动生成
  • 6、序列化的缺点
  • 7、参考文献

 

前言

 

Oracle 公司计划废除 Java 中的古董:序列化技术,由于它带来了许多严重的安全问题(如序列化存储安全、反序列化安全、传输安全等),据统计,至少有3分之1的漏洞是序列化带来的,这也是 1997 年诞生序列化技术的一个巨大错误。可是,序列化技术如今在 Java 应用中无处不在,特别是如今的持久化框架和分布式技术中,都须要利用序列化来传输对象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即对象要存储或者传输都不可避免要用到序列化技术,因此删除序列化技术将是一个长期的计划。程序员

 

你在实际工做中可能会很难有机会真正用到Java自带的序列化技术了,工业界通常也会选择一些更安全的对象编解码方案例如Google的Protobuf等。因此,对于Java序列化,咱们没必要再投入过多的精力学习,你花20分钟读完本文所掌握的知识,对于应付平常源码阅读中遇到的遗留的Java序列化技术应该是足够了。算法

 

1、序列化是什么

 

序列化机制容许将实现序列化的Java对象转换成字节序列,这些字节序列能够保存在磁盘上,或经过网络传输,以备之后从新恢复成原来的对象。序列化机制使得对象能够脱离程序的运行而独立存在。编程

  • 序列化:将一个Java对象写入IO流中
  • 反序列化:从IO流中恢复该Java对象

 

本文中用序列化来简称整个序列化和反序列化机制。 浏览器

 

2、为何须要序列化

 

全部可能在网络上传输的对象的类都应该是可序列化的,不然程序将会出现异常,好比RMI(Remote Method Invoke,即远程方法调用,是JavaEE的基础)过程当中的参数和返回值;全部须要保存到磁盘里的对象的类都必须可序列化,好比Web应用中须要保存到HttpSession或ServletContext属性的Java对象。安全

 

由于序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础——全部的分布式应用经常须要跨平台、跨网络,因此要求全部传递的参数、返回值必须实现序列化。所以序列化机制是Java EE平台的基础。一般建议:程序建立的每一个JavaBean类都实现Serializable。服务器

 

3、序列化怎么用

 

若是一个类的对象须要序列化,那么在Java语法层面,这个类须要:网络

  • 实现Serializable接口
  • 使用ObjectOutputStream将对象输出到流,实现对象的序列化;使用ObjectInputStream从流中读取对象,实现对象的反序列化

 

下面咱们经过代码示例来看看序列化最基本的用法。咱们建立了Person类,其拥有两个基本类型的属性,并实现了Serializable接口。testSerialize方法用来测试序列化,testDeserialize方法用来测试反序列化。框架

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 148.2);
10         Person two = new Person(35, 177.8);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable {
40     int age;
41     double height;
42 
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }

 

4、序列化深度探秘

4.1 为何必须实现Serializable接口

若是某个类须要支持序列化功能,那么它必须实现Serializable接口,不然会报 java.io.NotSerializableException。Serializable接口是一个标志性接口(Marker Interface),也就是说,该接口并不包含任何具体的方法,是一个空接口,仅仅用来判断该类是否可以序列化。JDK8中Serializable接口的源码以下:dom

1 package java.io;
2 
3 public interface Serializable {
4 }

 

在 ObjectOutputStream.java 的 writeObject0 方法中,咱们确实能够看到对对象是否实现了 Serializable接口进行了验证(第15行),不然会抛出 NotSerializableException 异常(第22行)。

 1     private void writeObject0(Object obj, boolean unshared)
 2         throws IOException
 3     {
 4         boolean oldMode = bout.setBlockDataMode(false);
 5         depth++;
 6         try {
 7             ...
 8             // remaining cases
 9             if (obj instanceof String) {
10                 writeString((String) obj, unshared);
11             } else if (cl.isArray()) {
12                 writeArray(obj, desc, unshared);
13             } else if (obj instanceof Enum) {
14                 writeEnum((Enum<?>) obj, desc, unshared);
15             } else if (obj instanceof Serializable) {
16                 writeOrdinaryObject(obj, desc, unshared);
17             } else {
18                 if (extendedDebugInfo) {
19                     throw new NotSerializableException(
20                         cl.getName() + "\n" + debugInfoStack.toString());
21                 } else {
22                     throw new NotSerializableException(cl.getName());
23                 }
24             }
25         } finally {
26             depth--;
27             bout.setBlockDataMode(oldMode);
28         }
29     }

 

4.2 被序列化对象的字段是引用时该怎么办

在第三部分“序列化怎么用”部分的示例中,Person类的字段全都是基本类型,咱们知道基本类型其地址中直接存放的就是它的值,那若是是引用类型呢?引用类型其地址中存放的是指向堆内存中的一个地址,难道序列化时就是将这个地址进行了保存吗?显然,这是说不通的,由于对象的内存地址是可变的,在同一系统的不一样运行时刻或者是不一样系统中,对象的地址确定是不一样的,所以,序列化内存地址没有意义。

 

若是被序列化对象的字段是引用,那么要求该引用的类型也是可序列化实现了Serializable接口的,不然没法序列化。当对某个对象进行序列化时,系统会自动把该对象的全部Field依次进行序列化,若是某个Field引用到另外一个对象,则被引用的对象也会被序列化;若是被引用的对象的Field也引用了其余对象,则被引用的对象也会被序列化,这种状况被称为递归序列化。

 

4.3 同一个对象会被序列化屡次吗

若是对象A和对象B同时引用了对象C,那么,当序列化对象A和对象B时,对象C会被序列化两次吗?答案显然是不会

 

要解释这个问题,就不得不说一下Java序列化的基本算法了:

  • 全部序列化到二进制流的对象都有一个序列化编号
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并赋予一个惟一的编号
  • 若是某个对象已经序列化过,程序将只是直接输出其序列化编号,而不是再次从新序列化该对象

 

4.4 只想序列化对象的部分字段该怎么办

 在一些特殊的场景下,若是一个类里包含的某些Field值是敏感信息,例如银行帐户信息等,这时不但愿系统将该Field值进行序列化;或者某个Field的类型是不可序列化的,所以不但愿对该Field进行递归序列化,以免引起java.io.NotSerializableException异常。

 

此时,咱们就须要自定义序列化了。自定义序列化的经常使用方式有两种:

  • 使用transient关键字
  • 重写writeObject与readObject方法

 

咱们先看第一种方式,使用transient关键字。transient关键字只能用于修饰Field,不可修饰Java程序中的其余成分。使用transient修饰的属性,java序列化时,会忽略掉此字段,因此反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

 

下列代码中,咱们把People的height字段设置为transient,在反序列化时,可观察到输出为默认值0.0。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected transient double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }

 

 程序输出:

Person{age=12, height=0.0}
Person{age=16, height=0.0}

Process finished with exit code 0

 

使用transient关键字修饰Field虽然简单、方便,但被transient修饰的Field将被彻底隔离在序列化机制以外,这样致使在反序列化恢复Java对象时没法取得该Field值。Java还提供了一种自定义序列化机制,经过这种自定义序列化机制可让程序控制如何序列化各Field,甚至彻底不序列化某些Field(与使用transient关键字的效果相同)。在序列化和反序列化过程当中须要特殊处理的类应该提供以下特殊签名的方法,这些特殊的方法用以实现自定义序列化。

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

 

  • writeObject()方法负责写入特定类的实例状态,以便相应的readObject()方法能够恢复它。经过重写该方法,程序员能够彻底得到对序列化机制的控制,能够自主决定哪些Field须要序列化,须要怎样序列化。在默认状况下,该方法会调用out.defaultWriteObject来保存Java对象的各Field,从而能够实现序列化Java对象状态的目的。
  • readObject()方法负责从流中读取并恢复对象Field,经过重写该方法,程序员能够彻底得到对反序列化机制的控制,能够自主决定须要反序列化哪些Field,以及如何进行反序列化。在默认状况下,该方法会调用in.defaultReadObject来恢复Java对象的非静态和非瞬态Field。在一般状况下,readObject()方法与writeObject()方法对应,若是writeObject()方法中对Java对象的Field进行了一些处理,则应该在readObject()方法中对其Field进行相应的反处理,以便正确恢复该对象。
  • 当序列化流不完整时,readObjectNoData()方法能够用来正确地初始化反序列化的对象。例如,接收方使用的反序列化类的版本不一样于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

下面的示例代码中,咱们在writeObject方法中对Person的字段进行了简单的加密处理,在readObject方法中对其进行了相应的解密。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     private void writeObject(java.io.ObjectOutputStream out)
52             throws IOException {
53         System.out.println("Encryption!");
54         out.writeInt(age + 1);
55         out.writeDouble(height - 1);
56     }
57     private void readObject(java.io.ObjectInputStream in)
58             throws IOException, ClassNotFoundException {
59         System.out.println("Decryption!");
60         this.age = in.readInt() - 1;
61         this.height = in.readDouble() + 1;
62     }
63 
64     @Override
65     public String toString() {
66         return "Person{" +
67                 "age=" + age +
68                 ", height=" + height +
69                 '}';
70     }
71 }

 

4.5 被序列化对象具备继承关系该怎么办

被序列化对象具备继承关系时无非就两种状况,第一,该类具备子类,第二,该类具备父类。

 

当该类实现了Serializable接口且具备子类时,根据官方文档中的说明,其子类自然具备可被序列化的属性,不须要显式实现Serializable接口;。

 All subtypes of a serializable class are themselves serializable. 

 

当该类实现了Serializable接口且具备父类时,,该类的父类须要实现Serializable接口吗?在JDK8中Serializable接口的官方文档中有这样一段话:

 1 /**
 2  * ......
 3  *
 4  * To allow subtypes of non-serializable classes to be serialized, the
 5  * subtype may assume responsibility for saving and restoring the
 6  * state of the supertype's public, protected, and (if accessible)
 7  * package fields.  The subtype may assume this responsibility only if
 8  * the class it extends has an accessible no-arg constructor to
 9  * initialize the class's state.  It is an error to declare a class
10  * Serializable if this is not the case.  The error will be detected at
11  * runtime. 
12  *
13  * During deserialization, the fields of non-serializable classes will
14  * be initialized using the public or protected no-arg constructor of
15  * the class.  A no-arg constructor must be accessible to the subclass
16  * that is serializable.  The fields of serializable subclasses will
17  * be restored from the stream. 
18  */

 

阅读文档咱们得知,为了使得不可序列化类的子类可以序列化,其子类必须担负起保存和恢复其超类的public、protected 和 package(if accessible)实例域的责任,且要求其父类必须有一个可访问的无参构造函数以使得在反序列化时可以初始化实例域。

 

咱们写代码验证一下,若是父类中没有可访问的无参构造函数会发生什么,注意Person类中没有无参构造函数。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Student.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Student.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42     
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }
56 
57 class Student extends Person implements Serializable{
58     private String id;
59 
60     public Student(int age, double height, String id) {
61         super(age, height);
62         this.id = id;
63     }
64 
65     @Override
66     public String toString() {
67         return "Student{" +
68                 "age=" + age +
69                 ", height=" + height +
70                 ", id='" + id + '\'' +
71                 '}';
72     }
73 }

 

程序输出产生异常:

java.io.InvalidClassException: Student; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
    at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at SerializableTest.testDeserialize(SerializableTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    ...

Process finished with exit code 0

 

当咱们为Person类添加默认构造函数时:

 1 class Person{
 2     protected int age;
 3     protected double height;
 4 
 5     public Person() {
 6     }
 7 
 8     public Person(int age, double height) {
 9         this.age = age;
10         this.height = height;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person{" +
16                 "age=" + age +
17                 ", height=" + height +
18                 '}';
19     }
20 }

 

程序输出以下,咱们可观察到,父类中的字段都是默认值,只有子类中的字段获得了正确的序列化。出现这种状况的缘由是子类并无担负起序列化父类中字段的责任。

Student{age=0, height=0.0, id='1234'}
Student{age=0, height=0.0, id='5678'}

Process finished with exit code 0

 

为了解决上述问题,咱们须要借助上一节中学到的知识,使用自定义的序列化方法writeObject和readObject来主动将父类中的字段进行序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Studnet.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }
59 
60 class Student extends Person implements Serializable{
61     private String id;
62 
63     public Student(int age, double height, String id) {
64         super(age, height);
65         this.id = id;
66     }
67 
68     private void writeObject(java.io.ObjectOutputStream out)
69             throws IOException {
70         out.defaultWriteObject();
71         out.writeInt(age);
72         out.writeDouble(height);
73     }
74     
75     private void readObject(java.io.ObjectInputStream in)
76             throws IOException, ClassNotFoundException {
77         in.defaultReadObject();
78         this.age = in.readInt();
79         this.height = in.readDouble();
80     }
81 
82     @Override
83     public String toString() {
84         return "Student{" +
85                 "age=" + age +
86                 ", height=" + height +
87                 ", id='" + id + '\'' +
88                 '}';
89     }
90 }

 

程序输出以下,能够看到彻底正确。

Student{age=12, height=156.6, id='1234'}
Student{age=16, height=177.7, id='5678'}

Process finished with exit code 0

 

5、serialVersionUID的做用及自动生成

 

咱们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级先后的兼容性呢?

 

java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即便更改了序列化属性,对象也能够正确被反序列化回来。若是反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。下面是JDK 8中ArrayList的源码中的serialVersionUID。

 

 1 public class ArrayList<E> extends AbstractList<E>
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 3 {
 4     private static final long serialVersionUID = 8683452581122892189L;
 5 
 6     /**
 7      * Default initial capacity.
 8      */
 9     private static final int DEFAULT_CAPACITY = 10;
10     ...  
11 }

 

序列化版本号可自由指定,若是不指定,JVM会根据类信息本身计算一个版本号,这样随着class的升级,就没法正确反序列化;不指定版本号另外一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不一样jvm可能计算的规则不同,这样也会致使没法反序列化。

 

什么状况下须要修改serialVersionUID呢?分三种状况。

  • 若是只是修改了方法,反序列化不容影响,则无需修改版本号
  • 若是只是修改了静态Field或瞬态Field,则反序列化不受任何影响
  •  若是修改类时修改了非静态Field、非瞬态Field,则可能致使序列化版本不兼容。若是对象流中的对象和新类中包含同名的Field,而Field类型不一样,则反序列化失败,类定义应该更新serialVersionUID Field值。若是只是新增了实例变量,则反序列化回来新增的是默认值;若是减小了实例变量,反序列化时会忽略掉减小的实例变量。

 

咱们在平常编程实践中,通常会选择使用IDE来自动生成serialVersionUID,这样能够最大化地减小重复的可能性。对于IntelliJ IDEA,自动生成serialVersionUID有三步:

  • 修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’

  • 类实现Serializable接口
  • 在类名上执行Alt+Enter,而后选择生成serialVersionUID便可

 

6、序列化的缺点

 

Java序列化存在四个致命缺点,致使其不适用于网络传输:

  • 没法跨语言:在网络传输中,常常会有异构语言的进程的交互,但Java序列化技术是Java语言内部的私有协议,其余语言没法进行反序列化。目前全部流行的RPC框架都没有使用Java序列化做为编解码框架。
  • 潜在风险高:不可信流的反序列化可能致使远程代码执行(RCE)、拒绝服务(DoS)和一系列其余攻击。
  • 序列化后的码流太大
  • 序列化的性能较低

 

在真正的生产环境中,通常会选择其它编解码框架,领先的跨平台结构化数据表示是 JSON 和 Protocol Buffers,也称为 protobuf。JSON 由 Douglas Crockford 设计用于浏览器与服务器通讯,Protocol Buffers 由谷歌设计用于在其服务器之间存储和交换结构化数据。JSON 和 protobuf 之间最显著的区别是 JSON 是基于文本的,而且是人类可读的,而 protobuf 是二进制的,但效率更高。

 

 7、参考文献

 

  1. 《疯狂Java讲义》第2版,李刚著,电子工业出版社
  2. 《Java核心技术》第10版,霍斯特曼等著,机械工业出版本
  3. 《Netty权威指南》第2版,李林锋著,电子工业出版社
  4. 《Effective Java》第2版,Joshua Bloch著,机械工业出版社
相关文章
相关标签/搜索