Thinking in java读书笔记 第18章 I/O系统

这篇日志发布时间已经超过一年,许多内容可能已经失效,请读者酌情参考。

 ~读书笔记,这章没记录完,有点长哈哈~

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

18.1 File类

主要介绍了File作为文件和文件夹(路径)的一些方法。File中的list方法,返回的是string,而listFiles返回的则是文件的集合。


18.1.3 文件的检查及创建

(图丢了。。)

测试File的mkdir和mkdirs方法。

当File()为绝对路径的时候,貌似没有任何区别,

当File()为相对路径的时候,mkdir不能创建二级目录(一级目录是可以的)。所以就是说,mkdir不能在不存在的目录中创建文件夹。

 

18.2 输入和输出

 

编程语言的I/O库中经常使用“流”这个概念。“流”屏蔽了实际的I/O设备中处理数据的细节。

任何从Inputstream或者Reader派生的类,都有read()方法,用于读单个字节或者字节数组。

任何从Outputstream或者Writer派生的类,都有write()方法,用于写单个字节或者字节数组。

但是我们通常不会使用这些方法,他们之所以存在是因为别的类可以使用它们,以便提供更有效的接口。

因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(装饰器设计模式).

In fact,java中的“流”类库让人迷惑的主要原因在于:创建一个单一的结果流,却需要创建多个对象。

18.2.1 InputStream类型

InputStream的作用是用来表示哪些从不同数据源产生的输入的类。这些数据源包括:


    1. 字节数组

    2. String对象

    3. 文件

    4. “管道”,工作方式与真实管道类似,从一端输入,从另一端输出。

    5. 一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内。

    6. 其他数据源,如Internet连接等。


    7. (图丢了。。)

18.2.2 OutputStream 类型

FilterOutputStream为“装饰器”类提供了一个基类,装饰器类把属性或者有用的接口与输出流连接起来,这些稍后会做讨论。P535

 

18.3 添加属性和有用的接口

(装饰器在15章引入….)

18.3.1 通过FilterInputStream从InputStream读取数据

18.3.2通过FilterOutputStream向OutputStream写入

BufferedOutputStream是一个修改过的OutputStream,它对数据流使用缓冲技术,因此每次向流写入时,不必每次都进行实际的物理写入操动作。所以在进行输出时,我们可能更经常的是使用它。

18.4 Reader和Writer

…有时候我们必须把来自“字节”层次结构中的类和“字符”层次中的类结合起来使用。为了实现这个目的,要用到“适配器”adapter类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamReader可以把OutputStream转换成Writer。

18.4.1 数据的来源和去处

因此,最明智的做法是尽量尝试使用Reader和Writer,一旦程序代码无法成功编译,我们就会发现自己不得不使用面向字节的类库。

(P538 表格,来源与去处)

18.4.2 更改流的行为

有一点很清楚:无论我们何时使用readLine(),都不应该使用DataInputStream(这样会遭到编译器的强烈反对),而应该使用BufferedReader,除了这一点,DataInputStream仍是I/O类库的首选成员。

18.5 自我独立的类:RandomAccessFile

说其独立,是因为他不是InputSteam或者OutputStream继承层次结构中的一部分。除了实现了DataInput和DataOutput接口之外,它和这两个继承层次结构完全没有关系。

只有RandomAccessFile支持搜寻方法,并且只是用于文件。

BufferedInputStream却能允许标注(mark())位置(其值存储于内部某个简单变量内)和重新设定位置(reset()),但这些功能有限,不是非常有用。

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

18.6 I/O流的典型使用方式

…异常处理都被简化为将异常传递给控制台,但是这只有在小型实例和工具中才适用….

18.6.1 缓冲输入文件

如果想要打开一个文件永远字符输入,可以使用以String或者File对象作为文件名的FileInputStream。

为了提高速度,我们希望对那个文件进行缓冲,那么我们将所产生的一用传给一个BufferedReader构造器。

由于BufferedReader也提供readLine()方法, 所以这好似我们最终对象和进行读取的接口。当readLine()返回null时,表明达到文件末尾。


String 对象是不可改变的。每次使用 System.String 类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。在需要对字符串执行重复修改的情况下,与创建新的 String 对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用 System.Text.StringBuilder 类。例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder 类可以提升性能。

方法名 使用

StringBuilder.Append 将信息追加到当前 StringBuilder 的结尾。

StringBuilder.AppendFormat 用带格式文本替换字符串中传递的格式说明符。

StringBuilder.Insert 将字符串或对象插入到当前 StringBuilder 对象的指定索引处。

StringBuilder.Remove 从当前 StringBuilder 对象中移除指定数量的字符。

StringBuilder.Replace 替换指定索引处的指定字符。

18.6.3 格式化的内存输入

要读取格式化数据,可以使用DataInputStream,它是一个面向字节的I/O类(不是面向字符的)。因此我们必须使用InputStream类而不是Reader类。

如果我们从DataInputStream用readByte()一次一个字节的读取字符,那么任何字节都是合法的结果,因此返回值不能用来检测输入是否结束。相反,我们可以使用available()方法查看还有多少可供存取的字符。

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

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

18.6.4 基本的文件输出

FileWriter对象可以向文件写入数据。首先,创建一个与指定文件连接的FileWriter。实际上,我们通常会用BufferedWriter将其封装起来用以缓冲输出(尝试移除此包来感受对性能的影响——缓冲往往能显著地增加I/O操作的性能)。

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

18.6.5  存储和恢复数据

如果我们使用DataOutputStream写入数据,Java保证我们可以使用DataOutputStream准确地读写数据——无论读和写数据的平台多么不同。[XML是另一种方案]

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

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

18.6.6 读写随机访问文件

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

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

18.6.7 管道流

PipedInputStream、PipedOutputStream、PipedReader和PipedWriter在本章只是简单地提到。但这并不表明他们没有什么用处,它们的价值只有在我们开始理解多线程之后才会显现,因为管道流用于任务之间的通信。这些在第21章会用一个示例进行讲述。

18.7 文件读写的实用工具

一个很常见的程序化任务就是读取文件到内存,修改,然后再写出。java I/O类库的问题之一就是:它需要编写相当多的代码去执行这些常用操作——没有任何基本的帮助功能可以为我们做这一切。更糟糕的是,装饰器会使得要记住如何打开文件变成一件相当困难的事。

这节主要是写了两个小工具,TextFile和BinaryFile,用于封装成常见的I/O操作小工具的示例。

18.8 标准I/O

标准I/O这个术语参考的是Unix中“程序所使用的单一信息流”这个概念(在windows和其他许多操作系统中,也有相似形式的实现)。

程序的所有输入都可以来自于标准输入,它的所有输出也都可以送达标准输出,以及所有的错误信息都可以发送到标准错误。

标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一个程序的标准输入。这真是一个强大的工具。

18.8.1 从标准输入中读取

按照标准I/O模型,Java提供了System.in、System.out和System.err。

其中System.out和System.err是已经被包装过的PrintStream,但是System.in却是一个没有被包装过的InputStream,这意味着我们可以立即使用System.out和System.err,但是在读取System.in之前必须对其进行包装。

18.8.2 将System.out转换成PrintWriter

System.out是一个PrintStream。而PrintStream是一个OutputStream。

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

代码如下


package com.prettydz.exercises;

import java.io.PrintWriter;

public class ChangeSystemOut {

public static void main(String[] args) {

PrintWriter out = new PrintWriter(System.out, true);

out.println("测试");

}

}

PrintWriter的第二个参数true表示自动清空,以便显示出来。

 

18.8.3 标准I/O重定向

 

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

setIn(InputStream)

setOut(PrintStream)

setErr(PrintStream)

如果我们突然开始在显示器上创建大量输出,而这些输出滚动得太快以至于无法阅读时,重定向输出就显得极为有用(第22章展示了一种更方便的解决方案:一个GUI程序,具有带滚动的文本区域)。

package com.prettydz.exercises;

 

import java.io.*;

 

public class Redirecting {

public static void main(String[] args) throws IOException {

PrintStream console = System.out;

BufferedInputStream in = new BufferedInputStream(new FileInputStream("D:\\我的桌面\\in.txt"));

PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("D:\\我的桌面\\out.txt")));

