Java I/O系统


对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务。

File类

File(文件)类这个名字有一定的误导性;我们可能会认为它指代的是文件,实际上却并非如此。它既能代表一个特定文件的名称,又能代表一个日录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一个字符数组。我们很容易就可以理解返回的是一个数组而不是某个更具灵活性的类容器,因为元素的个数是固定的,所以如果我们想取得不同的目录列表,只需要再创建一个不同的File对象就可以了。

目录列表器

假设我们想査看一个目录列表,可以用两种方法来使用File对象。如果我们调用不带参数的list()方法,便可以获得此File对象包含的全部列表。然而,如果我们想获得一个受限列表,例如想得到所有扩展名为.java的文件,那么我们就要用到“目录过滤器”,这个类会告诉我们怎样显示符合条件的File对象。例:
下面是一个示例,注意通过使用java.utils.Arrays.sort()和String.CASE_INSENSITIVE.ORDERComparator,可以很容易地对结果进行排序(按字母顺序)。

public class Test {
	public static void main(String[] args) {
		File f = new File("C:/Users/G50/Desktop/学习");
		String[] list = f.list(new DirFilter("01java"));
		Arrays.sort(list,String.CASE_INSENSITIVE_ORDER);
		System.out.println(Arrays.toString(list));
	}
}

class DirFilter implements FilenameFilter{
	private Pattern p;
	public DirFilter(String str) {
		this.p = Pattern.compile(str);
	}
	@Override
	public boolean accept(File dir, String name) {
		return p.matcher(name).matches();
	}
	//输出:[01java]
}

DirFilter这个类存在的唯一原因就是将accept()方法。创建这个类的目的在于把accept()方法提供给list()使用,使list()可以回调acccept(),进而以决定哪些文件包含在列表中。因此,这种结构也常常称为回调。更具体地说,这是一个策略模式的例子,因为list()实现了基本的功能,而且按照FilenameFilter的形式提供了这个策略,以便完善list()在提供服务时所需的算法。因为list()接受FilenameFilter对象作为参数,这意味着我们可以传递实现了FilenameFilter接口的任何类的对象,用以选择(甚至在运行时)list()方法的行为方式。策略的目的就是提供了代码行为的灵活性。

accept()方法必须接受一个代表某个特定文件所在目录的File对象,以及包含了那个文件名的一个String。记住一点:list()方法会为此目录对象下的每个文件名调用accept(),来判断该文件是否包含在内;判断结果由accept()返回的布尔值表示。

accept()会使用一个正则表达式的matcher对象,来査看此正则表达式regex是否匹配这个文件的名字。通过使用accept(),list()方法会返回一个数组。

目录的检查及创建

File类不仅仅只代表存在的文件或目录。也可以用File对象来创建新的目录或尚不存在的整个目录路径。我们还可以査看文件的特性(如:大小,最后修改日期,读/写),检査某个File对象代表的是一个文件还是一个目录,并可以删除文件。下面的示例展示了File类的一些其他方法(请参考http://java.sun.com上的HTML文档以全面了解它们)。

public class Test {
	public static void main(String[] args) {
		File f = new File("D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java");
		if(f.exists()){
			if(f.isFile()){
				System.out.println("getAbsolutePath:"+f.getAbsolutePath());
				System.out.println("canRead:"+f.canRead());
				System.out.println("canWrite:"+f.canWrite());
				System.out.println("getName:"+f.getName());
				System.out.println("getParent:"+f.getParent());
				System.out.println("length:"+f.length());
				System.out.println("lastModified:"+f.lastModified());
				File f2 = new File("D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.java");
				if(!f2.exists()){
					if(f2.mkdirs()){
						f.renameTo(f2);
					}
				}
			}
		}
	}
	//输出:
	//getAbsolutePath:D:\13Java\04eclipse_workspace\02workspace_study\Test\src\thinkinjava\io\Test.java
	//canRead:true
	//canWrite:true
	//getName:Test.java
	//getParent:D:\13Java\04eclipse_workspace\02workspace_study\Test\src\thinkinjava\io
	//length:745
	//lastModified:1541032722672
}

在上例中可以看到用到了多种不同的文件特征査询方法来显示文件或目录路径的信息。main()方法中调用了renameTo(),用来把一个文件重命名(或移动)到由参数所指示的另一个完全不同的新路径(也就是另一个File对象)下面。这同样通用于任意长度的文件目录。我们可以产生任意复杂的目录路径,因为mkdirs()可以为我们做好这一切。

输入和输出

编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。

Java类库中的I/O类分成输入和输出两部分,可以在JDK文档里的类层次结构中査看到。通过继承任何自InputStream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。同样,任何自OutputStream或Write派生而来的类都含有名为write()的基本方法,用于写单个字节或者字节数组。但是我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。因此我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式)。实际上,Java中“流“类库让人迷惑的主要原因就在于:创建单一的结果流,却需要创建多个对象。

在Java 1.0中,类库的设计者首先限定与输入有关的所有类都应该从InputStream继承,而与输出有关的所有类都应该从OutputStream继承。

InputStream类型

InputStream的作用是用来表示那些从不同数据源产生输入的类。如表18-1所示,这些数据源包括:

  1. 字节数组
  2. String对象
  3. 文件
  4. “管道”,工作方式与实际管道相似,即从一端输入从另一端输出。
  5. 一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内。
  6. 其他数据源,如Internet连接等

每一种数据源都有相应的InputStream子类。另外,FileInputStream也属于一种InputStream,为“装饰器”(decorator)类提供基类,其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起。我们稍后再讨论它。
在这里插入图片描述

