4.对象输入/输出流与序列化
存储同类型数据时很少具有相同的类型(继承会出现存储多态集合),Java语言支持一种称为对象序列化(object serialization),它可以将任何对象写出到输出流中,并在之后将其读回。
保存和加载序列化对象
为了保存对象数据,首先需要打开一个ObjectOutputStream对象:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"));
为了保存对象,可以直接使用ObjectOutputStream的writeObject方法。
为了将这些对象读回,首先需要获得一个ObjectInputStream对象:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"));
然后用readObject方法以这些对象写入时的顺序获得它们:
Employee e1 = (Employee) in.readObject();
但是对希望在对象输出流中存储或从对象输入流中恢复的所有类都应该实现Serializable接口:
class Employee implements Serializable {...}
Serializable接口没有任何方法,与Cloneable接口相似。但为了是类可克隆,仍需覆盖Object类的clone方法,而序列化不需要做任何事。
只有在写出对象时才能用writeObject/readObject方法,对于基本类型需要使用writeInt等(对象流都实现了DataInput/DataOutput接口)。
在幕后,是ObjecyOutputStream在浏览对象的所有域,并存储它们的内容。
当一个对象被多个对象共享,做为它们各自状态的一部分时:
class Manager extends Employee {
private Employee secretary;
...
}
//两个经理共用一个秘书
harry = new Employee("Harry Hacker", ...);
Manager carl = new Manager("Carl Carcker", ...);
carl.setSecretary(harry);
Manager tony = new Manager("Tony Tester", ...);
tony.setSecretary(harry);
这里不能去保存和恢复秘书对象的内存地址,因为当对象被重新加载时,它可能占据的是与原来完全不同的内存地址。与此不同的是,每个对象都用一个序列号(serial number)保存的,这就是这种机制之所以成为对象序列化的原因。其算法:
1.对你遇到的每一个对象引用都关联一个序列号
2.对于每个对象,当第一次遇到时,保存其对象数据到输出流中
3.如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”
在读回对象时,过程是反过来的:
1.对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个序列号和新对象之间的关联
2.当遇到“与之前保存过的序列号为x的对象相同”标记时,标记时,获取与这个顺序号相关联的对象引用
public class ObjectStreamTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Employee harry = new Employee("Harry Hacker", 50000, 2019, 12, 23);
Manager carl = new Manager("Carl Cracker", 80000, 2019, 12, 20);
carl.setSecretary(harry);
Manager tony = new Manager("Tony Tester", 40000, 2019, 12, 19);
tony.setSecretary(harry);
Employee[] staff = new Employee[3];
staff[0] = carl;
staff[1] = harry;
staff[2] = tony;
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"))) {
out.writeObject(staff);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"))) {
Employee[] newStaff = (Employee[]) in.readObject();
// 秘书对象重新加载之后是唯一的,当秘书被恢复时,会反映到经理们的secretary域中
newStaff[1].raiseSalary(10);
for (Employee e : newStaff) {
System.out.println(e);
}
}
}
}
理解对象序列化的文件格式
修改默认的序列化机制
某些数据域是不可以序列化的。将它们标记为transient(瞬时)的。瞬时的域在对象被序列化时总被跳过。
序列化机制为单个类提供了一种方式,去向默认的读写行为添加验证或任何想要的行为。可序列化的类可以定义下列方法:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStream out) throws IOException;
之后数据域就不再会自动序列化,取而代之的是调用这些方法。
在java.aut.geom包中有大量的类都是不可序列化的。例如Point2D.Double,想要序列化一个LabeledPoint类,需要存储一个String和一个Point2D.Double。首先将Point2D.Double标记为transient,以避免NotSerializableException。
public class LabeledPoint implements Serializable {
private String label;
private transient Point2D.Double point;
// 调用defaultWriteObject方法写出描述符和String域label,这是ObjectOutputStream的一个特殊方法,只能在可序列化的writeObject方法中被调用
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeDouble(point.getX());
out.writeDouble(point.getY());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
double x = in.readDouble();
double y = in.readDouble();
point = new Point2D.Double(x, y);
}
}
java.util.Date类,它提供了自己的readObject和writeObject方法,这些方法将日期写出为纪元(UTC时间1970年1月1日0点)开始的毫秒。Date类有一个复杂的内部表示,为了优化查询它存储一个Calendar对象和一个毫秒计数值。Calendar的状态是冗余的,因此不需要保存。
readObject和writeObject方法只需要保存和加载它们的数据域,而不需要关心超类数据和任何其他类的信息。
除了让序列化机制来保存和恢复对象数据,类还可以定义自己的机制。但必须实现Externalizable接口的writeExternal和readExternal。
与read/writeObject不同,这些方法对包含超类数据在内的整个对象的存储和恢复负全责。在写出对象时,序列化机制在输出流中仅仅只是记录该对象所属的类。在读入可外部化类时,对象输入流将用无参构造器创建一个对象,然后调用readExternal方法:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeDouble(salary);
out.writeLong(hireDay.toEpochDay());
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
salary = in.readDouble();
hireDay = LocalDate.ofEpochDay(in.readLong());
}
read/writeObject是私有的,并且只能被序列化机制调用。read/writeExternal是公有的,readExternal还潜在地允许修改现有对象的状态。
序列化单例和类型安全的枚举
在序列化和反序列化时,如果目标对象是唯一的,这通常会在实现单例和类型安全的枚举时发生。
如果使用Java语言的enum结构,不必担心序列化。可以使用==操作符来测试对象的等同性。但是:
public class Orientation{
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
private Orientation(int x) {
value = x;
}
}
这种风格在枚举被添加到Java之前很普遍。其构造器是私有的,因此不可能创建出HORIZONTAL和VERTICAL 之外的对象。
当类型安全的枚举实现Serializable接口时,默认序列化是不适用的。
Orientation original = Orientation.HORIZONTAL;
System.out.println(original == Orientation.HORIZONTAL);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("orientation.dat"));
out.writeObject(original);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("orientation.dat"));
Orientation saved = (Orientation) in.readObject();
System.out.println(saved == Orientation.HORIZONTAL);
事实上saved是一个全新的对象,即使构造器是私有的,序列化机制也可以创建新对象。
此时,需要定义readResolve的特殊序列化方法,在对象被序列化之后会调用它,它必须返回一个对象,而该对象会成为readObject的返回值:
protected Object readResolve() throws Exception {
if (value == 1) {
return Orientation.HORIZONTAL;
}
if (value == 2) {
return Orientation.VERTICAL;
}
throw new Exception();
}
请想遗留代码中所有类型安全的枚举以及所有支持单例设计模式的类中添加readResolve方法。
版本管理
为克隆使用序列化
序列化机制提供了一种克隆对象的简便途径,只要对应的类是可序列化的即可。直接将对象序列化到输出流,然后读回,产生的新对象是对现有对象的一个深拷贝(deep copy)。不必将对象写入文件,可以用ByteArrayOutputStream将数据保存在字节数组中:
public class SerialCloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Employee harry = new Employee("Harry Hacker", 35000, 2019, 12, 24);
Employee harry2 = (Employee) harry.clone();
harry.raiseSalary(10);
System.out.println(harry);
System.out.println(harry2);
}
}
class SerialCloneable implements Cloneable, Serializable {
@Override
public Object clone() throws CloneNotSupportedException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(this);
}
try (InputStream bin = new ByteArrayInputStream(bout.toByteArray())){
ObjectInputStream in = new ObjectInputStream(bin);
return in.readObject();
}
} catch (IOException | ClassNotFoundException e) {
CloneNotSupportedException e2 = new CloneNotSupportedException();
e2.initCause(e);
throw e2;
}
}
}
class Employee extends SerialCloneable {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
}
5.操作文件
Path接口和Files类是在java SE 7中添加进来的,它们用起来比自JDK 1.0以来就一直使用的File类要方便的多。
Path
Path表示的是一个目录名序列,其后还可以跟着一个文件名。路径中的第一个部件可以是根部件,例如/或C:\,而允许访问的根部件取决于文件系统。以根部件开始的路径是绝对路径,否则就是相对路径(相对于项目的根目录,即src的上级)。
Path absolute = Paths.get("D:\\", "test", "test2.txt");
Path relative = Paths.get("test", "test.txt");
静态的Paths.get方法接受一个或多个字符串,并用默认的文件系统的路径分隔符(Unix是/,Windows是\)连接起来。如果解析出来的不是给定文件系统的合法路径,就抛出InvalidPathException异常。这个连接起来的结果是一个Path对象。
get方法可以获取包含多个部件构成的单个字符串:
// base.dir是一个存储路径的属性
String baseDir = props.getProperty("base.dir");
Path basePath = Paths.get(baseDir);
调用p.resolve(q)将按照下列规则返回一个路径:
如果q是绝对路径,则结果就是q,否则根据文件系统规则,将p后面跟着q作为结果。
resolveSibling,它通过解析指定路径的父路径产生其兄弟路径:
Path basePath = Paths.get("E:\\corejava\\src");
Path workRelative = Paths.get("chapter3");
Path chapter3 = basePath.resolve(workRelative);
Path chapter4 = chapter3.resolveSibling("chapter4");
System.out.println(chapter3.toString());
System.out.println(chapter4.toString());
//E:\corejava\src\chapter3
//E:\corejava\src\chapter4
resolve的对立面是relativize,即调用p.relativize(r)将产生路径q,而对q进行解析的结果正是r。以/home/cay为目标对/home/fred/myprog进行相对化操作,会产生../fred/myprog,其中..表示文件系统中的父目录。
normalize方法将移除所有冗余的.和..部件(或者文件系统认为冗余的所有部件)。
toAbsolutePath方法将产生给定路径的绝对路径,该绝对路径从根部件开始。
Path类有许多有用方法用来将路径断开:
Path p = Paths.get("/home", "fred", "myprog.properties");
Path parent = p.getParent(); // /home/fred
Path file = p.getFileName(); // myprog.properties
Path root = p.getRoot(); // /
还可以从Path对象中构建Scanner对象:
Scanner in = new Scanner(Paths.get("/home/fred/input.txt"));
读写文件
Files类可以很容易地读取文件的所有内容:
byte[] bytes = Files.readAllBytes(path);
// 将文件当作字符串读入
String content = new String(bytes, StandardCharsets.UTF_8);
// 将文件当做行序列读入
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
相反的,写出字符串到文件中:
Files.write(path, content.getBytes(StandardCharsets.UTF_8));
// 向指定文件追加内容
Files.write(path, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
// 将一个行的集合写出到文件
Files.write(path, lines);
这些简单的方法适用于中等长度的文本文件,如果要处理的文件长度比较长,或者是二进制文件,还是应该使用输入输出流和读入器写出器。
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader reader = Files.newBufferedReader(path);
Writer writer = Files.newBufferedWriter(path);
创建文件和目录
创建新目录可以调用:
Files.createDirectory(path)除了路径中最后一个部件外,其他部分必须都是已经存在的。
要创建路径中的中间目录要使用Files.createDirectories(path)。
可以使用Files.createFile(path)创建一个空文件,如果文件已存在,就会抛出异常。检查文件是否存在和创建文件是原子性的,如果文件不存在会被创建,并且其他程序在此过程是无法执行文件创建操作的。
可以在给定位置或系统指定位置创建临时文件或临时目录:
Path newPath = Files.createTempFile(dir, prefix, suffix);
Path newPath = Files.createTempFile(prefix, suffix);
Path newPath = Files.createTempDirectory(dir, prefix);
Path newPath = Files.createTempDirectory(prefix);
dir是一个Path独享,prefix和suffix可以为null。调用Files.createTempFile(null, "".txt")会返回一个像/tmp/214565234.txt的路径。
复制、移动和删除文件
将文件从一个位置复制到另一个位置可以直接用:
Files.copy(fromPath, toPath);
移动文件(复制并删除原文件)可以调用:
File.move(fromPath, toPath),如果目标路径已经存在,那么复制或移动将失败。如果想覆盖已有目标路径,可以使用REPLACE_EXISTING选项。如果想复制所有文件属性,可以使用COPY_ATTRIBUTES选项:
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
使用ATOMIC_MOVE选项来实现原子性,保证要么移动操作完成,要么源文件保持在原来位置。
还可以将一个输入流复制到Path中,表示想要将输入流存储到硬盘上。同样的,可以将一个Path复制到输出流:
Files.copy(inputStream, toPath);
Files.copy(fromPath, outputStream);
删除文件:
Files.delete(path),如果要删除的文件不存在,就会抛出异常。因此需要调用:
boolean deleted = Files.deleteIfExists(path),该方法还可以用来移除空目录。
选项 | 描述 |
StandarOpenOption与newBufferedWrite,newInputStream,newOutputStream,write一起使用 | |
READ | 用于读取而打开 |
WRITE | 用于写入而打开 |
APPEND | 如果用于写入而打开,那么在文件末尾追加 |
TRUNCATE_EXISTING | 如果用于写入而打开,那么移除已有内容 |
CREATE_NEW | 创建新文件并且在文件已存在情况下会创建失败 |
CREATE | 自动在文件不存在的情况下创建新文件 |
DELETE_ON_CLOSE | 当文件被关闭时,尽可能地删除该文件 |
SPARSE | 给文件系统一个提示,表示该文件是稀疏的 |
DSYN|SYN | 要求对文件数据|数据和元数据的每次更新都必须同步地写入到存储设备中 |
StandardCopyOption与copy,move一起使用 | |
ATOMIC_MOVE | 原子性地移动文件 |
COPY_ATTRIBUTES | 复制文件的属性 |
REPLACE_EXISTING | 如果目标已存在,则替换它 |
LinkOption与上面所有方法以及exists,isDirectory,isRegularFile一起使用 | |
MOFOLLOW_LINKS | 不要跟踪符号链接 |
FileVisitOption与find,walk,walkFileTree一起使用 | |
FOLLOW_LINKS | 跟踪符号链接 |
获取文件信息
下面的静态方法都将返回一个boolean值,表示检查路径的某个属性的结果:
exists, isHidden, isReadable, isWritable, isExecutable, isRegularFile, isDirectory, isSymbolicLink
size方法将返回文件的字节数:
long fileSize = Files.size(path);
getOwner方法将文件的拥有者作为java.nio.file.attribute.UserPrincipal的一个实例返回。
所有文件系统都会报告一个基本属性集,被封装在BasicFileAttributes接口中,基本文件属性包括:
1.创建文件、最后一次访问以及最近一次修改文件的时间,这些时间都表示成java.nio.file.attribute.FileTime
2.文件是常规文件、目录还有符号链接,抑或三种都不是
3.文件尺寸
4.文件主键,这是某种类的对象
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
如果文件系统兼容POSIX:
PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class);
从中找到组拥有者,以及文件的拥有者、组合访问权限。