《Thinking in Java》第18章的内容是相当丰富精彩的,也在网络学习参考了很多前辈们的笔记,个人由于能力有限(毕竟和大神Bruce Eckel的能力相差甚远),将这一章的内容分三个部分来写,希望能够慢慢品味和领悟Java IO的精粹:
第一部分、字节流和字符流,可以认为是传统的Java IO;
第二部分、新IO,指JDK 1.4引入的java.nio.*
类库;
第三部分、Java序列化和反序列化技术,包括了对象序列化、XML等内容。
当然,笔者的能力相当有限,纯属一个菜鸟做得笔记,希望各位前辈不吝赐教!言归正传,本篇主要讲Java IO的字节流和字符流,章节安排如下:
令我很吊胃口的一件事情是,当我翻开圣经,想拜读Java IO的精髓时,Eckel告诉我,在学习真正用于流读写数据的类之前,让我们先学习如何处理文件目录问题(潜台词仿佛在说,对不起,菜鸟,你得从基础班开始!)
File类:Java中
File
既能代表一个特定文件的名称,又能代表一个目录下的一组文件(相当于Files)
我们要学会的是,如何从一个目录下筛选出我们想要的文件?可以通过目录过滤器FilenameFilter
+正则表达式实现对文件的筛选:
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* 目录过滤器
* 显示符合条件的File对象
* @author 15070229
*
*/
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();
}
}
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);
}
}
这个用匿名内部类的方式来实现是更合适的,DirFilter
在内部实现,使程序变的更小巧灵活:
* 通过内部类方式实现的目录列表器
*/
package c18;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.regex.Pattern;
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(".");
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);
}
}
针对一些文件集上的常用操作我们可以封装成一个工具类,比如:
/**
* 目录实用工具类
* 本地目录操作:local方法产生经过正则表达式筛选的本地目录的文件数组
* 目录树操作:walk方法产生给定目录下的由整个目录树中经过正则表达式筛选的文件构成的列表
*/
package c18;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
public final class Directory {
/**
* 根据正则表达式,筛选产生File数组
* @param dir
* @param regex
* @return
*/
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(name).matches();
}
});
}
// 方法重载
public static File[] local(String path, final String regex) {
return local(new File(path), regex);
}
/*
* TreeInfo的使命是收集返回的目录和文件信息
*/
public static class TreeInfo implements Iterable<File> {
public List files = new ArrayList();
public List dirs = new ArrayList();
//
public Iterator 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);
}
}
/**
* 遍历目录
* @param start
* @param regex
* @return
*/
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), ".*");
}
/**
* 递归遍历文件目录,收集更多的信息(区分普通文件和目录)
* @param startDir
* @param regex
* @return
*/
static TreeInfo recurseDirs(File startDir, String regex) {
TreeInfo result = new TreeInfo();
for(File item : startDir.listFiles()) {
if (item.isDirectory()) {
//持有目录
result.dirs.add(item);
} else if(item.getName().matches(regex))
//持有普通文件
result.files.add(item);
}
return result;
}
public static void main(String[] args) {
//
PPrint.pprint(Directory.walk(".").dirs);
//
for(File file : Directory.local(".", "T.*"))
System.out.println(file);
}
}
/**
- 格式化打印机,打印格式如下:
- [
- .\.settings
- .\bin
- .\src
- ]
*/
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(Arrays.asList(c));
}
}
Java IO中处理字节流和字符流输入输出的基类和派生类繁多,本节主要做两件事情:(1)给出类的关系图;(2)回答一个问题,为什么字节流和字符流要分开处理,为什么既要有Reader/Writer
,又保留InputStream/OutputStream
?
字节流和字符流输入输出的基类和派生类的关系图如下所示:(图片来源于网络,笔者想吐槽这张关系图太难画了,废了半小时力气最终还是放弃了,可见JAVA IO类关系的复杂性)
Java 1.0中是只存在InputStream/OutputStream
的,设计Reader/Writer
主要是为了国际化需要。老的IO结构设计仅支持8位字节流,并且不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化,因此添加了Reader/Writer
。
那为什么还要继续使用InputStream/OutputStream
,二者的用处是不一样的,有些时候使用InputStream/OutputStream
才能得到正确的结果:
所以,不同的场景下究竟是使用字节流还是字符流就体现了一个程序员对Java的理解程度了。
Java IO中分别用到了装饰者模式和适配者模式。
装饰模式(Decorator)又称为包装模式(Wrapper),通过创建一个装饰(包装)对象,来装饰真实的对象。
Java I/O类库需要多种不同功能的组合,这正是使用装饰器模式的理由所在。为什么不使用继承而采用装饰器模式呢?如果说Java IO的各种组合是通过继承方式来实现的话,那么每一种组合都需要一个类,这样就会出现大量重复性的问题。而通过装饰器来实现组合,恰恰避免了这个问题。
对于字节流而言,FilterInputStream
和FilterOutputStream
是用来提供装饰器接口以控制特定输入\输出的两个类。需要注意的是,对于字符流而言,同样用到的是装饰者模式,但是有一点不同,Reader体系中的FilterRead类和InputStream体系中的FilterInputStream的功能不同,它不再是装饰者。
这一点可以从BufferReader
和BufferStreamReader
的实现不同可以看出。
BufferReader:public class BufferedReader extends Reader
BufferedInputStream:public class BufferedInputStream extends FilterInputStream
举一个例子如下所示,其中BufferedReader是装饰对象,FileReader是被装饰对象。
package c18;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedInputFile {
public static String
read(String filename) throws IOException{
//BufferedReader是装饰对象,FileReader是被装饰对象
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();
}
}
适配者模式又可以分为类适配方式(类似多继承)和对象适配方式。而Java IO中使用的是对象适配方式。我们以FileOutputStream
为例,可以看到源码中,FileOutputStream
继承了OutputStream
类型,同时持有一个对FileDiscriptor
对象的引用。这是一个将FileDiscriptor
接口适配成OutputStream
接口形式的对象形适配器模式。
public class FileOutputStream extends OutputStream
{
private FileDescriptor fd;
public FileOutputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkWrite(fdObj);
}
fd = fdObj;
fd.incrementAndGetUseCount();
}
}
本节主要介绍基于字节流的输入InputStream
和输出OutputStream
,以及用于实现装饰模式的FilterInputStream
和FilterOutputStream
。
InputStream
的作用是表示从不同数据源产生输入的类:
下面列表展现了InputStream
的派生类,所有的派生类都要联合装饰类FilterInputStream
组合使用:
/**
* Creates a ByteArrayInputStream
* so that it uses buf
as its
* buffer array.
* The buffer array is not copied.
* The initial value of pos
* is 0
and the initial value
* of count
is the length of
* buf
.
*
* @param buf the input buffer.
*/
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
举个例子:
/**
* 格式化内存输出
*/
package c18;
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面向字节的装饰类
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(
//BufferedInputFile.read上例中已经实现了
BufferedInputFile.read("D:/workspace/java_learning/"
+ "Java_Learning/src/c18/DirList2.java").getBytes()));
while (true) {
//readByte按字节输出
System.out.println((char)in.readByte());
}
} catch (EOFException e) {
System.err.println("End of stream");
}
}
}
String
作为输入String
需要注意的是StringBufferInputStream
已经过时了,JDK给出的过时原因如下:
This class does not properly convert characters into bytes. As of JDK 1.1, the preferred way to create a stream from a string is via the
StringReader
class.
StringBufferInputStream
已经不再适合将字符转化为字节,更优的选择是通过StringReader
将字符转化为流。
//String
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
//File
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
fd = new FileDescriptor();
fd.incrementAndGetUseCount();
open(name);
}
//FileDescriptor
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
fd.incrementAndGetUseCount();
}
PipedOutputStream
中的数据,实现“管道化”PipedOutputStream
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException {
initPipe(pipeSize);
connect(src);
}
管道流可以实现两个线程之间,二进制数据的传输。管道流就像一条管道,一端输入数据,别一端则输出数据。通常要分别用两个不同的线程来控制它们。(这里埋个伏笔,目前笔者对多线程掌握还不够成熟,等到后面学习Java并发中会继续提到PipedIntputStream
/PipedOutputStream
)
InputStream
对象转换成单一InputStream
InputStream
或一个容器Enumeration
public SequenceInputStream(Enumeration extends InputStream> e) {
this.e = e;
try {
nextStream();
} catch (IOException ex) {
// This should never happen
throw new Error("panic");
}
}
public SequenceInputStream(InputStream s1, InputStream s2) {
Vector v = new Vector(2);
v.addElement(s1);
v.addElement(s2);
e = v.elements();
try {
nextStream();
} catch (IOException ex) {
// This should never happen
throw new Error("panic");
}
}
举两个例子(源于网络):
import java.io.*;
import java.util.*;
class SequenceDemo1
{
public static void main(String[] args)throws IOException
{
Vector v = new Vector();
v.add(new FileInputStream("c:\\1.txt"));
v.add(new FileInputStream("c:\\2.txt"));
v.add(new FileInputStream("c:\\3.txt"));
Enumeration en = v.elements();
SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("c:\\4.txt");
byte[] buf = new byte[1024];
int len = 0;
while((len=sis.read(buf))!=-1)
{
fos.write(buf,0,len);
}
fos.close();
sis.close();
}
}
import java.io.*;
import java.util.*;
class SequenceDemo2
{
public static void main(String[] args)throws IOException
{
InputStream is1 = null;
InputStream is2 = null;
OutputStream os = null;
SequenceInputStream sis = new null;
is1 = new FileInputStream("d:"+File.separator+"a.txt");
is2 = new FileInputStream("d:"+File.separator+"b.txt");
os = new FileOutputStream("d:"+File.separator+"ab.txt");
sis = new SequenceInputStream(is1,is2);
int temp = 0;
while((temp)=sis.read()!=-1)
{
os.write(temp);
}
sis.close();
is1.close();
is2.close();
os.close();
}
}
另外,还有FilterInputStream
,它是抽象类,作为“装饰器”的接口,笔者将单独用一节来详述。
OutputStream
的作用是表示从不同数据源产生输入的类:
下面是OutputStream的派生类。同样,所有的派生类都要联合装饰类FilterOutputStream
组合使用。
与ByteArrayInputStream
相对应,
- 作用:在内存中创建缓冲区,所有送往“流”的数据都要放置在此缓冲区
- 构造器参数:缓冲区初始化尺寸(可选的)
public ByteArrayOutputStream() {
this(32);
}
public ByteArrayOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: " + size);
}
buf = new byte[size];
}
与FileInputStream
相对应,
- 作用:将信息写至文件
- 构造器参数:表示文件路径的字符串、File对象、FileDescriptor对象
PipedInputStream
输出,实现“管道化”PipedInputStream
FilterInputStream/FilterOutputStream
同样也是InputStream/OutputStream
的派生类,但与其他派生类不同,它们是实现装饰器的抽象类。
InputStream/OutputStream
public DataInputStream(InputStream in) {
super(in);
}
public DataOutputStream(OutputStream out) {
super(out);
}
InputStream/OutputStream
举个例子:
/**
* 使用缓冲区,读取二级制文件
*/
package c18;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class BinaryFile {
public static byte[] read(File bFile) throws IOException {
BufferedInputStream bf = new BufferedInputStream(
new FileInputStream(bFile));
try {
byte [] data = new byte[bf.available()];
bf.read(data);
return data;
} finally {
bf.close();
}
}
public static byte[] read(String bFile) throws IOException {
return read(new File(bFile).getAbsoluteFile());
}
}
getLineNumber()
和setLineNumber(int)
InputStream
该类已经被废弃了,推荐使用字符流的类来操作。
unread()
方法实现InputStream
PushbackInputStream
类实现了这一思想,提供了一种机制,可以“偷窥”来自输入流的内容而不对它们进行破坏。
//允许将size大小的字节回推回流
public PushbackInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("size <= 0");
}
this.buf = new byte[size];
this.pos = size;
}
//每次允许回推一个字节
public PushbackInputStream(InputStream in) {
this(in, 1);
}
package c18;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.PushbackInputStream;
public class PushbackInputStreamDemo {
public static void main(String[] args) {
byte[] arrByte = new byte[1024];
byte[] byteArray = new byte[]{'H', 'e', 'l', 'l', 'o',};
/*
* new PushbackInputStream(is, 10)一次能回推10个字节的缓存
*/
InputStream is = new ByteArrayInputStream(byteArray);
PushbackInputStream pis = new PushbackInputStream(is, 10);
try {
for (int i = 0; i < byteArray.length; i++) {
arrByte[i] = (byte) pis.read();
System.out.print((char) arrByte[i]);
}
//换行
System.out.println();
byte[] b = {'W', 'o', 'r', 'l', 'd'};
/*
* unread()回推操作
* 将World回推到PushbackInputStream流中
* 下次read()会将这个字节再次读取出来
*/
pis.unread(b);
for (int i = 0; i < byteArray.length; i++) {
arrByte[i] = (byte) pis.read();
System.out.print((char) arrByte[i]);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
输出结果:
Hello
World
InputStream
需要注意的是,与其他输出流不同, PrintStream 永远不会抛出 IOException ;它产生的IOException会被自身的函数所捕获并设置错误标记, 用户可以通过 checkError() 返回错误标记,从而查看PrintStream内部是否产生了IOException。
另外, PrintStream 提供了自动flush 和字符集设置功能 。所谓自动flush,就是往PrintStream写入的数据会立刻调用flush()函数(flush函数的作用是强制刷新缓存)。
设计Reader和Writer继承层次结构主要是为了国际化的16位Unicode字符编码。下表展示了Reader/Writer
和InputStream/OutputStream
对应关系:
Java 1.0 | Java 1.1 | 备注 |
---|---|---|
InputStream | Reader | 适配器:InputStreamReader |
OutputStream | Writer | 适配器:OutputStreamWriter |
FileInputStream | FileReader | |
FileOutputStream | FileWriter | |
StringReader | ||
StringWriter | ||
ByteArrayInputStream | CharArrayReader | |
ByteArrayOutputStream | CharArrayWriter | |
PipedInputStream | PipedReader | |
PipedOutputStream | PipedWriter | |
装饰器对应关系 | ||
FilterInputStream | FilterReader | FilterReader没有子类 |
FilterOutputStream | FilterWriter | FilterWriter没有子类 |
BufferInputStream | BufferReader | BufferReader有readline() |
BufferOutputStream | BufferWriter | |
PrintStream | PrintWriter | |
LineNumberReader | ||
PushbackInputStream | PushbackReader | |
以下Java1.0类在1.1中无相应类 | ||
DataInputStream | ||
File | ||
RandomAccessFile | ||
SequenceInputStream |
举一个文件读写工具例子:
/**
* 文件读写工具
*/
package c18;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
public class TextFile {
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);
}
}
}
需要注意以下几点:
1. 再次强调,FilterReader/Writer
是没有子类的,BufferedReader/Write
r等虽然实现了装饰模式,但继承的是Reader/Writer
;
2. 无论何时使用readline
函数,强烈反对使用DataInputStream
,推荐使用BufferedReader
;
3. PrintWriter
提供了PrintStream
的所有打印方法。与PrintStream的区别:作为处理流使用时,PrintStream
只能封装OutputStream
类型的字节流,而PrintWriter
既可以封装OutputStream
类型的字节流,还能够封装Writer
类型的字符输出流并增强其功能。
感谢并致敬以下前辈的文章: