Java I/O系统

                                              **Java I/O系统**
           
           **Java I/O系统概述**
我们知道,一个Java程序可以从控制台接收数据,并输出到控制台中。数据保存在内存中的对象里,一旦Java程序运行完毕后,对象就会在内存中消失,对象所包含的数据也就消失了。那么,有没有什么方法可以将内存中的数据保存下来,以便下次运行程序的时候可以使用?此外,除了通过控制台设备接收和输出数据外,我们能不能从其它设备接收数据,并将数据保存或输出到其它设备呢?
Java类库提供了大量类,可以帮助我们从不同的设备读取数据,并保存或输出到不同的设备中。这些类统一放在java.io包和java.nio包中,统称Java I/O系统。这里,I是英文单词Input的缩写,中文含义为输入;O是英文单词Output的缩写,中文含义为输出。
如果我们查看JDK说明文档java.io包,会发现Java I/O系统是相当庞大的,要完全掌握这些类并非易事。然而,如果我们在学习I/O系统时,掌握I/O系统设计的一些规律,就能很快掌握这些类的用法。
要深入学习Java I/O系统,首先必须理解流(Stream)的概念。
那么,什么是流呢?流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源设备的流,这个数据源设备可以是文件、内存或网络连接。
类似的,当程序需要写入数据的时候,就会开启一个通向目的地设备的流。这时候我们就可以想象数据好像在这其中“流”动一样。这个概念和自然界中的水流、气流类似
流是Java I/O的基础,是Java语言对I/O的一个最基本的抽象。之所有采用流作为I/O最基本的抽象,是因为流有两个最基本的特性:一是它含有流质,而是它有方向。在计算机中,流质就是数据,方向就是读或写。
对流的读或写就是针对设备进行信息的输入或输出。我们可以将流理解为传送数据的管道。管道的一段是固定的,就是系统的内存;管道的另一端连的是不同的设备。当管道所连是输入设备时,可以通过这个管道将输入设备所得到的数据读取到内存的变量中。同样,如果管道所连的是输出设备,可以通过它将内存变量中的数据输出到输出设备中。
流使得输入输出的方式变得统一,Java I/O类库基于流的思想来构建,并提供了各类流和创建流的机制来完成对不同设备的读写。

       **流的分类**
对于Java I/O类库对应的java.io包,我们可以将它看成是存放各种类型流的仓库。根据流中的流质和流向的不同,io包中的流被分为许多类型。以下我们讲解流的分类规则。
1)输入流和输出流
流按照数据流的方向分,可以分为两种类型,分别是:
输入流。从数据源读取数据到程序中。只能从中读取数据,而不能向其写入数据。io包中的输入流都继承自抽象类InputStream或Reader。
输出流。将数据从程序写入数据目的地。只能从中写入数据,而不能从中读取数据。io包中的输出流都继承自抽象类OutputStream或Writer。
在I/O库中的每个输入流都可以找到对应的输出流。
2)字节流和字符流
流按照处理数据的最小单位的不同,可以分为两种类型,分别是:
字节流。字节流是以byte为最小单位进行数据传送。io包中的字节流都继承自抽象类InputStream或OutputStream。
字符流。字符流是以char为最小单位进行数据传送。io包中的字符流都继承自抽象类Reader或者Writer。
I/O库做出这种区分是为了满足处理文字型数据的需要。我们知道byte是8位的,char是16位的。在Java中,各种语言的文字都以Unicode形式编码,Unicode编码是基于16位的char,所以在读写文字型的数据时通过字符流会更有效率、更方便。而一些二进制数据的读写,比如图像、声音等,则仍必须通过字节流来完成。
字节流的类名以InputStream或OutputStream结尾,字符流的类名以Reader或Writer结尾。字节流对应的内存变量类型是byte[],字符流对应的内存变量类型是char[]。
3)节点流和处理流
流按照流的功能分,可以分为两种类型,分别是:
节点流。节点流是可以直接从/向一个特定的数据源(例如磁盘文件、内存、网络)读/写数据的流。节点流也称低级流。例如所有从InputStream或Reader派生的类都有一个基本的read()方法,用于读取单一的字节或者字节数组;而对所有从OutputStream或Writer派生的类都有一个基本的write()方法,用于写入单一的字节或者整个字节数组。但在实际应用中,一般很少使用单一的流类来产生输入输出流,而是使用这两个方法给其他的流类提供数据,通过多个流对象的连接和封装使用实际的流。
处理流。处理流不直接连接到设备,而是连接在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更强大的读写功能。处理流也称高级流。
如果我们对照流的这些分类方法,研究Java I/O类,我们会发现:I/O库中的这些类,呈现出一种对称性。这种对称体现在两个方面:一是输入流和输出流的对称,二是字节流和字符流的对称。以上的对称性将I/O库分为4个区间。各个区间内分布的流都是平行的相互对应的,同一区间的流对应的I/O类都有相同的后缀。