OutputStream类型

如表18-2所示,该类別的类决定了输出所要去往的目标:字节数组(但不是String,不过你当然可以用字节数组自己创建)、文件或管道。

另外,FileOutputStream为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来,这些稍后会讨论。
在这里插入图片描述

添加属性和有用的接口

Java I/O类库需要多种不同功能的组合,这正是使用装饰器模式的理由所在。这也是Java I/O类库里存在filter(过滤器)类的原因所在抽象类filter是所有装饰器类的基类。装饰器必须具有和它所装饰的对象相同的接口,但它也可以扩展接口,而这种情况只发生在个别filter类中。

但是装饰器模式也有一个缺点:在编写程序时,它给我们提供了相当多的灵活性(因为我们可以很容易地混合和匹配属性),但是它同时也增加了代码的复杂性。Java I/O类库操作不便的原因在于,我们必须创建许多类——“核心”I/O类型加上所有的装饰器,才能得到我们所希望的单个I/O对象。

FilterInputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流(InputStream)和输出流(OutputStream)的两个类,它们的名字并不是很直观。FilterInputStream和FilterOutputStream分別自I/O类库中的基类InputStream和OutputStream派生而来,这两个类是装饰器的必要条件(以便能为所有正在被修饰的对象提供通用接口)。

通过FilterInputStream从InputStream读取数据

FilterInputStream类能够完成两件完全不同的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象(所有方法都以“read开头,例如readByte()、readFloat()等等)。搭配相应的DataOutputStream,我们就可以通过数据“流”将基本类型的数据从一个地方迁移到另一个地方。具体是哪些“地方”是由表18-1中的那些类决定的。

其他FilterInputStream类则在内部修改InputStream的行为方式:是否缓冲,是否保留它所读过的行(允许我们查询行数或设置行数),以及是否把单一字符推回输入流等等。最后两个类看起来更像是为了创建一个编译器(它们被添加进来可能是为了对“用Java构建编译器”实验提供支持),因此我们在一般编程中不会用到它们。

我们几乎每次都要对输入进行缓冲——不管我们正在连接的是什么I/O设备,所以I/O类库把无缓冲输入(而不是缓冲输入)作为特殊情况(或只是方法调用)就显得更加合理了。FilterInputStream的类型及功能如表18_3所示。
在这里插入图片描述

通过FilterOutputStream从OutputStream读取数据

与DataInputStream对应的是DataOutputStream,它可以将各种基本数据类型以及String对象格式化输出到“流”中;这样以来,任何机器上的任何DataInputStream都能够读取它们。所有方法都以“wirte”开头。

PrintStream最初的目的便是为了以可视化格式打印所有的基本数据类型以及String对象。这和DataOutputStream不同,后者的目的是将数据元素置入“流”中,使DataInputStream能够可移植地重构它们。

PrintStream可能会有些问题,因为它捕捉了所有的IOExceptions(因此,我们必须使用checkError()自行测试错误状态,如果出现错误它返回true)。另外,PrintStream也未完全国际化,不能以平台无关的方式处理换行动作(这些问题在printWriter中得到了解决,这在后面讲述)。

BufferedOutputStream是一个修改过的OutputStream,它对数据流使用缓冲技术,因此当每次向流写入时,不必每次都进行实际的物理写动作。所以在进行输出时,我们可能更经常的使用他。 FilterOutputStream的类型及功能如表18-4所示。
在这里插入图片描述

Reader和Writer

Java 1.1对基本的I/O流类库进行了重大的修改。当我们初次看见Reader和writer类时,可能会以为这是两个用来替代lnputStream和OutputStream的类;但实际上并非如此。尽管一些原始的“流”类库不再被使用(如果使用它们,则会收到编译器的警告信息),但是InputStream和OutputStream在以面向字节形式的I/O中仍可以提供极有价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。另外:

  1. Java 1.1向InputStream和OutpuStream继承层次结构中添加了一些新类,所以显然这两个类是不会被取代的。
  2. 有时我们必须把来自于“字节”层次结构中的类和“字符”层次结构中的类结合起来使用。为了实现这个目的,要用到“适配器(adapter)类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writet。

设计Reader和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化(Java本身的char也是16位的Unicode),所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode另外,新类库的设计使得它的操作比旧类库更快。

数据的来源和去处

几乎所有原始的Java I/O流类都有相应的Reader和Writer类来提供天然的Unicode操作。

然而在某些场合,面向字节的InputStream和OutputStream才是正确的解决方案;特别是java.util.zip类库就是面向字节的而不是面向字符的。因此最明智的做法是尽量要试使用Reader和Writer,一旦程序代码无法成功编译,我们就会发现自己不得不使用面向字节的类库。

下面的表展示了在两个继承层次结构中,信息的来源和去处(即数据物理上来自哪里及去向哪里)之间的对应关系:
在这里插入图片描述
更改流的行为

对于InputStream和OutputStream来说,我们会使用FilterInputStream和FilterOutputStrean的装饰器子类来修改“流”以满足特殊需要。Reader和Writer的类继承层次结构继续沿用相同的思想——但是并不完全相同。

在下表中,相对于前一表格来说,左右之问的对应关系的近似程度更加粗略一些。造成这种差别的原因是因为类的组织形式不同;尽管BufferedOutputStream是FilterOutputStream的子类,但是BufferedWriter井不是FilterWriter的子类(尽管FilterWriter是抽象类,没有任何子类,把它放在那里也只是把它作为一个占位符,或仅仅让我们不会对它所在的地方产生疑惑)。然而这些类的接口却十分相似。
在这里插入图片描述
有一点很清楚:无论我们何时使用readLine(),,都不应该使用DataInputStream(这会遭到编译器的强烈反对),而应该使用BufferReader。除了这一点,DataInputStream仍是I/O类库的首选成员。

