博客记录-day016-JavaIO序列化Serializable、transient、打印流

91 阅读8分钟

一、沉默王二-JavaIO

1、Serializable

Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。

序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;通过 ObjectOutputStream 将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过 ObjectInputStream 将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。

序列化有一条规则,就是要序列化的对象必须实现 Serializbale 接口,否则就会报 NotSerializableException 异常。

好,来看看 Serializbale 接口的定义吧:

public interface Serializable {
}

通过 ObjectOutputStream 将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过 ObjectInputStream 将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。

假如 Wanger 实现了 Serializable 接口,就可以序列化和反序列化了。

class Wanger implements Serializable{
    private static final long serialVersionUID = -2095916884810199532L;
    
    private String name;
    private int age;
}

具体怎么序列化呢?

以 ObjectOutputStream 为例吧,它在序列化的时候会依次调用 writeObject()writeObject0()writeOrdinaryObject()writeSerialData()invokeWriteObject()defaultWriteFields()

那怎么反序列化呢?

以 ObjectInputStream 为例,它在反序列化的时候会依次调用 readObject()readObject0()readOrdinaryObject()readSerialData()defaultReadFields()

Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。

static 和 transient 修饰的字段是不会被序列化的。原因如下:

1)序列化前,pre 的值为“沉默”,序列化后,pre 的值修改为“不沉默”,反序列化后,pre 的值为“不沉默”,而不是序列化前的状态“沉默”。

为什么呢?因为序列化保存的是对象的状态,而 static 修饰的字段属于类的状态,因此可以证明序列化并不保存 static 修饰的字段。

2)序列化前,meizi 的值为“王三”,反序列化后,meizi 的值为 null,而不是序列化前的状态“王三”。

为什么呢?transient 的中文字义为“临时的”(论英语的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null

Externalizable 和 Serializable 都是用于实现 Java 对象的序列化和反序列化的接口,但是它们有以下区别

①、Serializable 是 Java 标准库提供的接口,而 Externalizable 是 Serializable 的子接口;

②、Serializable 接口不需要实现任何方法,只需要将需要序列化的类标记为 Serializable 即可,而 Externalizable 接口需要实现 writeExternal 和 readExternal 两个方法

③、Externalizable 接口提供了更高的序列化控制能力,可以在序列化和反序列化过程中对对象进行自定义的处理,如对一些敏感信息进行加密和解密。

2、transient

我们知道,一个对象只要实现了 Serilizable 接口,它就可以被序列化。

在实际开发过程中,我们常常会遇到这样的问题,一个类的有些字段需要序列化,有些字段不需要,比如说用户的一些敏感信息(如密码、银行卡号等),为了安全起见,不希望在网络操作中传输或者持久化到磁盘文件中,那这些字段就可以加上 transient 关键字。

需要注意的是,被 transient 关键字修饰的成员变量在反序列化时会被自动初始化为默认值,例如基本数据类型为 0,引用类型为 null。

transient 使用小结

1)一旦字段被 transient 修饰,成员变量将不再是对象持久化的一部分,该变量的值在序列化后无法访问。

2)transient 关键字只能修饰字段,而不能修饰方法和类。

3)被 transient 关键字修饰的字段不能被序列化,一个静态变量(static关键字修饰)不管是否被 transient 修饰,均不能被序列化

transient 关键字用于修饰类的成员变量,在序列化对象时,被修饰的成员变量不会被序列化和保存到文件中。其作用是告诉 JVM 在序列化对象时不需要将该变量的值持久化,这样可以避免一些安全或者性能问题。但是,transient 修饰的成员变量在反序列化时会被初始化为其默认值(如 int 类型会被初始化为 0,引用类型会被初始化为 null),因此需要在程序中进行适当的处理。

transient 关键字和 static 关键字都可以用来修饰类的成员变量。其中,transient 关键字表示该成员变量不参与序列化和反序列化,而 static 关键字表示该成员变量是属于类的,不属于对象的,因此不需要序列化和反序列化。