Java I/O系统

在I/O类库中,流名称的后缀表明了它的类别,流名称的前缀一般和它要处理的对象或设备有关。在每个具体的区间中,管道又可以根据节点流和处理流进一步分类。例如,BufferedInputStream类,前缀为Buffered,表明它要处理的是内存缓冲区中的数据,后缀为InputStream,表明它是一个字节流和输入流。如果查看它的构造器,我们会发现它的构造器带一个InputStream类型参数,因此它又是一个高级流。
从上面的分类我们可以看出,所有I/O类中的具体类都是继承自四个抽象类InputSream、OutputStream、Reader、Writer,从而把具体类分为四个对称区间。那么,Java为什么要这样设计呢?
读取和写入流的数据源可以有多种,所以,即使要传输的数据是相同的,而数据的来源分别是从不同的设备读取,例如键盘、磁盘或者网络,那么从不同设备和数据源中获得数据的机制也是不同的。
假如我们要用Java编写输入和输出程序,通过不同的硬件设备上读写文件,是否要为每种不同的设备编写不同的程序呢?很显然,这种做法不可取。那么,我们可是否可以编写独立于硬件的程序呢?在Java中,可以借助于java.io中对抽象类的支持来解决这一问题。
那么,如何对数据进行读取和写入呢?想像一下这样的情形,家里需要使用自来水,自来水公司要把水输到用户家里,应该先把自来水公司到用户家里的连接水管建好。当用户需要使用水的时候,打开水笼头,取出水,用完了之后,关闭水笼头。我们操作IO流是不是也应该是这样一个流程呢?建立流的通道,操作流中的数据,关闭流释放资源。
既然IO操作都是相似的流程,我们就可以将不同数据源共同的方法和属性抽象成父类,以达到代码重用和便于扩展的效果。
Java中是如何设计呢?以面向字节的读取流为例,所有的面向字节的读取流都继承于java.io.InputStream。该类中定义了所有读取共同的方法和属性。但有一个read()的抽象方法,该方法用于读取数据。由于数据源有多种,所以具体的子类会根据数据源的不同分别实现read方法。(相同的行为,不同的实现方式),所以在InputStream前面加上数据源就构成了具体的子类。
要确保足够的通用性,以便在无需了解源数据的情况下就可以读取任何数据,最好的办法就是围绕抽象类来编写程序,而不是围绕具体的子类来编写。
所有的IO操作都由以下步骤构成:
1)	建立流。根据数据源和具体的操作(是读取还是写入)选择流,然后建立流。通过流的建立,创建内存到数据源之间的数据通道,以传输数据。
2)	操作流。将数据读取到内存,或将内存中的数据写入数据源。
3)	关闭流。流操作结束后,释放所有与该流相关联的系统资源。
现在,我们已经掌握了Java I/O库中各类流的分布规律和使用规律。下面我们开始深入学习Java I/O库的主要类。

                   **文件类**