为了更容易地过渡到使用PrintWriter,它提供了一个既能接受Writer对象又能接受任何OutputStream对象的构造器。PrintWriter的格式化接口实际上与PrintStream相同。

未发生的类
有一些类在Java 1.0和Java 1.1之间则未做改变
在这里插入图片描述

自我独立的类:RandomAccessFile

RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要我们能够确定那些记录有多大以及它们在文件中的位置即可。

RandomAccessFile拥有和別的I/O类型本质不同的行为:我们可以在一个文件内向前和向后移动。从本质上来说,RandomAccessFile的工作方式类似于把DataInputStream和DataOutStream组合起来使用,还添加了一些方法。其中方法getFilePointer()用于査找当前所处的文件位置,seek()用干在文件内移至新的位置,length()用于判断文件的最大尺寸。另外,其构造器还需要第二个参数,用来指示我们只是“随机读”(r)还是“既读又写”(rw)。它并不支持只写文件,这表明RandomAccessFile若是从DataInputStream继承而来也可能会运行得很好。
只有RandomAccessFile支持搜寻方法,并且只适用于文件。BufferedInputStream却能允许标注(mark())位置(其值存储于内部某个简单变量内)和重新设定位置(reset()),但这些功能很有限,不是非常有用。

在JDK1.4中,RandomAccessFile的大多数功能(但不是全部)由nio存储映射文件所取代。

I/O流的典型使用方式

尽管可以通过不同的方式组合I/O流类,但我们可能也就只用到其中的几种组合。下面的例子可以作为典型的I/O用法的基本参考。

缓冲输入文件

如果想要打开一个文件用于字符输入,可以使用以String或File对象作为文件名的FileInputReader。为了提高速度,我们希望对那个文件进行缓冲,那么我们将所产生的引用传给一个BufferedReader构造器。由于BufferedReader也提供readLine()方法,所以这是我们的最终对象和进行读取的接口。当Line()将返回null时,你就达到了文件的末尾。

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		BufferedReader br = new BufferedReader(new FileReader(fstr));
		String s;
		StringBuffer sb = new StringBuffer();
		while((s = br.readLine()) != null){
			sb.append(s);
		}
		br.close();
		System.out.println(sb.toString());
	}
	//输出:package thinkinjava.io;import java.io.Buffere....
}

从内存输入

用read()每次读取一个字符,并把它发送到控制合。

public class Test {
	public static void main(String[] args) throws IOException {
		StringReader sr = new StringReader("BufferedReader");
		int c;
		while((c = sr.read())!= -1){
			System.out.print((char)c);
		}
	}
	//输出:BufferedReader
}

格式化的内存输入

要读取格式化数据,可以使用DataInputStream它是一个面向字节的I/O类(不是面向字符的)。因此我们必须使用InputStream类而不是Reader类。当然我们可以用InputStream以字节的形式读取任何数据(例如一个文件),不过在这里使用的是字符串。

public class Test {
	public static void main(String[] args) throws IOException {
		try {
			DataInputStream dis = new DataInputStream(
					new ByteArrayInputStream("BufferedReader".getBytes()));
			while(true){
				System.out.print((char)dis.readByte());
			}
		} catch (Exception e) {
			System.out.println(":over");
		}
	}
}

必须为ByteArrayInputStream提供字节数组,为了产生该数组String包含了一个可以实现此项工作的getBytes()方法。所产生的ByteArrayInputStream是一个适合传递给DataInputStream的InputStream。

如果我们从DataInputStream用readByte()一次一个字节地读取字符,那么任何字节的值都是合法的结果,因此返回值不能用来检测输入是否结束。相反,我们可以使用availabIe()方法査看还有多少可供存取的字符。下面这个例子演示了怎样一次一个字节地读取文件:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		DataInputStream dis = new DataInputStream(
				new BufferedInputStream(new FileInputStream(fstr)));
		while(dis.available() != 0){
			System.out.print((char)dis.readByte());
		}
	}
	//输出:package thinkinjava.io;.......
}

注意available()的工作方式会随着所读取的媒介类型的不同而有所不同;字面意思就是在没有阻塞的情况下所能读取的字节数”。对于文件,这意味着整个文件;但是对于不同类型的流,可能就不是这样的,因此要谨慎使用。

我们也可以通过捕获异常来检测输入的末尾。但是使用异常进行流控制,被认为是对异常特性的错误使用 。

基本的文件输出

FileWriter对象可以向文件写入数据。首先创建一个与指定文件连接的FileWriter。实际上,我们通常会用BufferedWriter将其包装起来用以缓冲输出(尝试移除此包装来感受对性能的影响——缓冲往往能显著地增加I/O操作的性能)。在本例中,为了提供格式化机制,它被装饰成了PrintWriter。按照这种方式创建的数据文件可作为普通文本文件读取。

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		BufferedReader in = new BufferedReader(new StringReader("BufferedReader"));
		PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fstr)));
		int l = 1;
		String s;
		while((s = in.readLine()) != null){
			out.println(l+":"+s);
		}
		out.close();
	}
}

当文本行被写入文件时,行号就会增加。注意并本用到LineNumberInputStream,因为这个类没有多大帮助,所以我们没必要用它。从本例中可以看出,记录自己的行号很容易。

一旦读完输入数据流,readLine()会返回null。我们可以看到要为out显式调用close()。如果我们不为所有的输出文件调用close(),就会发现缓冲区内容不会被刷新清空,那么它们也就不完整。

