压缩
IO
其中DeflaterOutputStream
继承自FilterOutputStream
,而InflaterInputStream
继承自FilterInputStream
,所以压缩相关的类操作的是字节流(这是显而易见的)。
用GZIP压缩单个文件
下面是一个将文件压缩和解压缩的简单示例:
package ch18.compress;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
final String CURRENT_DIR = "./xyz/icexmoon/java_notes/ch18/compress/";
final String SOURCE_FILE = CURRENT_DIR + "Main.java";
final String COMPRESSED_FILE = CURRENT_DIR + "Main.gz";
final String UNCOMPRESSED_FILE = CURRENT_DIR + "Main.txt";
compress(SOURCE_FILE, COMPRESSED_FILE);
uncompress(COMPRESSED_FILE, UNCOMPRESSED_FILE);
}
/**
* 将指定文件压缩为目标文件
*
* @param sourceFile 源文件
* @param desFile 压缩后的文件
* @throws FileNotFoundException
*/
private static void compress(String sourceFile, String desFile) throws IOException {
BufferedInputStream bi = new BufferedInputStream(new FileInputStream(sourceFile));
GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream(desFile));
BufferedOutputStream bos = new BufferedOutputStream(gos);
try {
do {
int b = bi.read();
if (b == -1) {
break;
}
bos.write(b);
} while (true);
} finally {
bi.close();
bos.close();
}
}
/**
* 将指定文件解压缩为目标文件
*
* @param sourceFile 源文件
* @param desFile 解压后的文件
* @throws IOException
* @throws FileNotFoundException
*/
private static void uncompress(String sourceFile, String desFile) throws IOException {
BufferedInputStream bi = new BufferedInputStream(new GZIPInputStream(new FileInputStream(sourceFile)));
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(desFile));
try {
do {
int b = bi.read();
if (b == -1) {
break;
}
bo.write(b);
} while (true);
} finally {
bi.close();
bo.close();
}
}
}
运行这个示例就能看到目录下出现一个将Main.java
压缩后的Main.gz
文件,以及一个由Main.gz
文件解压缩后的Main.txt
文件。这其中Main.java
和Main.txt
文件的大小和内容完全一致,而Main.gz
的大小是原始文件的1/3
左右。
就像示例中的那样,使用GZIPInputStream
和GZIPOutputStream
压缩和解压文件相对简单,因为他们都是字节流的装饰器类,只要嵌套在输入流或输出流中就可以使用。
这里将
BufferedOutputStream
嵌套在GZIPOutputStream
外是有意义的,因为压缩的原理是尽可能合并多个相同的bit位来节省空间,这就意味着压缩算法需要读取尽可能多的内容来实现较高的压缩比,显然先缓冲再传递给GZIPOutputStream
相比一个个传入byte
更为有效。当然,这只是我个人的一个猜想,并没有研究和证明。
用ZIP压缩多个文件
除了压缩单个文件,更多的时候我们需要压缩多个文件到一个压缩文件:
package ch18.compress2;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
final String CURRENT_DIR = "./xyz/icexmoon/java_notes/ch18/compress2/test/";
String[] files = new String[] { "./LICENSE", "./README.md", "./.gitignore" };
String compressedFile = CURRENT_DIR + "files.zip";
compress(files, compressedFile);
uncompress(compressedFile, CURRENT_DIR);
}
private static void compress(String[] files, String compressedFile) throws IOException {
if (files.length == 0) {
return;
}
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(compressedFile), new Adler32());
ZipOutputStream zos = new ZipOutputStream(cos);
BufferedOutputStream bos = new BufferedOutputStream(zos);
zos.setComment("A compressed file.");
try {
for (String file : files) {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
try {
zos.putNextEntry(new ZipEntry(file));
do {
int b = bis.read();
if (b == -1) {
break;
}
bos.write(b);
} while (true);
bos.flush();
} finally {
bis.close();
}
}
System.out.println("checksum:" + cos.getChecksum().getValue());
} finally {
bos.close();
}
}
private static void uncompress(String compressedFile, String desDir) throws IOException {
CheckedInputStream cis = new CheckedInputStream(new FileInputStream(compressedFile), new Adler32());
ZipInputStream zis = new ZipInputStream(cis);
BufferedInputStream bis = new BufferedInputStream(zis);
try {
do {
ZipEntry entry = zis.getNextEntry();
if (entry == null) {
break;
}
String fileName = desDir + entry.getName();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
try {
do {
int b = bis.read();
if (b == -1) {
break;
}
bos.write(b);
} while (true);
} finally {
bos.close();
}
} while (true);
System.out.println("checksum:" + cis.getChecksum().getValue());
} finally {
bis.close();
}
}
}
这个示例展示了如何用ZipOutputStream
压缩多个文件到一个文件,和之前不同的是,每次写入一个文件的数据前,需要先通过ZipOutputStream.putNextEntry
写入一个文件标识,其它部分几乎没有什么区别。相应的,在解压时也需要先使用ZipInputStream.getNextEntry
获取文件标识,然后再读取数据到文件中。
需要注意的是,在解压文件时,因为我们要批量写入所有源文件后再关闭输出流,并且每个被压缩的文件用ZipEntry
进行区分,所以每写完一个源文件的数据后,都必须立即调用flush
方法刷新数据到输出流,否则就会出现压缩后的文件被错误分割的情况(如果你尝试不刷新,就可以看到效果)。
示例中还使用了用于校验压缩和解压有没有出错的CheckedInputStream
和CheckedOutputStream
,这并非是必须的。
JAR包
Java程序通常都会被打成一个JAR包(Java ARchive,Java档案文件)使用,实际上JAR包也是一种压缩包,包含所有需要的Java代码和相关资源。使用JAR包可以更方便地传播和部署Java程序。
JDK包含一个命令行工具jar
,可以创建和查看JAR包,但现代IDE基本都具备生成JAR包的能力,所以这里不做赘述,需要了解的可以自行查找相关内容。
序列化
序列化简单地说就是将对象转化为字节序列或者字符串,这是持久化技术的一种。相比数据库、XML、JSON等更常见的持久化技术,序列化的优点是可以简单直观地将对象持久化和恢复,缺点在于通常何种技术只能在同一编程语言内使用,也就是说用Java序列化后的对象只能用Java恢复,用PHP序列化的对象也只能由PHP来恢复。
相比与其它语言,Java实现序列化的方式相对简单。下面用一个简单示例来说明,在这个示例中,假设存在一个表示系统配置的Config
对象,需要在程序加载时恢复,程序退出时保存,而这个持久化工作我们由对象序列化的方式实现。
package ch18.serilize2;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class Config implements Serializable {
private String name;
private String password;
public Config() {
System.out.println("Config's constructor is called.");
}
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setName(String name) {
this.name = name;
}
}
public class Main {
private static Config config;
private static final String CURRENT_DIR = "./xyz/icexmoon/java_notes/ch18/serilize2/";
private static final String SAVED_FILE = CURRENT_DIR + "config.out";
public static void main(String[] args) {
try {
loadConfig();
checkLogin();
saveConfig();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private static void checkLogin() throws IOException {
String name = config.getName();
if (name != null && name.length() != 0) {
System.out.println("Hello, " + name);
} else {
System.out.println("please login.");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("enter your name:");
String nameInput = br.readLine();
System.out.print("enter your password:");
String pwdInput = br.readLine();
config.setName(nameInput);
config.setPassword(pwdInput);
}
}
private static void loadConfig() throws IOException, ClassNotFoundException {
File file = new File(SAVED_FILE);
if (file.exists() && file.isFile()) {
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
try {
config = (Config) ois.readObject();
} finally {
ois.close();
}
} else {
config = new Config();
}
}
private static void saveConfig() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(SAVED_FILE)));
oos.writeObject(config);
oos.close();
}
}
第一次运行这个程序的时候会提示你输入用户名和密码,之后再运行就可以正常显示一段包含用户名的欢迎信息。
整个程序并没有使用数据库等常见的持久化技术,只使用了序列化并写入文件,就完成了类似的功能。
如果你需要让某个类实例能够序列化,就要像示例中的Config
类那样,让其实现一个Serializable
接口,该接口是一个标记接口,并不包含任何方法。之后你就可以使用ObjectOutputStream.writeObject
方法将其写入一个输出流中保存。在这个示例中是写入到文件,当然也可以通过输出流写入到网络或者别的什么地方。
将序列化后的对象“恢复”也被称作“反序列化”,同样很简单,只要使用ObjectInputStream.readObject
方法读取即可,不过该方法返回的是一个Object
类型,需要我们向下转型为相应的实际类型。
总的来说Java的序列化和反序列化都相对简单,主要的工作都由Java虚拟机完成,开发者只要完成一小部分工作即可。不过有一个细节需要注意,序列化的过程中并不涉及构造函数的调用,而是虚拟机直接从序列化后的字节流中恢复数据来生成实例。此外,如果通过网络传递字节流并反序列化,必须有一个前提:本地的JVM要能够加载相应的类定义的class
文件,否则就会产生ClassNotFoundException
异常。
自定义序列化
大多数情况下,Java默认的序列化方式就够用了,但如果你需要对序列化有更详细的控制,可以使用Externalizable
接口。
Externalizable
接口继承自Serializable
,它包含两个方法writeExternal
和readExternal
,分别对应序列化和反序列化。
我们用Externalizable
改写之前的示例进行说明:
...
class Config implements Externalizable {
...
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.name);
out.writeObject(this.password);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
password = (String) in.readObject();
}
}
...
让Config
实现Externalizable
接口后,通过ObjectOutputStream.writeObject
方法输出Config
实例时,就会调用Config.writeExternal
方法向输出流写入字节序列。在示例中就是简单地调用out.writeObject
依次写入name
属性和password
属性(标准类库都实现了序列化相关接口)。相应的,在通过ObjectInputStream.readObject
反序列化时,Config.readExternal
方法就会被调用。
Externalizable
与Serializable
有一点有很大不同——在反序列化时,前者会调用默认构造函数,而后者不会。如果你重复执行上边的示例就会发现这一点。
这就意味着使用Externalizable
接口存在一些限制,比如说如果该类有一个非public
的默认构造函数,就会导致反序列化失败。
使用Externalizable
接口的好处在于可以实现一些自定义的序列化和反序列化逻辑,比如我们可能不希望将用户密码已明码的方式序列化后保存,这样是危险的,通常的做法是只保存密码的MD5值:
...
class Config implements Externalizable {
...
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.name);
String md5 = MyMD5Util.encrypt(password);
out.writeObject(md5);
}
...
}
...
MD5的具体生成方式是我参考自网上一篇文章的,具体可以查看本文的Github仓库或者末尾的参考资料列表。
这个示例只是用于说明
Externalizable
的可能用途,实际上通常需要在获取到用户输入的密码后就立即对其MD5,然后所有的密码效验等都是比对MD5值是否相等,是不会将明码保存在Config
实例的属性中的。目前这个示例是有缺陷的,第一次登陆时我们写入Config.password
中的是明文密码,但之后反序列化后获取的是MD5后的值,这是有歧义的。但作为一个玩具示例,我不打算修正这个问题。
如果我们压根不需要保存用户的密码信息,可以更简单:
...
class Config implements Serializable {
private String name;
private transient String password;
public Config() {
System.out.println("Config's constructor is called.");
}
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setName(String name) {
this.name = name;
}
}
...
需要注意的是,这里的Config
实现的是Serializable
接口,同时为password
属性添加了一个transient
关键字,其它具体的序列化细节依然是交给JVM来实现,如果运行这个版本的示例你就会发现,反序列化后的Config
属性中包含name
属性,但是password
属性是null
,换句话说,序列化时并没有保存password
属性。
这就是transient
关键字的用途:可以让Serializable
对象的相应属性排除出“自动”序列化和反序列中。
需要注意的是,transient
因为是作用于JVM执行的“自动序列化”过程中的,所以只能和Serializable
接口搭配使用,而不能用于Externalizable
接口。
如果你需要在使用Serializable
接口时实现类似Externalizable
那样的“自定义序列化逻辑”,同样是可以实现的:
...
class Config implements Serializable {
...
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.writeObject(name);
out.writeObject(password);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
password = (String) in.readObject();
}
}
...
就像示例展示的,只要实现writeObject
方法和readObject
方法即可。JVM在具体执行序列化和反序列化操作时会“检查”Config
实例,如果其具备这两个方法,就会调用以实现序列化和反序列化,而不会执行默认的逻辑。
有意思的是这两个方法是private
的,且并不属于任何接口,但却被JVM执行序列化的相关组件可以探查和调用。这种方式在Java中是很不常见的,更类似于Python中的协议。实际上是通过反射机制来实现的。
最后要说明的是,如果需要在writeObject
和readObject
方法中执行默认的序列化和反序列化逻辑,可以这样做:
...
class Config implements Serializable {
...
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
}
...
写法很简单,只要调用ObjectOutputStream.defaultWriteObject
和ObjectInputStream.defaultReadObject
即可。比较奇怪的是这两个方法并不需要传入当前对象,也不会返回一个Config
实例。这是因为在序列化和反序列化的过程中,输入流和输出流是持有当前对象的引用的。
此外,因为是执行的默认序列化和反序列化逻辑,所以此时transient
关键字是生效的,你会看到被标记为transient
的password
属性没有被序列化。
持久化
如果只是将序列化用于简单地保存和恢复某个对象,可能没有什么问题,但如果是系统性地用序列化对某些数据进行持久化,就会有一些额外问题。
比如说有多个对象持有同一个对象的引用,那么序列化和反序列化后,得到的结果依然是指向同一个对象还是分别指向一个对象?
package ch18.serilize6;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
class Student implements Serializable {
private ClassRoom classRoom;
private String name;
public Student(String name) {
this.name = name;
}
public void setClassRoom(ClassRoom classRoom) {
this.classRoom = classRoom;
}
@Override
public String toString() {
return super.toString() + "-" + classRoom.toString();
}
}
class ClassRoom implements Serializable {
public void addStudent(Student student) {
student.setClassRoom(this);
}
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ClassRoom cr = new ClassRoom();
Student s1 = new Student("Li Lei");
Student s2 = new Student("Han Meimei");
Student s3 = new Student("Brus Lee");
cr.addStudent(s1);
cr.addStudent(s2);
cr.addStudent(s3);
List<Student> students = new LinkedList<>();
students.add(s1);
students.add(s2);
students.add(s3);
System.out.println(students);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);
oos.writeObject(students);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Student s4 = (Student) ois.readObject();
Student s5 = (Student) ois.readObject();
Student s6 = (Student) ois.readObject();
List<Student> students2 = (List<Student>) ois.readObject();
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(students2);
}
}
// [ch18.serilize6.Student@372f7a8d-ch18.serilize6.ClassRoom@2f92e0f4,
// ch18.serilize6.Student@28a418fc-ch18.serilize6.ClassRoom@2f92e0f4,
// ch18.serilize6.Student@5305068a-ch18.serilize6.ClassRoom@2f92e0f4]
// ch18.serilize6.Student@5cb0d902-ch18.serilize6.ClassRoom@46fbb2c1
// ch18.serilize6.Student@1698c449-ch18.serilize6.ClassRoom@46fbb2c1
// ch18.serilize6.Student@5ef04b5-ch18.serilize6.ClassRoom@46fbb2c1
// [ch18.serilize6.Student@5cb0d902-ch18.serilize6.ClassRoom@46fbb2c1,
// ch18.serilize6.Student@1698c449-ch18.serilize6.ClassRoom@46fbb2c1,
// ch18.serilize6.Student@5ef04b5-ch18.serilize6.ClassRoom@46fbb2c1]
在这个示例中,有三个Student
对象,他们都持有同一个ClassRoom
对象的引用,而经过序列化和反序列化后,依然会保留这种关系,无论是按照单个Student
对象操作,还是将它们添加进List
中统一操作,都是相通的结果。
当然,前提条件是使用的是同一个输出/输入流,否则会建立两套“类关系网”:
package ch18.serilize7;
...
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ClassRoom cr = new ClassRoom();
Student s1 = new Student("Li Lei");
Student s2 = new Student("Han Meimei");
Student s3 = new Student("Brus Lee");
cr.addStudent(s1);
cr.addStudent(s2);
cr.addStudent(s3);
List<Student> students = new LinkedList<>();
students.add(s1);
students.add(s2);
students.add(s3);
System.out.println(students);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
ObjectOutputStream oos2 = new ObjectOutputStream(bos2);
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);
oos2.writeObject(students);
oos.flush();
oos2.flush();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ObjectInputStream ois2 = new ObjectInputStream(new ByteArrayInputStream(bos2.toByteArray()));
Student s4 = (Student) ois.readObject();
Student s5 = (Student) ois.readObject();
Student s6 = (Student) ois.readObject();
List<Student> students2 = (List<Student>) ois2.readObject();
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(students2);
}
}
// [ch18.serilize7.Student@372f7a8d-ch18.serilize7.ClassRoom@2f92e0f4,
// ch18.serilize7.Student@28a418fc-ch18.serilize7.ClassRoom@2f92e0f4,
// ch18.serilize7.Student@5305068a-ch18.serilize7.ClassRoom@2f92e0f4]
// ch18.serilize7.Student@5cb0d902-ch18.serilize7.ClassRoom@46fbb2c1
// ch18.serilize7.Student@1698c449-ch18.serilize7.ClassRoom@46fbb2c1
// ch18.serilize7.Student@5ef04b5-ch18.serilize7.ClassRoom@46fbb2c1
// [ch18.serilize7.Student@5f4da5c3-ch18.serilize7.ClassRoom@443b7951,
// ch18.serilize7.Student@14514713-ch18.serilize7.ClassRoom@443b7951,
// ch18.serilize7.Student@69663380-ch18.serilize7.ClassRoom@443b7951]
这个示例中使用了两套输入和输出流,分别用于序列化三个独立的Student
对象和统一保存在List
中的Student
对象。结果是虽然反序列化后的两套Student
对象依然保留着指向同一个ClassRoom
对象这种特点,但两套Student
对象之间不再有联系,他们的地址是不同的,也就是说反序列化后产生了6个Student
对象和2个ClassRoom
对象。
虽然目前来看序列化表现的都很好,但实际上它还有个缺陷——“无法保存static
属性”。
package ch18.static1;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import util.Fmt;
class Student implements Serializable {
public static int counter = 0;
private final int id = ++counter;
private String name;
public Student(String name) {
this.name = name;
}
@Override
public String toString() {
return Fmt.sprintf("Student(%d.%s)", id, name);
}
}
public class Main {
private static String CURRENT_DIR = "./xyz/icexmoon/java_notes/ch18/static1/";
private static String FILE = CURRENT_DIR + "data.out";
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File(FILE);
if (file.exists()) {
read();
file.delete();
} else {
write();
}
}
private static void write() throws IOException {
Student s1 = new Student("Han Meimei");
Student s2 = new Student("Li Lei");
Student s3 = new Student("Brus Lee");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(FILE));
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);
oos.close();
}
private static void read() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(FILE)));
Student s4 = (Student) ois.readObject();
Student s5 = (Student) ois.readObject();
Student s6 = (Student) ois.readObject();
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(Student.counter);
ois.close();
}
}
// Student(1.Han Meimei)
// Student(2.Li Lei)
// Student(3.Brus Lee)
// 0
在这个例子中,Student
有一个静态属性counter
,用于记录已创建的Student
对象个数,并用于为Student
对象初始化id
属性。之后在第一次运行程序时,会创建三个Student
对象,并在序列化后保存到文件。第二次运行会从文件反序列化,但如果此时打印Student.counter
,就会发现其被初始化为0
,而非我们期待的3
。
这是因为序列化和反序列化只涉及对象的属性,而类属性是保存在类的Class
对象中的,而Class
对象并不会被同时序列化和反序列化。
当然
Class
类本身也支持序列化,但问题在于将它们序列化保存是容易的,但利用它们恢复静态属性比较麻烦,可能需要手动编写一些反射代码,那样反而会让问题复杂化。
如果需要对static
属性序列化和反序列化,就需要我们做出额外努力,比如:
package ch18.static2;
...
class Student implements Serializable {
...
public static void serializeStaticMember(ObjectOutputStream oos) throws IOException {
oos.writeInt(counter);
}
public static void unserializeStaticMember(ObjectInputStream ois) throws IOException {
counter = ois.readInt();
}
}
public class Main {
...
private static void write() throws IOException {
Student s1 = new Student("Han Meimei");
Student s2 = new Student("Li Lei");
Student s3 = new Student("Brus Lee");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(FILE));
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);
Student.serializeStaticMember(oos);
oos.close();
}
private static void read() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(FILE)));
Student s4 = (Student) ois.readObject();
Student s5 = (Student) ois.readObject();
Student s6 = (Student) ois.readObject();
Student.unserializeStaticMember(ois);
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(Student.counter);
ois.close();
}
}
// Student(1.Han Meimei)
// Student(2.Li Lei)
// Student(3.Brus Lee)
// 3
这里使用了一种最简单的方式,在Student
中添加了两个类方法serializeStaticMember
和unserializeStaticMember
,它们分别负责将静态成员写入输出流或者从输入流中恢复,然后只要在合适的地方调用它们即可。
当然也可以使用我们之前介绍的方法来持久化静态属性:
package ch18.static3;
...
class Student implements Serializable {
...
private void writeObject(ObjectOutputStream out)
throws IOException {
out.defaultWriteObject();
out.writeInt(counter);
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
counter = in.readInt();
}
}
...
// Student(1.Han Meimei)
// Student(2.Li Lei)
// Student(3.Brus Lee)
// 3
这样做甚至不需要修改其它部分的代码,但问题的关键在于实际上我们只需要在最后一个Student
对象序列化后序列化Student
的静态属性,并且在最后一个Student
对象反序列化后反序列化Student
的静态属性,也就是说只需要进行2次Student
静态属性的序列化和反序列化工作,但示例中这种将其绑定到对象序列化工作中的做法,就会导致额外不必要的序列化和反序列化工作,这是一种额外开销。当然,实际上类似的取舍在计算机领域很常见,如果你觉得这些性能开销是可接受的,就可以采用这种方式。
Preferences
接触过Android开发的应该对Preferences
不陌生,这是一种系统管理的,轻量级的持久化工具。可以帮助你将用户偏好等一些轻量级数据进行持久化。
有意思的是,Java也支持Preferences
,且用途类似。
下面这个例子是用Preference
改写之前用序列化实现的那个登录程序:
package ch18.references;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.prefs.Preferences;
class Config {
private String name;
private transient String password;
public Config() {
System.out.println("Config's constructor is called.");
}
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setName(String name) {
this.name = name;
}
}
public class Main {
private static Config config;
private static Preferences preferences = Preferences.userNodeForPackage(Main.class);
public static void main(String[] args) {
loadConfig();
try {
checkLogin();
} catch (IOException e) {
throw new RuntimeException(e);
}
saveConfig();
}
private static void checkLogin() throws IOException {
String name = config.getName();
if (name != null && name.length() != 0) {
System.out.println("Hello, " + name);
System.out.println("password:" + config.getPassword());
} else {
System.out.println("please login.");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("enter your name:");
String nameInput = br.readLine();
System.out.print("enter your password:");
String pwdInput = br.readLine();
config.setName(nameInput);
config.setPassword(pwdInput);
}
}
private static void loadConfig() {
String name = preferences.get("config.name", null);
if (name != null && name.length() != 0) {
config = new Config();
config.setName(name);
config.setPassword(preferences.get("config.pwd", ""));
} else {
config = new Config();
}
}
private static void saveConfig() {
preferences.put("config.name", config.getName());
preferences.put("config.pwd", config.getPassword());
}
}
可以通过Preferences.userNodeForPackage
方法来获取一个Preferences
,除此之外还有Preferences.systemNodeForPackage
,它们没有本质上的区别,不过习惯上会使用前者保存用户个人数据,用后者保存程序的通用数据。
Preferences
中的数据采用键值对形式保存,可以使用put
方法保存数据,使用get
方法获取数据。除了像示例中那样保存和获取字符串以外,还支持其它基础类型的数据,比如putInt
和getInt
等。但不能直接用于保存其它对象,并且对于字符串大小也有限制(不超过8K)。
需要注意的是,使用get
时需要用第二个参数指定一个“缺省值”,也就是没有键值对时会返回的值,类似的操作其实在Map
中也存在。
有意思的是改用Preferences
后,你会发现不会生成数据文件,可能你会疑惑数据保存到哪里去了。实际上具体的实现方式与操作系统相关,如果是Windows,Preferences
会使用注册表保存数据(注册表中的数据本身也是以键值对形式存在)。
因为本篇内容是上篇的补充,所以较短,除了这些内容以外,《Java编程思想》还介绍了使用XML进行持久化的内容,但我觉得书中使用的第三方类库已经很古老,并且JSON已经变得比XML更流行,且这部分内容更适合在用到时查询,不是太需要在持久化部分过多涉猎。
总之,就到这里了,谢谢阅读。
文章评论