java.io包中的File类提供了管理磁盘文件和目录的基本功能。我们可以将一个File对象看成是代表一个文件或目录的名称和位置的字符串。File类有四个构造方法:
    public File(String pathname)。创建一个与指定路径名关联的File对象。
    public File(String parent, String child)。使用指定参数创建一个File对象。参数parent代表一个目录,参数child代表在parent中的子目录或文件。
    public File(File parent, String child)。与上一个构造器相同,除了目录是用一个File对象而不是一个字符串来表示。
    public File(URI uri)。使用给定的java.net.URI对象创建一个File对象。URI是统一资源标识符,一个文件的URI是“file:///目录/文件名”格式。
因为File对象类似于一个字符串,只代表一个文件或者目录的路径名,所以,即使指定的文件或目录不存在,这些构造器也能成功执行。例如,如下的语句是可以成功执行的,不过“somefile.txt”是否是一个实际存在的文件
    File f = new File("somefile.txt");
但是,现在我们有了一个与文件名somefile.txt关联的File对象,而File类又包含了很多有用的方法来判断关于该文件的信息。列出了File类中常用的方法,更多方法的详细信息,请参考JDK文档。

Java I/O系统

除了提供了大量目录和文件操作的方法外,File类还提供了separator、separatorChar两个常量,用于返回与操作系统平台无关的文件分隔符。为什么要提供这两个常量呢?这是因为各个操作系统平台下的文件分隔符有可能不一样。例如,Windows操作系统使用反斜线“\”作为文件分隔符,而Unix和Linux操作系统使用正斜线符号“/”作为文件分隔符。如果我们在程序中对C盘src目录下input.txt硬编码为“c:\src\input.txt”,在Windows平台下可以正确运行,当程序部署到Unix或Linux操作系统下,就无法正确运行。因此,我们在程序员中应该采用“"c:" + File.separator+"src"+File.separator+"input.txt"”这样的灵活编码方式,保证文件路径与平台无关。
File类可以用于整个java.io包,通常作为一个参数或者返回值类型。使用File对象而不是字符串来代表文件,是一个好的编程习惯。在为I/O操作之前,它可以让我们对文件执行如FileDemo程序中所示的某种检查。
虽然File类可以操作文件,但是File类不是流,它不能操作文件的内容。要操作文件内容,就必须使用我们下面要学习的各种流。

                **字节流**
字节流用于处理二进制文件。所有字节流都继承于抽象类InputStream和OutputStream两个父类。其中InputStream为读取字节流的父类,OutputStream为写入字节流的父类。

Java I/O系统

列出了java.io.InputStream类中提供的方法,这些方法提供了管理流的信息、从流中读取数据、查明流中有多少可用数据、前进到流中的一个新位置以及关闭流等功能。所有的输入流类都至少有这些方法可用。掌握了父类中子类共有的方法后,再去学习子类特有的方法,这样可以减少学习的难度

Java I/O系统

列出了java.io.OutputStream类中提供的方法,这些方法提供了向流中写入数据、将流中数据刷入目标并关闭该流的基本功能。同样,所有的输出流类都至少有这些方法可用。掌握了父类中子类共有的方法后,再去学习子类特有的方法,这样可以减少学习的难度。

Java I/O系统

                 低级字节流
字节流中的主要低级流,根据其输入和输出的设备或数据源,分为三类:
    对二进制文件进行读写操作的FileInputStream和FileOutputStream类,其数据源是磁盘文件。
    对内存缓冲区中的字节数组进行读写操作的ByteArrayInputStream和ByteArrayOutputStream类,其数据源是内存中的字节数组。
    对线程管道进行读写操作的PipedInputStream和PipedOutputStream类,其数据源是线程管道。