文本文件输出的快捷方式

Java SE5在PrintWriter中添加了一个辅助构造器,使得你不必在每次希望创建文本文件并向其中写入时都去执行所有的装饰工作。下面是用这种快捷方式的重写:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		BufferedReader in = new BufferedReader(new StringReader("BufferedReader"));
		PrintWriter out = new PrintWriter(new File(fstr));
		int l = 1;
		String s;
		while((s = in.readLine()) != null){
			out.println(l+":"+s);
		}
		out.close();
	}
}

你仍旧是在进行缓存,只是不必自己去实现。遗憾的是,其他常见的写入任务都没有快捷方式。

存储和数据恢复

PrintWriter可以对数据进行格式化,以便人们的阅读。但是为了输出可供另一个“流”恢复的数据,我们需要用DataOutputStream写入数据,并用DataInputStream恢复数据。当然,这些流可以是任何形式,但在下面的示例中使用的是一个内存,并且对于读和写都进行了缓冲处理。注意DataOutputStream和DataInputStream是面向字节的,因此要使用InputStream和OutputStream。

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		DataOutputStream out = new DataOutputStream(
				new BufferedOutputStream(
						new FileOutputStream(new File(fstr))));
		out.writeDouble(3.1415926);
		out.writeUTF("java");
		out.close();
		DataInputStream in = new DataInputStream(
				new BufferedInputStream(new FileInputStream(new File(fstr))));
		System.out.println(in.readDouble());
		System.out.println(in.readUTF());
	}
	//输出:
	//3.1415926
	//java
}

如果我们使用DataOutputStream写入数据,Java保证我们可以使用DataInputStream准确地读取数据——无论读和写数据的平合多么不同。 这一点很有价值。

当我们使用DataOutputStream时,写字符串并且让DataInputStream能够恢复它的唯一可靠的做法就是使用UTF-8编码,在这个示例中是用writeUTF()和readUTF()来实现的。UTF-8是一种多字节格式,其编码长度根据实际使用的字符集会有所变化。如果我们使用的只是ASCII或者几乎都是ASCII字符(只占7位),那么就显得极其浪费空间和带宽,所以UTF-8将ASCII字符编码成单一字节的形式,而非ASCII字符编码成两到三个字节的形式。另外字符串的长度存储在UTF-8字符串的前两个字节中。但是,writeUTF()和readUTF()使用的是适合于Java的UTF-8变体(JDK文档中有这些方法的详尽描述),因此如果我们用一个非Java程序读取用writeUTF()所写的字符串时,必须编写一些特殊代码方能正确读取字符串。

有了writeUTF()和readUTF(),我们就可以用DataOutputStream把字符串和其他数据类型相混合,我们知道字符串完全可以作为Unicode来存储,并且可以很容易地使用DataInputStream来恢复。

writeDouble()将doubIe类型的数字存储到流中,并用相应的readDouble()恢复它。但是为了保证所有的读方法都能够正常工作,我们必须知道流中数据项所在的确切位置。

读写随机访问文件

使用RandomAccessFiIe,类似于组合使用了DataInputStream和DataOutputStream(因为它实现了相同的接口DataInput和DataOutput)。另外我们可以看到,利用seek()可以在文件中到处移动,并修改文件中的某个值。

在使用RandomAccessFile时,你必须知道文件排版,这样才能正确地操作它RandomAccessFile拥有读取基本类型和UTF-8字符串的各种具体方法。下面是示例:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		
		RandomAccessFile rf = new RandomAccessFile(new File(fstr), "rw");
		for(int i = 0; i < 7;i++){
			rf.writeDouble(i*1.414);
		}
		rf.writeUTF("over");
		rf.close();
		
		rf = new RandomAccessFile(new File(fstr), "r");
		for(int i = 0; i < 7;i++){
			System.out.println(i+":"+rf.readDouble());
		}
		System.out.println("7:"+rf.readUTF());
	}
	//输出:
	//0:0.0
	//1:1.414
	//2:2.828
	//3:4.242
	//4:5.656
	//5:7.069999999999999
	//6:8.484
	//7:over
}

正如先前所指, RandomAccessFile除了实现DataInput和DataOutput接口之外,有效地与I/O继承层次结构的其他部分实现了分离。因为它不支持装饰,所以不能将其与InputStream及OutputStream子类的任何部分组合起来。我们必须假定RandomAccessFile已经被正确缓冲,因为我们不能为它添加这样的功能。

可以自行选择的是第二个构造器参数:我们可指定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。

你可能会考虑使用“内存映射文件”来代替RandomAccessFile。

管道流

PipedInputStream、PipedOutputStream、PipedReader及PipedWriter类的介绍,请看多线程篇。

标准I/O

按照标准I/O模型,Java提供了System.in、System.out和System.err,其中System.out和System.err已经事先被包装成了PrintStream对象,但System.in却是一个没有被包装过的未经加工的InputStream。这意味尽管我们可以立即使用System.out和system.err,但是在读取System.in之前必须对其进行包装。

下面的例子将回显你所输入的每一行:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		BufferedReader br = new BufferedReader(
				new InputStreamReader(System.in));
		String s;
		while((s = br.readLine()) != null && s.length() > 0){
			System.out.println(s);
			System.err.println(s);
		}
	}
}

将System.out转换成PrintWrite

System.out是一个PrintStream,而PrintStream是一个OutputStream。PrintWriter有一个可以接受OutputStream作为参数的构造器。因此,只要需要,就可以使用那个构造器把System.out转换成PrintWriter:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		PrintWriter out = new PrintWriter(System.out,true);
		out.println("123");
	}
	//输出:
	//123
}

