1. Java的I/O系统
在Java 1.7之前对于程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务。Java的I/O类库在1.0版本引入了InputStream与OutputStream面向字节的体系,在1.1版本引入了Reader与Writer面向字符的体系,在1.4引入了nio(nonblocking,非阻塞式)体系,在1.7引入了异步非阻塞I/O方式,放在了java.nio.file包下面。
1.1. File类
在学习那些真正用于在流中读写数据的类之前,让我们先看一下File类,这个实用类库工具可以帮助我们处理文件目录问题。
File类既可以代表一个特定文件的名称,也能代表一个目录下的一组文件的名称。
1.1.1. 目录列表器
调用File对象的不带参数的list()方法可以获得此File对象包含的全部列表,而如果想对目录进行过滤可以使用目录过滤器。
下面的示例过滤出特定目录下txt后缀的文件,并且做了排序:
public class DirList { public static void main(String[] args) { File path = new File("/Users/MyDear/Desktop/practise"); String[] list = path.list(new DirFilter(".*\\.txt")); 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) { this.pattern = Pattern.compile(regex); } @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); }}/* Output:a.txtb.txtc.txt*/
其中使用到java.io类库中的FilenameFilter接口是这样的:
public interface FilenameFilter { boolean accept(File dir, String name);
创建DirFilter类的目的在于把accept()方法提供给list()方法使用,使list()方法可以回调accept()方法,进而以决定哪些文件包含在列表中。这是一个回调的例子,更具体的说是一个策略模式的例子,策略的目的就是提供代码行为的灵活性。
这个例子很适合用一个匿名内部类进行改写:
public class DirList2 { public static FilenameFilter filter(final String regex) { return new FilenameFilter() { private Pattern pattern = Pattern.compile(regex); @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }; } public static void main(String[] args) { File path = new File("/Users/MyDear/Desktop/practise"); String[] list = path.list(filter(".*\\.txt")); Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for (String dirItem : list) { System.out.println(dirItem); } }}
不过要注意的是,传给filter()方法的参数必须是final的。
可以进一步改写这个方法,定义一个匿名内部类来作为list()方法的参数,这样程序会更简短。下面的示例利用了匿名内部类来创建特定的、一次性的类来解决问题。这个方法的优点在于将解决特定问题的代码隔离、聚扰于一点,不过这种方法不易于阅读。
public class DirList3 { public static void main(String[] args) { File path = new File("/Users/MyDear/Desktop/practise"); String[] list = path.list(new FilenameFilter() { private Pattern pattern = Pattern.compile(".*\\.txt"); @Override 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); } }}
1.1.2. 目录实用工具
这个自定义的实用工具可以通过使用local()方法产生由本地目录中的文件构成的File对象数组,或者通过使用walk()方法产生给定目录下的由整个目录树中所有文件构成的列表:
import java.io.File;import java.io.FilenameFilter;import java.util.ArrayList;import java.util.Arrays;import java.util.Iterator;import java.util.List;import java.util.regex.Pattern;public final class Directory { public static File[] local(File dir, final String regex) { return dir.listFiles(new FilenameFilter() { private Pattern pattern = Pattern.compile(regex); @Override 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 fileList = new ArrayList<>(); public List dirList = new ArrayList<>(); @Override public Iteratoriterator() { return fileList.iterator(); } void addAll(TreeInfo other) { fileList.addAll(other.fileList); dirList.addAll(other.dirList); } @Override public String toString() { return "dirs: " + PPrint.pformat(dirList) + "\n\nfiles: " + PPrint.pformat(fileList); } } public static TreeInfo walk(String start, String regex) { return recurseDirs(new File(start), regex); } public static TreeInfo walk(File start, String regex) { return recurseDirs(start, regex); } public static TreeInfo walk(File start) { return recurseDirs(start, ".*"); } public static TreeInfo walk(String start) { return recurseDirs(new File(start), ".*"); } // 递归目录,分别将目录和文件放入列表中 private static TreeInfo recurseDirs(File startDir, String regex) { TreeInfo result = new TreeInfo(); for (File item : startDir.listFiles()) { if (item.isDirectory()) { result.dirList.add(item); // 递归下级目录 result.addAll(recurseDirs(item, regex)); } else { if (item.getName().matches(regex)) { result.fileList.add(item); } } } return result; } public static void main(String[] args) { System.out.println("--- 目录结构如下 ---"); System.out.println(walk("/Users/MyDear/Desktop/practise")); System.out.println("--- 使用local()方法进行过滤文件 ---"); List fileList = Arrays.asList(local("/Users/MyDear/Desktop/practise", ".*\\.jpeg")); System.out.println(PPrint.pformat(fileList)); }}/* Output:--- 目录结构如下 ---dirs: [ /Users/MyDear/Desktop/practise/e /Users/MyDear/Desktop/practise/e/f]files: [ /Users/MyDear/Desktop/practise/a2.txt /Users/MyDear/Desktop/practise/c.txt /Users/MyDear/Desktop/practise/b.txt /Users/MyDear/Desktop/practise/a.txt /Users/MyDear/Desktop/practise/rabbit.jpeg /Users/MyDear/Desktop/practise/e/diu.jpeg]--- 使用local()方法进行过滤文件 ---[/Users/MyDear/Desktop/practise/rabbit.jpeg]*/
因为容器默认的toString()方法会在单个行中打印容器中的所有元素,对于大型集合来说,这会变得难以阅读。下面是一个可以添加新行并缩排所有元素的工具:
import java.util.Arrays;import java.util.Collection;public class PPrint { public static String pformat(Collection> collection) { if (collection.size() == 0) { return "[]"; } StringBuilder result = new StringBuilder("["); for (Object item : collection) { if (collection.size() != 1) { result.append("\n "); } result.append(item); } if (collection.size() != 1) { result.append("\n"); } result.append("]"); return result.toString(); } public static void pprint(Collection> collection) { System.out.println(pformat(collection)); } public static void pprint(Object[] objects) { System.out.println(pformat(Arrays.asList(objects))); }}
我们可以更进一步,创建一个工具,它可以在目录中穿行,并且根据Strategy对象来处理这些目录中的文件,也是策略设计模式的另一个示例:
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("/Users/MyDear/Desktop/practise")); } 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 (IOException e) { throw new RuntimeException(e); } } public void processDirectoryTree(File root) throws IOException { for (File file : Directory.walk(root.getAbsolutePath(), ".*\\." + ext)) { strategy.process(file.getCanonicalFile()); } } public static void main(String[] args) { new ProcessFiles(new ProcessFiles.Strategy() { @Override public void process(File file) { System.out.println(file); } }, "txt").start(args); }}/* Output:/Users/MyDear/Desktop/practise/c.txt/Users/MyDear/Desktop/practise/b.txt/Users/MyDear/Desktop/practise/a.txt*/
1.2. 输入和输出
编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产生数据的数据源对象或是有能力接收数据的接收端对象。流屏蔽了实际I/O设备中处理数据的细节。
在Java中,我们很少使用单一的类来创建流对象。而Java中流类库让人迷惑的主要原因就在于创建单一的结果流,却需要创建多个对象。
在Java 1.0中,类库的设计者首先限定与输入有关的类都应该从InputStream继承,而与输出有关的所有类都应该从OutputStream继承。
1.2.1. InputStream类型
InputStream的作用是用来表示那些从不同数据源产生输入的类,这些数据源包括:
- 字节数组
- String对象
- 文件
- “管道”,工作方式与实际管道类似,从一端输入,从另一端输出
- 一个由其他种类的流组成的序列,以便我们可以把它们收集合并到一个流内
- 其他数据源,如Internat连接等
类 | 功能 | 构造器参数 | 如何使用 |
ByteArrayInputStream | 允许将内存的缓冲区当作InputStream使用 | 缓冲区 | 作为一种数据源,将其与FilterInputStream对象相连以提供有用接口 |
StringBufferInputStream | 将String转换成InputStream | 字符串,底层实现实际使用StringBuffer | 作为一种数据源,将其与FilterInputStream对象相连以提供有用接口 |
FileInputStream | 用于从文件中读取信息 | 字符串,表示文件名、文件或FileDescriptor对象 | 作为一种数据源,将其与FilterInputStream对象相连以提供有用接口 |
PipedInputStream | 产生用于写入相关PipedOutputStream的数据,实现管道化概念 | PipedOutputStream | 作为一种数据源,将其与FilterInputStream对象相连以提供有用接口 |
SequenceInputStream | 将两个或多个InputStream对象转换成单一InputStream | 两个InputStream对象或一个容纳InputStream对象的容器Enumeration | 作为一种数据源,将其与FilterInputStream对象相连以提供有用接口 |
FilterInputStream | 抽象类,作为“装饰器”的接口。其中,“装饰器”为其他的InputStream类提供有用功能 | 见下文 | 见下文 |
1.2.2. OutputStream类型
这个类别的类决定了输出所要去往的目标:字节数组、文件或管道。
类 | 功能 | 构造器参数 | 如何使用 |
ByteArrayOutputStream | 在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓冲区初始化尺寸(可选的) | 指定数据的目的地:将其与FilterOutputStream对象相连以提供有用接口 |
FileOutputStream | 用于将信息写至文件 | 字符串,表示文件名、文件或FileDescriptor对象 | 指定数据的目的地:将其与FilterOutputStream对象相连以提供有用接口 |
PipeOutputStream | 任何写入其中的信息都会自动作为相关PipedInputStream的输出。实现管道化概念 | PipedInputStream | 指定用于多线程的数据的目的地:将其与FilterOutputStream对象相连以提供有用接口 |
FilterOutputStream | 抽象类,作为“装饰器”的接口,其中“装饰器”为其他OutputStream提供有用功能 | 见下文 | 见下文 |
1.3. 添加属性和有用的接口
在编写程序时,装饰器模式给我们提供了相当多的灵活性,但是它也增加了代码的复杂性。Java I/O类库操作不便的原因就在于,我们必须创建许多类,核心的I/O类型加上所有的装饰器,才能得到我们所希望的单个I/O对象。
FilterInputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流和输出流的两个类。这两个类是装饰器的必要条件,以便能为所有正在被修饰的对象提供通用接口。
1.3.1. 通过FilterInputStream从InputStream读取数据
在FilterInputStream的子类中,DataInputStream允许我们读取不同的基本类型数据以及String对象,其中的方法都以read开头,如readByte()、readFloat()等等。搭配相应的DataOutputStream,就可以通过数据流将基本类型的数据从一个地方迁移到另一个地方。
其他的子类则在内部修改InputStream的行为方式:是否缓冲,是否保留它所读过的行,以及是否是否把单一字符推回输入流等。下表中的后面两个类,它们被添加进类库可能是为了对“用Java构建编译器”实验提供支持,我们在一般编程中不会用到它们。
下面是FilterInputStream的类型及功能:
类 | 功能 | 构造器参数 | 如何使用 |
DataInputStream | 与DataOutputStream搭配使用,可以按照可移植方式从流读取基本数据类型 | InputStream | 包含用于读取基本类型数据的全部接口 |
BufferedInputStream | 使用它可以防止每次读取时都得进行实际写操作 | InputStream,可以指定缓冲区大小 | 本质上不提供接口,只不过是向进程中添加缓冲区所必需的。与接口对象搭配 |
LineNumberInputStream | 跟踪输入流中的行号;可调用getLineNumber()和setLineNumber(int) | InputStream | 仅增加了行号,因此可能要与接口对象搭配使用 |
PushbackInputStream | 具有能弹出一个字节的缓冲区。因此可以将读到的最后一个字符回退 | InputStream | 通常作为编译器的扫描器,之所以包含在内是因为Java编译器的需要 |
1.3.2. 通过FilterOutPutStream向OutputSream写入
与DataInputStream对应的是DataOutputStream,它可以将各种基本数据类型以及String对象格式化输出到“流”中,其中的方法都以write开头,如writeByte()、writeFloat()等等。
作为子类,PrintStream最初的目的就是为了以可视化格式打印所有的基本数据类型以及String对象。而与之不同的是DataOutputStream的目的是将数据元素置入流中,使DataInputStream能够可移植地重构它们。PrintStream有两个重要的方法:print()和println(),后面的方法会在操作完毕后添加一个换行符。有问题的是PrintStream捕捉了所有的IOExceptions,也没有完全国际化,不能以平台无关的方式处理换行动作。不过这些问题在printWriter中得到了解决。
BufferedOutputStream是一个修改过的OutputStream,它对数据流使用缓冲技术,这样当每次向流写入时,不必每次都进行实际的物理写动作。所以在进行输出时,我们可能更经常的是使用它。
类 | 功能 | 构造器参数 | 如何使用 |
DataOutputStream | 与DataInputStream搭配使用,可以按照可移植方式向流中写入基本类型数据 | OutputStream | 包含用于写入基本类型数据的全部接口 |
PrintStream | 用于产生格式化输出。DataOutputStream处理数据的存储,PrintStream处理数据的显示 | OutputStream | 可以用boolean值指示是否在每次换行时清空缓冲区(可选的)应该是对OutputStream对象的final封装。可能会经常使用它。 |
BufferedOutputStream | 使用它以避免每次发送数据时都要进行实际的写操作。代表“使用缓冲区”。可以调用flush()清空缓冲区 | OutpuStream,可以指定缓冲区大小(可选的) | 本质上并不提供接口,只不过是向进程中添加缓冲区所必需的,与接口对象搭配 |
1.4. Reader和Writer
Java 1.1对基本的I/O类库进行了重大修改,也有向InputStream和OutputStream继承层次结构中添加了一些新类。尽管一些原始的流类库不再被使用,但是InputStream和OutputStream在以面向字节形式的I/O中仍可以提供极有价值的功能,而新增的Reader和Writer则提供兼容Unicode和面向字符的I/O功能。
当我们需要把来自于“字节”层次结构中的类和“字符”层次结构中的类结合起来使用时,就要使用适配器类,例如InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换成Writer。
字节流与字符流的继承体系总结如下图:
1.5. 自我独立的类:RandomAccessFile
RandomAccessFile适用于由大小已知的记录组成的文件,可以用seek()方法将记录从一处转移到另一处,然后读取或者修改记录。这个类并不属于InputStream和OutputStream继承层次结构,不过与这二者一样,也实现了DataInput和DataOutput接口。它是一个完全独立的类,直接从Object派生而来,其中的大多数方法都是本地的。
从本质上来说,RandomAccessFile的工作方式类似于把DataInputStream和DataOutputStream组合起来使用。只有RandomAccessFile支持搜寻方式,并且只适用于文件。在JDK1.4中,RandomAccessFile的大多数功能,但不是全部,由nio存储映射文件所取代。
1.6. I/O流的典型使用方式
虽然可以通过不同的方式组合I/O流类,但经常用到的就几种组合。下面的示例可以作为典型的I/O用法的参考。
1.6.1. 缓冲输入文件
可以使用以String或File对象为文件名的FileInputReader,来打开一个文件用于字符输入。为了提高速度,可以把打开的文件产生的引用传递给一个BufferedReader,来对文件进行缓冲。BufferedReader类中的readLine()方法是我们最终对象和进行读取的接口,当readLine()方法返回null时,就达到了文件的末尾。
import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;public class BufferedInputFile { public static String read(String filename) throws IOException { BufferedReader in = new BufferedReader(new FileReader(filename)); String s; StringBuilder stringBuilder = new StringBuilder(); while ((s = in.readLine()) != null) { // readLine()方法会删除换行符,所以需要自己添加 stringBuilder.append(s).append("\n"); } // 关闭文件 in.close(); return stringBuilder.toString(); } public static void main(String[] args) throws IOException { System.out.println(read("BufferedInputFile.java")); }}
1.6.2. 从内存输入
在下面的示例中,从BufferedInputFile.read()读入的String结果被用来创建一个StringReader,然后调用read()方法每次读取一个字符,并把它发送到控制台。注意,read()方法是以int形式返回下一字节,因此必须类型转换成char才能正确打印。
import java.io.IOException;import java.io.StringReader;public class MemoryInput { public static void main(String[] args) throws IOException { StringReader in = new StringReader(BufferedInputFile.read("MemoryInput.java")); int c; while ((c = in.read()) != -1) { System.out.println((char)c); } }}
1.6.3. 格式化的内存输入
要读取格式化数据,可以使用DataInputStream。它是一个面向字节的I/O类。在下面的示例中,必须为ByteArrayInputStream提供字节数组,在这里使用了String类的getBytes()方法。
import java.io.ByteArrayInputStream;import java.io.DataInputStream;import java.io.EOFException;import java.io.IOException;public class FormattedMemoryInput { public static void main(String[] args) throws IOException { try{ DataInputStream in = new DataInputStream(new ByteArrayInputStream(BufferedInputFileread("FormattedMemoryInput.java").getBytes())); while (true){ System.out.println((char)in.readByte()); } }catch (EOFException e){ System.err.println("End of stream"); } }}
如果从DataInputStream用readByte()一次一个字节地读取字符,那么任何字节的值都是合法的结果,返回值就不能用来检测输入是否结束。这时,我们可以使用available()方法查看还有多少可供存取的字符。不过available()方法的工作方式会随着所读取的媒介类型的不同而有所不同。
对于文件,available()方法在没有阻塞的情况下所能读取的字节数是整个文件,而对于不同类型的流,可能就不是整个数据,因此要谨慎使用。
import java.io.ByteArrayInputStream;import java.io.DataInputStream;import java.io.EOFException;import java.io.IOException;public class TestEOF { public static void main(String[] args) throws IOException { try { DataInputStream in = new DataInputStream(new ByteArrayInputStream(BufferedInputFile.read("TestEOF.java").getBytes())); while (in.available() != 0) { System.out.println((char) in.readByte()); } } catch (EOFException e) { System.err.println("End of stream"); } }}
1.6.4. 基本的文件输入
FileWriter对象可以向文件写入数据。这里先创建了一个与指定文件连接的FileWriter,然后用BufferedWriter将其包装起来用以缓冲输出。并且为了提供格式化机制,用了PrintWriter进行装饰。下面的示例会创建BasicFileOutput.out文件,并把BasicFileOutput.java中的内容写入这个新文件中。
import java.io.*;public class BasicFileOutput { private 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(); System.out.println(BufferedInputFile.read(file)); }}
Java SE5在PrintWriter中添加了一个辅助构造器,使我们不必在每次希望创建文本文件并向其中写入时,都要去执行所有的装饰工作。下面是修改后的示例:
import java.io.BufferedReader;import java.io.IOException;import java.io.PrintWriter;import java.io.StringReader;public class FileOutputShortcut { private static String file = "FileOutputShortcut.out"; public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new StringReader(BufferedInputFile.read("FileOutputShortcut.java"))); PrintWriter out = new PrintWriter(file); String s; int lineCount = 1; while ((s = in.readLine()) != null) { out.println(lineCount++ + ": " + s); } out.close(); }}
遗憾的是,其他常见的写入任务都没有快捷方式,因此典型的I/O仍旧包含大量的冗余文本。不过后文的TextFile工具简化了这些常见任务。
1.6.5. 存储和恢复数据
为了使输出可以提供给另一个流来恢复数据,我们可以使用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.14159); out.writeUTF("This apple is red"); out.writeDouble(233.33); out.writeUTF("This puppy is cute"); 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()); }}/* Output:3.14159This apple is red233.33This puppy is cute*/
当我们使用DataOutputStream时,写字符串并让DataInputStream能够恢复它的唯一可靠做法就是使用UTF-8编码,在这个示例中使用的是writeUTF()方法和readUTF()方法。
1.6.6. 读写随机访问文件
在使用RandomAccessFile时,必须知道文件的排版,这样才能正确的操作它。RandomAccessFile拥有读取基本类型和UTF-8字符串的各种具体方法。
import java.io.IOException;import java.io.RandomAccessFile;public class UsingRandomAccessFile { private static String file = "readTest.dat"; private static void display() throws IOException { // r: 只读 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); for (int i = 0; i < 6; i++) { System.out.println("Value " + i + " : " + randomAccessFile.readDouble()); } System.out.println(randomAccessFile.readUTF()); randomAccessFile.close(); } public static void main(String[] args) throws IOException { // rw: 读写 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); for (int i = 0; i < 6; i++) { randomAccessFile.writeDouble(i * 2.33); } randomAccessFile.writeUTF("The end of the file"); randomAccessFile.close(); display(); randomAccessFile = new RandomAccessFile(file, "rw"); // 因为double总是8字节长,所以查找第5个双精度值需要用5*8 randomAccessFile.seek(5 * 8); randomAccessFile.writeDouble(47.0001); randomAccessFile.close(); display(); }}/* Output:Value 0 : 0.0Value 1 : 2.33Value 2 : 4.66Value 3 : 6.99Value 4 : 9.32Value 5 : 11.65The end of the fileValue 0 : 0.0Value 1 : 2.33Value 2 : 4.66Value 3 : 6.99Value 4 : 9.32Value 5 : 47.0001The end of the file*/
1.7. 文件读写的实用工具
一个很常见的程序化任务是读取文件到内存,修改,然后再写出。下面的TextFile类可以用来简化对文件的读写操作,这个类用一个ArrayList来保存文件的每一行。
import java.io.*;import java.util.ArrayList;import java.util.Arrays;import java.util.TreeSet;public class TextFile extends ArrayList<String> { public static String read(String fileName) { StringBuilder stringBuilder = new StringBuilder(); try { BufferedReader in = new BufferedReader(new FileReader(new File(fileName).getAbsoluteFile())); try { String s; while ((s = in.readLine()) != null) { stringBuilder.append(s); stringBuilder.append("\n"); } } finally { in.close(); } } catch (IOException e) { throw new RuntimeException(e); } return stringBuilder.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("TextFile.java"); write("test.txt", file); TextFile text = new TextFile("test.txt"); text.write("test2.txt"); TreeSet words = new TreeSet<>(new TextFile("TextFile.java", "\\W+")); // 打印出大写单词 System.out.println(words.headSet("a")); }}/* Output:[0, ArrayList, Arrays, BufferedReader, File, FileReader, IOException, PrintWriter, RuntimeException, String, StringBuilder, System, TextFile, TreeSet, W]*/
另一种解决读取文件问题的方法是使用在Java SE5中引入的java.util.Scanner类。不过这个类只能用于读取文件,不能用于写文件,并且这个类主要是设计来创建编程语言的扫描器或“小语言”的。
1.7.1. 读取二进制文件
这个工具与TextFile类似,它简化了读取二进制文件的过程。
import java.io.BufferedInputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;public class BinaryFile { public static byte[] read(File byteFile) throws IOException { BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(byteFile)); try { byte[] data = new byte[bufferedInputStream.available()]; bufferedInputStream.read(data); return data; } finally { bufferedInputStream.close(); } } public static byte[] read(String byteFile) throws IOException { return read(new File(byteFile).getAbsoluteFile()); }}
1.8. 标准I/O
标准I/O这个术语参考的是Unix中“程序所使用的单一信息流”这个概念。标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一程序的标准输入。
1.8.1. 从标准输入中读取
按照标准I/O模型,Java提供了System.in、System.out和System.err。基中System.out和System.err已经被包装成printStream,而System.in却没有。所以我们在使用System.in时,需要自己对其包装。
通常我们会用readLine()一次一行地读取输入,为此我们需要把Sysem.in进行包装,下面是示例:
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class Echo { public static void main(String[] args) throws IOException { BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String s; while ((s = stdin.readLine()) != null && s.length() != 0) { System.out.println(s); } }}
1.8.2. 将System.out转换成PrintWriter
System.out是一个PrintStream,而PrintStream是一个OutputStream。PrintWriter有一个可以接受OutputStream作为参数的构造器。因此,可以很容易的将System.out转换成PrintWriter。下面的示例中,在PrintWriter的构造器中把第二个参数设为true,以便开启自动清空功能;否则,可能会看不到输出。
import java.io.PrintWriter;public class ChangeSystemOut { public static void main(String[] args) { PrintWriter out = new PrintWriter(System.out, true); out.println("I like to eat apples"); }}
1.8.3. 标准I/O重定向
如果我们会在显示器上创建大量输出,而这些输出滚动得太快以至无法阅读时,或者想重复测试某个特定用户的输入序列,重定向输入和输出就会很有用。在Java的System类提供了一些简单的静态方法调用,来进行对标准输入、输出和错误I/O流进行重定向。例如:setIn(InputStream)、setOut(PrintStream)和setErr(PrintStream)。
下面的例子展示了如何将标准输入链接到一个文件,然后把标准输出和标准错误重定向到另一个文件。
import java.io.*;public class Redirecting { public static void main(String[] args) throws IOException { PrintStream console = System.out; BufferedInputStream in = new BufferedInputStream(new FileInputStream("Redirecting.java")); PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); String s; while ((s = bufferedReader.readLine()) != null) { System.out.println(s); } out.close(); System.setOut(console); }}
1.9. 进程控制
你经常会需要在Java内部执行操作系统的其他程序,并且要控制这些程序的输入和输出,而Java类库提供了执行这些操作的类。
一项常见的任务是运行程序,并将产生的输出发送到控制台。这里有一个可以简化这项任务的实用工具。在使用这个工具前,我们先来定义一个单独的异常来报告进程自身执行过程中产生的错误:
public class OSExecuteException extends RuntimeException { public OSExecuteException(String reason){ super(reason); }}
然后定义工具类,用OSExecute.command()方法接收运行程序需要执行的命令,这个命令与在控制台中键入的命令相同。这个命令会被传递到java.lang.ProcessBuilder构造器,然后所产生的ProcessBuiler对象调用start()方法。
import java.io.BufferedReader;import java.io.InputStreamReader;public class OSExecute { 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.err.println(s); err = true; } } catch (Exception e) { System.out.println(e); if (!command.startsWith("CMD /C")) { command("CMD /C" + command); } else { throw new RuntimeException(e); } } if (err) { throw new OSExecuteException("Errors executing " + command); } }}
现在可以来使用这个工具,这里以javap命令为例,要先手动javac一下这个示例:
public class OSExecuteDemo { public static void main(String[] args) { OSExecute.command("javap OSExecuteDemo"); }}
1.10. 新I/O
JDK 1.4的java.nio.*包中引入了新的Java I/O类库,其目的在于提高速度。而旧的I/O包已经使用nio重新实现过,以便充分利用nio带来的速度提高。
速度的提高来自于新I/O所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。可以想象有一个煤矿,通道是一个包含煤层的(数据)的矿藏,而缓冲器则是派送到矿藏的矿车。矿车运煤,我们再从矿车上获得煤矿。也就是我们只与缓冲器交互,并没有直接与通道交互。通道要么从缓冲器获取数据,要么向缓冲器发送数据。
唯一直接与通道交互的缓冲器是ByteBuffer,也就是可以存储未加工字节的缓冲器。这个类通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或者基本数据类型输出和读取数据。但是这个类不能用于输出或者读取对象,即使是字符串对象也不行。
旧I/O类库中被修改来产生FileChannel的类有三个:FileInputStream、FileOutputStream和RandomAccessFile。这三个是字节操作流,而Reader和Writer这种字符模式类不能用于产生通道,不过在java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。
下面的示例展示了用这三种类型的流来产生可写、可读可写的及可读的通道:
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class GetChannel { private static final int B_SIZE = 1024; public static void main(String[] args) throws Exception { // 写一个文件 FileChannel fileChannel = new FileOutputStream("data.txt").getChannel(); fileChannel.write(ByteBuffer.wrap("Some text ".getBytes())); fileChannel.close(); // 把数据添加到文件末尾 fileChannel = new RandomAccessFile("data.txt", "rw").getChannel(); fileChannel.position(fileChannel.size()); fileChannel.write(ByteBuffer.wrap("Some more".getBytes())); fileChannel.close(); // 读取文件 fileChannel = new FileInputStream("data.txt").getChannel(); ByteBuffer buff = ByteBuffer.allocate(B_SIZE); fileChannel.read(buff); buff.flip(); StringBuilder stringBuilder = new StringBuilder(); while (buff.hasRemaining()) { stringBuilder.append((char) buff.get()); } System.out.println(stringBuilder.toString()); }}/* Output:Some text Some more*/
这示例中,三个流类的getChannel()方法将会产生一个FileChannel。可以用通道来传送用于读写的ByteBuffer,可以锁定文件的某些区域用于独占式访问等等。
想要把字节存放于ByteBuffer中,可以使用wrap()方法把己存在的字节数组包装到ByteBuffer中。
对于只读访问,我们必须显式地使用静态的allocate()方法来分配ByteBuffer。nio的目标就是快速移动大量数据,所以需要为ByteBuffer设定合适的大小。
一旦调用read()方法来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的filp(),让它做好需要读取字节的准备。如果准备使用缓冲器执行进一步的read()操作,就要调用clear()来为每个read()做好准备。
import java.io.FileInputStream;import java.io.FileOutputStream;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class ChannelCopy { private static final int B_SIZE = 1024; public static void main(String[] args) throws Exception { FileChannel in = new FileInputStream("ChannelCopy.java").getChannel(), out = new FileOutputStream("test.txt").getChannel(); ByteBuffer buffer = ByteBuffer.allocate(B_SIZE); while (in.read(buffer) != -1) { buffer.flip(); out.write(buffer); buffer.clear(); } }}
在这个示例中,打开了一个FileChannel用来读,而另外一个用于写。当FileChannel.read()返回-1时(一个分界符),表示已经达到了输入的末尾。每次read()操作之后,就会将数据输入到缓冲器中,flip()则会准备缓冲器以便它的信息可以由write()提取。write()操作之后,信息会仍在缓冲器中,所以需要用clear()方法对所有内部指针重新安排,以便另一个read()操作期间能够做好接收数据的准备。
不过这并不是处理此类操作的理想方式。特殊方法transferTo()和transferFrom()则允许我们将一个通道和另一个通道直接相连,例如:
in.transferTo(0, in.size(), out);// 也可以这样:out.transferFrom(in, 0, in.size());
1.10.1. 转换数据
在GetChannel.java中,我们为了输入文件中的信息,必须每次只读取一个字节的数据,然后将每个byte类型强制转换成char类型。不过在java.nio.CharBuffer这个类中,有个toString()方法,能返回一个包含缓冲器中所有字符的字符串。而ByteBuffer是可以看作具有asCharBuffer()方法的CharBuffer。下面的示例这样使用了ByteBuffer,但发现并不能正常工作:
import java.io.FileInputStream;import java.io.FileOutputStream;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.nio.charset.Charset;public class BufferToText { private static final int B_SIZE = 1024; public static void main(String[] args) throws Exception { FileChannel fileChannel = new FileOutputStream("data2.txt").getChannel(); fileChannel.write(ByteBuffer.wrap("Some text".getBytes())); fileChannel.close(); fileChannel = new FileInputStream("data2.txt").getChannel(); ByteBuffer buffer = ByteBuffer.allocate(B_SIZE); fileChannel.read(buffer); buffer.flip(); // 并不能工作 System.out.println(buffer.asCharBuffer()); // 使用系统默认的字符集来编码 buffer.rewind(); String encoding = System.getProperty("file.encoding"); System.out.println("Decoded using " + encoding + ": " + Charset.forName(encoding).decode(buffer)); fileChannel = new FileOutputStream("data2.txt").getChannel(); fileChannel.write(ByteBuffer.wrap("Some text".getBytes("UTF-16BE"))); fileChannel.close(); // 编码后,现在再来读取数据 readAndDisplay(buffer); // 使用CharBuffer来写入 fileChannel = new FileOutputStream("data2.txt").getChannel(); // 分配24个字节,一个字符需要2个字节 buffer = ByteBuffer.allocate(24); buffer.asCharBuffer().put("Some text"); fileChannel.write(buffer); fileChannel.close(); readAndDisplay(buffer); } private static void readAndDisplay(ByteBuffer buffer) throws Exception { // 读取并展示出来 FileChannel fileChannel = new FileInputStream("data2.txt").getChannel(); buffer.clear(); fileChannel.read(buffer); buffer.flip(); System.out.println(buffer.asCharBuffer()); }}/* Output:卯浥⁴數Decoded using UTF-8: Some textSome textSome text*/
但是由于缓冲器容纳的是普通的字节,为了把它们转换成字符,我们需要在输入它们的时候对其进行编码,或者在把其从缓冲器输出时对它们进行解码。可以使用java.nio.charset.Charset类实现这些功能,这个类提供了把数据编码成多种不同类型的字符集的工具。可以来看看支持的编码方式:
import java.nio.charset.Charset;import java.util.Iterator;import java.util.SortedMap;public class AvailableCharSets { public static void main(String[] args) { SortedMap charSets = Charset.availableCharsets(); Iterator 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(); } }}/* Output:Big5: csBig5Big5-HKSCS: big5-hkscs, big5hk, Big5_HKSCS, big5hkscsCESU-8: CESU8, csCESU-8EUC-JP: csEUCPkdFmtjapanese, x-euc-jp, eucjis, Extended_UNIX_Code_Packed_Format_for_Japanese, euc_jp, eucjp, x-eucjpEUC-KR: ksc5601-1987, csEUCKR, ksc5601_1987, ksc5601, 5601, euc_kr, ksc_5601, ks_c_5601-1987, euckrGB18030: gb18030-2000GB2312: gb2312, euc-cn, x-EUC-CN, euccn, EUC_CN, gb2312-80, gb2312-1980...*/
返回BufferToText.java这个示例中,如果想对缓冲器调用rewind()方法来返回到数据开始部分,然后使用平台的默认字符集对数据进行decode(),那么作为结果的CharBuffer可以很好地输出打印到控制台。
另一种方式是在读文件时,使用合适的字符集进行encode(),在这里是用UTF-16BE来把文本写到文件中。在读取时,把ByteBuffer转换成CharBuffer就能产生所期望的文本。
在使用CharBuffer来向ByteBuffer写入时,我们为ByteBuffer分配了24个字节,一个字符需要2个字节,所以ByteBuffer可以容纳12个字符。而写入的文本“Some text”只有9个字符,可以发现多出部分的字节仍然会出现在输出结果中。
1.10.2. 获取基本数据
虽然ByteBuffer只能保存字节类型的数据,但是它可以从其所容纳的字节中产生出各种不同基本类型值的方法。下面的示例展示了如果使用这些方法来插入和抽取各种数值:
import java.nio.ByteBuffer;public class GetData { private static final int B_SIZE = 1024; public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(B_SIZE); // 检查ByteBuffer自动分配内容为0 int i = 0; while (i ++ < buffer.limit()){ if(buffer.get() != 0){ System.out.println("nonzero"); } } System.out.println("i = " + i); buffer.rewind(); // 存储并读取一个char数组 buffer.asCharBuffer().put("Howdy!"); char c; while((c = buffer.getChar()) != 0){ System.out.print(c + " "); } System.out.println(); buffer.rewind(); // 存储并读取一个short buffer.asShortBuffer().put((short)12345); System.out.println(buffer.getShort()); buffer.rewind(); // 存储并读取一个int buffer.asIntBuffer().put(12345678); System.out.println(buffer.getInt()); buffer.rewind(); // 存储并读取一个long buffer.asLongBuffer().put(12345678); System.out.println(buffer.getLong()); buffer.rewind(); // 存储并读取一个float buffer.asFloatBuffer().put(12345678); System.out.println(buffer.getFloat()); buffer.rewind(); // 存储并读取一个double buffer.asDoubleBuffer().put(12345678); System.out.println(buffer.getDouble()); buffer.rewind(); }}/* Output:i = 1025H o w d y ! 1234512345678123456781.2345678E71.2345678E7*/
1.10.3. 视图缓冲器
视图缓冲器(view buffer)可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer。ByteBuffer依然是实际存储数据的地方,对视图的任何修改都会映射成为对ByteBuffer中数据的修改。
这不仅使我们能很方便地向ByteBuffer插入数据,还能从ByteBuffer一次一个地(ByteBuffer本身就支持)或成批地读取放入数组中的基本类型值。
import java.nio.ByteBuffer;import java.nio.IntBuffer;public class InBufferDemo { private static final int B_SIZE = 1024; public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(B_SIZE); IntBuffer intBuffer = buffer.asIntBuffer(); // 存储一个int数组 intBuffer.put(new int[]{2, 4, 6, 8, 10}); // 绝对地址的读写 System.out.println(intBuffer.get(3)); intBuffer.put(3, 1024); intBuffer.flip(); while (intBuffer.hasRemaining()){ int i = intBuffer.get(); System.out.println(i); } }}/* Output:8246102410*/
在示例中,通过直接与ByteBuffer对话访问绝对位置的方式也同样适用于基本类型。
一旦底层的ByteBuffer通过视图缓冲器填满了数据,就可以直接被写到通道中了。使用视图缓冲器可以把任何数据都转化成某一特定的基本类型。
在下面的示例中,通过在同一个ByteBuffer上建立不同的视图缓冲器,将同一字节序列转换成了short、int、float、long和double类型的数据。
import java.nio.*;public class ViewBuffers { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.wrap( new byte[]{0, 0, 0, 0, 0, 0, 0, 'a'}); buffer.rewind(); System.out.print("Byte buffer "); while (buffer.hasRemaining()) { System.out.print(buffer.position() + " -> " + buffer.get() + ", "); } System.out.println(); CharBuffer charBuffer = ((ByteBuffer) buffer.rewind()).asCharBuffer(); System.out.print("Char buffer "); while (charBuffer.hasRemaining()) { System.out.print(charBuffer.position() + " -> " + charBuffer.get() + ", "); } System.out.println(); FloatBuffer floatBuffer = ((ByteBuffer) buffer.rewind()).asFloatBuffer(); System.out.print("Float buffer "); while (floatBuffer.hasRemaining()) { System.out.print(floatBuffer.position() + " -> " + floatBuffer.get() + ", "); } System.out.println(); IntBuffer intBuffer = ((ByteBuffer) buffer.rewind()).asIntBuffer(); System.out.print("Int buffer "); while (intBuffer.hasRemaining()) { System.out.print(intBuffer.position() + " -> " + intBuffer.get() + ", "); } System.out.println(); LongBuffer longBuffer = ((ByteBuffer) buffer.rewind()).asLongBuffer(); System.out.print("Long buffer "); while (longBuffer.hasRemaining()) { System.out.print(longBuffer.position() + " -> " + longBuffer.get() + ", "); } System.out.println(); ShortBuffer shortBuffer = ((ByteBuffer) buffer.rewind()).asShortBuffer(); System.out.print("Short buffer "); while (shortBuffer.hasRemaining()) { System.out.print(shortBuffer.position() + " -> " + shortBuffer.get() + ", "); } System.out.println(); DoubleBuffer doubleBuffer = ((ByteBuffer) buffer.rewind()).asDoubleBuffer(); System.out.print("Double buffer "); while (doubleBuffer.hasRemaining()) { System.out.print(doubleBuffer.position() + " -> " + doubleBuffer.get() + ", "); } System.out.println(); }}/* Output:Byte buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5 -> 0, 6 -> 0, 7 -> 97, Char buffer 0 -> , 1 -> , 2 -> , 3 -> a, Float buffer 0 -> 0.0, 1 -> 1.36E-43, Int buffer 0 -> 0, 1 -> 97, Long buffer 0 -> 97, Short buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 97, Double buffer 0 -> 4.8E-322, */
可以看到,从不同类型的缓冲器读取时,数据显示的方式也不同。转换成图的形式是这样的:
1.10.3.1. 字节存放次序
不同的机器可能会使用不同的字节排序方法来存储数据,当存储量大于一个字节时,像int、float等,就需要考虑字节的顺序问题。有两种字节排序方式:
- “big endian”是指高位优先,也称大端序,会将最重要的字节存放在地址最低的存储器单元。
- “little endian”是指低位优先,也称小端序,会将最重要的字节存放在地址最高的存储器单元。
ByteBuffer是以高位优先的形式存储数据的。可以用带有参数ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的order()方法来改变ByteBuffer的字节排序方式。
例如有下面两个字节的ByteBuffer:
如果用short形式读取数据,也就是用ByteBuffer.asShortBuffer()方法,得到的是数字97,对应的二进制是00000000 01100001。但是如果改为低位优先形式,仍以short形式读取数据,得到的数字却是24832,对应的二进制形式是01100001 00000000。
下面的示例展示了如何修改字节次序:
import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.Arrays;public class Endians { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.wrap(new byte[12]); buffer.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(buffer.array())); buffer.rewind(); buffer.order(ByteOrder.BIG_ENDIAN); buffer.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(buffer.array())); buffer.rewind(); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.asCharBuffer().put("abcdef"); System.out.println(Arrays.toString(buffer.array())); }}/* Output[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102][0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102][97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0]*/
1.10.4. 用缓冲器操纵数据
下面的图展示了nio类之间的关系,便于我们理解怎么移动和转换数据。例如:想要把一个字节数组写到文件中去,那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来,然后用getChannel()方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写到FileChannel中。
注意,ByteBuffer是将数据移进移出通道的唯一方式。并且我们只能创建一个独立的基本类型缓冲器,或者使用“as”方法从ByteBuffer获得数据,而不能把基本类型的缓冲器转换成ByteBuffer。由于我们可以用视图缓冲器将基本类型数据移进移出ByteBuffer,所以这个限制并没什么影响。
1.10.5. 缓冲器的细节
Buffer由数据和可以高效访问及操纵这些数据的四个索引组成,这四个索引是:mark、position、limit和capacity。在缓冲器中插入和提取数据的方法会更新这些索引。下面是用于设置和复位索引以及查询它们的值的方法:
方法名 | 说明 |
capacity() | 返回缓冲区容量 |
clear() | 清空缓冲区,将position设置为0,limit设置为容量。可以调用这个方法来覆盖缓冲区。 |
flip() | 将limit设置为position,position设置为0。这个方法用于准备从缓冲区读取已经写入的数据 |
limit() | 返回limit值 |
limit(int lim) | 设置limit值 |
mark() | 将mark设置为position |
position() | 返回position值 |
position(int pos) | 设置position值 |
remaining() | 返回(limit - position) |
hasRemaining() | 若有介于position和limit之间的元素,返回true |
下面的示例是一个交换相邻字符的算法,来对CharBuffer中的字符进行编码(scramble)和译码。
import java.nio.ByteBuffer;import java.nio.CharBuffer;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 buffer = ByteBuffer.allocate(data.length * 2); CharBuffer charBuffer = buffer.asCharBuffer(); charBuffer.put(data); System.out.println(charBuffer.rewind()); symmetricScramble(charBuffer); System.out.println(charBuffer.rewind()); symmetricScramble(charBuffer); System.out.println(charBuffer.rewind()); }}
下面是进行symmetricScramble()方法时缓冲器的样子,position指针指向缓冲器中的第一个元素,capacity和limit则指向最后一个元素:
程序执行到while循环时,使用mark()方法来设置mark的值。此时缓冲器的状态如下:
在while循环中,调用get()方法,把前两个字符分别保存到变量c1和c2中。此时缓冲器的状态如下:
为了实现交换,需要在position=0时写入c2,position=1时写入c1。可以使用绝对的put()方法来实现,也可以用reset()方法来把position的值设为mark的值。
这两个put()方法先写c2,接着写c1。
在下一次循环期间,将mark设置为position的当前值:
执行完循环后,字符完成两两交换,position指向缓冲器的末尾。因为打印缓冲器,只能打印出position和limit之间的字符,所以如果要显示缓冲器中的全部内容,需要使用rewind()把position设置到缓冲器开始的位置。下面是调用rewind()方法后缓冲器的状态:
1.10.6. 内存映射文件
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,就可以假定整个文件都放在内存中,可以完全把它当作非常大的数组来访问。这样就能极大地简化用于修改文件的代码:
import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;public class LargeMappedFiles { // 128MB private static int length = 0x8FFFFFF; public static void main(String[] args) throws Exception { MappedByteBuffer out = new RandomAccessFile("test.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开始,获得该文件上的通道,然后调用map()产生MappedByteBuffer,这是一个特殊类型的直接缓冲器。在这一步,我们必须指定映射文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的较小部分。
MappedByteBuffer由ByteBuffer继承而来,在这个示例中,只使用了put()和get()方法,当然也可以使用asCharBuffer()方法。
尽管“旧”的I/O流在使用nio实现后性能有所提高,但是映射文件访问往往可以更加显著地加快速度。下面的程序进行简单的性能比较:
import java.io.*;import java.nio.IntBuffer;import java.nio.channels.FileChannel;public class MappedIO { private static int numOfInts = 4000000; // 读写很耗性能,所以用了更小的值 private static int numOfUbuffInts = 200000; private static final String FILE_NAME = "temp.tmp"; private abstract static class Tester { private String name; public Tester(String name) { this.name = name; } public void runTest() { System.out.println(name + ": "); try { long start = System.nanoTime(); test(); double duration = System.nanoTime() - start; System.out.format("%.2f\n", duration / 1.0e9); } catch (IOException e) { throw new RuntimeException(e); } } public abstract void test() throws IOException; } private static Tester[] tests = { new Tester("Stream Write") { @Override public void test() throws IOException { DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(new File(FILE_NAME)))); for (int i = 0; i < numOfInts; i++) { out.writeInt(i); } out.close(); } }, new Tester("Mapped Write") { @Override public void test() throws IOException { FileChannel fileChannel = new RandomAccessFile(FILE_NAME, "rw").getChannel(); IntBuffer intBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()).asIntBuffer(); for (int i = 0; i < numOfInts; i++) { intBuffer.put(i); } fileChannel.close(); } }, new Tester("Stream Read") { @Override public void test() throws IOException { DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(FILE_NAME))); for (int i = 0; i < numOfInts; i++) { in.readInt(); } in.close(); } }, new Tester("Mapped Read") { @Override public void test() throws IOException { FileChannel fileChannel = new FileInputStream(new File(FILE_NAME)).getChannel(); IntBuffer intBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).asIntBuffer(); while (intBuffer.hasRemaining()) { intBuffer.get(); } fileChannel.close(); } }, new Tester("Stream Read/Write") { @Override public void test() throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile(new File(FILE_NAME), "rw"); randomAccessFile.writeInt(1); for (int i = 0; i < numOfUbuffInts; i++) { randomAccessFile.seek(randomAccessFile.length() - 4); randomAccessFile.writeInt(randomAccessFile.readInt()); } randomAccessFile.close(); } }, new Tester("Mapped Read/Write") { @Override public void test() throws IOException { FileChannel fileChannel = new RandomAccessFile(new File(FILE_NAME), "rw").getChannel(); IntBuffer intBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()).asIntBuffer(); intBuffer.put(0); for (int i = 1; i < numOfUbuffInts; i++) { intBuffer.put(intBuffer.get(i - 1)); } fileChannel.close(); } } }; public static void main(String[] args) { for (Tester tester : tests) { tester.runTest(); } }}/* OutputStream Write: 0.78Mapped Write: 0.06Stream Read: 0.57Mapped Read: 0.01Stream Read/Write: 12.04Mapped Read/Write: 0.01*/
1.10.7. 文件加锁
在JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地的操作系统的加锁工具。
import java.io.FileOutputStream;import java.nio.channels.FileLock;import java.util.concurrent.TimeUnit;public class FileLocking { public static void main(String[] args)throws Exception { FileOutputStream out = new FileOutputStream("file.txt"); FileLock fileLock = out.getChannel().tryLock(); if(fileLock != null){ System.out.println("Locked file"); TimeUnit.MILLISECONDS.sleep(100); // 释放锁 fileLock.release(); System.out.println("Released lock"); } out.close(); }}
通过对FileChannel调用tryLock()或lock()方法,就可以获得整个文件的FileLock。这两个方法的区别在于:
- tryLock()是非阻塞式的。它会设法获取锁,如果不能获得锁,例如其他一些进程已经持有相同的锁,并且不共享时,它将直接从方法调用返回。
- lock()则是阻塞式的,它会阻塞进程直到可以获得锁,除非调用lock()的线程中断,或调用lock()的通道关闭。
也可以使用下面的方法对文件的一部分上锁,其中加锁的区域由size-position决定,而第三个参数指定是否为共享锁:
- tryLock(long position, long size, boolean shared)
- lock(long position, long size, boolean shared)
使用无参方法是对整个文件进行加锁。而使用带参数的方法是对文件的某个区域进行加锁,当文件增大时,position+size之外的部分不会被锁定。
对独占锁或者共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁并为每一个请求都创建一个锁,那么它就会使用独占锁。可以用FileLock.isShared()查询是否为共享锁。
1.10.7.1. 对映射文件的部分加锁
文件映射通常应用于极大的文件。我们可以对这种大文件进行部分加锁,以便其他进程可以修改文件中未被加锁的部分。下面的示例用了两个线程来分别对文件的不同部分进行加锁:
import java.nio.channels.FileLock;public class LockingMappedFiles { // 128MB private static final int LENGTH = 0x8FFFFFF; private static FileChannel fileChannel; public static void main(String[] args) throws Exception { fileChannel = new RandomAccessFile("test.dat", "rw").getChannel(); MappedByteBuffer out = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH); for (int i = 0; i < LENGTH; i++) { out.put((byte) 'x'); } new LockAndModify(out, 0, LENGTH / 3); new LockAndModify(out, LENGTH / 2, LENGTH / 2 + LENGTH / 4); } private static class LockAndModify extends Thread { private ByteBuffer buffer; private int start, end; public LockAndModify(ByteBuffer buffer, int start, int end) { this.start = start; this.end = end; buffer.limit(end); buffer.position(start); this.buffer = buffer.slice(); start(); } public void run() { try { // 互斥锁,没有重叠 FileLock fileLock = fileChannel.lock(start, end, false); System.out.println("Locked: " + start + " to " + end); // 执行修改 while (buffer.position() < buffer.limit() - 1) { buffer.put((byte) (buffer.get() + 1)); } fileLock.release(); System.out.println("Released: " + start + " to " + end); } catch (IOException e) { throw new RuntimeException(e); } } }}/* OutputLocked: 0 to 50331647Locked: 75497471 to 113246206Released: 75497471 to 113246206Released: 0 to 50331647*/
1.11. 压缩
Java I/O类库中的类支持读写压缩格式的数据流,可以用它们对其他的I/O类进行封装,以提供压缩功能。这些类是属于InputStream和OutputStream继承层次结构的一部分。下面的表格中是常用的压缩类,其中Zip和GZIP是最常用的。
压缩类 | 功能 |
CheckedInputStream | GetCheckSum()方法为任何InputStream产生校验和 |
CheckedOutputStream | GetCheckSum()方法为任何OutputStream产生校验和 |
DeflaterOutputStream | 压缩类的基类 |
ZipOutputStream | 用于将数据压缩成Zip文件格式 |
GZIPOutputStream | 用于将数据压缩成GZIP文件格式 |
InflaterInputStream | 解压缩类的基类 |
ZipInputStream | 用于解压缩Zip文件格式的数据 |
GZIPInputStream | 用于解压缩GZIP文件格式的数据 |
1.11.1. 用GZIP进行简单压缩
GZIP接口非常简单,适合对单个数据流进行压缩。下面是对单个文件进行压缩的示例:
import java.io.*;import java.util.zip.GZIPInputStream;import java.util.zip.GZIPOutputStream;public class GZIPCompress { public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new FileReader("GZIPCompress.java")); 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.closes(); 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); } }}
压缩类的使用很直观:直接将输出流封装成GZIPOutputStream或ZipOutputStream,并将输入流封装成GZIPInputStream或ZipInputStream即可。其他全部操作就是通常的I/O读写。这个例子把面向字符的流与面向字节的流混合了起来,输入用Reader类,而GZIPOutputStream的构造器只能接受OutputStream对象。在打开文件时,GZIPInputStream就会被转换成Reader。
1.11.2. 用Zip进行多文件保存
支持Zip格式的Java类库可以方便地保存多个文件。下面的示例使用了Checksum类来执行校验和,有Adler32和CRC32两种Checksum类型,前者要快一些,后者则更准确。
import java.io.*;import java.util.Enumeration;import java.util.zip.*;public class ZipCompress { public static void main(String[] args) throws IOException { // 压缩文件 FileOutputStream fileOut = new FileOutputStream("test.zip"); CheckedOutputStream checkOut = new CheckedOutputStream(fileOut, new Adler32()); ZipOutputStream zipOut = new ZipOutputStream(checkOut); BufferedOutputStream out = new BufferedOutputStream(zipOut); String arg = "ZipCompress.java"; System.out.println("Writing file " + arg); BufferedReader in = new BufferedReader(new FileReader(arg)); zipOut.putNextEntry(new ZipEntry(arg)); int c; while ((c = in.read()) != -1) { out.write(c); } in.close(); out.close(); System.out.println("Checksum: " + checkOut.getChecksum().getValue()); // 解压缩文件 System.out.println("Reading file"); FileInputStream fileIn = new FileInputStream("test.zip"); CheckedInputStream checkIn = new CheckedInputStream(fileIn, new Adler32()); ZipInputStream zipIn = new ZipInputStream(checkIn); BufferedInputStream bufferIn = new BufferedInputStream(zipIn); ZipEntry zipEntry; while ((zipEntry = zipIn.getNextEntry()) != null) { System.out.println("Reading file " + zipEntry); int x; while ((x = bufferIn.read()) != -1) { System.out.write(x); } } bufferIn.close(); // 解压缩文件更方便的方法 ZipFile zipFile = new ZipFile("test.zip"); Enumeration enumeration = zipFile.entries(); while (enumeration.hasMoreElements()) { ZipEntry zipEntry2 = (ZipEntry) enumeration.nextElement(); System.out.println("File: " + zipEntry2); } }}
对于每一个要加入压缩档案的文件,都必须调用putNextEntry()方法,并将其传递给一个ZipEntry对象。ZipEntry对象包含了一个功能很广泛的接口,允许获取和设轩Zip文件内该特定项上所有可利用的数据:名字、压缩的和未压缩的文件大小、日期、CRC校验和、额外字段数据、注释、压缩方法以及它是否是一个目录入口等等 。
虽然Zip格式提供了设置密码的方法,但Java的Zip类库并不提供这方面的支持。而且虽然CheckedInputStream和CheckedOutputStream都支持Adler32和CRC32两种类型的校验和,但是ZipEntry类只有一个支持CRC的接口,所以在Java中不能使用速度更快的Adler32。
为了能够解压缩文件,ZipInputStream提供了一个getNextEntry()方法返回下一个ZipEntry。解压缩文件有一个更简便的方法:利用ZipFile对象读取文件,这个对象有一个entried()方法用来向ZipEntries返回一个Enumeration。
为了读取校验和,必须拥有与之相关联的Checksum对象的访问权限。在这个示例中保留了指向CheckedOutputStream和CheckedInputStream对象的引用,也可以只保留一个指向Checksum对象的引用。
1.11.3. Java档案文件
Zip格式也被应用于JAR(Java ARchive,Java档案文件)文件格式中。这种文件格式也可以将一组文件压缩到单个压缩文件中,声音和图像文件可以像类文件一样被包含在其中,并且JAR文件也是跨平台的。
一个JAR文件由一组压缩文件构成,同时还有一张描述所有这些文件的“文件清单”。这个文件清单可以自行创建,也可以由jar程序自动生成。
Sun的JDK自带的jar程序可以根据我们的选择自动压缩文件。可以用命令行的形式调用它,其中options只是一个字母集合,不必输入“-”或其他标识符:jar [options] destination [manifest] inputfile(s)
。下面的选项字符在Unix系统和Linux系统中的tar文件中也具有相同的意义:
字符 | 说明 |
e | 创建一个新的或空的压缩文档 |
t | 列出目录表 |
x | 解压所有文件 |
x file | 解压该文件 |
f | 指定一个文件名。如果没有用这个选项,jar假设所有的输入都来自于标准输入,或者在创建一个文件时,输出对象也假设为标准输出 |
m | 表示第一个参数将是用户自建的清单文件的名字 |
v | 产生详细输出,描述jar所做的工作 |
O | 只储存文件,不压缩文件,可以用来创建一个可放在类路径中的JAR文件 |
M | 不自动创建文件清单 |
下面是一些调用jar的典型方法:
jar cf myJarFile.jar *.class
:创建一个名为myJarFile.jar的JAR文件,该文件包含了当前目录的所有子目录中的所有类文件,以及自动产生的清单文件。jar cmf myJarFile.jar myManifestFile.mf *.class
:与前一个类似,还添加了一个名为myManifestFile.mf的用户自建清单文件jar tf myJarFile.jar
:产生myJarFile.jar内所有文件的一个目录表jar tvf myJarFile.jar
:可以提供有关myJarFile.jar中的文件的更详细的信息jar cvf myApp.jar audio classes image
:假定audio、classes和image是子目录,这个命令将所有子目录合并到文件myApp.jar中。
如果没有用可选选项创建了一个JAR文件,那么这个文件可以放入类路径变量(CLASSPATH)中:CLASSPATH="lib1.jar;lib2.jar"
,然后Java就可以在lib1.jar和lib2.jar中搜索目标类文件了。
jar工具的功能没有zip工具那么强大,既不能对已有的JAR文件进行添加或更新,只能从头创建一个JAR文件,也不能将文件移动到一个JAR文件,并在移动后将它们删除。不过在一个平台上创建的JAR文件可以被在其他任何平台上的jar工具透明的阅读。
1.12. 对象序列化
有的时候我们需要对象能够在程序不运行的情况下仍然存在并保存其信息,在下次运行程序时,该对象将被重建并且拥有的信息与在程序上将运行时它所拥有的信息相同。
Java的对象序列化是把实现了Serializable接口的对象转换成一个字节序列,并能在之后将这个字节完全恢复为原来的对象。
可以利用对象的序列化来实现轻量级持久性(lightweight persistence)。
- “持久性”意味着一个对象的生存周期并不取决于程序是否正在执行,这个对象可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新运行程序时恢复该对象,就能够实现持久性的效果。
- “轻量级”是因为不能用某种“persistent”关键字来简单定义一个对象,并让系统自动维护其他细节问题。相反,对象必须在程序中显式地序列化和反序列化还原。
对象序列化的概念加入到语言中是为了支持两种主要特性:
- Java的远程方法调用(Remote Method Invocation,RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。当像远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
- 对Java Beans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并且在程序启动时进行后期恢复,也就是用对象序列化来完成。
对象序列化与反序列化的步骤:
- 首先需要序列化的对象要实现Serializable接口,这个接口仅是一个标记接口,不包括任何方法方法。
- 对象序列化需要创建OutputStream对象,将其封装进ObjectOutputStream对象内。这时,调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream。
- 对象反序列化需要创建InputStream对象,将其封装进ObjectInputStream内,然后调用readObject()。
- 反序列化后获得的是一个引用,这个引用指向一个向上转型的Object,需要向下转型才能使用。
例如,现在有一个Alien类需要被序列化:
import java.io.Serializable;public class Alien implements Serializable {}
序列化Alien的方式:
import java.io.FileOutputStream;import java.io.ObjectOutput;import java.io.ObjectOutputStream;public class FreezeAlien { public static void main(String[] args) throws Exception{ ObjectOutput out = new ObjectOutputStream(new FileOutputStream("X.file")); Alien alien = new Alien(); out.writeObject(alien); }}
反序列化的方式:
import java.io.FileOutputStream;import java.io.ObjectOutput;import java.io.ObjectOutputStream;public class FreezeAlien { public static void main(String[] args) throws Exception{ ObjectOutput out = new ObjectOutputStream(new FileOutputStream("X.file")); Alien alien = new Alien(); out.writeObject(alien); }}
对象序列化的特点在于它不仅保存了对象的全部信息,还能追踪对象内所包含的所有引用,并保存那些对象,还能对这些引用进行追踪。这种情况也被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。
下面的示例通过对链接的对象生成一个worm对序列化机制进行测试,每个对象都与worm中的下一段链接,同时又与属于不同类(Data)的对象引用数据链接:
import java.io.*;import java.util.Random;class Data implements Serializable { private int n; public Data(int n) { this.n = n; } @Override public String toString() { return Integer.toString(n); }}public class Worm implements Serializable { // 随机生成3个数字 private static Random random = new Random(47); private Data[] dataArray = { new Data(random.nextInt(10)), new Data(random.nextInt(10)), new Data(random.nextInt(10)) }; private Worm next; private char c; // 递归生成链接的Worm列表 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"); } @Override public String toString() { StringBuilder result = new StringBuilder(":"); result.append(c); result.append("("); for (Data data : dataArray) { result.append(data); } result.append(")"); if (next != null) { result.append(next); } return result.toString(); } public static void main(String[] args) throws ClassNotFoundException, IOException { Worm worm = new Worm(6, 'a'); System.out.println("w = " + worm); // 序列化到文件worm.out ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out")); out.writeObject("Worm storage\n"); out.writeObject(worm); out.close(); // 反序列化 ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out")); // 读取出来的字符串是"Worm storage\n" String s = (String) in.readObject(); Worm w2 = (Worm) in.readObject(); System.out.println(s + "w2 = " + w2); // 把反序列化后的worm对象写入字节流中 ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(byteOut); out2.writeObject("Worm storage\n"); out2.writeObject(worm); out2.flush(); // 从字节流中读取数据 ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray())); s = (String) in2.readObject(); Worm w3 = (Worm) in2.readObject(); System.out.println(s + "w3 = " + w3); }}/* OutputWorm constructor: 6Worm constructor: 5Worm constructor: 4Worm constructor: 3Worm constructor: 2Worm constructor: 1w = :a(853):b(119):c(802):d(788):e(199):f(881)Worm storagew2 = :a(853):b(119):c(802):d(788):e(199):f(881)Worm storagew3 = :a(853):b(119):c(802):d(788):e(199):f(881)*/
1.12.1. 序列化的控制
如果不希望对象的某一部分被序列化;或者一个对象被还原后,某子对象需要重新创建,从而不必将该子对象序列化。在这些特殊情况下,可以通过实现Externalizable接口来代替Serializable接口,对序列化进行控制。
下面的示例展示了Externalizable接口方法的简单实现:
import java.io.*;class Blip1 implements Externalizable { public Blip1() { System.out.println("Blip1 Constructor"); } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip1.writeExternal"); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip1.readExternal"); }}class Blip2 implements Externalizable { Blip2() { System.out.println("Blip2 Constructor"); } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip2.writeExternal"); } @Override 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 blip1 = new Blip1(); Blip2 blip2 = new Blip2(); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Blips.out")); System.out.println("Saving objects"); out.writeObject(blip1); out.writeObject(blip2); out.close(); // 现在来取回数据 ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blips.out")); System.out.println("Recovering blip1: "); blip1 = (Blip1) in.readObject(); // 因为Blip2的构造器不是public的,会发生异常:java.io.InvalidClassException: Blip2; no valid constructor// System.out.println("Recovering blip2: ");// blip2 = (Blip2) in.readObject(); }}/* Output:Constructing objectsBlip1 ConstructorBlip2 ConstructorSaving objectsBlip1.writeExternalBlip2.writeExternalRecovering blip1: Blip1 ConstructorBlip1.readExternal*/
对于恢复一个Seriallizable对象,对象完全以它存储的二进制位为基础来构造,而不会调用构造器。而对于一个Externalizable对象,会调用默认的构造器,然后调用readExternal()方法,才能使这个对象产生正确的行为。
下面的示例展示了如何完整保存和恢复一个Externalizable对象:
import java.io.*;public class Blip3 implements Externalizable { private int i; private String s; public Blip3() { System.out.println("Blip3 Constructor"); } public Blip3(int i, String s) { System.out.println("Blip3(int i, String s)"); this.i = i; this.s = s; } @Override public String toString() { return s + i; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip3.writeExternal"); out.writeObject(s); out.writeInt(i); } @Override 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 blip3 = new Blip3(47, "A String "); System.out.println(blip3); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Blip3.out")); System.out.println("Saving object: "); out.writeObject(blip3); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out")); System.out.println("Recovering blip3: "); // 可以从输出看到,恢复对象时调用的是默认的无参构造器 blip3 = (Blip3) in.readObject(); System.out.println(blip3); }}/* Output:Constructing objects: Blip3(int i, String s)A String 47Saving object: Blip3.writeExternalRecovering blip3: Blip3 ConstructorBlip3.readExternalA String 47*/
1.12.1.1. transient关键字
当我们对序列化进行控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复,如密码这样的敏感信息。即使对象中的这些信息是private属性,一经序列化处理,依然可以通过读取文件或拦截网络传输的方式来访问到它。
一种防止对象的敏感部分被序列化的方法是把类实现为Externalizable,这样没有任何数据可以自动序列化,需要在writeExternal()方法内部对所需要的部分进行显式的序列化。
另一种方法是在Serializable对象中使用transient关键字逐个字段地关闭序列化。由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serialiable对象一起使用。
例如有一个Login对象是用来保存登录会话信息的,登录的合法性通过校验后,我们想把数据保存下来,但不包括密码。要实现这一目标,最简单的方法是实现Serializable接口:
import java.io.*;import java.util.Date;import java.util.concurrent.TimeUnit;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; } @Override 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("MyDear", "littleGoldFish"); 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); }}/* Output:logon = logon info: username: MyDear date: Wed Sep 09 20:05:07 CST 2020 password: littleGoldFishRecovering object at Wed Sep 09 20:05:09 CST 2020logon = logon info: username: MyDear date: Wed Sep 09 20:05:07 CST 2020 password: null*/
在输出结果中可以看到,password字段在对象被恢复时变成了null。
1.12.1.2. Externalizable的替代方法
如果不想实现Externalizable接口,又想控制序列化过程。那么可以实现Serializable接口,并添加private的writeObejct()方法和readObject()方法,这样就可以使用自定义的方法而不是默认的序列化机制。
下面的示例展示了使用方法:
import java.io.*;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; } @Override public String toString() { return a + "\n" + b; } private void writeObject(ObjectOutputStream outputStream) throws IOException { // 依然可以执行默认的序列化机制 outputStream.defaultWriteObject(); // 自定义的部分,可以从输出中看到transient修饰的字段依然被序列化了 outputStream.writeObject(b); } private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); b = (String) inputStream.readObject(); } public static void main(String[] bu) throws Exception { SerialCtl serialCtl = new SerialCtl("Test1", "Test2"); System.out.println("Before:\n" + serialCtl); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(buffer); out.writeObject(serialCtl); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); SerialCtl result = (SerialCtl) in.readObject(); System.out.println("After:\n" + result); }}/* Output:Before:Not transient: Test1Transient: Test2After:Not transient: Test1Transient: Test2*/
1.12.2. 使用“持久化”
有一种使用序列化技术的想法是:存储程序的一些状态,以便我们随后可以很容易地将程序恢复到当前状态。在下面的示例中,创建了一个Animal列表并将其两次序列化,分别送至不同的流:
import java.io.*;import java.util.ArrayList;import java.util.List;class House implements Serializable {}class Animal implements Serializable { private String name; private House preferredHouse; public Animal(String name, House preferredHouse) { this.name = name; this.preferredHouse = preferredHouse; } @Override public String toString() { return name + "[" + super.toString() + "], " + preferredHouse + "\n"; }}public class MyWorld { public static void main(String[] args) throws Exception { House house = new House(); List animalList = new ArrayList<>(); animalList.add(new Animal("dog", house)); animalList.add(new Animal("cat", house)); animalList.add(new Animal("fish", house)); System.out.println("animals: " + animalList); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(buffer); out.writeObject(animalList); out.writeObject(animalList); ByteArrayOutputStream buffer2 = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(buffer2); out2.writeObject(animalList); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buffer2.toByteArray())); List animalList1 = (List) in.readObject(), animalList2 = (List) in.readObject(), animalList3 = (List) in2.readObject(); System.out.println("animalList1: " + animalList); System.out.println("animalList2: " + animalList); System.out.println("animalList3: " + animalList); }}/* OutputanimalList: [dog[Animal@2b193f2d], House@355da254, cat[Animal@4dc63996], House@355da254, fish[Animal@d716361], House@355da254]animalList1: [dog[Animal@4edde6e5], House@70177ecd, cat[Animal@1e80bfe8], House@70177ecd, fish[Animal@66a29884], House@70177ecd]animalList2: [dog[Animal@4edde6e5], House@70177ecd, cat[Animal@1e80bfe8], House@70177ecd, fish[Animal@66a29884], House@70177ecd]animalList3: [dog[Animal@4769b07b], House@cc34f4d, cat[Animal@17a7cec2], House@cc34f4d, fish[Animal@65b3120a], House@cc34f4d]*/
我们可以通过一个字节数组来使用对象序列化,从而实现对Serializable对象的“深度复制”(deep copy),也就是复制整个对象网。
虽然我们期望这些反序列化还原的对象地址与原来的地址不同,但是在输出结果中animalList1与animalList2出现了相同的地址,包括二者共享的引用。而反序列化animalList3时,因为系统无法知道另一个流内的对象是第一个流内的对象的别名,所以输出产生完全不同的对象网。
如果我们想保存系统状态,最安全的做法是将其作为“原子”操作进行序列化。将构成系统状态的所有对象都置入单一容器内,并在一个操作中将该容器直接写出,然后同样只需一次方法调用,就可以将其恢复。
下面这个例子是一个想象的计算机辅助设计(CAD)系统,在示例中直接对Class对象序列化来保存static字段。
import java.io.*;import java.util.ArrayList;import java.util.List;import java.util.Random;abstract class Shape implements Serializable { public static final int RED = 1, BLUE = 2, GREEN = 3; private int xPos, yPos, dimension; private static Random random = new Random(47); private static int counter = 0; public abstract void setColor(int color); public abstract int getColor(); public Shape(int xPos, int yPos, int dimension) { this.xPos = xPos; this.yPos = yPos; this.dimension = dimension; } @Override public String toString() { return getClass() + "color[" + getColor() + "] xPos[" + xPos + "] yPos[" + yPos + "] dim[" + dimension + "]\n"; } public static Shape randomFactory() { int xVal = random.nextInt(100); int yVal = random.nextInt(100); int dim = random.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 xPos, int yPos, int dimension) { super(xPos, yPos, dimension); } @Override public void setColor(int color) { Circle.color = color; } @Override public int getColor() { return color; }}class Square extends Shape { private static int color; public Square(int xPos, int yPos, int dimension) { super(xPos, yPos, dimension); color = BLUE; } @Override public void setColor(int color) { Square.color = color; } @Override public int getColor() { return color; }}class Line extends Shape { private static int color = RED; public Line(int xPos, int yPos, int dimension) { super(xPos, yPos, dimension); } @Override public void setColor(int color) { Line.color = color; } @Override public int getColor() { return color; } public static void serializeStaticState(ObjectOutputStream out) throws IOException { out.writeInt(color); } public static void deserializeStaticState(ObjectInputStream in) throws IOException { Line.color = in.readInt(); }}public class StoreCADState { public static void main(String[] args) throws Exception { List> shapeTypes = new ArrayList<>(); shapeTypes.add(Circle.class); shapeTypes.add(Square.class); shapeTypes.add(Line.class); // 生成一些形状 List shapeList = new ArrayList<>(); for (int i = 0; i < 10; i++) { shapeList.add(Shape.randomFactory()); } // 给所有的形状修改颜色 for (int i = 0; i < 10; i++) { shapeList.get(i).setColor(Shape.GREEN); } // 序列化形状的类列表 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("CADState.out")); out.writeObject(shapeTypes); // 序列化形状列表 Line.serializeStaticState(out); out.writeObject(shapeList); System.out.println(shapeList); }}/* Output:[class Circlecolor[3] xPos[58] yPos[55] dim[93], class Squarecolor[3] xPos[61] yPos[61] dim[29], class Linecolor[3] xPos[68] yPos[0] dim[22], class Circlecolor[3] xPos[7] yPos[88] dim[28], class Squarecolor[3] xPos[51] yPos[89] dim[9], class Linecolor[3] xPos[78] yPos[98] dim[61], class Circlecolor[3] xPos[20] yPos[58] dim[16], class Squarecolor[3] xPos[40] yPos[11] dim[22], class Linecolor[3] xPos[4] yPos[83] dim[6], class Circlecolor[3] xPos[75] yPos[10] dim[42]]*/
现在来反序列化这个CAD示例:
import java.io.FileInputStream;import java.io.ObjectInputStream;import java.util.List;public class RecoverCADState { @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception{ ObjectInputStream in = new ObjectInputStream(new FileInputStream("CADState.out")); List> shapeTypes = (List>) in.readObject(); Line.deserializeStaticState(in); List shapeList = (List) in.readObject(); System.out.println(shapeList); }}/* Output[class Circlecolor[1] xPos[58] yPos[55] dim[93], class Squarecolor[0] xPos[61] yPos[61] dim[29], class Linecolor[3] xPos[68] yPos[0] dim[22], class Circlecolor[1] xPos[7] yPos[88] dim[28], class Squarecolor[0] xPos[51] yPos[89] dim[9], class Linecolor[3] xPos[78] yPos[98] dim[61], class Circlecolor[1] xPos[20] yPos[58] dim[16], class Squarecolor[0] xPos[40] yPos[11] dim[22], class Linecolor[3] xPos[4] yPos[83] dim[6], class Circlecolor[1] xPos[75] yPos[10] dim[42]]*/
可以发现,形状的颜色这个static字段并没有按照预期恢复成3,而是类中初始化的值。所以虽然Class类是Serializable的,但是想序列化static值还需要自己手动去实现,也就是显示调用Line类中的serializeStaticState()方法和deserializeStaticState()方法。
1.13. Preferences
Preferences API与对象序列化相比,可以自动存储和读取信息,不过只能用于基本类型和字符串,并且每个字符串的存储长度不能超过8K。也就是说Preferences API是用于存储和读取用户偏好以及程序配置项的设置。
Preferences是一个类似映射的键值集合,存储在一个节点层次的结构中。通常会创建以类名命名的单一节点,然后将信息存储于其中。
import java.util.prefs.Preferences;public class PreferencesDemo { public static void main(String[] args) throws Exception { Preferences preferences = Preferences.userNodeForPackage(PreferencesDemo.class); preferences.put("Background image", "xxx.jpg"); preferences.put("Font color", "black"); preferences.putInt("Font size", 24); preferences.putBoolean("Animate Window", true); int usageCount = preferences.getInt("UsageCount", 0); usageCount++; preferences.putInt("UsageCount", usageCount); for (String key : preferences.keys()) { // 取数据时需要提供默认值 System.out.println(key + ": " + preferences.get(key, null)); } }}
在示例中使用的节点是userNodeForPackage(),一般用于个别用户偏好;而systemNodeForPackage()一般用于通用的系统配置。一旦我们创建了节点,就可以用它来加载或者读取数据了。在读取数据的get()方法中,第二个参数是需要提供了默认值。
在我们第一次运行程序时,UsageCount的值是0,但是在随后的引用中,它将每次都加1。Preferences API会利用合适的系统资源来存储数据,例如在Windows系统中是使用注册表。