下面,我们以最常用的对文件进行读写操作的FileInputStream和FileOutputStream为例,演示低级字节流的操作。
java.io.FileInputStream是InputStream的子类。从开头File名称上就可以知道,FileInputStream与从指定的文件中读取数据至目的地有关。而java.io.FileOutputStream是OutputStream的子类,顾名思义,FileOutputStream主要与从来源地写入数据至指定的文件中有关。
当建立一个FileInputStream或FileOutputStream的实例时,必须指定文件位置及文件名称,实例被建立时文件的流就会开启;而不使用流时,必须关闭文件流,以释放与流相依的系统资源,完成文件读/写的动作。
FileInputStream可以使用read()方法一次读入一个字节,并以int类型返回,或者是使用read()方法时读入至一个byte数组,byte数组的元素有多少个,就读入多少个字节。在将整个文件读取完成或写入完毕的过程中,这么一个byte数组通常被当作缓冲区,因为这么一个byte数组通常扮演承接数据的中间角色。

              高级字节流
高级字节流对低级字节流进行了封装,并有许多功能的扩展。高级字节流根据其功能分为三大类:
    过滤流类:过滤流类提供了额外的功能,在将数据实际写入到输出流之前对输出进行预处理,或者在读取数据之后对输入进行后期处理。过滤流类都是抽象类FilterInputStream 和 FilterOutputStream的子类。
    缓冲流:包括BufferedInputStream 和 BufferedOutputStream,这两个类实现了带缓冲的过滤流。
    数据流:包括DataInputStream 和 DataOutputStream,可以用与计算机无关的格式读写Java的基本数据类型以及String对象。
    计数:包括LineNumberInputStream类,允许在翻译行结束符的基础上,维护一个计数器,该计数器表明正在读取的是哪一行。
    推回输入流:包括PushbackInputStream类,代表一个流允许从流中读取数据,然后在需要时推回该流。
    打印输出流:包括PrintStream类,允许将基本类型数据打印输出到字符串流中,带有自动刷新功能。
    对象流:包括ObjectInputStream和ObjectOutputStream,允许直接对对象执行读写操作。
    合并:SequenceInputStream类可以实现两个文件的合并操作,它从两个低级流中读取数据字节,当到达流的末尾时从一个流转到另一个流 ,从而实现合并操作。
因为高级流不能直接与数据源进行通讯,所以使用时不能单独使用,必须附加在一个已经存在的低级流或高级流上,也就是必须用一个已经存在的流作为它的构造器参数。
这些高级字节流中,最常用的是过滤流(缓冲流、数据流、打印输出流)和对象流。对象流我们将在后面小节详细讲述。下面我们主要讲解缓冲流、数据流和打印输出流。
1)字节缓冲流
前面我们的示例是使用没有缓冲的I/O,这意味着每次读写请求都是直接通过下层操作系统来处理。每次这种请求都要触发磁盘访问、网络流量或者其它相对比较耗时的操作,因此效率会比较低下。
BufferedInputStream与BufferedOutputStream可以为InputStream、OutputStream类的对象增加缓冲区功能。创建BufferedInputStream流时,需要给定一个InputStream类型的实例,实现BufferedInputStream时,实际上最后是实现InputStream实例。同样地,在创建BufferedOutputStream流时,也需要给定一个OutputStream实例,实现BufferedOutputStream时,实际上最后是实现OutputStream实例。
BufferedInputStream的数据成员buf是一个字节数组,默认为2048字节。当读取数据源时,例如文件,BufferedInputStream会尽量将buf填满。当使用read()方法时,实际上是先读取buf中的数据,而不是直接读取数据源。当buf中的数据不足时,BufferedInputStream才会再实现给定的InputStream对象的read()方法,从指定的设备中提取数据

Java I/O系统

BufferedOutputStream的数据成员buf是一个字节数组,默认为512字节。当使用write()方法写入数据时,实际上会先将数据写至buf中,当buf已满时才会实现给定的OutputStream对象的write()方法,将buf数据写至目的地,而不是每次都对目的地作写入的动作。

                   **字符流**