重要的是要使用有两个参数的PrintWriter的构造器,并将第二个参数设为true,以便开启自动清空功能;否则你可能看不到输出。

标准I/O重定向

Java的System类提供了一些简单的静态方法调用,以允许我们对标准输入、输出和错误I/O流进行重定向:

setIn(InputStream)

setOut(PrintStream)

setErr(PrintStream)

如果我们突然开始在显示器上创建大量输出,而这些输出滚动得太快以至于无法阅读时,重定向输出就显得极为有用:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		PrintStream console = System.out;
		BufferedInputStream in = new BufferedInputStream(
				new FileInputStream(new File(fstr)));
		PrintStream out = new PrintStream(
				new BufferedOutputStream(
						new FileOutputStream(fstr2)));
		System.setIn(in);
		System.setOut(out);
		System.setErr(out);
		
		BufferedReader br = new BufferedReader(
				new InputStreamReader(System.in));
		String s;
		while((s = br.readLine()) != null){
			System.out.println(s);
		}
		out.close();
		System.setOut(console);
	}
}

这个程序将标准输入附接到文件上,井将标准输出和标准错误重定向到另一个文件。注意,在程序开头处存储了对最初的System.out对象的引用,并且在结尾处将系统输出恢复到了该对象上。

I/O重定向操纵的是字节流,而不是字符流;因此我们使用的是InputStream和OutputStrem,而不是Reader和Writer。

新I/O

JDK 1.4的java.nio.*包中引入了新的Java I/O类库,其目的在于提高速度。实际上,旧的I/O包已经使用nio重新实现过,以便充分利用这种速度提高,因此即使我们不显式地用nio编写代码码,也能从中受益。速度的提高在文件I/O和网络I/O中都有可能发生,我们在这里只研究前者;对于后者,在《Thinking in Enterprise Java》中有论述。

速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。我们可以把它想像成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车载满煤炭而归,我们再从卡车上获得煤炭。也就是说,我们并没有直接和通道交互;我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向级冲器发送数据。

唯一直接与通道交互的缓冲器是ByteBuffer——也就是说,可以存储未加工字节的缓冲器。当我们査询JDK文档中的java.nio.ByteBuffer时,会发现它是相当基础的类:通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好因为这是大多数操作系统中更有效的映射方式。

旧I/O类库中有三个类被修改了,用以产生FileChannel。这三个被修改的类是FileInputStream、FileOutputStream以及用于既读又写的RandomAccessFiIe。注意这些是字节操纵流,与低层的nio性质一致。Reader和Writer这种字符模式类不能用于产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。例:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		FileChannel fc = new FileOutputStream(fstr2).getChannel();
		fc.write(ByteBuffer.wrap("ASDFGH".getBytes()));
		fc.close();
		
		fc = new RandomAccessFile(fstr2, "rw").getChannel();
		fc.position(fc.size());
		fc.write(ByteBuffer.wrap("ZXCVBN".getBytes()));
		fc.close();
		
		fc = new FileInputStream(fstr2).getChannel();
		ByteBuffer buff = ByteBuffer.allocate(1024);
		fc.read(buff);
		buff.flip();
		while(buff.hasRemaining()){
			System.out.print((char)buff.get());
		}
	}
	//输出:
	//ASDFGHZXCVBN
}

对于这里所展示的任何流类,getChannel()将会产生一个FiIeChannel。通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问(稍后讲述)。

将字节存放于ByteBuffer的方法之一是:使用一种“put”方法直接对它们进行填充,填入一个或多个字节,或基本数据类型的值。不过正如所见,也可以使用warp()方法将已存在的字节数组“包装”到ByteBuffer中。一旦如此就不再复制底层的数组,而是把它作为所产生的ByteBuffer的存储器,我们称之为数组支持的ByteBuffer。

文件用RandomAccessFile被再次打开。注意我们可以在文件内随处移动FileChanneI;在这里我们把它移到最后,以便附加其他的写操作。

对于只读访问,我们必须显式地使用静态的allocate()方法来分配ByteBuffer。nio的目标就是快速移动大量数据,因此ByteBuffer的大小就显得尤为重要——实际上这里使用的1K可能比我们通常要使用的小一点(必须通过实际运行应用程序来找到最佳尺寸)。

甚至达到更高的速度也有可能,方法就是使用allocateDirect()而不是allocate()以产生一个与操作系统有更高耦合性的“直接”缓冲器。但是这种分配的开支会更大,并且具体实现也随操作系统的不同而不同,因此必须再次实际运行应用程序来査看直接缓冲是否可以使我们获得速度上的优势。

一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备(是的,这似乎有一点拙劣,但是请记住它是很拙劣的,但却适用于获取最大速度)。如果我们打算使用缓冲器执行进一步的read()操作,我们也必须得调用clear()来为每个read()做好准备。在下面的简单文件复制程序中可以看到:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		FileChannel in = new FileInputStream(fstr).getChannel();
		FileChannel out = new FileOutputStream(fstr2).getChannel();
		ByteBuffer buff = ByteBuffer.allocate(1024);
		while(in.read(buff) != -1){
			buff.flip();
			out.write(buff);
			buff.clear();
		}
	}
}

