第18章 Java I/O系统
本章主要介绍Java标准类库中各种各样的类以及它们的用法。
18.1 File类
File:既能代表一个特定的文件,又能代表一个目录下的一组文件。
18.1.1 目录列表器
我们有两种方式可以查看一个目录列表:
- File.list():获取目录下的所有文件名,以字符数组的形式返回。
- File.list(FilenameFilter):获取目录下经过过滤的文件名,以字符数组的形式返回。
下面的例子演示了这两种方式:
public class DirList {
public static void main(String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new DirFilter(args[0]));
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for (String dirItem : list)
System.out.println(dirItem);
}
}
class DirFilter implements FilenameFilter{
private Pattern pattern;
public DirFilter(String regex) {
pattern = Pattern.compile(regex);
}
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
}
list(FilenameFilter)方法使用了策略设计模式:list()方法实现了基本的功能,FilenameFilter则提供了策略,以便完善list()在提供服务时所需的算法。策略的目的就是提供了代码行为的灵活性,这得我们可以根据自己的需求来随意改变这个策略。
下面,我们通过匿名内部类的形式,简化了上述代码:
public class DirList2 {
public static void main(final String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new FilenameFilter() {
private Pattern pattern = Pattern.compile(args[0]);
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for (String dirItem : list)
System.out.println(dirItem);
}
}
该例展示了匿名内部类如何通过创建特定的、一次性的类来解决问题。
18.1.2 目录实用工具
下面的实用工具具有两个功能:
- local():返回由指定目录下的所有文件和文件夹构成的File数组。
- walk():返回指定目录下整个目录树中的所有文件夹集合和文件集合。
public final class Directory {
public static File[] local(File dir, final String regex) {
return dir.listFiles(new FilenameFilter() {
private Pattern pattern = Pattern.compile(regex);
public boolean accept(File dir, String name) {
return pattern.matcher(new File(name).getName()).matches();
}
});
}
public static File[] local(String path, final String regex) {
return local(new File(path), regex);
}
public static class TreeInfo implements Iterable<File> {
public List<File> files = new ArrayList<File>();
public List<File> dirs = new ArrayList<File>();
public Iterator<File> iterator() {
return files.iterator();
}
void addAll(TreeInfo other) {
files.addAll(other.files);
dirs.addAll(other.dirs);
}
public String toString() {
return "dirs: " + PPrint.pformat(dirs) +
"\n\nfiles: " + PPrint.pformat(files);
}
}
public static TreeInfo walk(String start, String regex) {
return recurseDirs(new File(start), regex);
}
public static TreeInfo walk(File start) {
return recurseDirs(start, ".*");
}
public static TreeInfo walk(String start) {
return recurseDirs(new File(start), ".*");
}
static TreeInfo recurseDirs(File startDir,String regex) {
TreeInfo result = new TreeInfo();
for (File item : startDir.listFiles()) {
if(item.isDirectory()) {
result.dirs.add(item);
result.addAll(recurseDirs(item, regex));
} else if(item.getName().matches(regex))
result.files.add(item);
}
return result;
}
public static void main(String[] args) {
if(args.length == 0)
System.out.println(walk("."));
else
for (String arg : args)
System.out.println(arg);
}
}
上述工具中所使用的PPrint类是一个可以添加新行并缩排所有元素的工具:
public class PPrint {
public static String pformat(Collection<?> c) {
if(c.size() == 0) return "[]";
StringBuilder result = new StringBuilder("[");
for (Object elem : c) {
if(c.size() != 1)
result.append("\n ");
result.append(elem);
}
if(c.size() != 1)
result.append("\n");
result.append("]");
return result.toString();
}
public static void pprint(Collection<?> c) {
System.out.println(pformat(c));
}
public static void pprint(Object[] c) {
System.out.println(pformat(Arrays.asList(c)));
}
}
下面,我们再次使用到了策略设计模式,它可以搜索整个目录树中,以指定后缀截尾的文件,并通过自定义的策略处理该文件:
public class ProcessFiles {
public interface Strategy {
void process(File file);
}
private Strategy strategy;
private String ext;
public ProcessFiles(Strategy strategy, String ext) {
this.strategy = strategy;
this.ext = ext;
}
public void start(String[] args) {
try {
if(args.length == 0)
processDirectoryTree(new File("."));
else
for (String arg : args) {
File fileArg = new File(arg);
if(fileArg.isDirectory())
processDirectoryTree(fileArg);
else {
if(!arg.endsWith("." + ext))
arg += "." + ext;
strategy.process(new File(arg).getCanonicalFile());
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void processDirectoryTree(File root) throws Exception {
for (File file : Directory.walk(root.getAbsolutePath(), ".*\\." + ext))
strategy.process(file);
}
public static void main(String[] args) {
new ProcessFiles(new Strategy() {
public void process(File file) {
System.out.println(file);
}
}, "java").start(args);;
}
}
18.1.3 目录的检查及创建
File类不仅仅只代表存在的文件或目录,我们也可以通过File对象来创建新的文件夹或尚不存在的整个目录路径。我们还可以查看文件的特性,检查某个File对象代表的是一个文件还是一个目录,删除文件或文件夹等。下面的示例展示了File类的一些常用方法:
public class MakeDirectories {
private static void fileData(File f) {
System.out.println(
"Absolute path: " + f.getAbsolutePath() +
"\n Can read: " + f.canRead() +
"\n Can Write: " + f.canWrite() +
"\n getName: " + f.getName() +
"\n getParent: " + f.getParent() +
"\n getPath: " + f.getPath() +
"\n length: " + f.length() +
"\n lastModified: " + f.lastModified());
if(f.isFile())
System.out.println("It's a file");
else if(f.isDirectory())
System.out.println("It's a directory");
}
public static void main(String[] args) {
if(args.length < 1) System.exit(1);
if(args[0].equals("-r")) {
if(args.length == 3) {
File old = new File(args[1]);
File rname = new File(args[2]);
old.renameTo(rname);
fileData(old);
fileData(rname);
return;
}
}
int count = 0;
boolean del = false;
if(args[0].equals("-d")) {
count++;
del = true;
}
count--;
while(++count < args.length) {
File f = new File(args[count]);
if(f.exists()) {
System.out.println(f + " exists");
if(del) {
System.out.println("deleting..." + f);
f.delete();
}
}else {
if(!del) {
f.mkdirs();
System.out.println("created " + f);
}
}
fileData(f);
}
}
}
18.2 输入和输出
编程语言的I/O类库中常使用流的概念:它代表任何有能力产处数据的数据源对象或有能力接收数据的接收端对象。
Java类库中的I/O分成输入和输出两部分:
- 输入流:字节输入流InputStream和字符输入流Reader。
- 输出流:字节输出流OutputStream和字符输出流Write。
输入和输出都是相对于CPU而言的:
- 输入流:CPU读入流信息。
- 输出流:CPU写出流信息。
在第15章中我们引入了装饰器模式,它和代理模式有所相似:代理直接对混入类型进行实例化,会产生过多的类的实例,而装饰器会在构造函数处要求传入混入类型的实例。
Java中的流类库让人迷惑的主要原因在于:创建单一的结果流,却需要创建多个对象。其原因是使用了装饰器设计模式,通过叠合多个对象来提供所期望的功能。
18.2.1 InputStream类型
InputStream用于表示那些从不同数据源产生输入的类,这些数据源包括:
- 字节数组
- String对象
- 文件
- 管道
- 一个由其他种类的流组成的序列
- 其他数据源:Internet连接等
每一种数据源都有相应的InputStream子类:
- ByteArrayInputStream:允许将内存的缓冲区当作InputStream使用。
- StringBufferInputStream:将String转换成InputStream。
- FileInputStream:用于从文件中读取信息。
- PipedInputStream:产生用于写入相关PipedOutputStream的数据,实现管道化概念。
- SequenceInputStream:将两个或多个InputStream对象转换成单一的InputStream。
- FilterInputStream:作为装饰器的基类。其中,装饰器为其他的InputStream类提供有用的功能。
18.2.2 OutputStream类型
OutputStream类决定了输出所要去往的目标:字节数组、文件或管道。
- ByteArrayOutputStream:在内存中创建缓冲区,所有送往流的数据都要放置在此缓冲区。
- FileOutputStream:用于将信息写至文件。
- PipedOutputStream:任何写入其中的信息都会自动作为相关PipedInputStream的输出,实现管道化概念。
- FilterOutputStream:作为装饰器基类。其中,装饰器为其他OutputStream提供有用功能。
18.3 装饰器基类
在Java I/O类库中使用装饰器模式的原因是:I/O类库需要多种不同功能的组合。但这也造成了Java I/O类库操作上的不便:我们必须创建核心I/O类型加上所有的装饰器,才能得到我们所期望的单个I/O对象。
FilterInputStream和FilterOutputStream是用来提供装饰器基类以控制特定输入流(InputStream)和输出流(OutputStream)的两个类。它们分别继承自InputStream和OutputStream,并且是装饰器的必要条件,以便能为所有正在被修饰的对象提供通用接口。
18.3.1 通过FilterInputStream从InputStream读取数据
FilterInputStream具有以下子类型:
- DataInputStream:与DataOutputStream搭配使用,我们可以按照可移植的方式从流读取基本数据类型(int,char,long等)。
- BufferedInputStream:使用它可以方式每次读取是都进行实际写操作。
- LineNumberInputStream:跟踪输入流中的行号,可调用getLineNumber()和setLineNumber(int)。
- PushbackInputStream:能弹出一个字节的缓冲区,可以将读到的最后一个字符回退。
18.3.2 通过FilterOutputStream向OutputStream写入
FilterOutputStream具有以下子类型:
- DataOutputStream:与DataInputStream搭配使用,可以按照可移植方式向流中写入基本类型数据(int,char,long等)。
- PrintStream:用于产生格式化输出。其中DataOutputStream处理数据存储,PrintStream处理显示。
- BufferedOutputStream:使用它可以避免每次发送数据时都要进行实际的写操作。可以调用flsh()清空缓冲区。
18.4 Reader和Writer
Java 1.1对基本的I/O流类库进行了重大的修改。增加了Reader和Writer以提供兼容Unicode与面向字符的I/O功能。并且,Java 1.1向InputStream和OutputStream继承层次结构中添加了一些新类。
设计Reader和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。
有时,我们必须将来自于字节层次结构的类和字符层次结构的类结合起来使用,就需要用到适配器类:InputStreamReader可以将InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer。
18.4.1 字节流和字符流对应关系
几乎所有原始的Java I/O流类都有相应的Reader和Writer类来提供Unicode操作:
- InputStream:Reader(适配器:InputStreamReader)
- OutputStream:Writer(适配器:OutputStreamWriter)
- FileInputStream:FileReader
- FileOutputStream:FileWriter
- StringBufferInputStream(已弃用):StringReader
- 无相应的类:StringWriter
- ByteArrayInputStream:CharArrayReader
- ByteArrayOutputStream:CharArrayWriter
- PipedInputStream:PipedReader
- PipedOutputStream:PipedWriter
18.4.2 字节流装饰器对应字符流装饰器
对于InputStream和OutputStream字节流层次来说,使用了FilterInputStream和FilterOutputStream的装饰器子类来修改流以满足特殊需求。Reader和Writer的字符流继承层次结构也沿用了相同的思想,但并不完全相同。 下面是它们的对应关系:
- FilterInputStream:FilterReader
- FilterOutputStream:FilterWriter
- BufferedInputStream:BufferedReader
- BufferedOutputStream:BufferedWriter
- DataInputStream:BufferedReader
- PrintStream:PrintWriter
- LineNumberInputStream(已弃用):LineNumberReader
- StreamTokenizer:StreamTokenizer(使用接收Reader的构造器)
- PushbackInputStream:PushbackReader
需要注意的是:当我们使用readLine()时,应该使用BufferedReader,除此之外,DataInputStream将是I/O类库的首选。
为了更容易地将字节流转换为字符流,PrintWriter的构造器既能接收Writer对象,又能接收OutputStream对象。并且有一种PrintWriter构造器有一个自动执行清空的选项,即在每个Println()执行之后,便会自动清空。
18.5 自我独立的类:RandomAccessFile
RandomAccessFile适用于由大小已知的记录组成的文件,它除了实现了DataInput和DataOutput接口外,有效地与I/O继承层次结构的其他部分实现了分离,是一个完全独立的类,其所有方法都是从头编写,大多数是本地方法。它和其他I/O类型本质不同的是:可以在一个文件内向前和向后移动。
RandomAccessFile主要具有以下方法:
- getFilePointer():查找当前所处的文件位置
- seek():在文件内移至新的位置
- length():判断文件的最大尺寸
- 其构造器的第二个参数用来指示文件的属性:r随机读、rw既读又写。不支持只写。
在JDK 1.4中,RandomAccessFile的大多数功能由nio存储映射文件所取代。
18.6 I/O流的典型使用方式
尽管可以通过不同的方式组合I/O流类,但我们可能也就只用到其中的几种组合。下面的例子可以作为典型的I/O用法的基本参考。
18.6.1 缓冲输入文件
如果想要打开一个文件用于字符输入,则可以使用以String或File对象作为文件名的FileInputStream,为了提高速度,可以对文件进行缓冲,使用BufferedReader:
public class BufferedInputFile {
public static String read(String filename) throws IOException {
BufferedReader in = new BufferedReader(new FileReader(filename));
String s;
StringBuilder sb = new StringBuilder();
while((s = in.readLine()) != null)
sb.append(s + "\n");
in.close();
return sb.toString();
}
public static void main(String[] args) throws IOException {
System.out.print(read("BufferedInputFile.java"));
}
}
18.6.2 读取字符串
在下面的示例,从BufferedInputFile.read()读入的String结果被用来创建一个StringReader:
public class MemoryInput {
public static void main(String[] args) throws IOException {
StringReader in = new StringReader(BufferedInputFile.read("/Users/weiqing.jiao/Documents/ThinkInJavaWorkspace/ThinkInJava18/src/com/jiao/thinkInJava/example/MemoryInput.java"));
int c;
while((c = in.read()) != -1)
System.out.println((char)c);
}
}
18.6.3 格式化的内存输入
要读取格式化数据,可以使用DataInputStream,它接收任意的InputStream作为构造器参数。这里,我们通过使用ByteArrayInputStream作为其构造参数:
public class FormattedMemoryInput {
public static void main(String[] args) throws IOException {
try {
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(BufferedInputFile.read("FormattedMemoryInput.java").getBytes()));
while((in.available()) != 0)
System.out.print((char)in.readByte());
} catch (EOFException e) {
System.out.println("End of stream");
}
}
}
readByte()方法可以一次读取一个字节,并且我们可以通过available()方法查看还有多少可供存取的字符。
18.6.4 基本的文件输出
FileWriter对象可以将数据输出到文件中。通常情况下,我们会使用BufferedWriter将其包装器来用以缓冲输出,以提高性能。这里,我们需要格式化机制,因此将其装饰成PrintWriter:
public class BasicFileOutput {
static String file = "BasicFileOutput.out";
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new StringReader(
BufferedInputFile.read("BasicFileOutput.java")));
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
int lineCount = 1;
String s;
while((s = in.readLine()) != null)
out.println(lineCount++ + ": " + s);
out.close();
in.close();
System.out.println(BufferedInputFile.read(file));
}
}
Java SE5在PrintWriter中添加了一个辅助构造器,使得我们可以直接使用文件名作为其构造参数,而不需要执行所有的装饰工作了:
public class BasicFileOutput {
static String file = "BasicFileOutput.out";
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new StringReader(
BufferedInputFile.read("BasicFileOutput.java")));
PrintWriter out = new PrintWriter(file);
int lineCount = 1;
String s;
while((s = in.readLine()) != null)
out.println(lineCount++ + ": " + s);
out.close();
in.close();
System.out.println(BufferedInputFile.read(file));
}
}
重要的是,这种方式仍旧使用了缓存,而不必我们自己实现。遗憾的是,其他常见的写入操作都没有快捷方式,因此典型的I/O仍旧包含大量的冗余文本。不过,本章稍后定义的TextFile工具会简化这些常见的装饰操作。
18.6.5 存储和恢复数据
在Java中,如果使用DataOutputStream写出数据,那么使用DataInputStream就能准确地读入所写的数据:
public class StoringAndRecoveringData {
public static void main(String[] args) throws IOException {
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("Data.txt")));
out.writeDouble(3.1415926);
out.writeUTF("That was pi");
out.writeDouble(1.41413);
out.writeUTF("Square root of 2");
out.close();
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("Data.txt")));
System.out.println(in.readDouble());
System.out.println(in.readUTF());
System.out.println(in.readDouble());
System.out.println(in.readUTF());
}
}
当我们使用DataOutputStream时,写字符串并且让DataInputStream正确读取的可靠做法就是使用UTF-8编码,该示例中使用writeUTF()和readUTF()实现。
writeDouble能够将double类型的数字存储到流中,并使用相应的readDouble()恢复它,前提是我们必须知道流中数据项所在的确切位置。对于其他的数据类型,也有类似的方法用于读写。
18.6.6 读写随机访问文件
RandomAccessFile实现了DataInput、DataOutput接口,因此使用它就相当于组合使用了DataInputStream和DataOutputStream。
在使用RandomAccessFile时,必须清楚文件排版才能正确操作。其拥有读取基本数据和UTF-8字符串的各种具体方法:
public class UsingRandomAccessFile {
static String file = "rtest.dat";
static void display() throws IOException {
RandomAccessFile rf = new RandomAccessFile(file, "r");
for (int i = 0; i < 7; i++)
System.out.println("Value " + i + ": " + rf.readDouble());
System.out.println(rf.readUTF());
rf.close();
}
public static void main(String[] args) throws IOException {
RandomAccessFile rf = new RandomAccessFile(file, "rw");
for (int i = 0; i < 7; i++)
rf.writeDouble(i * 1.414);
rf.writeUTF("The end of the file");
rf.close();
display();
rf = new RandomAccessFile(file, "rw");
rf.seek(5*8);
rf.writeDouble(47.00001);
rf.close();
display();
}
}
18.6.7 管道流
PipedInputStream、PipedOutputStream、PipedReader及PipedWriter在本章只是简单地提到。由于管道流用于任务之间的通信,它们的价值只有在多线程时才会显现,我们将在第21章中对它们进行详细介绍。
18.7 文件读写的实用工具
常见的文件操作就是:读取文件到内存,修改,最后再写出。Java I/O类库的问题之一就是:它需要编写相当多的代码去执行这些常用操作。更糟糕的是,装饰器会使打开文件变成复杂的事情,并且一些常见操作需要反复地执行。
下面的TextFile类可以用来简化对文件的读写操作:
public class TextFile extends ArrayList<String> {
public static String read(String fileName) {
StringBuilder sb = new StringBuilder();
try {
BufferedReader in = new BufferedReader(new FileReader(new File(fileName).getAbsoluteFile()));
try {
String s ;
while((s=in.readLine()) != null) {
sb.append(s);
sb.append("\n");
}
} finally {
in.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
public static void write(String filename,String text) {
try{
PrintWriter out = new PrintWriter(new File(filename).getAbsoluteFile());
try {
out.print(text);
} finally {
out.close();
}
} catch (IOException e){
throw new RuntimeException(e);
}
}
public TextFile(String fileName,String splitter){
super(Arrays.asList(read(fileName).split(splitter)));
if(get(0).equals("")) remove(0);
}
public TextFile(String fileName){
this(fileName, "\n");
}
public void write(String filename) {
try {
PrintWriter out = new PrintWriter(new File(filename).getAbsoluteFile());
try {
for (String item : this )
out.println(item);
} finally {
out.close();
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
String file = read("/Users/weiqing.jiao/Documents/workspace/ThinkInJava13/src/com/jiao/thinkInJava/example/TextFile.java");
write("test.txt", file);
TextFile text = new TextFile("test.txt");
text.write("test2.txt");
TreeSet<String> words = new TreeSet<String>(new TextFile("/Users/weiqing.jiao/Documents/workspace/ThinkInJava13/src/com/jiao/thinkInJava/example/TextFile.java","\\W+"));
System.out.println(words.headSet("a"));
}
}
18.7.1 读取二进制文件
下面的工具用于简化读取二进制文件的过程:
public class BinaryFile {
public static byte[] read(File file) throws IOException {
BufferedInputStream bf = new BufferedInputStream(new FileInputStream(file));
try {
byte[] data = new byte[bf.available()];
bf.read(data);
return data;
} finally {
bf.close();
}
}
public static byte[] read(String file) throws IOException {
return read(new File(file).getAbsoluteFile());
}
}
18.8 标准I/O
标准I/O这个术语参考于Unix中程序所使用的单一信息流的概念。程序的所有输入都可以来自于标准输入,它的所有输出也都可以发送到标准输出,以及所有的错误信息都可以发送到标准错误。
18.8.1 从标准输入中读取
按照标准I/O模型,Java提供了:
- System.in:没有被包装的未经加工的InputStream
- Ststem.out:PrintStream
- System.err:PrintStream
我们在读取System.in之前必须对其进行包装,通常情况下,我们将其包装成BufferedReader:
public class Echo {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s;
while((s = in.readLine()) != null && s.length() != 0)
System.out.println(s);
}
}
18.8.2 将System.out转换成PrintWriter
下面的示例展示了如何将System.out转换成PrintWriter:
public class ChangeSystemOut {
public static void main(String[] args) {
PrintWriter out = new PrintWriter(System.out, true);
out.println("Hello, world!");
}
}
PrintWriter的构造器中接收两个参数:
- OutputStream:字节输出流。
- boolean autoFlush:是否开启自动清空功能。
第二个参数设为true,则可以自动清空缓冲区,否则将无法显示输出。
18.8.3 标准I/O重定向
Java的System类提供了一些简单的静态方法,以允许我们对标准输入、输出和错误I/O流进行重定向:
- setIn(InputStream)
- setOut(InputStream)
- setErr(InputStream)
如果控制台的输出过量,以至于滚动太快而无法阅读时,重定向则可以帮助我们将信息持久化在文件中:
public class Redirecting {
public static void main(String[] args) throws IOException {
PrintStream console = System.out;
BufferedInputStream in = new BufferedInputStream(new FileInputStream("/Users/weiqing.jiao/Documents/ThinkInJavaWorkspace/ThinkInJava18/src/com/jiao/thinkInJava/example/Redirecting.java"));
PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out")));
System.setIn(in);
System.setOut(out);
System.setErr(out);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String s;
while((s = reader.readLine()) != null)
System.out.println(s);
out.close();
System.setOut(console);
}
}
Java I/O重定向操作的是字节流,而不是字符流。因此我们需要使用的是InputSream和OutputStream,而不是Reader和Writer。
18.9 进程控制
我们有时会需要在Java内部执行其他操作系统的程序,并且要控制这些程序的输入和输出。Java类库提供了执行这些操作的类。
一项常见的任务就是运行程序,并将产生的输出发送到控制台。我们将这个操作产生的错误分为两类:
- Java自身程序所抛出的异常:我们将其包装为RuntimeException抛出。
- 进程自身的执行过程中所产生的异常:我们通过单独的异常来报告。
下面是该单独的异常:
public class OSExecuteException extends RuntimeException {
public OSExecuteException(String why) { super(why); }
}
下面的示例将演示如何通过Process类进行进程的的处理:
public class OSExcute {
public static void command(String command) {
boolean err = false;
try {
Process process = new ProcessBuilder(command.split(" ")).start();
BufferedReader results = new BufferedReader(new InputStreamReader(process.getInputStream()));
String s;
while((s = results.readLine()) != null)
System.out.println(s);
BufferedReader errors = new BufferedReader(new InputStreamReader(process.getErrorStream()));
while((s = errors.readLine()) != null){
System.out.println(s);
err = true;
}
} catch (Exception e) {
if(!command.startsWith("CMD /C"))
command("CMD /C " + command);
else
throw new RuntimeException(e);
}
if(err)
throw new OSExecuteException("Errors executing " + command);
}
}
以下是Process的几个常用API:
- new ProcessBuilder(String...).start():启动进程
- Process.getInputStream():获取标准输入流
- Process.getErrorStream():获取错误输入流
下面是展示如何使用OSExecute的示例:
public class OSExcuteDemo {
public static void main(String[] args) {
OSExcute.command("javap OSExcuteDemo.java");
}
}
18.10 新I/O
JDK 1.4的java.nio.*包中引入了新的JavaI/O类库,其目的在于提高速度。
速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。我们并没有直接和通道交互,只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获取数据,要么向缓冲器发送数据。
唯一直接与通道交互的缓冲器是ByteBuffer:存储未加工字节的缓冲器。
旧I/O类库中有三个类被修改了,用以产生FileChannel:
- FileInputStream
- FileOutputStream
- RandomAccessFile
下面的示例演示了使用上述三种类型的流产生可读、可写及可读可写的通道:
public class GetChannel {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
FileChannel fc = new FileOutputStream("data.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text ".getBytes()));
fc.close();
fc = new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size());
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
fc = new FileInputStream("data.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
while(buff.hasRemaining())
System.out.print((char)buff.get());
}
}
对于上述三种类型的流,通过getChannel()方法均可产生一个FileChannel。
通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。
我们通常使用两种方式向ByteBuffer中存放数据:
- ByteBuffer.put():填充一个或多个字节,或基本数据类型的值。
- ByteBuffer.wrap():静态方法,将已存在的字节数组包装到ByteBuffer中。
对于只读访问时:首先,我们需要显式地使用静态allocate()方法来分配ByteBuffer,然后,我们可以调用read()来告知FileChannel向ByteBuffer存储字节,此时,必须调用缓冲器上的flip()方法,让它做好让别人读取字节的准备。如果我们打算使用缓冲器再次进行read()操作,则必须通过clear()清空缓冲器。
下例是简单文件的复制程序:
public class ChannelCopy {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
if(args.length != 2) {
System.out.println("arguments: sourcefile destfile");
System.exit(1);
}
FileChannel in = new FileInputStream(args[0]).getChannel();
FileChannel out = new FileOutputStream(args[1]).getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
while((in.read(buff) != -1)) {
buff.flip();
out.write(buff);
buff.clear();
}
}
}
上述程序并不是处理此类操作的理想方式。transferTo()和transferFrom()允许我们将一个通道与另一个通道直接相连:
public class TransferTo {
public static void main(String[] args) throws IOException {
if(args.length != 2) {
System.out.println("arguments: sourcefile destfile");
System.exit(1);
}
FileChannel in = new FileInputStream(args[0]).getChannel();
FileChannel out = new FileOutputStream(args[1]).getChannel();
in.transferTo(0, in.size(), out);
}
}
18.10.1 转换数据
在GetChannel.java的程序中,我们通过while遍历缓冲器,每次获取一个字节的数据,并将其强制转换为char类型。这种方式显然很落后,下例演示了如何通过CharBuffer打印信息:
public class BufferToText {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
FileChannel fc = new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text".getBytes()));
fc.close();
fc = new FileInputStream("data2.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer()); //Doesn't work
buff.rewind();
String encoding = System.getProperty("file.encoding");
System.out.println("Decoded using " + encoding + ": " + Charset.forName(encoding).decode(buff));
fc = new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text".getBytes("UTF-16BE")));
fc.close();
fc = new FileInputStream("data2.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
fc = new FileOutputStream("data2.txt").getChannel();
buff = ByteBuffer.allocate(24);
buff.asCharBuffer().put("Some text");
fc.write(buff);
fc.close();
fc = new FileInputStream("data2.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
}
}
当我们直接打印CharBuffer时,会发生乱码。其原因是:缓冲器容纳的是普通字节,为了将它们转换成字符串,我们要么在输入它们的时候对其进行编码,要么在将其从缓冲器输出时对他们进行解码。
在BufferToText.java中使用了以下方法:
- ByteBuffer.asCharBuffer():将ByteBuffer转换为CharBuffer。
- ByteBuffer.rewind():返回到数据的开始部分。
- CharBuffer.toString():返回包含缓冲器中所有字符的字符串。
- System.getProperty("file.encoding"):返回代表默认字符集的名称的字符串。
- Charset.forName(String charsetName):产生Charset对象,从而对字符串解码。
- Charset.decode(ByteBuffer):对缓冲器中数据进行解码。
- String.getBytes(String charsetName):获取指定字符编码的字节数组。
- ByteBuffer.asCharBuffer().put(String):通过CharBuffer向ByteBuffer存入数据。
java.nio.charset.Charset类可以实现这些功能,该类提供了把数据编码成多种不同类型的字符集的工具:
public class AvailableCharSets {
public static void main(String[] args) {
SortedMap<String, Charset> charSets = Charset.availableCharsets();
Iterator<String> it = charSets.keySet().iterator();
while(it.hasNext()) {
String csName = it.next();
System.out.print(csName);
Iterator aliases = charSets.get(csName).aliases().iterator();
if(aliases.hasNext())
System.out.print(": ");
while(aliases.hasNext()) {
System.out.print(aliases.next());
if(aliases.hasNext())
System.out.print(", ");
}
System.out.println();
}
}
}
18.10.2 获取基本类型
尽管ByteBuffer只能保存字节类型的数据,但它具有可以从所容纳的的字节中产生出各种不同基本类型值的方法:
public class GetData {
private static final int BSIZE = 1024;
public static void main(String[] args) {
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
int i = 0;
while(i++ < buff.limit())
if(buff.get() != 0) System.out.println("nonzero");
System.out.println("i = " + i);
buff.rewind();
buff.asCharBuffer().put("Howdy!");
char c;
while((c = buff.getChar()) != 0)
System.out.print(c + " ");
System.out.println();
buff.rewind();
buff.asShortBuffer().put((short)471142);
System.out.println(buff.getShort());
buff.rewind();
buff.asIntBuffer().put(9991111);
System.out.println(buff.getInt());
buff.rewind();
}
}
这里,在分配了一个ByteBuffer后,通过检测证明了缓冲器分配后会自动将其内容置零。ByteBuffer.limit()方法返回该缓冲器容量的上界。
可以看到,向ByteBuffer插入基本类型数据的最简单方法是:利用ByteBuffer.asCharBuffer()、ByteBuffer.asShortBuffer等获得该缓冲器上的视图,然后使用该视图的put()方法。
需要注意的是:在使用ShortBuffer.put()方法时,需要进行类型转换。而其他所有的视图缓冲器则不需要。
18.10.3 视图缓冲器
视图缓冲器可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer,对视图的任何修改都会映射称为对ByteBuffer中数据的修改。视图还允许我们一次一个或成批地读取所存入的基本类型值。
下面的示例通过IntBuffer操纵ByteBuffer中的int型数据:
public class IntBufferDemo {
private static final int BSIZE = 1024;
public static void main(String[] args) {
IntBuffer ib = ByteBuffer.allocate(BSIZE).asIntBuffer();
ib.put(new int[]{ 11, 22, 33, 44, 55, 66, 77, 88, 99 });
System.out.println(ib.get(3));
ib.put(3, 444);
ib.flip();
while(ib.hasRemaining())
System.out.println(ib.get());
}
}
在下面的例子中,通过在同一个ByteBuffer上建立不同的视图缓冲器,将一个字节序列翻译成了各种基本类型的数据:
public class ViewBuffers {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.wrap(new byte[] { 0, 0, 0, 0, 0, 0, 0, 'a' });
bb.rewind();
System.out.print("ByteBuffer: ");
while(bb.hasRemaining())
System.out.print(bb.position() + ":" + bb.get() +", ");
System.out.println();
CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer();
System.out.print("CharBuffer: ");
while(cb.hasRemaining())
System.out.print(cb.position() + ":" + cb.get() +", ");
System.out.println();
FloatBuffer fb = ((ByteBuffer)bb.rewind()).asFloatBuffer();
System.out.print("FloatBuffer: ");
while(fb.hasRemaining())
System.out.print(fb.position() + ":" + fb.get() +", ");
System.out.println();
IntBuffer ib = ((ByteBuffer)bb.rewind()).asIntBuffer();
System.out.print("IntBuffer: ");
while(ib.hasRemaining())
System.out.print(ib.position() + ":" + ib.get() +", ");
System.out.println();
LongBuffer lb = ((ByteBuffer)bb.rewind()).asLongBuffer();
System.out.print("LongBuffer: ");
while(lb.hasRemaining())
System.out.print(lb.position() + ":" + lb.get() +", ");
System.out.println();
ShortBuffer sb = ((ByteBuffer)bb.rewind()).asShortBuffer();
System.out.print("ShortBuffer: ");
while(sb.hasRemaining())
System.out.print(sb.position() + ":" + sb.get() +", ");
System.out.println();
DoubleBuffer db = ((ByteBuffer)bb.rewind()).asDoubleBuffer();
System.out.print("DoubleBuffer: ");
while(db.hasRemaining())
System.out.print(db.position() + ":" + db.get() +", ");
System.out.println();
}
}
ByteBuffer由一个8字节数组包装生成,可以发现:通过各种不同的基本类型的视图缓冲器显示结果也有所不同。
- byte:0,0,0,0,0,0,0,a
- char:,,,a
- short:0,0,0,97
- int:0,97
- float:0.0,1.36E-43
- long:97
- double:4.8E-322
字节存放次序
不同的机器可能会使用不同的字节排序方法来存储数据:
- 高位优先:将最重要的字节存放在地址最低的存储器单元。
- 低位优先:将最重要的字节放在地址最高的存储器单元。
ByteBuffer是以高位优先的形式存储数据的,并且数据在网上传输时也常常使用高位优先。我们也可以使用ByteBuffer.order(ByteOrder)来改变字节排序方式:
- ByteOrder.BIG_ENDIAN
- ByteOrder.LITTLE_ENDIAN
下面是包含两个字节的ByteBufferr:
00000000 01100001
如果以short形式读取数据,结果为97,但将ByteBuffer改为低位优先,仍以short形式读取数据,则结果为24832。(二进制形式为01100001 00000000)
下面的例子展示了如何通过字节存放模式设置来改变字符中的字节次序:
public class Endians {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.wrap(new byte[12]);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.BIG_ENDIAN);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.asCharBuffer().put("abcdef");
System.out.println(Arrays.toString(bb.array()));
bb.rewind();
}
}
18.10.4 用缓冲器操纵数据
ByteBuffer是将数据移进移出通道的唯一方式,我们可以通过as方法从ByteBuffer中获取各种基本类型的视图缓冲器,从而操纵各种基本数据类型。
18.10.5 缓冲器的细节
Buffer:由数据和可以高效地访问及操纵这些数据的四个索引组成。这四个索引分别为:mark(标记)、position(位置)、limit(界限)和capacity(容量)。下面是用于设置和复位索引以及查询它们的值的方法:
- capacity():返回缓冲区容量。
- clear():清空缓冲区,将position设置为0,limit设置为容量。
- rewind():将position设置为缓冲器开始位置0。
- flip():将limit设置为position,position设置为0。
- limit():返回limit值。
- limit(int):设置limit值。
- mark():将mark设置为position。
- reset():将position设置为mark。
- position():返回position值。
- position(int):设置position值。
- remaining():返回(limit - position)。
- hasRemaining():若有介于position和limit之间的元素,返回true。
在缓冲器中插入和提取数据的方法会更新这些索引,用于反应所发生的变化。 下面是对上述方法的使用:
public class UsingBuffers {
private static void symmetricScramble(CharBuffer buffer) {
while(buffer.hasRemaining()) {
buffer.mark();
char c1 = buffer.get();
char c2 = buffer.get();
buffer.reset();
buffer.put(c2).put(c1);
}
}
public static void main(String[] args) {
char[] data = "UsingBuffers".toCharArray();
ByteBuffer bb = ByteBuffer.allocate(data.length * 2);
CharBuffer cb = bb.asCharBuffer();
cb.put(data);
System.out.println(cb.rewind());
symmetricScramble(cb);
System.out.println(cb.rewind());
symmetricScramble(cb);
System.out.println(cb.rewind());
}
}
18.10.6 内存映射文件
内存映射文件允许我们创建和修改哪些因为太大而不能放入内存的文件。
下面是一个应用内存映射文件的小例子:
public class LargeMappedFiles {
static int length = 0x8FFFFFF; //128 MB
public static void main(String[] args) throws Exception {
MappedByteBuffer out = new RandomAccessFile("text.dat", "rw").getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, length);
for (int i = 0; i < length; i++)
out.put((byte)'x');
System.out.println("Finished writing");
for (int i = length/2 ; i < length/2 +6 ; i++)
System.out.print((char)out.get(i));
}
}
首先,我们通过RandomAccessFile获取能读能写的通道,然后调用FileChannel.map()获取MappedByteBuffer,必须指定映射文件的初始位置和映射区域长度,即可以映射文件的一部分。并且,MappedByteBuffer是ByteBuffer的子类,具有其所有方法。
18.10.7 文件加锁
JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竞争同一文件的两个线程可能在不同的Java虚拟机上;或者一个是Java线程,另一个是操作系统中其他的某个本地线程。由于Java的文件加锁直接映射到了本地操作系统的加锁工具,所以文件锁对其他操作系统进程是可见的。
下面是一个关于文件加锁的简单示例:
public class FileLocking {
public static void main(String[] args) throws Exception {
FileOutputStream out = new FileOutputStream("file.txt");
FileLock lock = out.getChannel().tryLock();
if(lock != null) {
System.out.println("Locked File");
TimeUnit.MILLISECONDS.sleep(100);
lock.release();
System.out.println("Released Lock");
}
out.close();
}
}
我们通常有两种方式获取文件锁:
- FileChannel.tryLock():非阻塞式,当其他进程已经持有相同的锁,并且不共享时,它将直接从方法调用返回。
- FileChannel.lock():阻塞式,阻塞进程直至锁可以获得,或当前线程中断,或当前通道关闭。
使用FileLock.release()可以释放锁。
我们也可以对文件的一部分进行上锁:
- tryLock(long position,long size,boolean shared)
- lock(long position,long size,boolean shared)
对独占锁或共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁,并为每一个请求都创建一个锁,那么它就会使用独占锁。我们可以通过FileLock.isShared()查询锁的类型。
对映射文件的部分加锁
如前所述,文件映射通常应用与极大的文件。我们可能需要对这种巨大文件进行部分加锁,以便其他进程可以修改文件中未被加锁的部分。例如,数据库就是如此,从而使得多个用户可以同时访问。
下面的例子中有两个线程,分别加锁文件的不同部分:
public class LockingMappedFiles {
static final int LENGTH = 0x8FFFFFF; // 128 MB
static FileChannel fc;
public static void main(String[] args) throws Exception {
fc = new RandomAccessFile("test.dat", "rw").getChannel();
MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
for (int i = 0; i < LENGTH; i++)
out.put((byte)'x');
new LockAndModify(out, 0, 0 + LENGTH/3);
new LockAndModify(out, LENGTH/2, LENGTH/2 + LENGTH/4);
}
private static class LockAndModify extends Thread {
private ByteBuffer buff;
private int start, end;
public LockAndModify(ByteBuffer mbb, int start, int end) {
this.start = start;
this.end = end;
mbb.limit(end);
mbb.position(start);
buff = mbb.slice();
start();
}
public void run() {
try {
FileLock lock = fc.lock(start, end, false);
System.out.println("Locked: " + start + " to " + end);
while(buff.position() < buff.limit() -1)
buff.put((byte)(buff.get() + 1));
lock.release();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
ByteBuffer.slice()方法:根据现有的缓冲区创建一个子缓冲区。即创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。
18.11 压缩
Java I/O类库中的类支持读写压缩格式的数据流。
由于压缩类库是按字节流方式处理的,所以这些类都是属于InputStream和OutputStream继承层次结构的一部分:
- CheckedInputStream:为任何InputStream产生校验和
- CheckedOutputStream:为任何OutputStream产生校验和
- DeflaterOutputStream:压缩类的基类
- ZipOutputStream:用于将数据压缩成Zip文件格式
- GZIPOutputStream:用于将数据压缩成GZIP文件格式
- InflaterInputStream:解压缩的基类
- ZipInputStream:用于解压缩Zip文件格式的数据
- GZIPInputStream:用于解压缩GZIP文件格式的数据
18.11.1 用GZIP进行简单压缩
下面是对单个文件进行压缩的例子:
public class GZIPcompress {
public static void main(String[] args) throws Exception {
if(args.length == 0)
System.exit(1);
BufferedReader in = new BufferedReader(new FileReader(args[0]));
BufferedOutputStream out = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream("test.gz")));
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
BufferedReader in2 = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
String s;
while((s = in2.readLine()) != null)
System.out.println(s);
in.close();
}
}
压缩类的使用非常直观:将输出流封装成压缩类,将输入流封装成解压缩类即可。
18.11.2 用Zip进行多个文件保存
支持Zip格式的Java库最为全面。利用该库可以方便地保存多个文件,并且其使用的是标准Zip格式,可以和压缩工具很好地协助。下面的示AsynchronousCloseException例展示了如何使用Zip保存多个文件:
public class ZipCompress {
public static void main(String[] args) throws Exception {
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream("test.zip"), new Adler32());
ZipOutputStream zos = new ZipOutputStream(cos);
BufferedOutputStream out = new BufferedOutputStream(zos);
for (String arg : args) {
System.out.println("Writing file " + arg);
BufferedReader in = new BufferedReader(new FileReader(arg));
zos.putNextEntry(new ZipEntry(arg));
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.flush();
}
out.close();
System.out.println("Checksum: " + cos.getChecksum().getValue());
System.out.println("Reading file");
CheckedInputStream cis = new CheckedInputStream(new FileInputStream("test.zip"), new Adler32());
ZipInputStream zis = new ZipInputStream(cis);
BufferedInputStream in = new BufferedInputStream(zis);
ZipEntry ze;
while((ze = zis.getNextEntry()) != null) {
System.out.println("Reading file " + ze );
int x;
while((x = in.read()) != -1)
System.out.print((char)x);;
}
System.out.println("Checksum: " + cis.getChecksum().getValue());
in.close();
}
}
本例中,通过使用CheckedInputStream和CheckedOutputStream来计算和校验文件的校验和,检验和类型一共有两种:Adler32(快)和CRC32(慢,但更准确)。
对于每一个要加入压缩档案的文件,都必须调用putNextEntry(ZipEntry)。而在解压缩时,我们可以通过ZipInputStream.getNextEntry()返回下一个ZipEntry对象。
GZIP和Zip库的使用并不仅仅局限于文件,它们可以压缩任何数据信息。
18.11.3 Java档案文件
Zip格式也被应用于JAR(Java ARchive,Java档案文件)文件格式中。
JAR文件是跨平台的,可以将一组文件压缩到单个压缩文件中,并且声音、图像等文件都可以包含在其中。通过是用JAR,Web浏览器只需要一次请求,就能下载构成一个应用的所有文件,并且压缩技术可以使传输时间更短。另外,JAR中的每个条目都可以加上数字化签名。
一个JAR文件由一组压缩文件构成,同时还有一张描述所有文件的清单文件。
Sun的JDK自带的jar程序可根据我们的选择自动压缩文件,下面是其命令:
jar [options] destination [manifest] inputfile(s)
其中options是一个字母集合,具体意义如下:
- c:创建一个新的或空的压缩文档
- t:列出目录表
- x:解压所有文件
- x file:解压该文件
- f:指定文件名
- m:第一个参数设为用户自建的清单文件的名字
- v:产生详细输出,描述jar所做的工作
- O:只存储文件,不压缩文件
- M:不自动创建文件清单
以下是一些调用jar的典型方法:
创建一个名为myJarFile.jar的JAR文件,该文件包含了当前目录中的所有类文件,以及自动产生的清单文件:
jar cf myJarFile.jar *.class
在前例的基础上,添加了一个名为myManifestFile.mf的用户自建清单文件:
jar cmf myJarFile.jar myManifestFile.mf *.class
产生myJarFile.jar内所有文件的一个目录表:
jar tf myJarFile.jar
添加v标志,提供详细信息:
jar tvf myJarFile.jar
18.12 对象序列化
Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。即程序在不运行时,仍然保存对象信息,在下次程序运行时,该对象将被重建并拥有之前的信息。
对象的序列化实现了轻量级持久性:
- 轻量级:对象必须在程序中显式地序列化和反序列化还原。
- 持久性:一个对象的生存周期并不取决于程序是否正在执行。
对象的序列化主要用于支持Java两种特性:远程方法调用和Java Beans。
由于对象序列化是基于字节的,因此需要使用InputStream和OutputStream继承层次结构:
- 序列化:ObjectOutputStream.writeObject(Object)
- 反序列化:ObjectInputStream.readObject()
下面通过一个简单示例演示了序列化及反序列化的过程:
class Data implements Serializable {
private int n;
public Data(int n) { this.n = n; }
public String toString() { return Integer.toString(n); }
}
public class Worm implements Serializable {
private static Random rand = new Random(66);
private Data[] d = {
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10))
};
private Worm next;
private char c;
public Worm(int i,char x) {
System.out.println("Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x+1));
}
public Worm() {
System.out.println("Default constructor");
}
public String toString() {
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for (Data data : d)
result.append(data);
result.append(")");
if(next != null)
result.append(next);
return result.toString();
}
public static void main(String[] args) throws Exception {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w );
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject("Worm storage\n");
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);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out2 = new ObjectOutputStream(bout);
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
s = (String) in2.readObject();
Worm w3 = (Worm) in2.readObject();
System.out.println(s + "w3 = " + w3);
}
}
可以发现:对象序列化并不仅仅是保存了对象的全景图,而且能够追踪到对象内所包含的所有引用,并保存那些对象。在对一个Serializable对象进行还原的过程中,都是通过从InputStream中取得数据恢复而来的,并没有调用任何构造器。
18.12.1 反序列化的必要条件
在上述的例子中,我们通常会思考一个问题:只要拥有序列化的字节信息,就能够进行饭序列化吗? 下面,我们将对象序列化后,将该对象的Class文件删除,再进行反序列化:
public class Alien implements Serializable {}
public class FreezeAlien {
public static void main(String[] args) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("X.file"));
Alien quellek = new Alien();
out.writeObject(quellek);
}
}
public class ThawAlien {
public static void main(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("X.file")));
Object mystery = in.readObject();
System.out.println(mystery.getClass());
}
}
结果证明:对象反序列化必须需要该对象的Class对象,如果JVM找不到Class文件,将抛出ClassNotFoundException异常。
序列化的控制
如果我们对序列化机制有特殊需求,则可以通过实现Externalizable接口。它继承了Serializable接口,并添加了两个方法:writeExternal()和readExternal()。这两个方法在序列化和饭序列的过程中会被自动调用,以便执行一些特殊操作。
下面的例子展示了Externalizable接口的简单实现:
class Blip1 implements Externalizable {
public Blip1() { System.out.println("Blip1 Constructor"); }
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip1.writeExternal");
}
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
System.out.println("Blip1.readExternal");
}
}
class Blip2 implements Externalizable {
Blip2() { System.out.println("Blip2 Constructor"); }
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip2 writeExternal");
}
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
System.out.println("Blip2 readExternal");
}
}
public class Blips {
public static void main(String[] args) throws Exception {
System.out.println("Constructing objects:");
Blip1 b1 = new Blip1();
Blip2 b2 = new Blip2();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Blips.out"));
System.out.println("Saving objects:");
out.writeObject(b1);
out.writeObject(b2);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blips.out"));
System.out.println("Recovering b1:");
b1 = (Blip1) in.readObject();
System.out.println("Recovering b2:");
// throws an exception
// b2 = (Blip2) in.readObject();
}
}
我们发现,Blip2的构造器并非public的,如果我们反序列化Blip2对象,则会有异常抛出。这是由于反序列化Externalizable对象和反序列化Serializable对象不同:
- 对于Serializable对象:对象以它存储的二进制为基础构造,而不是调用构造器。
- 对象Externalizable对象:首先调用默认构造器,然后调用readExternal()方法。
对于Externalizable对象,我们需要显式地保存和恢复对象中的数据信息。下面的例子示范了如何完整保存和恢复一个Externalizable对象:
public class Blip3 implements Externalizable {
private int i;
private String s;
public Blip3() {
System.out.println("Blip3 Constructor");
}
public Blip3(String x, int a) {
s = x;
i = a;
}
public String toString() { return s + i; }
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip3.writeExternal");
out.writeObject(s);
out.writeInt(i);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip3.readExternal");
s = (String) in.readObject();
i = in.readInt();
}
public static void main(String[] args) throws Exception {
System.out.println("Constructing objects:");
Blip3 b3 = new Blip3("A String " , 66);
System.out.println(b3);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Blip3.out"));
System.out.println("Saving object:");
out.writeObject(b3);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out"));
System.out.println("Recovering b3:");
b3 = (Blip3) in.readObject();
System.out.println(b3);
}
}
我们如果从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()方法来为基类组件提供恰当的存储和恢复功能。
transient 关键字
如前面所示,我们可以实现Externalizable接口,达到控制序列化的要求,并通过writeExternal()方法对所需的字段进行显式地序列化。
当我们正在操作的是一个Serializable对象时,所有的序列化操作都会自动进行。此时如果希望将某个字段关闭序列化,则可以使用java提供的关键字transient。
下面的示例展示了transient关键字的用法:
public class Logon implements Serializable {
private Date date = new Date();
private String username;
private transient String password;
public Logon(String username, String password) {
this.username = username;
this.password = password;
}
public String toString() {
return "Logon info: \n username: " + username +
"\n date: " + date + "\n password: " + password ;
}
public static void main(String[] args) throws Exception {
Logon logon = new Logon("Hulk", "myLittlePony");
System.out.println("logon = " + logon);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Logon.out"));
out.writeObject(logon);
out.close();
TimeUnit.SECONDS.sleep(1);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Logon.out"));
System.out.println("Recovering object at " + new Date());
logon = (Logon) in.readObject();
System.out.println("logon = " + logon);
}
}
我们在本例中,将password设置为transient,因此,它不会被自动保存到磁盘,并且在恢复时,password域会被置为null。
Externalizable的代替方法
我们通过实现Serializable接口,并添加writeObject()和readObject()方法可以代替Externalizable接口。即只要我们提供了这两个方法,就会使用它们而不是默认的Serializable序列化机制。
这两个方法必须具有准确的方法特征签名:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException
需要注意的是:它们被声明为private。
在序列化中调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象是否具有writeObject()方法。如果有,则跳过正常的序列化过程并调用该方法。反序列化时也是如此。
当然,我们也可以在writeObject()方法内部,调用defaultWriteObject()方法来执行默认的序列化操作:
public class SerialCtl implements Serializable {
private String a;
private transient String b;
public SerialCtl(String a, String b) {
this.a = "Not Transient: " + a;
this.b = "Transient: " + b;
}
public String toString() { return a + "\n" + b; }
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(b);
}
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException {
in.defaultReadObject();
b = (String) in.readObject();
}
public static void main(String[] args) throws Exception {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buf);
out.writeObject(sc);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
sc = (SerialCtl) in.readObject();
System.out.println("After:\n" + sc);
}
}
在本例中,有两个字段,一个普通字段和一个transient字段。由于transient字段无法被默认序列化机制所存储,所以我们通过显式地进行写入。
序列化的操作发生在out.writeObject(sc):该方法会通过反射搜索该对象是否具有writeObject()方法。如果有,会使用它,而非默认序列化机制。
18.13 XML
对象序列化的一个限制条件就是:只有Java程序才能进行对象序列化和反序列化。一种更具普遍性的方案是将数据转换为XML格式,这种格式可以被任何平台或语言所解析。
下面使用将Person对象序列化到XML文件中的一个简单示例:
public class Person {
private String first, last;
public Person(String first, String last) {
this.first = first;
this.last = last;
}
public Element getXML(Document document) throws Exception {
Element person = document.createElement("person");
Element firstName = document.createElement("first");
firstName.setTextContent(first);
Element lastName = document.createElement("last");
lastName.setTextContent(last);
person.appendChild(firstName);
person.appendChild(lastName);
return person;
}
public Person(Element person) {
first = person.getElementsByTagName("first").item(0).getTextContent();
last = person.getElementsByTagName("last").item(0).getTextContent();
}
public String toString() { return first + " " + last; }
public static void format(StringWriter out,Document document ) throws Exception {
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
DOMSource domSource = new DOMSource(document);
StreamResult xmlResult = new StreamResult(out);
transformer.transform(domSource, xmlResult);
System.out.println(out.toString());
}
public static void main(String[] args) throws Exception {
List<Person> people = Arrays.asList(
new Person("Dr. Bunsen", "Honeydew"),
new Person("Gonzo", "The Great"),
new Person("Phillip J.", "Fry"));
System.out.println(people);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
document.setXmlVersion("1.0");
document.setXmlStandalone(true);
Element root = document.createElement("people");
document.appendChild(root);
for (Person person : people) {
root.appendChild(person.getXML(document));
}
format(new StringWriter(), document);
}
}
18.14 Preferences
Preferences API可以自动存储和读取信息,用于存储基本类型和字符串。
Preferences是一个键值集合,存储在一个节点层次结构中。下面是一个简单示例:
public class PreferencesDemo {
public static void main(String[] args) throws Exception {
Preferences prefs = Preferences.userNodeForPackage(PreferencesDemo.class);
prefs.put("Location", "Oz");
prefs.put("Footwear", "Ruby Slippers");
prefs.putInt("Companions", 4);
prefs.putBoolean("Are there witches?", true);
int usageCount = prefs.getInt("UsageCount", 0);
usageCount++;
prefs.putInt("UsageCount", usageCount);
for (String key : prefs.keys())
System.out.println(key + ": " + prefs.get(key, null));
System.out.println("How many companions does Dorothy have? " + prefs.getInt("Companions", 0));
}
}
需要注意的是:每次运行该程序,UsageCount的值都会自增,即这些数据已经被储存起来了。
18.15 总结
Java I/O流类库可以满足我们的基本需求:通过控制台、文件、内存块,甚至因特网进行读写。但由于使用了装饰器模式,导致我们在创建类时有些麻烦。