字符流用于处理文本文件。所有字符流都继承于抽象类Reader和Writer两个父类。其中Reader为读取字符流的父类, Writer为写入字符流的父类。图17.15是字符流中输入流的层次关系示意图,图17.16是字符流中输出流的层次关系示意图。

Java I/O系统

Java I/O系统

Reader与InputStream相似,用于从流中读取数据。Reader是以字符为单位操作流。

Java I/O系统

Writer和OutputStream类似,用于向流中写入数据。只是Writer是以字符为单位写入数据。

Java I/O系统

在处理字符流时,主要问题是进行字符编码的转换。Java语言采用Unicode字符编码方式,对于每个字符,JVM为其分配2个字节的内存。但是,在文本文件中,字符有可能采用其它类型的编码,例如UTF-8、GBK、GB2312等。Reader类可以将数据源中采用其它编码类型的字符转换为Unicode字符,然后在内存中为这些Unicode字符分配内存;Writer类能够把内存中的Unicode字符转换为其它编码类型的字符,再写到数据目的地。默认情况下,Reader和Writer会在本地操作系统平台默认字符编码和Unicode编码之间进行转换。对于中文操作系统平台,默认的字符编码通常为“GBK”。
由于Reader和Writer采用过了字符编码转换技术,所以它们能够正确访问各种字符编码的文本文件。另一方面,在为字符分配内存时,JVM在内存中统一采用Unicode编码,所以Java程序处理字符具有平*立性。这就省去了程序员在编写程序时手动进行字符编码转换的繁琐。

             低级字符流
字符流中的低级流包括:
    对内存数组进行操作的CharArrayReader和CharArrayWriter类,与ByteArrayInputStream和ByteArrayOutputStream类似,其数据源为内存中的字符数组。
    对内存中的String对象进行操作的StringReader和StringWriter类,其数据源为内存中的String对象。
    对线程管道进行操作的PipedReader和PipedWriter类,与PipedInputStream和PipedOutputStream类似,其数据源为线程管道。
    对文本文件进行读写操作的FileReader和FileWriter类,与FileInputStream和FileOutputStream类似,其数据源为磁盘文件。FileReader和FileWriter按照本地操作系统平台的默认字符编码,读写文件。

             高级字符流
字符流中常用的高级流包括:
    缓冲流:包括BufferedReader和BufferedWriter类,利用缓冲区来提高读写数据的效率。
    转换流:用于字节数据到字符数据之间的转换,包括InputStreamReader和OutputStreamWriter。
打印输出流:包括PrintWriter类,允许将基本类型数据打印输出到字符串流中,PrintWriter带有带自动刷新(Flush)功能。
下面,我们分别讲述缓冲流、转换流和打印输出流类的用法。
1)字符缓冲流
BufferedReader和BufferedWriter类,与BufferedInputStream和BuffedOutputStream类似,可以利用缓冲区来提高读写数据的效率,用来装饰其它的字符流。比BufferedInputStream和BuffedOutputStream更强大的是,这两个类可以整行读写字符。
BufferedReader类提供了readLine()方法,可以一次读入一行字符,以字符串形式返回,如果读到文件末尾,就返回null。BufferedWriter类提供了newLine()方法,写入一个行分隔符。
2)转换流
Java既支持字节流,也支持字符流,但是有时候需要在字节流和字符流之间转换。Java I/O库中提供了InputStreamReader和OutputStreamWriter这两个类用于字节数据到字符数据之间的转换。InputStreamReader用于将一个字节流中的字节转换为字符,而OutputStreamWriter用于将写入的字符转换成字节后写入一个字节流,此外,这两个类可以根据指定的编码方式读写流中的字符。

Java I/O系统

3)字符打印输出流
前面我们已经介绍了字节流中的打印输出流PrintStream,它可以将Java的基本数据类型等数据,直接转换为系统默认编码下对应的字符,再输出至OutputStream中。而这里要介绍的 PrintWriter在功能上与PrintStream类似,除了接受OutputStream实例作为变量之外,PrintWriter还可以接受Writer对象作为输出的对象。当原先是使用Writer对象在作字符处理,而现在想要套用println()之类的方法时,使用PrintWriter会是比较方便的做法。

              **对象流**