可以看到打开一个FileChannel以用于读,而打开另一个以用于写。ByteBuffer被分配了空间,当FileChannel.read()返回-1时(一个分界符,毋庸置疑,它源于Unix和C),表示我们已经到达了输入的末尾。每次read()操作之后,就会将数据输入到缓冲器中,flip()则是准备缓冲器以便它的信息可以由write()提取。write()操作之后,信息仍在缓冲器中,接着clear()操作则对所有的内部指针重新安排,以便缓冲器在另一个read()操作期间能够做好接受数据的准备。

然而.上面那个程序并不是处理此类操作的理想方式。特殊方法transferTo()和transferFrom()则允许我们将一个通道和另一个通道直接相连:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		FileChannel in = new FileInputStream(fstr).getChannel();
		FileChannel out = new FileOutputStream(fstr2).getChannel();
		in.transferTo(0, in.size(),out);
	}
}

转换数据

上面的例子中我们为了输出文件中的信息,必须每次只读取一个字节的数据,然后将每个byte类型强制转换成char类型。这种方法似乎有点原始——如果我们査看一下java.nio.CharBuffer这个类,将会发现它有一个toString()方法是这样定文的:“返回一个包含缓冲器中所有字符的字符串。”既然ByteBuffer可以看作是具有asCharBuffer()方法的CharBuffer,那么为什么不用它呢?但是这种方法并不能解决问题。

缓冲器容纳的是普通的字节,为了把它们转换成字符,我们要么在输入它们的时候对其进行编码(这样,它们输出时才具有意义),要么在将其从缓冲器输出时对它们进行解码。可以使用java.nio.charset类实现这些功能。

如果我们想对缓冲器调用rewind()方法(调用该方法是为了返回到数据开始部分),接着使用平台的默认字符集对数据进行decode(),那么作为结果的CharBuffer可以很好地输出打印到控制台。可以使用System.getProperty(“file.encoding”)发现默认字符集,它会产生代表字符集名称的字符串。把该字符串传送给Charset.forName()用以产生Charset对象,可以用它对字符串进行解码。

另一选择是在读文件时,使用能够产生可打印的输出的字符集进行encode()。当读取时我们只需要把它转换成CharBuffer,就会产生所期望的文本。

获取基本类型

尽管ByteBuffer只能保存字节类型的数据,但是它具有可以从其所容纳的字节中产生出各种不同基本类型值的方法。例:

public class Test {
	public static void main(String[] args) throws IOException {
		ByteBuffer bb = ByteBuffer.allocate(1024);
		int i = 0;
		while(i++<bb.limit()){
			if(bb.get() != 0){
				System.out.print("nozero");
			}
		}
		System.out.println("i="+i);
		bb.rewind();
		bb.asCharBuffer().put("Hi");
		char c;
		while((c = bb.getChar())!=0){
			System.out.print(c+" ");
		}
		
		bb.asShortBuffer().put((short)14711);
		System.out.println(bb.getShort());

		bb.asIntBuffer().put(445);
		System.out.println(bb.getInt());
		
		//...
	}
	//输出:
	//i=1025
	//H i 14711
	//445
}

在分配一个ByteBuffer之后,可以通过检测它的值来査看缓冲器的分配方式是否将其内容自动置零——它确实是这样做了。这里一共检查了1024个值(由缓冲器的limit()决定),并且所有的值都是零。

向ByteBuffer插入基本类型数据的最筒单的方法是利用asCharBuffer()、asShortBuffer()等获得该缓冲器上的视图,然后使用视图的put()方法。我们会发现此方法适用于所有基本数据类型。仅有一个小小的例外,即使用ShortBuffer的put()方法时,需要进行类型转换(注意类型转换会截取或改变结果)。而其他所有的视图缓冲器在使用put()方法时,不需要进行类型转换。

视图缓冲器

视图援冲器(view buffer)可以让我们通过某个特定的基本数据类型的视窗査看其底层的ByteBuffer。ByteBuffer依然是实际存储数据的地方,“支持”着前面的视图,因此对视图的任何修改都会映射成为对ByteBuffer中数据的修改。正如我们在上一示例看到的那样,这使我们可以很方便地向ByteBuffer插入数据。视图还允许我们从ByteBuffer一次一个地(与ByteBuffer所支持的方式相同)或者成批地(放入数组中)读取基本类型值。例:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		IntBuffer ib = bb.asIntBuffer();
		ib.put(new int[]{1,2,3,4,5});
		System.out.println(ib.get(3));
		ib.put(3,12);
		ib.flip();
		while(ib.hasRemaining()){
			int i = ib.get();
			System.out.println(i);
		}
	}
	//输出:
	//4
	//123125
}

先用重载后的put()方法存储一个整数数组。接着get()和put()方法调用直接访同底层ByteBuffer中的某个整数位置。注意这些通过直接与ByteBuffer对话访问绝对位置的方式也同样适用于基本类型。

一旦底层的ByteBuffer通过视图缓冲器填满了整数或其他基本类型时,就可以直接被写到通道中了。正像从通道中读取那样容易,然后使用视图缓冲器可以把任何数据都转化成某一特定的基本类型。

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.java";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		
		ByteBuffer bb = ByteBuffer.wrap(new byte[]{0,0,0,0,0,0,0,'a'});
		bb.rewind();
		while(bb.hasRemaining()){
			System.out.print(bb.position()+"->"+bb.get()+",");
		}
		System.out.println("");
		LongBuffer lb = ((ByteBuffer)bb.rewind()).asLongBuffer();
		while(lb.hasRemaining()){
			System.out.print(lb.position()+"->"+lb.get()+",");
		}
		//...
	}
	//输出:
	//0->0,1->0,2->0,3->0,4->0,5->0,6->0,7->97,
	//0->97,
}