在 Serializable 和 Externalizable 接口中,transient 关键字的表现也不同,在 Serializable 中表示该成员变量不参与序列化和反序列化,在 Externalizable 中不起作用,因为 Externalizable 接口需要实现 readExternal 和 writeExternal 方法,需要手动完成序列化和反序列化的过程。

3、打印流

打印流具有以下几个特点:

  • 可以自动进行数据类型转换:打印流可以将各种数据类型转换为字符串,并输出到指定的输出流中。
  • 可以自动进行换行操作:打印流可以在输出字符串的末尾自动添加换行符,方便输出多个字符串时的格式控制。
  • 可以输出到控制台或者文件中:打印流可以将数据输出到控制台或者文件中,方便调试和日志记录(尽管生产环境下更推荐使用 Logback、ELK 等)。

PrintStream 类的常用方法包括:

  • print():输出一个对象的字符串表示形式。
  • println():输出一个对象的字符串表示形式,并在末尾添加一个换行符。
  • printf():使用指定的格式字符串和参数输出格式化的字符串。

下面是 Java 的常用转换说明符及对应的输出格式:

  • %s:输出一个字符串。
  • %d 或 %i:输出一个十进制整数。
  • %x 或 %X:输出一个十六进制整数,%x 输出小写字母,%X 输出大写字母。
  • %f 或 %F:输出一个浮点数。
  • %e 或 %E:输出一个科学计数法表示的浮点数,%e 输出小写字母 e,%E 输出大写字母 E。
  • %g 或 %G:输出一个浮点数,自动选择 %f 或 %e/%E 格式输出。
  • %c:输出一个字符。
  • %b:输出一个布尔值。
  • %h:输出一个哈希码(16进制)。
  • %n:换行符。

除了转换说明符之外,Java 的 printf 方法还支持一些修饰符,用于指定输出的宽度、精度、对齐方式等。

  • 宽度修饰符:用数字指定输出的最小宽度,如果输出的数据不足指定宽度,则在左侧或右侧填充空格或零。
  • 精度修饰符:用点号(.)和数字指定浮点数或字符串的精度,对于浮点数,指定小数点后的位数,对于字符串,指定输出的字符数。
  • 对齐修饰符:用减号(-)或零号(0)指定输出的对齐方式,减号表示左对齐,零号表示右对齐并填充零。
int num = 123;
System.out.printf("%5d\n", num); // 输出 "  123"
System.out.printf("%-5d\n", num); // 输出 "123  "
System.out.printf("%05d\n", num); // 输出 "00123"

double pi = Math.PI;
System.out.printf("%10.2f\n", pi); // 输出 "      3.14"
System.out.printf("%-10.4f\n", pi); // 输出 "3.1416    "

String name = "沉默王二";
System.out.printf("%10s\n", name); // 输出 "     沉默王二"
System.out.printf("%-10s\n", name); // 输出 "沉默王二     "

具体来说,

  • 我们使用 %5d 来指定输出的整数占据 5 个字符的宽度,不足部分在左侧填充空格。
  • 使用 %-5d 来指定输出的整数占据 5 个字符的宽度,不足部分在右侧填充空格。
  • 使用 %05d 来指定输出的整数占据 5 个字符的宽度,不足部分在左侧填充 0。
  • 使用 %10.2f 来指定输出的浮点数占据 10 个字符的宽度,保留 2 位小数,不足部分在左侧填充空格。
  • 使用 %-10.4f 来指定输出的浮点数占据 10 个字符的宽度,保留 4 位小数,不足部分在右侧填充空格。
  • 使用 %10s 来指定输出的字符串占据 10 个字符的宽度,不足部分在左侧填充空格。
  • 使用 %-10s 来指定输出的字符串占据 10 个字符的宽度,不足部分在右侧填充空格。