System.setIn(in);

System.setOut(out);

System.setOut(out);

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String zs;

while((zs = br.readLine())!=null){

System.out.println(zs);

}

out.close();

System.setOut(console);

System.out.println("测试");

}

}

这个示例将System.in重定向为in.txt,即从指定文件输入。当读入的是ANSI文件时候,会产生乱码,处理方法就是用Charset类decode一下。

程序开头存储了System.out以便在程序末尾恢复重定向,最后的输出用于console测试。

I/O重定向操作的是字节流,而不是字符流;因此我们使用的是InputStream和OutputStream而不是Reader和Writer。(这个可以用于写程序日志)

 

18.9 进程控制

 

你经常会需要在Java内部执行其他操作系统的程序,并且要控制这些程序的输入和输出。Java类库提供了执行这些操作的类。

 

一个常见的任务就是运行程序,并将产生的结果输出到控制台。

 

要想运行一个程序,你需要像OSExecute.command()传递一个command字符串,它与你在控制台上运行该程序所需要键入的命令相同。这个命令被传递给java.lang.ProcessBuilder构造器,然后所产生的ProcessBuilder对象被启动。

package com.prettydz.exercises;

 

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 zs;

while ((zs = results.readLine()) != null) {

System.out.println(zs);

}

BufferedReader errs = new BufferedReader(new InputStreamReader(process.getErrorStream()));

while ((zs = errs.readLine()) != null) {

System.err.println(zs);

err = true;

}

} catch (Exception e) {

// 针对windows的测试

if (!command.startsWith("CMD /C ") && !command.startsWith("cmd /c ")) {

command("CMD /C " + command);

} else

throw new RuntimeException(e);

}

if (err) {

System.err.println("Errors executing :" + command);

}

}

 

public static void main(String[] args) {

OSExecute.command("dir");

}

}

18.10 新I/O

 

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

速度的提升来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。

唯一直接与通道交互的缓冲器是ByteBuffer——也就是说,可以存储未加工字节的缓冲器。

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

方法:getChannel()

方法摘要
abstract voidforce(boolean metaData)强制将所有对此通道的文件更新写入包含该文件的存储设备中。
FileLocklock()获取对此通道的文件的独占锁定。
abstract FileLocklock(long position, long size, boolean shared)获取此通道的文件给定区域上的锁定。
abstract MappedByteBuffermap(FileChannel.MapMode mode, long position, long size)将此通道的文件区域直接映射到内存中。
abstract longposition()返回此通道的文件位置。
abstract FileChannelposition(long newPosition)设置此通道的文件位置。
abstract intread(ByteBuffer dst)将字节序列从此通道读入给定的缓冲区。
longread(ByteBuffer[] dsts)将字节序列从此通道读入给定的缓冲区。
abstract longread(ByteBuffer[] dsts, int offset, int length)将字节序列从此通道读入给定缓冲区的子序列中。
abstract intread(ByteBuffer dst, long position)从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。
abstract longsize()返回此通道的文件的当前大小。
abstract longtransferFrom(ReadableByteChannel src, long position, long count)将字节从给定的可读取字节通道传输到此通道的文件中。
abstract longtransferTo(long position, long count, WritableByteChannel target)将字节从此通道的文件传输到给定的可写入字节通道。
abstract FileChanneltruncate(long size)将此通道的文件截取为给定大小。
FileLocktryLock()试图获取对此通道的文件的独占锁定。
abstract FileLocktryLock(long position, long size, boolean shared)试图获取对此通道的文件给定区域的锁定。
abstract intwrite(ByteBuffer src)将字节序列从给定的缓冲区写入此通道。
longwrite(ByteBuffer[] srcs)将字节序列从给定的缓冲区写入此通道。
abstract longwrite(ByteBuffer[] srcs, int offset, int length)将字节序列从给定缓冲区的子序列写入此通道。
abstract intwrite(ByteBuffer src, long position)从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。

  

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

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

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

一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。

……..P554

 

18.10.1 转换数据

 

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

…….18章后边表示没看完….


留言交流

没有评论
点击换图