ByteBuffer通过一个被“包装”过的8字节数组产生,然后通过各种不同的基本类型的视图缓冲器显示了出来。我们可以在下图中看到,当从不同类型的缓冲器读取时,数据显示的方式也不同。
在这里插入图片描述
字节存放次序

不同的机器可能会使用不同的字节排序方法来存储数据。“big endian”(高位优先)将最重要的字节存放在地址最低的存储器单元。而“little endian”(低位优先)则是将最重要的字节放在地址最高的存储器单元。当存储量大于一个字节时,像int、float等,就要考虑字节的顺序问题了。ByteBuffer是以高位优先的形式存储数据的,并且数接在网上传送时也常常使用高位优先的形式。我们可以使用带有参数ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的order()方法改变ByteBuffer的字节排序方式。

考虑包含下面两个字节的ByteBuffer:
在这里插入图片描述
如果我们以short(ByteBuffer.asShortBuffer())形式读取数据,得到的数字是97(二进制形式为00000000 01100001)但是如果将ByteBuffer更改成低位优先形式,仍以Short形式读取数据,得到的数字却是24832(二进制形式为01100001 00000000)。

用缓存操纵数据

下面的图阐明了nio类之间的关系,便于我们理解怎么移动和转换数据。例如如果想把一 个字节数组写到文件中去,那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来,然后用getChannel()方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写到FileChannel中(如下图所示)。
在这里插入图片描述
注意,ByteBuffer是将数据移进移出通道的唯一方式,并且我们只能创建一个独立的基本类型缓冲器,或者使用“as”方法从ByteBuffer中活得。也就是说,我们不能把基本类型的缓冲器转换成ByteBuffer。然而,由于我们可以经由视图缓冲器将基本类型数据移进移出ByteBuffer,所以这也就不是什么真正的限制了。

缓冲器的细节

Buffer由数据和可以高效地访问及操纵这些数据的四个索引组成,这四个索引是:mark(标记),position(位置),limit(界限)和capacity(容量)。下面是用于设置和复位索引以及查询它们的值的方法。
在这里插入图片描述
在缓冲器中插入和提取数据的方法会更新这些索引,用于反映所发生的变化。

下面的示例用到一个很简单的算法(交换相邻字符),以对CharBuffer中的字符进行编码(scramble)和译码(unscramble)。

public class Test {
	public static void main(String[] args) throws IOException {
		char[] data = "UsingBuffers".toCharArray();
		ByteBuffer bb = ByteBuffer.allocate(data.length*2);
		CharBuffer cb = bb.asCharBuffer();
		cb.put(data);
		System.out.println(cb.rewind());
		while(cb.hasRemaining()){
			cb.mark();
			char c1 = cb.get();
			char c2 = cb.get();
			cb.reset();
			cb.put(c2).put(c1);
		}
		System.out.println(cb.rewind());
	}
	//输出:
	//UsingBuffers
	//sUniBgfuefsr
}

上例中CharBuffer只是ByteBuffer上的一个视图而已。 这里要强调的是,我们总是以操纵ByteBuffer为目标,因为它可以和通道进行交互。

下面是循环之前缓冲器的样子:
在这里插入图片描述
position指针指向缓冲器中的第一个元素,capacity和limit则指向最后一个元素。
一旦调用缓冲器上相对的get()或put()函数,position指针就会随之相应改变。我们也可以调用绝对的、包含一个索引参数的get()和put()方法,不过这些方法不会改变缓冲器的position指针。

当操纵到while循环时,使用mark()调用来设置mark的值。此时缓冲器状态如下:
在这里插入图片描述
两个相对的get()调用把前两个字符保存到变量c1和c2中,调用完这两个方法后缓冲器如下:
在这里插入图片描述
为了实现交换,我们要在position=0时写入c2,position=1时写入c1。我们也可以使用绝对的put()方法来实现,或者使用reset()把position的值设为mark的值:
在这里插入图片描述
这两个put()方法先写c2,接着写c1:
在这里插入图片描述
在下一次循环迭代期间,将mark设置成position的当前值:
在这里插入图片描述
这个过程将会持续到遍历完整个缓冲器。在while循环的最后position指向缓冲器的末尾。如果要打印缓冲器,只能打印出position和limit之间的字符。因此如果想显示缓冲器的全部内容,必须使用rewind()把position设置到缓冲器的开始位置。下面是调用rewind()之后缓冲器的状
态(mark的值则变得不明确):
在这里插入图片描述

内存映射文件

内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。这种方法极大地简化了用于修改文件的代码。下面是一个小例子:

public class Test {
	public static void main(String[] args) throws IOException {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		int length = 1000;
		MappedByteBuffer out = new RandomAccessFile(fstr, "rw").getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
		for(int i = 0;i<length;i++){
			out.put((byte)'x');
		}
		
		for(int i = length/2; i <length/2+6;i++){
			System.out.println((char)out.get(i));
		}
	}
	//输出:
	//xxxxxx
}

为了既能写又能读,我们先由RandomAccessFile开始,获得该文件上的通道,然后调用map()产生MappedByteBuffer,这是一种特殊类型的直接缓冲器。注意我们必须指定映射文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的较小的部分。

注意底层操作系统的文件映射工具是用来最大化提高性能。

文件加锁

JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竟争同一文件的两个线程可能在不同的Java虚拟机上;或者一个是Java线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。下面是一个简单例子:

public class Test {
	public static void main(String[] args) throws Exception {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		FileOutputStream  fos = new FileOutputStream(fstr);
		FileLock fl = fos.getChannel().tryLock();
		if(fl != null){
			System.out.println("is lock");
			Thread.sleep(2000);
			fl.release();
			System.out.println("Release lock");
		}
		fos.close();
	}
	//输出:
	//is lock
	//Release lock
}

通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。(SocketChannel、DatagramChannel和ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来我们通常不在两个进程之间共享网络socket。)tryLock()是非阻塞式的,它设法获取锁,但是如果不能获得(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。lock()则是阻塞式的,它要阻塞进程直至锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭,使用FileLock.release()可以释放锁。

也可以使用如下方法对文件的一部分上锁:

tryLock(long position, long size, boolean shared)

或者

lock(loog position, long size, boolean shared)

其中,加锁的区域由size-position决定。第三个参数指定是否是共享锁。

尽管无参数的加锁方法将根据文件尺寸的变化而变化,但是具有固定尺寸的锁不随文件尺寸的变化而变化。如果你获得了某一区域(从position到position+size)上的锁,当文件增大超出Position+size时,那么在position+size之外的部分不会被锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。

对独占锁或者共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁并为每一个请求都创建一个锁,那么它就会使用独占锁。锁的类型(共享或独占)可以通过FileLock.isShared进行査询。

对映射文件的部分加锁

如前所述,文件映射通常应用于极大的文件。我们可能需要对这种巨大的文件进行部分加锁,以便其他进程可以修改文件中未被加锁的部分。例如数据库就是这样。下面例子中有两个线程,分別加锁文件的不同部分。

public class Test {
	static FileChannel fc;
	static int l = 100;
	public static void main(String[] args) throws Exception {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		fc = new RandomAccessFile(fstr, "rw").getChannel();
		MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, l);
		for(int i = 0;i<l;i++){
			out.put((byte)'X');
		}
		new LockAndModify(out, 0,l/3);
		new LockAndModify(out, l/2,l/2+l/4);
	}
	static class LockAndModify extends Thread{
		ByteBuffer mbb;
		int start,end;
		public LockAndModify(ByteBuffer mbb , int start,int end) {
			this.mbb =mbb;
			this.start = start;
			this.end = end;
			mbb.limit(end);
			mbb.position(start);
			mbb = mbb.slice();
			start();
		}
		@Override
		public void run() {
			try {
				FileLock fl = fc.lock(start,end,false);
				System.out.println("locked:"+start+" to "+end);
				while(mbb.position() < mbb.limit() -1){
					mbb.put((byte)(mbb.get()+1));
				}
				fl.release();
				System.out.println("Release:"+start+" to "+end);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	//输出:
	//locked:0 to 33
	//locked:50 to 75
	//Release:0 to 33
	//Release:50 to 75
}

线程类LockAndModify创建了缓冲区和用于修改的slice(),然后在run()中,获得文件通道上的锁(我们不能获得缓冲器上的锁,只能是通道上的)。lock()调用类似于获得一个对象的线程锁——我们现在处在“临界区”,即对该部分的文件具有独占访问权。

如果有Java虚拟机,它会自动释放锁,或者关闭加锁的通道。不过我们也可以像程序中那样,显式地为FileLock对象调用release()来释放锁。

压缩

Java I/O类库中的类支持读写压缩格式的数据流。你可以用它们对其他的I/O类进行封装,以提供压缩功能。

这些类不是从Reader和Writer类派生而来的,而是属于InputStream和OutputStream继承层次结构的一部分。这样做是因为压缩类库是按字节方式而不是字符方式处理的。不过有时我们可能会被追要混合使用两种类型的数据流(注意我们可以使用InputStreamReader和OutputStreamWriter在两种类型间方便地进行转换)。
在这里插入图片描述
尽管存在许多种压缩算法,但是Zip和GZlP可能是最常用的。因此我们可以很容易地使用多种可读写这些格式的工具来操纵我们的压缩数据。

用GZIP进行简单压缩

GZIP接口非常简单,因此如果我们只想对单个数据流(而不是一系列互异数据)进行压缩,那么它可能是比较适合的选择。下面是对单个文件进行压缩的例子:

public class Test {
	static FileChannel fc;
	static int l = 100;
	public static void main(String[] args) throws Exception {
		String fstr = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test2.txt";
		String fstr2 = "D:/13Java/04eclipse_workspace/02workspace_study/Test/src/thinkinjava/io/Test.txt";
		
		BufferedReader in = new BufferedReader(new FileReader(fstr));
		
		BufferedOutputStream out = new BufferedOutputStream(
				new GZIPOutputStream(new FileOutputStream(fstr2)));
		System.out.println("Writer file");
		int c;
		while((c = in.read()) != -1){
			out.write(c);
		}
		in.close();
		out.close();
		System.out.println("Read file");
		BufferedReader in2 = new BufferedReader(
				new InputStreamReader(new GZIPInputStream(
						new FileInputStream(fstr2))));
		String s;
		while((s = in2.readLine()) != null){
			System.out.print(s);
		}
		in2.close();
	}
	//输出:
	//Writer file
	//Read file
	//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyxyxyxyxyxyxyxyxyxyxyxyxxxxxxxxxxxxxxxxxxxxxxxxxx
}

压缩类的使用非常直观——直接将输出流封装成GZIPOutputStream,并将输入流封装成GZIPInputStream或ZipInputStream即可。其他全部操作就是通常的I/O读写。这个例子把面向字符的流和面向字节的流混合了起来,输入(in)用Reader类,而GZIPOutputStream的构造器只能接受OutputStream对象,不能接受Writer对象。在打开文件时,GZIPInputStream,就会被转换成Reader。

注:原文后续还有对象序列化、XML和Preferences章节。


  1. 本文来源《Java编程思想(第四版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值