Java I/O提供了各类流完成数据的读写,这些流读写数据的“口径”是不一样的。这里的“口径”对应的是每次读写发生时数据的复杂程度。最基本的流提供了基于byte或char的读写;高级一些的,例如BufferedReader/BufferedWriter提供了基于String的读写;更高级的,比如DataInputStream/DataOutputStream提供过了各类基本类型int、long、short等的读写;但*的,也是最自然的读写是基于对象的读写,这是由ObjectInputStream/ObjectOutputStream来提供的。
ObjectInputStream/ObjectOutputStream之所以能完成基于对象的读写,是因为Java提供了一套对象序列化机制。序列化机制就是一套向流写入或读取对象数据的自动机制,这套机制将读写的实现封装在对象自身的实现之中。
为了让对象可以实现可以通过ObjectInputStream/ObjectOutputStream进行读写,必须让它的类可以被序列化。而为了实现对象序列化,对应的类必须实现java.io.Serializable接口。
在Serializable接口中没有定义任何的方法,它是我们在接口一章中讲到的“标记接口”。这种类型的接口仅仅用于标示它的实现类特性,而没有具体的方法定义。

             **流的使用原则**
前面我们用了很大的篇幅介绍了Java I/O中主要的流类,现在我们对流的使用做一个总结。
对Java I/O库中的流类有如下的使用原则:
一、按数据源分类
    1)	如果数据源是文件,对于字节流,使用FileInputStream和FileOutputStream;对于字符流,使用FileReader和 FileWriter。
    2)	如果数据源是字节数组byte[],则使用ByteArrayInputStream和ByteArrayOutputStream。
    3)	如果数据源是字符数组Char[],则使用CharArrayReader和CharArrayWriter。
    4)	如果数据源是String对象,对于字节流,则使用StringBufferInputStream和StringBufferOuputStream;对于字符流,则使用StringReader和StringWriter。
    5)	如果数据源是网络数据流,对于字节流,使用InputStream和OutputStream;对于字符流,使用Reader和Writer。
二、按是否格式化输出分:要格式化输出,则使用PrintStream或PrintWriter。
三、按是否要缓冲分,要缓冲的话,对于字节流使用BufferedInputStream和BufferedOutputStream;对于字节流,使用BufferedReader和BufferedWriter。
四、按数据格式分
    1)	二进制格式(只要不能确定是纯文本的):使用InputStream、OutputStream 及其所有带 Stream结束的子类。
    2)	纯文本格式(含纯英文与汉字或其他编码方式):使用Reader、Writer 及其所有带 Reader、Writer 的子类。
五、按输入输出分
    1)	输入:使用Reader、InputStream 类型的子类。
    2)	输出:使用Writer、OutputStream 类型的子类。
六、特殊需要
    1)	从Stream到Reader、Writer的转换类:InputStreamReader、OutputStreamWriter。
    2)	对象输入输出:ObjectInputStream、ObjectOutputStream。
    3)	线程间通信:PipeInputStream、PipeOutputStream、PipeReader、PipeWriter。
    4)	合并输入:SequenceInputStream。
    5)	更特殊的需要:PushbackInputStream、PushbackReader、LineNumberInputStream、LineNumberReader。
在不考虑特殊需求的情况,决定使用哪个类以及它的构造进程的一般准则如下:
首先,考虑最原始的数据格式是什么:原则四。
第二,是输入还是输出:原则五。
第三,是否需要转换流:原则六第1点。
第四,数据源是什么:原则一。
第五,是否要缓冲:原则三(特别注明:一定要注意的是readLine()是否有定义,有什么比 read()、write()更特殊的输入或输出方法)。
第六,是否要格式化输出:原则二。