(这一个章节将讲到Java里面比较重要的一个章节,这里说一句抱歉,因为最近换工作的原因,一直没有时间继续书写教程,不过接下来我会一直坚持写下去的哈,希望大家能够支持。这个章节主要涉及到常用的文件读写,包括高级的文件IO内容——java.nio,因为这些内容在如今的一些项目里面也属于相当常见的一部分,如果有什么遗漏或者笔误的话,希望读者来Email告知:silentbalanceyh@126.com,谢谢!这一部分篇幅可能比前边章节长很多,也是为了保证能够将Java里面IO和文件操作部分内能写的都写入,如果有遗漏希望读者来Email,概念上有混淆的地方请告知,里面有些内容参考了一些原文数据进行了翻译以及思考注解。)
本章目录:
1.IO类相关内容
2.文件和目录
3.文件高级操作
1.IO类相关内容
i.基本概念:
Java编程里面针对IO的操作主要提供了两个包java.io和java.nio,这两个包都可以用来进行IO编程,实现应用程序所需要的相关IO操作,但是性能上可能存在比较大的区别。在Java API中,读取对象的时候一般都是按照一个有序的字节流进行读取,这种读取方式称为流式读取;按照这种方式读取一个对象的字节流称作输入流(Input Stream),写入一个对象的字节流称作输出流(Output Stream),至于输入和输出的目标以及源可以是控制台,同样可以是文件、网络连接甚至是内存块。Java IO类里面处理这种需求(字节流)的最高抽象类主要是InputStream和OutputStream,这两个类是整个Java处理IO的最高层的抽象类,这两个类直接继承于java.lang.Object。
在Java IO类里面,一般情况将输入输出分为两种,一种就是上边讲到的字节流,还有一种,高层抽象类为Reader和Writer,称为字符流,字符流和字节流不一样。字节流是将对象或者读取以及写入的内容直接序列化成为流式数据进行传输和读写,而字符流是将这些内容直接转换称为Unicode的字符来进行读取或者写入操作。
在Java IO类的层次结构里面,它的整体结构如下:
字节流部分的类层次结构:
字符流部分的类层次结构:
从JDK 1.5过后,这些内容规范化了很多,记忆起来也不是很麻烦,所有以Stream结尾的类都是字节流的IO类,而所有以Writer和Reader结尾的类都是字符流的IO类,而Java IO类里面的接口层次结构如下:
ii.IO字节流——的输入输出:
[1]InputStream和OutputStream简介:
这两个类是字节流的抽象读写类,两个类都为抽象类,类定义如下:
public abstract class InputStream extends Object implements Closeable
public abstract class OutputStream extends Object implements Closeable,Flushable
这里引入一点题外话,就是二者实现的两个接口的简单讲解:
Closeable接口(1.5):Closeable接口是从JDK 1.5才引入的一个新的接口,该接口是可以关闭IO数据源或者目标的,调用close方法可释放对象保存的资源,比如我们常用的打开的文件。该接口里面只有一个方法:
void close() throws IOException
【个人思考*:这里提供一点点比较新的资料,在JDK 1.7里面开始支持C#里面使用的资源闭包,也就是类似C#里面的using语句的扩展方式,只是Java语言的关键字是try,这种设计刚好为这种实现做了一定层次的铺垫,也就是在JDK 1.7过后所有的资源在读写过程不需要显示调用close()方法,同样类似C#里面的using语句使用try块语句实现在一个代码块里面使得里面的输入输出流自动关闭,该接口的引入也使得所有输入输出流在操作过程更加倾向于规范,因为在JDK 1.5之前,所有的close()操作都是由类本身提供的,这种方式无疑使得整个关闭显得不够正规。而且需要谨记的是,所有的IO资源最好在最终处理的时候使用正常的代码方式关闭,否则会占用系统资源。】
Flushable接口(1.5):Flushable接口也是JDK 1.5才引入的一个新接口,该接口是可刷新数据的目标接口,调用flush方法将所有已经缓冲输出的数据写入底层流,该接口和Closeable一样,里面仅仅包含了一个输出流的常用方法:
void flush() throws IOException
【*:初学者对flush方法会存在很多困惑,有时候不知道该方法到底是怎么在执行,而在很多输出流中都会使用到flush方法。首先需要理解的是操作系统文件写入的原理,一般情况下,不论是写入的目标源是文件、控制台还是网络,其写入的原理都类似,使用Java语言编程的时候,Java语言本身只会衔接到JVM平台这一层,除非使用JNI(其实JNI也没有直接使用Java语言调用操作系统的API),否则Java语言很难直接操作操作系统底层的一些数据以及相关内容交互。而IO输出的时候,往往是将数据写入缓冲区,一般情况下缓冲区存在于操作系统的内存,针对文件系统的操作有四个:打开、关闭、读、写,JVM在执行命令过后,会将底层的操作递交给操作系统本身来完成。flush的操作就负责发送请求,请求操作系统将缓冲区的内容写入到磁盘上的物理文件,如果不定义缓冲区的大小,就根据JVM本身的配置和操作系统的本身性质由操作系统决定什么时候写入,一般情况下不进行该操作的话会实时写入,如果定义了缓冲区情况可能就不一样了。】
[2]字节流类结构详解:
InputStream和OutputStream都是抽象类,在实例化的时候,一般情况下是使用它子类进行实例化,它们本身不存在实例。
InputStream字节流输入的类结构:
[A]InputStream(1.0)
|—[C]javax.sound.sampled.AudioInputStream(1.3)
|—[C]ByteArrayInputStream(1.0)
|—[C]FileInputStream(1.0)
|—[C]FilterInputStream(1.0)
|—[C]BufferedInputStream(1.0)
|—[C]java.util.zip.CheckedInputStream(1.6)
|—[C]javax.crypto.CipherInputStream(1.4)
|—[C]DataInputStream(1.0)
|—[C]java.util.zip.DeflaterInputStream(1.6)
|—[C]java.security.DigestInputStream(1.6)
|—[C]java.util.zip.InflaterInputStream(1.6)
|—[C]java.util.zip.GZIPInputStream(1.6)
|—[C]java.util.zip.ZipInputStream(1.6)
|—[C]java.util.jar.JarInputStream(1.2)
|—[C]LineNumberInputStream(1.0)【已过时】
|—[C]javax.swing.ProgressMonitorInputStream
|—[C]PushbackInputStream(1.0)
|—[A]org.omg.CORBA.portable.InputStream(1.2)
|—[A]org.omg.CORBA_2_3.portable.InputStream(1.2)
|—[C]ObjectInputStream(1.1)
|—[C]PipedInputStream(1.0)
|—[C]SequenceInputStream(1.0)
|—[C]StringBufferInputStream(1.0)【已过时】
InputStream是表示字节流输入的所有类的超类,它的几个子类的介绍如下:
——AutioInputStream(1.3)类(javax.sound.sampled.AudioInputStream):该类是音频输入流,具有指定音频格式和长度的输入流,长度用实例帧表示而不使用字节。该类提供了几种方法,用于从流读取一定数量的字节,或未指定数量的字节,音频输入流跟踪所读取的最后一个字节,可以跳过任意数量的字节以达到稍候的读取位置。该输入流可支持标记,设置标记的时候,会记住当前的位置,以便可以稍候返回该位置。
Java的AudioSystem类包括了许多操作AudioInputStream对象的方法,这些方法可以做以下的事情:
- 从外部音频文件、流或URL获得音频输入流
- 从音频输入流写入外部文件
- 将音频输入流转换为不同的音频格式
该类不是属于java.io的包,所以这个地方针对该类就不做过多的讨论。
——ByteArrayInputStream(1.0)类:该类包含了Java定义的一个内部的缓冲区,该缓冲区包含从流中读取的字节,内部计数器跟踪read方法要提供的下一个字节,若该输入流被关闭就无效,但是此类中的方法在关闭此流过后仍然可以被调用,而且不会产生IOException
——FileInputStream(1.0)类:该类从文件系统中的某个文件中获得输入字节,哪些文件可以取决于定义的主机环境,该类也可以用于读取图像数据之类的原始字节流
——FilterInputStream(1.0)类:该类包含了其他一些输入流,它将这些流作用其基本数据源,它可以直接传输数据或提供一些额外的功能,FilterInputStream类本身只是简单地重写那些将所有请求传递给所包含输入流的InputStream的所有方法,FilterInputStream的子类可进一步重写这些方法中的一些方法,并且还可以提供一些额外的方法和字段。
——BufferedInputStream(1.0)类:该类为另一个输入流添加一些功能,即缓冲输入以及支持mark和reset方法的能力,在创建BufferedInputStream时,会创建一个内部缓冲区数组,在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。mark操作记录输入流中的某个点,reset操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次mark操作后读取的所有字节。
——java.util.zip.CheckedInputStream(1.6)类:该类是读取一个压缩包里面的类,是一个需要维护所读取数据校验和的输入流,校验和用来验证输入数据的完整性。
——javax.crypto.CipherInputStream(1.4)类:该类由一个InputStream和一个Cipher组成,这样read()方法才能返回从底层InputStream读入但已经由该Cipher另外处理过的数据,在由CipherInputStream使用之前,该Cipher必须充分初始化。如果Cipher初始化为解密,在返回解密的数据之前,CipherInputStream将尝试读入数据并且将其解密。该类严格遵守此语义,尤其是其祖先类FilterInputStream和InputStream的语义。此类具有其祖先类中指定的所有方法,并且对所有的这些方法进行了重写,除此之外,此类还对其祖先类未抛出的所有异常进行捕获。
——DataInputStream(1.0)类:数据输入流允许应用程序以与机器无关的方式从底层输入流中读取基本Java数据类型,应用程序可以使用数据输出流写入由数据输入流读取的数据,DataInputStream有一点需要注意,对于多线程访问不一定是安全的,线程安全是可以通过方法设置,它由此方法的使用者负责。
——java.util.zip.DeflaterInputStream(1.6)类:该类为使用“deflate”压缩格式压缩数据实现输入流过滤器。
——java.security.DigestInputStream(1.6)类:使用通过流的位更新关联消息摘要的透明流,要完成消息摘要计算,先要调用此摘要输入流的一个read方法,之后在关联的信息摘要上调用一个digest方法。开启或关闭此流都是可能的,开启的时候,调用read方法之一将导致消息摘要的更新,但是关闭的时候,不更新消息摘要,该流在默认情况下是开启的。
——java.util.zip.InflaterInputStream(1.6)类:此类为解压缩“deflate”压缩格式的数据实现流过滤器,它还用做其他解压过滤器的基础。
——java.util.zip.GZIPInputStream(1.6)类:此类为读取GZIP文件格式的压缩数据实现流过滤器
——java.util.zip.ZipInputStream(1.6)类:该类为读取ZIP文件格式的文件输入流过滤器。包括对已知和未压缩条目的支持。
——java.util.jar.JarInputStream(1.2)类:JarInputStream类用于从任何输入流读取JAR文件内容,它扩展了java.util.zip.ZipInputStream类,使之支持读取可选的Manifest条目,Manifest可用于存储有关JAR文件及其条目的元信息
——LineNumberInputStream(1.0)类【已过时】:该类已经过时这里不做讨论
——javax.swing.ProgressMonitorInputStream类:监视读取某些InputStream的进度,这可以创建一个进度监视器,以监视读取输入流的进度,如果需要一段时间,将会弹出ProgressDialog,以通知用户,该类一般在Java Swing的开发中更加常见。
——PushbackInputStream(1.0)类:该类为另一个输入流添加性能,即“推回(push back)”或“取消读取(unread)”一个字节的能力,在代码片段可以很方便地读取由特定字节值分割的不定数据的数据字节时,这很有用;在读取终止字节后,代码片段可以“取消读取”该字节,这样,输入流上的下一个读取操作将会重新读取被推回的字节。
【*:org.omg的子包里面的两个类这里不做说明,IO部分暂时不涉及这一块内容,而且我自己平时用得也很少】
——ObjectInputStream(1.1)类:【该类在序列化和反序列化章节进行介绍】
——PipedInputStream(1.0)类:管道输入流应该连接到通道输出流,管道输入流提供要写入管道输出流的所有数据字节,通常数据由某个线程从PipedInputStream对象读取,并由其他线程将其写入到相应的PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。管道输入流包含一个缓冲区,可以在缓冲区限定的范围内将读和写操作分离开,如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。
——SequenceInputStream(1.0)类:该类表示其他输入流的逻辑串联,它从输入流的有序集合开始,并且从第一个输入流开始读取,直到文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
OutputStream字节流输出的类结构:
[A]OutputStream(1.0)
|—[C]ByteArrayOutputStream(1.0)
|—[C]FileOutputStream(1.0)
|—[C]FilterOutputStream(1.0)
|—[C]BufferedOutputStream(1.0)
|—[C]java.util.zip.CheckedOutputStream(1.6)
|—[C]javax.crypto.CipherOutputStream(1.4)
|—[C]DataOutputStream(1.0)
|—[C]java.util.zip.DeflaterOutputStream(1.6)
|—[C]java.util.zip.GZIPOutputStream(1.6)
|—[C]java.util.zip.ZipOutputStream(1.6)
|—[C]java.util.jar.JarOutputStream(1.6)
|—[C]java.security.DigestOutputStream
|—[C]java.util.zip.InflaterOutputStream(1.6)
|—[C]PrintStream(1.0)
|—[C]java.rmi.server.LogStream(1.1)【已过时】
|—[C]ObjectOutputStream(1.1)
|—[A]org.omg.CORBA.portable.OutputStream(1.2)
|—[A]org.omg.CORBA_2_3.portable.OutputStream(1.2)
|—[C]PipedOutputStream(1.0)
OutputStream类是表示字节流输出的所有类的超类,它的子类如下:
——ByteArrayOutputStream(1.0)类:此类实现了一个输出流,其中的数据被写入一个byte数组,缓冲区会随着数据的不断写入而自动增长,可使用toByteArray()和toString()方法获取数据。
——FileOutputStream(1.0)类:文件输出流是用于将数据写入File或FileDescriptor的输出流,文件是否可用或能否可以被创建取决于基础平台,特别是某些平台一次只允许一个FileOutputStream打开文件进行写入,在这种情况下,如果所涉及的文件已经打开,则此类的构造方法将会失败。
——FilterOutputStream(1.0)类:此类是过滤输出流的所有类的超类,这些流位于已存在的输出流之上,它们将已存在的输出流作为基本数据接收器,但可能直接传输数据或提供一些额外的功能。FilterOutputStream类本身只是简单地重写那些将所有请求传递给所包含输出的OutputStream的所有方法。FilterOutputStream的子类可进一步地重写这些方法中的一些方法,并且还可以提供一些额外的方法和字段。
——BufferedOutputStream(1.0)类:该类实现缓冲的输出流,通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。
——java.util.zip.CheckedOutputStream(1.6)类:需要维护写入压缩包的输出流,主要用来维护校验和,校验和可以用于验证输出数据的完整性。
——javax.crypto.CipherOutputStream(1.4)类:该类由一个OutputStream和一个Cipher组成,这样writer()方法才能在将数据写出到底层OutputStream之前对数据进行处理,在由CipherOutputStream使用之前,密码必须充分进行初始化。比如密码初始化加密,CipherOutputStream将在写出该加密数据之前尝试加密该数据,该类和CipherInputStream相对应
——DataOutputStream(1.0)类:数据输出流允许应用程序以适当的方式将基本Java数据类型写入输出流中,然后,该应用程序可以使用数据输入流将数据读入。
——java.util.zip.DeflaterOutputStream(1.6)类:此类为使用“deflate”压缩格式压缩数据实现输出流过滤器,它还用作其他类型的压缩过滤器的基础
——java.util.zip.GZIPOutputStream(1.6)类:此类为使用GZIP文件格式写入压缩数据实现流过滤器
——java.util.zip.ZipOutputStream(1.6)类:该类为以ZIP文件格式写入文件实现输出流过滤器,包括对已压缩和未压缩的条目的支持
——java.util.zip.JarOutputStream(1.4)类:该类用于向任何输出流写入JAR文件内容,它扩展了ZipOutputStream类,使之支持编写可选的Manifest条目,Manifest可用于指定有关JAR文件以及条目的元信息。
——java.security.DigestOutputStream类:使用通过流的位更新关联消息摘要的透明流,要完成消息摘要的计算,先要调用此摘要输出流的一个write方法,之后在关联的消息摘要上调用digest方法之一。开启或关闭此流都是可能见的,开启时,调用write方法之一将导致消息摘要的更新,但是关闭的时候不需要消息更新,流在默认情况下是开启的。
——java.util.zip.InflaterOutputStream(1.6)类:为解压缩“deflate”压缩格式存储的数据实现输出流过滤器
——PrintStream(1.0)类:该类为其他输出流添加了功能,使它们能够方便地答应各种数据值表示形式,它还提供其他两项功能,与其他输出流不同,PrintStream永远不会抛出IOException;而是,异常情况仅设置可通过checkError方法测试的内部标志,另外,为了自动刷新,可以创建一个PrintStream;这意味着可在写入byte数组之后自动调用flush方法,不需要手动调用,可以调用其中一个println方法,该方法末尾会写入一个“/n”符
——java.rmi.server.LogStream(1.1)类:【已过时】,该方法提供一种记录错误的机制,这一机制专门用来监视系统运行情况
——ObjectOutputStream(1.1)类:【该类在序列化和反序列化章节进行介绍】
【*:org.omg的子包里面的两个类这里不做说明,IO部分暂时不涉及,而且我自己平时用得也很少】
——PipedOutputStream(1.0)类:可以将管道输出流连接到管道输入流来创建通信管道,管道输出流是管道的发送端,通常,数据由某个线程写入PipedOutputStream对象,并由其他线程从连接的PipedInputStream读取,不建议对这两个对象尝试使用单个线程,因为这样有可能造成使用线程死锁,如果某个线程正从连接的管道输入流中读取数据字节,但该线程不再处于活动状态,则该管道损毁。
[3]字节流IO类的操作:
InputStream抽象类中的操作:
abstract int read():
该方法用来读取字节,并且返回读取到的字节,如果这个字节流结束的时候,这个方法会返回-1。
int read(byte[] b):
该方法将读取到的字节存储在传入的一个字节缓冲区也就是一个字节数组里面,而且每一次按照该数组的长度(b.length)读取字节,和上边方法一样,如果读取到字节流的末尾该方法会返回-1。
int read(byte[] b,int off,int len):
和上边方法一模一样,唯一不同的是三个参数:
- b:将数据读入的缓冲区字节数组
- off:将字节放入的数组的第一个位置
- len:需要读取的字节的最大长度
【*:这两个方法都是使用一个自定义的字节数组作为读取过程的读取缓冲区,只是操作方法不一样,具体操作可以根据当时的需求来选择。】
long skip(long n):
在输入流里面跳过n个字节,该方法返回跳过的字节数(前提条件是跳过的字节长度比流数据的总长度要小)
int available():
返回数据流里面合法数据的流数据块的数据流的字节长度
void close():
关闭输入流
void mark(int readlimit):
在输入流的某个地方放入一个标记,如果超过readlimit的字节数需要读取,则该字节流会自动忽略该标记
void reset():
返回输入流的最后一个标记,一般情况用来重置该输入流
boolean markSupported():
判断一个输入流是否支持标记功能,如果不支持标记功能就直接返回false,反之返回true
OutputStream抽象类中的操作:
abstract void write(int n):
写入n个字节的数据
void write(byte[] b):
void write(byte[] b,int off,int len):
使用一个缓冲区字节数组来进行数据的写入,先把数据写入一个字节缓冲区,然后再进行流数据写入,和read方法相对应
void close():
关闭输出流
void flush():
该方法就是输出流里面的特殊方法,把操作系统缓冲区里面的数据直接向物理目标源进行写入操作,这里缓冲区可以理解为缓冲区字节数组。
【*:其实从Java语言可以知道,当定义一个对象的时候,该对象是存储在内存里面的,也就是当创建了一个字节数组的时候,可以默认定义了一个内存里面的缓冲区,用来进行数据读写的缓冲做辅助使用。】
iii.IO字符流——输入和输出:
[1]Reader和Writer简介:
这两个类是字符流的IO读写超类,二者的类定义如下:
public abstract class Reader extends Object implements Readable,Closeable
public abstract class Writer extends Object implements Appendable,Closeable,Flushable
这里需要介绍两个新接口:
Readable接口(1.5):该接口是一个以字符为输入源的接口,一般只有字符流才会用到该接口,字节流不会使用这个接口
Appendable接口(1.5):能够被添加char序列和值的对象,如果某个类的实例打算收取自Formatter的格式化输出,那么该类必须实现Appendable接口,要添加的字符应该是有效的Unicode字符,增补字符可能由多个16位char值组成,同样该接口不是线程安全的。
[2]字符流类结构详解:
Reader字符流输入的类结构:
[A]Reader(1.1)
|—[C]BufferedReader(1.1)
|—[C]LineNumberReader(1.1)
|—[C]CharArrayReader(1.1)
|—[A]FilterReader(1.1)
|—[C]PushbackReader(1.1)
|—[C]InputStreamReader(1.1)
|—[C]FileReader(1.1)
|—[C]PipedReader(1.1)
|—[C]StringReader(1.1)
Reader是字符流输入的超类,它的子类解析如下:
——BufferedReader(1.1):从字符输入流中读取文本,缓冲每个字符,从而实现字符、数组和行的高效读取。和Stream字节输入流一样,这个字符输入流同样可以指定缓冲区的大小,或者使用默认大小。
【*:这里有一天我自己也很困惑,仔细思考一下。照理说,上边讲到的字节的实时读取是不对的,BufferedReader的说法,实际上系统默认是存在一个缓冲区的,当写程序的时候如果没有自定义缓冲区,输入和输出使用的都是系统的缓冲区而不是实时读取,不过这一点影响不会太大,当我们真正需要提高IO性能的时候,下边会讲到一些比较简单的IO部分性能的提升法则。】
——LineNumberReader(1.1):跟踪行号的缓冲字符输入流,此类定义了方法setLineNumber(int)和getLineNumber(),它们可分别用于设置和获取当前行号。默认情况下,行号编号从0开始,该行号随数据读取在每个行结束的地方开始递增,并且可以通过调用setLineNumber(int)更改行号。但是需要注意的是,该方法不会实际更改流当中的当前位置,它只是修改了getLineNumber()的返回值,其识别符号为:换行符“/n”、回车“/r”、或者回车后边紧跟换行符。
——CharArrayReader(1.1):此类直接实现一个用作字符输入流的字符缓冲区
——FilterReader(1.1):用于读取已过滤的字符流的抽象类,该类自身提供了一些将所有请求传递给包含的默认方法,它的子类应该重写这些方法中的一些,并且还可以提供一些额外的方法和字段。
——PushbackReader(1.1):允许将字符推回到流的字符流Reader。
——InputStreamReader(1.1):这是字节流到字符流的桥梁类,它使用指定的charset编码格式读取字节并且将其解码为字符,它使用的字符集可以由名称指定或者显示给定,或者可以接受平台默认的字符集。每次调用InputStreamReader中的一个read()方法都会导致从底层输入流中读取一个或者多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节,为了达到更高效率,可要考虑在BufferedReader内包装InputStreamReader。
——PipedReader(1.1):直接传送字符的管道字符流输入类
——StringReader(1.1):直接使用输入源为字符串的字符流输入类
Writer字符流输出的类结构:
[A]Writer(1.1)
|—[C]BufferedWriter(1.1)
|—[C]CharArrayWriter(1.1)
|—[A]FilterWriter(1.1)
|—[C]OutputStreamWriter(1.1)
|—[C]FileWriter(1.1)
|—[C]PipedWriter(1.1)
|—[C]PrintWriter(1.1)
|—[C]StringWriter(1.1)
Writer是字符流输出的超类,它的子类解析如下:
——BufferedWriter(1.1):该类将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
——CharArrayWriter(1.1):此类实现一个可用作Writer的字符缓冲区,缓冲区会随向流中写入数据而自动增长,可使用toCharArray()和toString()获取数据,在此类上调用close()无效,并且在关闭该流后可以调用此类中的方法,而不会产生任何IOException。
——FilterWriter(1.1):用于写入已过滤的字符流的抽象类,该类自身提供了所有请求传递给所包含字符流的默认方法,FilterWriter的子类应该重写这些方法中的一些方法,并且可以提供一些额外的方法和字段。
——OutputStreamWriter(1.1):该类是字符流通向字节流的桥梁,可使用指定的charset将要写入流中的字符编码成字节,它使用的字符集可以由名称指定或显示给定,否则将接受平台默认的字符集,在调用过程,考虑使用OutputStreamWriter包装到BufferedWriter中,以避免频繁调用转换器
——FileWriter(1.1):用来写入字符文件的便捷类,此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可以接受的,如果要自己指定,可以在FileOutputStream上构造一个OutputStreamWriter。文件是否可用或是否可以被创建取决于底层平台,特别是某些平台一次只允许一个FileWriter打开文件进行写入,这样的情况下,如果文件已经打开则就会构造失败。
——PipedWriter(1.1):直接传送字符的管道字符流输出类
——PrintWriter(1.1):向文本输出流打印对象的格式化表示形式,此类实现在PrintStream中的所有方法,它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。【*:该类和PrintStream不一样,该类不是等到特定的符号才完成刷新,如果启用了自动刷新过后,调用了println和printf以及format几个方法中的一个的时候才可能完成此操作,同样的这个类不会抛出IOException异常】
——StringWriter(1.1):一个字符流,主要限制了目标输出源,这个方法主要用于目标源为String类型的对象的时候
iv.使用范例
上边介绍了IO的大部分类,下边使用代码针对一些特殊的情况进行说明和讲解,以帮助读者了解IO类的一些用法,关于File的部分保留到文件章节去讲,这里主要讲解上边比较常用的一些类以及一些常用的场景。
[1]普通使用范例介绍:
——[$]输入输出的拷贝——
package org.susan.java.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StreamCopier {
public static void main(String args[]){
try{
copy(System.in, System.out);
}catch(IOException ex){
System.err.println(ex);
}
}
public static void copy(InputStream in,OutputStream out) throws IOException{
byte[] buffer = new byte[1024];
while(true){
int bytesRead = in.read(buffer);
if( bytesRead == -1)
break;
out.write(buffer, 0, bytesRead);
}
}
}
该类在输入流和输出流里面进行了拷贝操作,在方法内部创建了一个1024字节长度的自定义缓冲区,然后把输入的字符读入并且通过输出流输出。
在Java语言标准的I/O模型中,提供了System.in、System.out、System.err三个
System.in:该类属于java.lang.System类的一个内部类,类类型为InputStream,这个类属于“标准”输入流,此流已打开并准备提供输入数据,通常此流对应键盘输入或者由主机环境或用户指定的另一个输入源,在Windows环境下,该输入流默认对应的是控制台里面的输入。
System.out:该类属于java.lang.System类的一个内部类,类类型为PrintStream,这个类属于“标准”输出流,此流已打开并准备接受输出数据,通常此流对应显示器输出或者由主机环境指定的另外一个输出目标,和System.in一样,在Windows环境下,该输出流默认对应的是控制台的输出。
System.err:“标准”错误输出流,此流已打开并准备接受输出数据,通常,此流对应于显示器或者由主机环境或用户指定的另一个输出目标,按照惯例,此输出流是用于错误信息输出的。
上边例子演示了从InputStream类型的System.in到PrintStream类的System.out的字节拷贝,是一个很简单的IO的例子,上边的程序这里不提供演示效果,是一个在Console上边的用户可交互的程序,输入的是什么内容,输出就是什么,类似我们最开始学习C++的时候的cin和cout的交互操作。
——[$]从URL读取流数据——
package org.susan.java.io;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
public class URLStream {
public static void main(String[] args) throws IOException{
InputStream inputStream = null;
try{
URL url = new URL("http://www.google.com");
inputStream = url.openStream();
for( int c = inputStream.read(); c != -1; c = inputStream.read()){
System.out.write(c);
}
inputStream.close();
}catch(MalformedURLException ex){
System.err.println("Not a URL Java understands.");
}finally{
if( inputStream != null)
inputStream.close();
}
}
}
这一段代码直接从一个在线的URL地址获取数据流,然后把从这个地址获取到的内容在控制台打印出来,读取的方式调用的是InputStream的read方法,它会逐个字节从字节流里面读取直到这个字节流被读取完为止。这里输出的是从http://www.google.com地址读取到的HTML的代码,为方便初学者,我这里提供该程序的输出,找了个比较简洁的的页面也是不占篇幅:
<!doctype html><html><head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"><title>Google</title><script>window.google={kEI:"zMMoS_3wMISGwgPJzKyYDQ",kEXPI:"17259,23129",kCSI:{e:"17259,23129",ei:"zMMoS_3wMISGwgPJzKyYDQ"},kHL:"en",time:function(){return(new Date).getTime()},log:function(b,d,c){var a=new Image,e=google,g=e.lc,f=e.li;a.οnerrοr=(a.οnlοad=(a.οnabοrt=function(){delete g[f]}));g[f]=a;c=c||"/gen_204?atyp=i&ct="+b+"&cad="+d+"&zx="+google.time();a.src=c;e.li=f+1},lc:[],li:0};
window.google.sn="webhp";window.google.timers={load:{t:{start:(new Date).getTime()}}};try{}catch(u){}window.google.jsrt_kill=1;
var _gjwl=location;function _gjuc(){var e=_gjwl.href.indexOf("#");if(e>=0){var a=_gjwl.href.substring(e);if(a.indexOf("&q=")>0||a.indexOf("#q=")>=0){a=a.substring(1);if(a.indexOf("#")==-1){for(var c=0;c<a.length;){var d=c;if(a.charAt(d)=="&")++d;var b=a.indexOf("&",d);if(b==-1)b=a.length;var f=a.substring(d,b);if(f.indexOf("fp=")==0){a=a.substring(0,c)+a.substring(b,a.length);b=c}else if(f=="cad=h")return 0;c=b}_gjwl.href="/search?"+a+"&cad=h";return 1}}}return 0}function _gjp(){!(window._gjwl.hash&&
window._gjuc())&&setTimeout(_gjp,500)};
………………
这里可能有点繁琐,只是为了方便初学者知道这个页面到底输出了什么,这里读取到的数据因为是在控制台做的输出,所以输出的就是这个地址最终生成的HTML代码,包括该代码里面拥有的JavaScript以及CSS部分的内容,这个例子演示的就是从一个URL地址读取HTML的简单范例。
——[$]String和InputStream——
package org.susan.java.io;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
*该类是从一个InputStream到String对象转换的程序
*这里使用了FileInputStream,就是直接从一个文件读取文件里面的数据
**/
public class InputStreamToString {
public static void main(String args[]) throws Exception{
InputStream is = new FileInputStream(new File("D:/data.txt")); // D://data.txt
System.out.println(convertStringToStream(is));
}
public static String convertStringToStream(InputStream in) throws Exception{
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder builder = new StringBuilder();
String line = null;
while((line = reader.readLine())!= null){
builder.append(line + "/n");
}
in.close();
return builder.toString();
}
}
这段程序的输出为:
This is a program convert the stream into a String Object
上边这段文字就是D盘下边data.txt文件里面的内容,读取的时候是使用的字符流操作,静态方法convertStringToStream就是将一个InputStream转换成为一个String构造出来,那么按照这段程序,只要所有的输入源都可以转换为InputStream,那么这个输入源读取到的内容就可以直接转换称为String。
——[$]使用BufferedInputStream拷贝文件——
package org.susan.java.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class BufferedStreamCopy {
public static void main(String args[]) throws Exception{
BufferedInputStream fin= new BufferedInputStream(new FileInputStream("in.dat"));
BufferedOutputStream fout= new BufferedOutputStream(new FileOutputStream("out.dat"));
int i;
do{
i = fin.read();
if( i != -1)
fout.write(i);
}while(i != -1);
fin.close();
fout.close();
}
}
上边这段程序演示了文件拷贝,使用的的是BufferedInputStream和BufferedOutputStream,这段程序运行过后,运行程序下边的in.dat文件的同一个文件夹内会出现一个out.dat的文件,而且两者的内容一模一样。
- 一般情况下,Buffered为前缀的四个类可以针对字节流和字符流进行包装,它们的构造函数可以传入一个参数为InputStream类型的对象,上边传入的是InputStream的子类FileInputStream,这种情况下,拷贝使用的方式是字节的方式,而不是字符的方式,这种包装有什么好处呢?相对来讲,下边四个类比较特殊:
字节流缓冲区辅助类:BufferedInputStream、BufferedOutputStream
字符流缓冲区辅助类:BufferedReader、BufferedWriter
这里写前缀的时候记得是Buffered而不是Buffer,需要加入ed,被这四个类包装过的输入输出性能上和普通的InputStream有一定的差距,因为这四个类在实现的时候已经提供了缓冲输入输出,结合操作系统的IO更加紧密,使用这四个类进行大数据量的拷贝或者传输可能性能比普通方式更加高,所以一般情况下在写的过程都针对一定的InputStream、OutputStream、Reader和Writer进行包装,而且它们四个类都拥有针对这四个超类作为参数的构造,而且是相互对应的。
——[$]ByteArray相关的IO——
package org.susan.java.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ByteArrayIOApp {
public static void main(String args[]) throws IOException{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
String inputStr = "This is a test.";
for( int i = 0; i < inputStr.length(); i++)
outputStream.write(inputStr.charAt(i));
System.out.println("outstream:" + outputStream);
System.out.println("size:" + outputStream.size());
ByteArrayInputStream inputStream;
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
int inBytes = inputStream.available();
System.out.println("inStream has " + inBytes + " available bytes");
byte inbuf[] = new byte[inBytes];
int bytesRead = inputStream.read(inbuf,0,inBytes);
System.out.println(bytesRead +" bytes were read");
System.out.println("They are:" + new String(inbuf));
}
}
上边这段代码的输出为:
outstream:This is a test.
size:15
inStream has 15 available bytes
15 bytes were read
They are:This is a test.
这段代码没有特别的内容,主要就是一个字节流的读取,应用的是ByteArrayOutputStream,这个类和ByteArrayInputStream是对应的。
【*:这里先简单给几段代码说明来解析IO类的基本用法,等到后边文件部分讲解结束后,我会给出更多的例子来说明IO类里面的普遍用法,先简单接触一下这些内容使得读者有个初步印象。】
[2]性能优化(摘录自《Java优化编程》):
Java语言中一般的输出流和输入流都是采用的单字节的读取办法,进行数据的IO操作,也就是说每次只能读取一个字节的数据,这种方法显然烦琐而且效率比较低下。当程序需要读取或者写入一个比较大的文件的时候,比如10M甚至更大,如果每次都是读取或者写入一个字节,那么完成这个操作的次数就会增加了,比如10M那么就需要10MB次这样的操作了。所以Java语言本身在IO读写的时候已经提供了系统缓冲类来进行此部分的操作,众所周知使用缓冲区进行读写操作的时候会使得性能有大大的提高,以下有Java语言中针对IO系统的性能优化的示例。
——[$]优化一:通过系统缓冲流提高IO效率——
package org.susan.java.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ReadWriterWithBuffer {
public static void main(String args[]){
ReadWriterWithBuffer rw = new ReadWriterWithBuffer();
try{
// 不使用缓冲区直接读写
long startTime = System.currentTimeMillis();
rw.readWrite("D:/setup.exe", "D:/myeclipse1.exe");
long endTime = System.currentTimeMillis();
System.out.println("Direct read and write time: " + (endTime - startTime) + "ms");
//使用系统自带缓冲类读写
startTime = System.currentTimeMillis();
rw.readWriterBuffer("D:/setup.exe", "D:/myeclipse2.exe");
endTime = System.currentTimeMillis();
System.out.println("Buffer read and write time: " + (endTime - startTime) + "ms");
}catch(IOException ex){
ex.printStackTrace();
}
}
/**
* 直接通过文件输入输出流读写文件
* @param fileFrom 源文件
* @param fileTo 目标文件
* @throws IOException IO异常
*/
public void readWrite(String fileFrom,String fileTo) throws IOException{
InputStream in = null;
OutputStream out = null;
try{
in = new FileInputStream(fileFrom);
out = new FileOutputStream(fileTo);
while(true){
int bytedata = in.read();
if( bytedata == -1)
break;
out.write(bytedata);
}
}finally{
if( in != null )
in.close();
if( out != null )
out.close();
}
}
/**
* 通过系统缓冲区类读取和写入文件的方法
* @param fileFrom 源文件
* @param fileTo 目标文件
* @throws IOException IO异常
*/
public void readWriterBuffer(String fileFrom,String fileTo) throws IOException{
InputStream inBuffer = null;
OutputStream outBuffer = null;
try{
InputStream in = new FileInputStream(fileFrom);
inBuffer = new BufferedInputStream(in);
OutputStream out = new FileOutputStream(fileTo);
outBuffer = new BufferedOutputStream(out);
while(true){
int bytedata = inBuffer.read();
if( bytedata == -1)
break;
outBuffer.write(bytedata);
}
}finally{
if( inBuffer != null )
inBuffer.close();
if( outBuffer != null )
outBuffer.close();
}
}
}
通过程序运行可以看到上边这段代码的输出:
Direct read and write time: 20959ms
Buffer read and write time: 53ms
从上边的代码可以看到拷贝这个文件的时候使用直接读写和使用缓冲读写有很大的性能差异,这里读取的setup.exe文件大小为990K,拷贝成功过后就可以看到两个拷贝文件,如果初学者需要运行这段代码就需要在D盘放置setup.exe文件,这样就可以看到使用系统缓冲类对IO的性能影响在文件很大的时候是很显著的。
——[$]优化二:通过自定义缓冲类提高IO性能——
package org.susan.java.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ReadWriteWithArray {
public static void main(String args[]){
ReadWriteWithArray rw = new ReadWriteWithArray();
try{
// 不使用缓冲区直接读写
long startTime = System.currentTimeMillis();
rw.readWrite("D:/setup.exe", "D:/myeclipse1.exe");
long endTime = System.currentTimeMillis();
System.out.println("Direct read and write time: " + (endTime - startTime) + "ms");
//使用系统自带缓冲类读写
startTime = System.currentTimeMillis();
rw.readWriteArray("D:/setup.exe", "D:/myeclipse2.exe");
endTime = System.currentTimeMillis();
System.out.println("Buffer read and write time: " + (endTime - startTime) + "ms");
}catch(IOException ex){
ex.printStackTrace();
}
}
/**
* 通过文件输入输出流读写文件
* @param fileFrom 源文件
* @param fileTo 目的文件
* @throws IOException IO异常
*/
public void readWrite(String fileFrom,String fileTo) throws IOException{
InputStream in = null;
OutputStream out = null;
try{
in = new FileInputStream(fileFrom);
out = new FileOutputStream(fileTo);
while(true){
int bytedata = in.read();
if( bytedata == -1)
break;
out.write(bytedata);
}
}finally{
if( in != null )
in.close();
if( out != null )
out.close();
}
}
/**
* 通过自定义缓冲区读写文件方法
* @param fileFrom 源文件
* @param fileTo 目标文件
* @throws IOException IO异常
*/
public void readWriteArray(String fileFrom,String fileTo) throws IOException{
InputStream in = null;
OutputStream out = null;
try{
in = new FileInputStream(fileFrom);
out = new FileOutputStream(fileTo);
int availableLength = in.available();
byte[] totalBytes = new byte[availableLength];
int bytedata = in.read(totalBytes);
out.write(totalBytes);
}finally{
if( in != null )
in.close();
if( out != null )
out.close();
}
}
}
该程序的输出为:
Direct read and write time: 26214ms
Buffer read and write time: 38ms
在自定义缓冲区的过程,这种方式是普通的方式,还有一种方式就是直接定义定制长度的,比如设置缓冲区:
byte[] buffer = new byte[4096];
这种情况是定制缓冲区大小为4096个字节,和上边的直接定义一个缓冲区的读写方式有点差异,根据这两个程序可以得到下边的结论:
- 采用默认的数据输入输出方式(直接读取与写入)将会导致系统性能下降
- 采用系统数据缓冲区读取与写入数据将会提升系统性能
- 采用自定制合理缓冲区读取与写入数据将会在更大程度上提升系统性能
其实这里还没有牵涉到字符流的读取,其读取方式和字节流读取方式大同小异。
v.针对IO先做个小结:
这里先针对IO部分做个简单小结,引导读者继续往下学习,也方便读者从整体上知道IO究竟包含了哪些内容,这里是个人的一个整理小结:
- Java语言IO结构里面包含两种类型:字节流和字符流;可以这样理解,字节流可以理解为低级流或者说底层流,它的数据单位以字节为主,计算机里面所有的数据都可以用字节作为单位来进行描述,虽然计算机最小的单位是位;Java语言里面进行字节流操作的类都是以Stream为后缀的,带Input前缀的是字节输入流,带Output前缀的是字节输出流,而Java进行字符流操作的类都是以Writer或者Reader结尾的,以Writer结尾的是字符输出流,以Reader结尾的是字符输入流,字符流又称为高级流。一般情况下,字节流是以字节为单位进行数据传输,在操作文件的时候,一般用来进行非文本文件的读取、写入和传输,虽然字节流也可以用来进行文本文件的读取、写入和传输,不过一般情况下为了保证效率,所有的文本文件都是使用字符流操作。
- InputStream、OutputStream、Writer、Reader:这四个类都是抽象类,分别为字节输入流、字节输出流、字符输出流、字符输入流的顶层抽象类,它们四个类是直接继承于Object,也是Java的IO结构的顶层抽象类
- BufferedOutputStream、BufferedInputStream、BufferedWriter、BufferedReader:这四个类是系统本身提供的缓冲区类,如果在编程过程不创建自定义缓冲区类,那么可以使用系统自带的这四个缓冲区输入输出类来提高IO的性能,前边的范例里面也演示了使用系统自带缓冲区输入输出类进行IO性能提升
- CharArrayWriter、CharArrayReader、ByteArrayInputStream、ByteArrayOutputStream:这四个类是最小单位的输入输出类,前两者的最小单位是字符,是字符流的最小单位输入输出类,后两者的最小单位是字节,是字节流的最小单位是输入输出类,一般情况下这四个类主要操作的对象就是内存,也就是这四个类的输入源和输出源一般情况下为操作系统的内存。
- FilterInputStream、FilterOutputStream、FilterReader、FilterWriter:这四个类是IO过滤类,主要是为了设置IO过程需要过滤的数据进行设计的四个类型,其中字节流的过滤类是具体类,因为它在Java API里面有很多子类型可以操作,而字符流的过滤类是抽象类,目前还没有太多子类型在Java API里面实现,不过后续版本就不一定了,有可能后续的Java版本会提供更多的FilterReader或者FilterWriter的子类型来进行字符数据过滤的操作。
- PrintStream、PrintWriter:这两个类是常用的字节流输出和字符流输出类,这两个类型一般情况下用于控制台操作,在Java Web里面,也可以使用这两个类型进行HTML的渲染操作,这里的渲染和浏览器渲染不一样,就是直接从后台进行Response的输出操作,学习的时候PrintWriter经常可以在Servlet里面使用。这两个类只有输出类可以使用,而输入类里面没有这种类型。
- StringWriter、StringReader、StringBufferInputStream【已过时】:这三个类限制了输出输入源,其中最后一个类已经过时,目前的JDK里面很少使用;而前两个类主要是针对Java编程里面常用的String对象设置的,有了这样两个类过后更加方便编程的常用写法,没有过多铁别的地方,这两个类就是两个字符流的输入输出类,父类分别是Writer和Reader,其特殊于其他类型就是针对输入输出源做了一个简单的限制操作(输出源是String对象)。
- PipedInputStream、PipedOutputStream、PipedWriter、PipedReader:这四个类主要是为管道输入输出设置的,这个是Java提供的管道通信功能,这个功能主要用于不同线程之间的通信,而这个类在使用的时候有一点点特殊:一个PipedInputStream实例对象必须和一个PipedOutputStream实例对象进行连接才能产生一个完整的通信管道,而后两者同样的,否则这个通信管道就会被称为不合法而使得管道通信失效。
- OutputStreamWriter、InputStreamReader:这两个类是低级流到高级流的桥梁类,主要是为了连接接低级流到高级流而使用的,这个地方有一点特殊,从这两个类的构造方法就可以看出来:构造函数里面的参数只有Stream类型的,那么就是说低级流和高级流之间只能由一个低级流转化为高级流,而这种转换是单向转化而不能进行双向转化。
- ObjectOutputStream、ObjectInputStream:最后需要解释的是这两个比较特殊的类,这两个类主要是用于序列化和反序列化操作的两个类,这两个类将在序列化章节进行详细讲解。
- 按照上边的流程就可以很清楚Java的IO到底是一个什么结构了,这里还需要理解的是包装和桥梁:IO中的桥梁一般指代的是低级流和高级流之间的转换操作,也同样是单向操作,使用类主要是OutputStreamWriter和InputStreamReader这两个类,主要用于转换;另外一个是包装,这里的包装指的是BufferedOutputStream、BufferedInputStream、BufferedWriter、BufferedReader四个类针对同类型的流操作进行包装,包装的主要目的是为了提高系统的IO性能。
- 最后剩下一个概念就是Filter,Filter类是存在链式操作的,可以针对数据的输入输出进行多次过滤,而过滤的过程存在一个过滤链,其操作图如下例子: