10.1 输入输出
InputStream
用于标志不同起源的输入类,包括字节数组、String
对象、文件、“管道”以及其他起源地比如Internet连接等,主要的InputStream
如下所示:
类 | 功能 | 构建器参数 | 使用方法 |
---|---|---|---|
ByteArrayInputStream | 允许内存中的一个缓冲区作为InputStream 使用 | 从中提取的字节缓冲区 | 作为一个数据源使用。通过将其同一个FilterInputStream 对象连接,提供一个有用的接口 |
StringBufferInputStream | 将一个String 转换成InputStream 一个String | 基础的实施方案采用一个StringBuffer (字串缓冲) | 作为一个数据源使用。通过将其同一个FilterInputStream 对象连接,可提供一个有用的接口 |
FileInputStream | 用于从文件读取信息 | 代表文件名的一个String ,或者一个File 或FileDescriptor 对象 | 作为一个数据源使用。通过将其同一个FilterInputStream 对象连接,可提供一个有用的接口 |
PipedInputString | 产生为相关的 PipedOutputStream 写的数据,实现了“管道化”的概念 | PipedOutputStream | 作为一个数据源使用。通过将其同一个FilterInputStream 对象连接,可提供一个有用的接口 |
SequenceInputStream | 将两个或更多的InputStream 对象转换成单个InputStream 使用 | 两个InputStream 对象或者一个Enumeration ,用于 InputStream 对象的一个容器 | 作为一个数据源使用。通过将其同一个FilterInputStream 对象连接,可提供一个有用的接口 |
FilterInputStream
对作为破坏器接口使用的类进行抽象;那个破坏器为其他InputStream
类提供了有用的功能。
OutputStream
决定了输入将去向何方:可以是一个字节数组或一个文件或者一个管道,主要的OutputStream
如下所示:
类 | 功能 | 构建参数 | 使用方法 |
---|---|---|---|
ByteArrayOutputStream | 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。 | 可选缓冲区的初始大小 | 用于指出数据的目的地。若将其同FilterOutputStream 对象连接到一起,可提供一个有用的接口 |
FileOutputStream | 将信息发给一个文件 | 用一个 String 代表文件名,或选用一个File 或FileDescriptor 对象 | 用于指出数据的目的地。若将其同FilterOutputStream 对象连接到一起,可提供一个有用的接口 |
PipedOutputStream | 我们写给它的任何信息都会自动成为相关的PipedInputStream 的输出。实现了“管道化”的概念 | PipedInputStream | 为多线程处理指出自己数据的目的地,将其同FilterOutputStream 对象连接到一起,便可提供一个有用的接口 |
FilterOutputStream
对作为破坏器接口使用的类进行抽象处理;那个破坏器为其他OutputStream
类提供了有用的功能。
10.2 添加属性和有用的接口
FilterInputStream
可以从InputStream
中读取数据,它的子类总结主要如下:
类 | 功能 | 构建器参数 | 使用方法 |
---|---|---|---|
DataInputStream | 与DataOutputStream 联合使用,使自己能以机动方式读取一个流中的基本数据类型(int ,char ,long 等等) | InputStream | 包含了一个完整的接口,以便读取基本数据类型 |
BufferedInputStream | 避免每次想要更多数据时都进行物理性的读取,告诉它“请先在缓冲区里找” | InputStream ,没有可选的缓冲区大小 | 本身并不能提供一个接口,只是发出使用缓冲区的要求。要求同一个接口对象连接到一起 |
LineNumberInputStream | 跟踪输入流中的行号 | InputStream | 可调用getLineNumber() 以及setLineNumber(int) ,所以可能需要同一个真正的接口对象连接 |
PushbackInputStream | 有一个字节的后推缓冲区,以便后推读入的上一个字符 | InputStream | 通常由编译器在扫描器中使用,因为 Java 编译器需要它。一般不在自己的代码中使用 |
FilterOutputStream
可以向OutputStream
中写入数据,DataOutputStream
对各个基本数据类型以及String对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream
都能正常地读取它们。所有方法都以“wirte”开头。FilterOutputStream
的子类总结主要如下:
类 | 功能 | 构建器参数 | 使用方法 |
---|---|---|---|
DataOutputStream | 与DataInputStream 配合使用,以便采用方便的形式将基本数据类型(int ,char ,long 等)写入一个数据流 | OutputStream | 包含了完整接口,以便我们写入基本数据类型 |
PrintStream | 用于产生格式化输出。DataOutputStream 控制的是数据的“存储”,而PrintStream 控制的是“显示” | OutputStream ,可选一个布尔参数,指示缓冲区是否与每个新行一同刷新 | 对于自己的OutputStream 对象,应该用final 将其封闭在内。可能经常都要用到它 |
BufferedOutputStream | 用它避免每次发出数据的时候都要进行物理性的写入,要求它“请先在缓冲区里找”。可调用flush() ,对缓冲区进行刷新 | OutputStream ,可选缓冲区大小 | 本身并不能提供一个接口,只是发出使用缓冲区的要求。需要同一个接口对象连接到一起 |
10.3 RandomAccessFile
RandomAccessFile
不属于InputStream
或OutputStream
,它的构建器为RandomAccessFile(String name, String mode)
和RandomAccessFile(File file, String mode)
,第二个参数指出自己只是随机读(”r”),还是读写兼施(”rw”)(没有提供对“只写文件”的支持),上述的构造器都是针对的文件输入流,但有时要在其他类型的数据流中搜索,比如一个ByteArrayInputStream
,但搜索方法只有RandomAccessFile
才会提供,但它只对文件才能操作,不能针对数据流操作。此时,BufferedInputStream
确实允许我们标记一个位置(使用mark()
,它的值容纳于单个内部变量中),并用reset()
重设那个位置。但这些做法都存在限制,并不是特别有用。
10.4 File
这一小节主要介绍了一些File
类的一些方法,这里主要记录一下目录过滤器的用法(即list(FilenameFilter filter)
用法),文中的案例如下:
public class DirList {
public static void main(String[] args) {
try {
File path = new File(".");
String[] list;
if(args.length==0) {
list = path.list();
} else {
//在调用list(filter)时会自动调用filter的accept方法
list = path.list(new DirFilter(args[0]));
}
for (int i = 0; i < list.length; i++) {
System.out.println(list[i]);
}
System.out.println("================");
String[] dir = path.list();
for (int i = 0; i < dir.length; i++) {
System.out.println(dir[i]);
}
} catch (Exception e) {
e.printStackTrace();
}
}
static class DirFilter implements FilenameFilter {
String afn;
DirFilter(String afn) {
this.afn = afn;
}
//测试是否name属于dir目录中
public boolean accept(File dir, String name) {
String f = new File(name).getName();
System.out.println(f.indexOf(afn));
//在f字符串中查找afn字符串,如果找到目标返回具体位置,否则返回-1
return f.indexOf(afn) != -1;
}
}
}
目录过滤器的基类是一个接口,其中只有一个方法:
public interface FilenameFilter {
boolean accept(File dir, String name);
}
各个过滤器的accept(File dir, String name)
方法(用于判断name
文件是否在目录dir
下)根据各自的需要进行实现,再看一下list(FilenameFilter filter)
的源码如下:
public String[] list(FilenameFilter filter) {
String names[] = list();
if ((names == null) || (filter == null)) {
return names;
}
List<String> v = new ArrayList<>();
for (int i = 0 ; i < names.length ; i++) {
if (filter.accept(this, names[i])) {
v.add(names[i]);
}
}
return v.toArray(new String[v.size()]);
}
上述的案例利用目录过滤器实现这样一种功能:当前代码所在目录是否存在X(X指我们手动配置的args[]
参数)。虽然功能实现了,但是个人觉得这一功能的实现实在太鸡肋了,而且繁杂,不如直接使用list()
方法来的简洁。文中还利用内部匿名类来改造了上述代码又搞出了2个版本,大同小异,这里仅记录一下匿名类的用法。
10.5 IO流的应用
这一小节主要以开始的代码来讲解各个分支的作用和用法,理解起来并不存在困难。
10.6 StreamTokenizer
StreamTokenizer
不是InputStream
和OutputStream
的子类,但它只随同InputStream
工作,所以将它归到I/O这里,StreamTokenizer
类用于将任何InputStream
分割为一系列记号(Token),这玩意儿我平时基本没有接触过,找了一下这货理解了一下书中的案例,发现还挺有意思,下面是案例:
class Counter {
private int i = 1;
int read() {
return i;
}
void increment() {
i++;
}
}
public class SortedWordCount {
private FileInputStream file;
private StreamTokenizer st;
private Hashtable counts = new Hashtable();
SortedWordCount(String filename) throws FileNotFoundException {
try {
file = new FileInputStream(filename);
st = new StreamTokenizer(file);
//指定该字符没有特别重要的意义,所以解析器不会把它当作
//自己创建的任何单词的一部分,这里指出的是“.”和“-”
st.ordinaryChar('.');
st.ordinaryChar('-');
} catch (FileNotFoundException e) {
System.out.println("Could not open " + filename);
throw e;
}
}
void cleanup() {
try {
file.close();
} catch (IOException e) {
System.out.println("file.close() unsuccessful");
}
}
void countWords() {
try {
//常量TT_EOF表示流的末尾
while (st.nextToken()!=StreamTokenizer.TT_EOF) {
String s;
//ttype字段将包含刚读取的标记的类型
switch (st.ttype) {
//常量TT_EOL表示读行的末尾
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
//常量TT_NUMBER表示读取到一个数字
case StreamTokenizer.TT_NUMBER:
//如果当前标记是一个数字,nval字段将包含该数字的值,st.navl默认解析出的格式是double
s = Double.toString(st.nval);
break;
//常量TT_WORD表示读到一个文字或字母
case StreamTokenizer.TT_WORD:
s = st.sval;
break;
//如果以上3种都不是,则认为是英文的标识符
default:
s = String.valueOf((char)st.ttype);
}
if(counts.containsKey(s)) {
((Counter)counts.get(s)).increment();
} else {
counts.put(s, new Counter());
}
}
} catch (IOException e) {
System.out.println("st.nextToken() unsuccessful");
}
}
Enumeration values() {
return counts.elements();
}
Enumeration keys() {
return counts.keys();
}
Counter getCounter(String s) {
return (Counter)counts.get(s);
}
Enumeration sortedKeys() {
Enumeration e = counts.keys();
StrSortVector sv = new StrSortVector();
while (e.hasMoreElements()) {
sv.addElement((String)e.nextElement());
}
return sv.elements();
}
public static void main(String[] args) {
try {
SortedWordCount wc = new SortedWordCount(args[0]);
wc.countWords();
Enumeration keys = wc.sortedKeys();
while (keys.hasMoreElements()) {
String key = (String)keys.nextElement();
System.out.println(key + ": " + wc.getCounter(key).read());
}
wc.cleanup();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述代码中我已经做了一些注释,其中StreamTokenizer
的ordinaryChar(x)
方法是将我们指定的一些字符当作是普通字符分割(在StreamTokenizer
中,存在一个默认的分隔符列表,可用一系列方法加入更多的分隔符),但又“不普通”,因为解析器不会把它当作自己创建的任何单词的一部分,StreamTokenizer
类会把它当作一个独立的标记部分来处理,另外对于连续的“字母串+数字”的形式统一处理为字母型号(即StreamTokenizer.TT_WORD
类型),对于“数字+字母串”的形式则会将前面的数字单独搞出来作为一个标记,然后后面的字母串同样会被处理为一个独立的StreamTokenizer.TT_WORD
类型的,上述的代码测试结果如下:
# 测试内容
a.2b- 48
s df8/
24bg ghf
78j.90bs-90bs
qwerwq
qwe.123
# 测试结果
-: 2
.: 3
123.0: 1
2.0: 1
24.0: 1
48.0: 1
78.0: 1
90.0: 2
a: 1
b: 1
bg: 1
bs: 2
df8: 1
ghf: 1
j: 1
qwe: 1
qwerwq: 1
s: 1
10.6.2 StringTokenizer
StringTokenizer
与StreamTokenizer
类似,它是每次返回字串内的一个记号(而StreamTokenizer
每次返回整个输入流内的一个记号),在构建StringTokenizer
时需要向构建器传递另一个参数,即想使用的分割字串,通过StringTokenizer
的源码发现确实到目前还存在StringTokenizer(String str, String delim)
方法,但是在书中案例这一点并没有得到体现,代码并复杂,主要学习该类的常用方法:
public class AnalyzeSentence {
public static void main(String[] args) {
analyze("I am happy about this");
analyze("I am not happy about this");
analyze("I am not! I am happy");
analyze("I am sad about this");
analyze("I am not sad about this");
analyze("I am not! I am sad");
analyze("Are you happy about this?");
analyze("Are you sad about this?");
analyze("It's you! I am happy");
analyze("It's you! I am sad");
}
static StringTokenizer st;
static void analyze(String s) {
prt("\nnew sentence >> " + s);
boolean sad = false;
st = new StringTokenizer(s);
while (st.hasMoreTokens()) {
String token = next();
//既不是“I”开头也不是“Are”开头为true,结束当前循环进入下次循环
if (!token.equals("I") && !token.equals("Are")) continue;
if (token.equals("I")) {
String tk2 = next();
if (tk2.equals("am")) break;
else {
String tk3 = next();
if (tk3.equals("sad")) {
sad = true;
break;
}
if (tk3.equals("not")) {
String tk4 = next();
if (tk4.equals("sad")) break;
if (tk4.equals("happy")) {
sad = true;
break;
}
}
}
}
if (token.equals("Are")) {
String tk2 = next();
if (!tk2.equals("you")) break;
String tk3 = next();
if (tk3.equals("sad")) sad = true;
break;
}
}
if(sad) prt("Sad detected");
}
static String next() {
if (st.hasMoreTokens()) {
String s = st.nextToken();
prt(s);
return s;
} else {
return "";
}
}
static void prt(String s) {
System.out.println(s);
}
}
10.7 Java1.1中的IO流
作者提出了新IO流,需要对其进行层次更多的封装,这一点是装饰者设计模式固有的毛病,同时也得知新IO推出的重要原因是国际化的需求:
老式 IO流层次结构只支持8 位字节流,不能很好地控制16 位Unicode 字符。由于Unicode 主要面向的是国际化支持(Java 内含的char 是16 位的Unicode),所以添加了Reader 和 Writer层次,以提供对所有IO 操作中的Unicode 的支持。除此之外,新库也对速度进行了优化,可比旧库更快地运行。
书中的旧库和新库中发起和接收之间的对应关系:
Java1.0库的类 | 对应Java1.1库的类 |
---|---|
InputStream | Reader ,转换器InputStreamReader |
OutputStream | Writer ,转换器OutputStreamWriter |
FileInputStream | FileReader |
FileOutputStream | FileWriter |
StringBufferInputStream | StringReader |
无对应的类 | StringWriter |
ByteArrayInputStream | CharArrayReader |
ByteArrayOutputStream | CharArrayWriter |
PipedInputStream | PieReader |
PipedOutputStream | PipedWriter |
对于数据修改行为类的对应关系如下
java1.0库中的类 | java1.1库中对应的类 |
---|---|
FilterInputStream | FilterReader |
FilterOutStream | FilterWriter (没有子类的抽象类) |
BufferedInputStream | BufferedReader (也有readLine() ) |
BufferedOutputStream | BufferedWriter |
DataInputStream | DataInputStream (除非使用readLine() ,那时需要使用一个BufferedReader ) |
PrintStream | PrintWriter |
LineNumberInputStream | LineNumberReader |
StreamTokenizer | StreamTokenize (用构建器取代Reader ) |
PushBackInputStream | PushBackReader |
10.8 压缩
主要的压缩类如下:
压缩类 | 功能 |
---|---|
CheckedInputStream | GetCheckSum() 为任何InputStream 产生校验和(用于校验目的地一组数据项的和) |
CheckedOutputStream | GetCheckSum() 为任何OutputStream 产生校验和 |
DeflaterOutputStream | 用于压缩类的基础类 |
ZipOutputStream | 一个DeflaterOutputStream ,将数据压缩成Zip文件格式 |
GZIPOutputStream | 一个DeflaterOutputStream ,将数据压缩成GZIP文件格式 |
InflaterInputStream | 用于解压类的基础类 |
ZipInputStream | 一个DeflaterInputStream ,解压用 Zip文件格式保存的数据 |
GZIPInputStream | 一个DeflaterInputStream ,解压用GZIP 文件格式保存的数据 |
对于”.gz”的压缩方式,借用作者的一句话就是:
只需将输出流封装到一个GZIPOutputStream 或者ZipOutputStream 内,并将输入流封装到GZIPInputStream 或者ZipInputStream 内即可。
文中的Demo(压缩文件的创建和读取)结合上面的话也很好理解:
public class GZIPCompress {
public static void main(String[] args) {
System.out.println(args[0]);
try {
BufferedReader in = new BufferedReader(new FileReader(args[0]));
BufferedOutputStream out = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream("test.gz")));
System.out.println("Writing file");
int c;
while((c=in.read())!=-1) {
out.write(c);
}
in.close();
out.close();
System.out.println("Reading file");
BufferedReader in2 = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
String s;
while ((s=in2.readLine())!=null) {
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于“.zip”形式的压缩,涉及到校验和、多个文件打包进一个“.zip”压缩包中(每个文件打包时必须调用putNextEntry
方法,以ZipEntry
为形参传入),可以使用setComment()
方法对压缩文件进行注释(压缩文档的说明),主要的案例如下:
public class ZipCompress {
public static void main(String[] args) {
FileOutputStream f = null;
try {
f = new FileOutputStream("test.zip");
//Checksum类来计算和校验文件的“校验和”(Checksum)。可选用两种类型的
//Checksum:Adler32(速度要快一些)和CRC32(慢一些,但更准确)。
CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(csum));
out.setComment("A Test of Java Ziping");
//对于给出的文件数组中的文件依次打包压缩
for(int i = 0; i < args.length; i++) {
System.out.println("Writing file " + args[i]);
BufferedReader in = new BufferedReader(new FileReader(args[i]));
out.putNextEntry(new ZipEntry(args[i]));
int c;
while ((c=in.read())!=-1) {
out.write(c);
}
in.close();
}
out.close();
System.out.println("Checksum1: " + csum.getChecksum().getValue());
System.out.println("Reading file");
FileInputStream fi = new FileInputStream("test.zip");
CheckedInputStream csui = new CheckedInputStream(fi, new Adler32());
ZipInputStream in2 = new ZipInputStream(new BufferedInputStream(csui));
ZipEntry ze;
System.out.println("Checksum: " + csui.getChecksum().getValue());
while ((ze = in2.getNextEntry())!=null) {
System.out.println("Reading file " + ze);
int x;
while((x=in2.read())!=-1) {
System.out.write(x);
}
System.out.println();
}
in2.close();
//使用ZipFile读取文件,更加简洁
ZipFile zf = new ZipFile("test.zip");
Enumeration e = zf.entries();
while (e.hasMoreElements()) {
ZipEntry ze2 = (ZipEntry) e.nextElement();
System.out.println("File: " + ze2);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
作者也提出JDK自带的一些命令行工具,结合案例可以大概了解到各自的作用。
10.9 对象的序列化
谈到这玩意儿可能很大一部分人都知道实现Serializable
接口,在初学Java时也确实这样,对于序列化的概念也很容易理解,将对象转换成一系列字节的过程,对应的反序列化,就是序列化的逆向过程(将字节恢复成对象的过程)。序列化只需要实现Serializable
接口即可,文中也大致总结了序列化的过程:首先创建某些OutputStream
对象,然后将其封装到ObjectOutputStream
对象内。此时,只需调用writeObject()
即可完成对象的序列化,并将其发送给OutputStream
。对应的反序列化则是创建某些InputStream
对象,然后将其封装到ObjectInputStream
对象内,再调用readObject()
强转对应的对象类型即可完成对象的序列化,书中的案例也比较容易理解,不过我觉得书中代码给的相当有意思,并不是仅仅是对象的序列化和读取,也涉及到其他的诸如链表、对象初始化等一些知识点,记录一下:
class Data implements Serializable {
private int i;
public Data(int i) {
this.i = i;
}
@Override
public String toString() {
return Integer.toString(i);
}
}
public class Worm implements Serializable {
private static int r() {
return (int) (Math.random()*10);
}
private Data[] d = {
new Data(r()), new Data(r()), new Data(r())
};
private Worm next;
private char c;
Worm(int i, char x) {
System.out.println("Worm constructor: " + i);
c = x;
if(--i > 0) {
next = new Worm(i, (char)(x+1));
}
}
Worm() {
System.out.println("Default constructor");
}
@Override
public String toString() {
String s = ": " + c + "(";
for (int i = 0; i < d.length; i++) {
s += d[i].toString();
}
s += ")";
if(next!=null) {
s += next.toString();
}
return s;
}
public static void main(String[] args) {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject("Worm storage");
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
String s = (String) in.readObject();
Worm w2 = (Worm) in.readObject();
System.out.println(s + ", w2 = " + w2);
} catch (Exception e) {
e.printStackTrace();
}
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject("Worm storage");
out.writeObject(w);
out.flush();
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
String s = (String)in.readObject();
Worm w3 = (Worm)in.readObject();
System.out.println(s + ", w3 = " + w3);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在9.1小节中寻找类的案例中,主要提醒我们在反序列化的时候需要保证强转的类型的class
文件必须对JVM是可见的。
10.9.1 序列化的控制
Externalizable
接口
前面学习了序列化的创建和回复,关于序列化的具体控制(对象的某个部分不需要序列化、某个子对象不要序列化而由自己重新创建),这时需要使用Externalizable
接口代替Serializable
接口,实际Externalizable
继承自Serializable
,它包含了两个抽象方法writeExternal(ObjectOutput out)
和readExternal(ObjectInput in)
,并且对于实现Externalizable
接口的类的反序列化而言,它会自动调用该类的默认构造器和readExternal((ObjectInput in)
方法(先调用构造器再调用readExternal(ObjectInput in)
方法)。
【注意】上述实现Externalizable
接口反序列化时<T>ObjectInputStream.readObject()
调用的构造器是默认的无参构造器!另外其实一个对象实现了Externalizable
接口后,没有任何东西可以自动序列化,只有writeExternal(ObjectOutput out)
方法中明确指出哪些东西可以进行序列化才会对这些东西进行序列化,而不像实现Serializable
之后直接全部自动序列化。
class Blip3 implements Externalizable {
int i;
String s;
public Blip3() {
System.out.println("Blip3 Constructor");
}
public Blip3(String x, int a) {
System.out.println("Blip3(String x, int a)");
s = x;
i = a;
}
@Override
public String toString() {
return s + i;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip3.writeExternal");
//这里的写入的顺序必须和readExternal中参数初始化的顺序一致
out.writeInt(i);
out.writeObject(s);
}
//先调用默认无参构造器,再调用该方法
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip3.readExternal");
//对必要的参数进行初始化,这里初始化的顺序必须和writeExternal中参数写入的顺序一致
i = in.readInt();
s = (String) in.readObject();
}
public static void main(String[] args) {
System.out.println("Constructing objects: ");
Blip3 b3 = new Blip3("A String", 47);
System.out.println(b3.toString());
try {
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip3.out"));
System.out.println("Saving object: ");
o.writeObject(b3);
o.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out"));
System.out.println("Recovering b3:");
b3 = (Blip3) in.readObject();
System.out.println(b3.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
transient
关键字
前面学习的Externalizable
接口可以完全控制序列化的行为,但是如果仅仅是一小部分的成员变量需要控制,用上述的方法反而会很麻烦,因为需要控制每一部分需要序列化的成员,存在大量内容需要序列化,这时,可以实现Serializable
接口,使用transient
关键字(临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我会自己处理的”,所以它不会自动保存到磁盘,自动序列化机制也不会作该字段的恢复尝试。书中的案例如下:
class Logon implements Serializable {
private Date deta = new Date();
private String username;
private transient String password;
public Logon(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public String toString() {
//当前版本的不会报空指针异常,这里的判断为空可要可不要
String pwd = (password==null) ? "(n/a)" : password;
return "Logon info: " +
"deta=" + deta +
", username='" + username + '\'' +
", password='" + pwd + '\'';
}
public static void main(String[] args) {
Logon a = new Logon("Hulk", "mypwd");
System.out.println("logon a = " + a);
try {
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Logon.out"));
o.writeObject(a);
o.close();
int seconds = 5;
long t = System.currentTimeMillis() + seconds * 1000;
while (System.currentTimeMillis()<t) {
}
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Logon.out"));
System.out.println("Recovering object at " + new Date());
a = (Logon) in.readObject();
System.out.println("logon a = " + a);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述案例在恢复时不会对加了transient
关键字的password
进行序列化或者反序列化的操作,所以在恢复时自动为null
(当前的jdk版本已经修复了toString
的空指针的异常,所以password.nn
的判断可以省去)。
此外作者还提供了实现Serializable
接口达到像实现Externalizable
接口的目的的方法。
public class SerialCtl implements Serializable {
String a;
transient String b;
public SerialCtl(String a, String b) {
this.a = "Not Transient: " + a;
this.b = "Transient: " + b;
}
@Override
public String toString() {
return a + "\n" + b;
}
//对象序列化时会调用该方法,如果提供了该方法则优先使用它而不是用默认的序列化机制
private void writeObject(ObjectOutputStream stream) throws IOException {
//非临时变量使用该方法保存,这里就是指a
stream.defaultWriteObject();
//临时变量会调用该方法保存,这里就是指b
stream.writeObject(b);
}
//对象重新装配时会调用该方法,如果提供了该方法则优先使用它而不是用默认的序列化机制
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
//非临时变量使用该方法回复,这里就是指a
stream.defaultReadObject();
//临时变量需要手动恢复,这里就是指b
b = (String) stream.readObject();
}
public static void main(String[] args) {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
try {
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
SerialCtl sc2 = (SerialCtl) in.readObject();
System.out.println("After: \n" + sc2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
10.9.2 持久性
对于同一流中的同一对象进行多次序列化(比如N次),恢复时N次(或小于N次),那么恢复的对象具备相同的内存地址(这里的相同是指恢复后的几个对象具备相同内存地址,而不是指序列化之前和恢复后的内存地址相同);对于不同流中的对同一对象进行序列化,那么不同流中恢复的对象的内存地址必定不同。
如果父类实现了序列化接口,那它的所有子类也都会自动可序列化,对于静态变量不可自动进行自动序列化的问题(即使该类已经是可自动序列化),需要对其进行手动处理,书中案例如下:
abstract class Shape implements Serializable {
public static final int RED = 1, BLUE = 2, GREEN = 3;
private int xPos, yPos, dimension;
private static Random r = new Random();
private static int counter = 0;
abstract public void setColor(int newColor);
abstract public int getColor();
public Shape(int xVal, int yVal, int dim) {
xPos = xVal;
yPos = yVal;
dimension = dim;
}
@Override
public String toString() {
return getClass().toString() + " color[" + getColor() +
"] xPos[" + xPos +
"] yPos[" + yPos +
"] dimension[" + dimension +
"]\n";
}
public static Shape randomFactory() {
int xVal = r.nextInt() % 100;
int yVal = r.nextInt() % 100;
int dim = r.nextInt() % 100;
switch (counter++ % 3) {
default:
case 0:
return new Circle(xVal, yVal, dim);
case 1:
return new Square(xVal, yVal, dim);
case 2:
return new Line(xVal, yVal, dim);
}
}
}
class Circle extends Shape {
private static int color = RED;
public Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
@Override
public void setColor(int newColor) {
color = newColor;
}
@Override
public int getColor() {
return color;
}
}
class Square extends Shape {
private static int color;
public Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
color = RED;
}
@Override
public void setColor(int newColor) {
color = newColor;
}
@Override
public int getColor() {
return color;
}
}
class Line extends Shape {
private static int color = RED;
public static void serializeStaticState(ObjectOutputStream os) throws IOException {
os.writeInt(color);
}
public static void deserializeStaticState(ObjectInputStream os) throws IOException, ClassNotFoundException {
color = os.readInt();
}
public Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
@Override
public void setColor(int newColor) {
color = newColor;
}
@Override
public int getColor() {
return color;
}
}
public class CADState {
public static void main(String[] args) throws Exception {
Vector shapeType, shapes;
if (args.length == 0) {
shapeType = new Vector();
shapes = new Vector();
shapeType.addElement(Circle.class);
shapeType.addElement(Square.class);
shapeType.addElement(Line.class);
for (int i = 0; i < 10; i++) {
shapes.addElement(Shape.randomFactory());
}
for (int i = 0; i < 10; i++) {
((Shape) shapes.elementAt(i)).setColor(Shape.GREEN);
}
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("CADState.out"));
out.writeObject(shapeType);
Line.serializeStaticState(out);
out.writeObject(shapes);
} else {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));
shapeType = (Vector) in.readObject();
Line.deserializeStaticState(in);
shapes = (Vector) in.readObject();
}
System.out.println(shapes);
}
}
在序列化的输出流中将10个图形的颜色统一设置为GREEN(3),但是在输入流中恢复后,除了Line的颜色是GREEN(3),Circle都变成了RED(1),而Square都变成了0,为了避免这一问题,可以在每个图像类中都增加对静态变量color的处理行为:
public static void serializeStaticState(ObjectOutputStream os) throws IOException {
os.writeInt(color);
}
public static void deserializeStaticState(ObjectInputStream os) throws IOException, ClassNotFoundException {
color = os.readInt();
}
在序列化时,每类图形额外单独调用各自的静态变量序列化方法:
Circle.serializeStaticState(out);
Square.serializeStaticState(out);
Line.serializeStaticState(out);
out.writeObject(shapes);
对应恢复的时候,也要额外调用静态变量的反序列化方法:
Circle.deserializeStaticState(in);
Square.deserializeStaticState(in);
Line.deserializeStaticState(in);
shapes = (Vector) in.readObject();