IO流与文件
IO流
第一节 简介
1.1 IO
IO
就是指应用程序对这些设备的数据输入与输出,Java语言定义了许多类专门负责各种方式的输入、输出,这些类都被放在 java.io
包中。
1.2 流
流可以根据传输的基本单位分为字节流和字符流,字节流一般以Stream命名,字符流则以Reader/Writer命名。计算机传输本质上都是字节流,字符流只是不需要再手动指定一次编码格式。以下直接用流来指代字节流。
能够读出或写入一个字节序列的对象分别叫输入流或输出流,这些对象可以是文件,网络连接或内存块等。
InputStream
和 OutputStream
为字节流设计,Reader
和 Writer
为字符流设计,字节流和字符流形成分离的层次结构。一般来说,处理字符或字符串使用字符流类,处理字节或二进制对象使用字节流。
操作文件流时,不管是字符流还是字节流,都可以按照以下方式进行:
- 使用File类找到一个对象。
- 通过File类的对象去实例化字节流或字符流的子类。
- 进行字节(字符)的读、写操作。
- 关闭文件流。
1.3 File
- File类是IO包中唯一代表磁盘文件本身的对象,File类定义了一些与平台无关的方法来操作文件。
- 通过调用File类提供的各种方法,能够完成创建、删除文件、重命名文件、判断文件的读写权限权限是否存在、设置和查询文件的最近修改时间等操作。
- File类没有无参构造方法,最常用的是使用下面的构造方法来生成File对象(注意分隔符可以使用”/“和”",但是使用”"必须写”\“,因为涉及转义的问题):
File(String pathName)
pathName指的是文件的路径名。
第二节 文件
2.1 File类
File定义了很多获取File对象标准属性的方法,如下示例代码。
1 | public class FileTest { |
运行结果为:
1 | file是一个文件夹 |
2.2 Path和Files
JDK 1.7时 Path
和 Files
随着 NIO
引入,位于 java.nio.file
包,用来代替File。
Path
表示一个目录名序列,还可以加一个文件名。
1 | Path p = Paths.get("/demo","test");//通过Paths.get()创建Path实例,可以使用绝对路径或相对路径。 |
Path相关方法如下所示。
1 | Path path = Paths.get("E:\\BlogUpload\\pic\\upload\\cover","1"); |
结果如下。
1 | 文件名:1 |
Path表示一个目录名序列,还可以加一个文件名。
Path是接口,而 Paths
和 Files
就是其对应的工具类,区别于File的面向对象设计,Path和Files是面向接口设计。以下为Files提供的一些功能。
1 | boolean pathExits = Files.exists(path,new LinkOption[]{LinkOption.NOFOLLOW_LINKS});//检测时不包含符号链接文件 |
Files也可以通过实现的read和write方法对文件进行简洁的读写,但如果是较大的文件还是应该使用流来处理。
Files相较File通过数组来存储目录中的所有文件,而是生成了一个Iterable对象,迭代器遍历对于声明了RandomAccessList的数组相较for循环要快。
面向接口设计,可以有效的降低耦合度,面向对象需要涉及到对象之间的交互问题,而接口本质上是规范和约束,也就是系统设计者的理解,比较适合设计和开发分离的情况。如今的Java流行框架都提倡这种模式,在接口中定义行为,然后在业务中去实现,将实现由框架管理,即控制反转。
但当系统业务比较简单,只是controller,service,dao之间相互调用,不存在设计和实现的分离,那么就没要再采用这种模式来增加工作量。
2.3 RandomAccessFile
2.3.1 简述
RandomAccessFile
类可以说是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。它支持随机访问方式,可以跳转到文件的任意位置处读写数据。要访问一个文件的时候,不想把文件从头读到尾,而是希望像访问一个数据库一样地访问一个文本文件,使用RandomAccessFile类是最佳选择。
RandomAccessFile对象类中有个位置指示器,指向当前读写处的位置,当读写n个字节后,文件指示器将指向这n个字节后的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,可以移动文件指示器到新的位置,随后的读写将从新的位置开始。
RandomAccessFile类在文件随机(相对于顺序)读取时有很大的优势,但该类仅限于操作文件,不能访问其他得IO设备,如网络、内存映像等。
2.3.2 构造方法
RandomAccessFile类为用户提供了两种构造方法:
- RandomAccessFile(File file, String mode)
- RandomAccessFile(String name, String mode)
其实第二种构造方法也是new一个File出来再调用第一种构造方法,建议使用第一种构造方法,因为第一篇文章就说了File是IO的基础,有一个File不仅仅可以通过RandomAccessFile对文件进行操作,也可以通过File对象对文件进行操作。至于mode,Java给开发者提供了四种mode:
模 式 | 作 用 |
---|---|
r | 表示以只读方式打开,调用结果对象的任何write方法都将导致抛出IOException |
rw | 打开以便读取和写入,如果该文件尚不存在,则尝试创建该文件 |
rws | 打开以便读取和写入,相对于”rw”,还要求对文件内容或元数据的每个更新都同步写入到底层存储设备 |
rwd | 打开以便读取和写入,相对于”rw”,还要求对文件内容的每个更新都同步写入到底层存储设备 |
2.3.3 API
方 法 | 作 用 |
---|---|
void close() | 重要,关闭此随机访问文件流并释放与该流关联的所有系统资源 |
FileChannel getChannel() | 返回与此文件关联的唯一FileChannel对象,NIO用到 |
long getFilePointer() | 返回此文件中的当前偏移量 |
long length() | 返回此文件的长度 |
int read() | 从此文件中读取一个数据字节 |
int read(byte[] b) | 将最多b.length个数据字节从此文件读入byte数组,返回读入的总字节数,如果由于已经达到文件末尾而不再有数据,则返回-1。在至少一个输入字节可用前,此方法一直阻塞 |
int read(byte[] b, int off, int len) | 将最多len个数据字节从此文件的指定初始偏移量off读入byte数组 |
boolean readBoolean() | 从此文件读取一个boolean,其余readByte()、readChar()、readDouble()等类似 |
String readLine() | 从此文件读取文本的下一行 |
void seek(long pos) | 重要,设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作 |
int skipBytes(int n) | 重要,尝试跳过输入的n个字节以丢弃跳过的字节,返回跳过的字节数 |
void write(byte[] b) | 将b.length个字节从指定byte数组写入到此文件中 |
void write(byte[] b, int off, int len) | 将len个字节从指定byte数组写入到此文件,并从偏移量off处开始 |
void write(int b) | 向此文件写入指定的字节 |
void writeBoolean(boolean v) | 按单字节值将boolean写入该文件,其余writeByte(int v)、writeBytes(String s)、writeChar(int v)等都类似 |
2.3.4 示例
1 | public class Employee { |
第一部分,写文件,该文件在路径下并没有,所以Java会自动帮我们创建:
1 | /** |
文件创建好了,D盘下的Files文件夹也有该文件了,所以读取一下,这里使用了一些小技巧来演示seek方法和skipBytes方法:
1 | /** |
运行结果:
1 | 第二个员工的信息: |
可能有人奇怪,”zhangsan”加上一个int跳过12个字节可以理解,但是”lisi”、”wangwu”为什么加上int要跳过12个字节呢?明明”lisi”只有4个字节,”wangwu”只有6个字节啊。这个就涉及到一个”字节对齐”的问题了,有兴趣的可以了解一下。另外,再说一下,RandomAccessFile使用完一定要及时close()。
第三节 字符集
不同字符集对应不同的编码格式,java.nio包用Charset类统一了对字符集的转换,本地编码方式模式无法表示所有的unicode字符,当遇到不能表示的字符时会用 ?
代替。
通过字符集可以在字符串和字节序列之间转换。
1 | //String -> 字节序列 |
第四节 字节流与字符流
4.1 字节流
抽象类 InputStream
和 OutputStream
是I/O结构中字节流的基础。其抽象读写方法是基于字节读写,但并不方便处理Unicode格式,且读写方法执行时会阻塞,可以通过 available()
检查当前可读入字节数量,避免等待。在使用流后切记要即时的 close()
释放资源,可以通过 try-with-resource
来实现无论何种情况自动释放资源。
Java通过分离职能来分类输入流和输出流,所以在实现某些功能时需要结合多种类型的流,Filter流的的子类用来向原生的字节流添加额外的功能,所以可以通过嵌套过滤器来添加多重功能。如下从文件中读取数字并引入缓冲区。
1 | DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("/test.txt"))); |
多个流链接在一起时可以通过 PushbackInputStream
跟踪各个中介流,PushbackInputStream
可以推回读入字节。
DataInput
和 DataOutput
定义了一些方法用来读写二进制格式数据。
1 | boolean readBoolean() throws IOException; |
ZipStream
读写ZIP格式的压缩文档。
4.2 字符流
字节流提供了处理任何类型输入/输出操作的功能(因为对于计算机而言,一切都是0和1,只需把数据以字节形式表示就够了),但它们不可以直接操作Unicode字符,因为一个Unicode字符占用2个字节,而字节流一次只能操作一个字节。既然Java的口号就是”一次编写、处处运行”,那么包含直接的字符输入/输出的支持是必要的。因此就有一些字符输入/输出流,之前已经说明过了,字符流顶层是Reader和Writer这两个抽象类,所以Unicode格式可以通过使用抽象类Reader和Writer的子类来处理(字符流)。
4.3 基础接口
基础接口包括:Closeable
,Flushable
,Readable
和 Appendable
接口。
- Closeable接口扩展了
AutoCloseable
接口,所以可以使用try-with-resource语句,Closeable的close()
方法限制只能抛出IOException
,而AutoCloseable
接口的close()
方法则没有限制。InputStream
,OutputStream
,Reader
和Writer
都实现了Closeable
接口。 - Flushable的
flush()
方法通过写入缓冲输出到底层流来刷新当前流。 - Readable的
read()
方法需要CharBuffer
类型参数,其表示内存中的缓冲区或内存映像的文件,可以顺序或随机的进行读写访问。 - Appendable接口则可以添加字符或字符序列。
1 | public abstract class InputStream implements Closeable |
4.4 OutputStream
OutputStream
是定义了Java流式字节输入模式的抽象类。该类的所有方法返回一个void值并且在出错的情况下引发一个 IOException
,OutputStream
提供的抽象方法有:
方 法 | 作 用 |
---|---|
void close() | 关闭输入流,关闭后的写操作会引发IOException |
flush() | 刷新此输入流并强制写出所有缓冲的输出字节 |
write(byte[] b) | 向输入流写入单个字节,注意参数是一个int型,它允许设计者不必把参数转换成字节型就可以调用write()方法 |
write(byte[] b, int off, int len) | 以b[off]为起点,向文件写入字节数组b中len个字节 |
write(int b) | 向一个输出流写一个完整的字节数组 |
FileOutpuStream
应该是Java中最常见的字节输出流了,它创建一个可向文件写入字节的类OutputStream,它常用的构造方法如下:
FileOutputStream(String name)
FileOutputStream(File file)
FileOutputStream(File file, boolean append)
前两个构造方法类似,前者输入文件的绝对路径,后者输入File的实例对象,和 RandomAccessFile
一样,推荐后者。第三个构造方法有一点不同,append如果设置为true,文件则以搜索路径模式打开。
FileOutputStream的创建不依赖于文件是否存在,在创建对象时,FileOutputStream
会在打开输出文件之前就创建它。这种情况下如果试图打开一个只读文件,会引发 IOException
。
写一个例子,现在D盘下的Files文件夹里并没有”stream.txt”这个文件:
1 | public class FileOutputStreamTest { |
到D盘下的Files文件夹里多了”stream.txt”,且文件中的内容和我们写入的一致,同样这个例子也证明了FileOutputStream并不依赖指定路径下的文件是否存在。那如果指定路径下本来就有文件,那么写将会覆盖而不是追加,很好证明:
1 | public class FileOutputStreamTest { |
4.5 InputStream
InputStream
是定义了Java流式字节输入模式的抽象类。该类所有方法在出错的时候都会引发一个 IOException
,InputStream
提供的抽象方法有:
方 法 | 作 用 |
---|---|
int available() | 返回当前可读的字节数 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源,关闭之后再读取会产生IOException |
int mark(int readlimit) | 在输入流中放置一个标记,该流在读取N个Bytes字节前都保持有效 |
boolean markSupported() | 如果调用的流支持mark()/reset()就返回true |
int read() | 如果下一个字节可读取则返回一个整形,遇到文件尾时返回-1 |
int read(byte b[]) | 试图读取buffer.length个字节到buffer中,并返回实际成功读取的字节数,遇到文件尾则返回-1 |
int read(byte b[], int off, int len) | 将输入流中最多len个数组直接读入byte数组,off表示数组b中写入数据的初始偏移量。注意,三个read方法,在输入数据可用、检测到流末尾或者抛出异常前,此方法将一直阻塞 |
void reset() | 重新设置输入指针到先前设置的标记处 |
long skip(long n) | 跳过和丢弃此输入流中数据的n个字节 |
FileInputStream
应该是Java中最常见的字节输入流了,它创建一个能从文件读取字节的 InputStream
类,它的两个常用构造方法如下:
FileInputStream(String name)
FileInputStream(File file)
和FileOutputStream差不多,推荐后者的用法。同样写一个例子,操作的是上面D盘下的Files文件夹生成的”stream.txt”:
1 | public class FileInputStreamTest { |
运行结果为:
1 | 26 |
要区分清楚,OutputStream
的作用是将内容由Java内存输出到文件中、InputStream
是将内容由文件输入到Java内存中。read(byte b[])
表示”**试图读取buffer.length个字节到buffer中,并返回实际读取的字节数”**,返回的是实际字节的大小。
4.6 Reader
Reader
是定义Java的字符输入流的抽象类,该类的所有方法在出错的情况下都将引发 IOException
。Reader
类中有这些方法:
方 法 | 作 用 |
---|---|
abstract void close() | 关闭该流并释放与之关联的所有资源 |
void mark(int readAheadLimit) | 标记流中的当前位置 |
boolean markSupported() | 判断此流是否支持mark()操作 |
int read() | 从文件中读取单个字符 |
int read(char[] cbuf) | 从文件中读取字符到cbuf |
abstract int read(char[] cbuf, int off, int len) | 将文件中的字符读入cbuf数组,从off位置开始,读取len个字符。三个read方法在字符可用、发生I/O异常或者已经到达流的末尾前,此方法会一直阻塞 |
int read(CharBuffer target) | 试图将文件中的字符读入指定的字符缓冲区 |
boolean ready() | 判断是否准备读取此流 |
voi reset() | 重置该流 |
long skip(long n) | 跳过n个字符 |
4.7 Writer
Writer
是定义字符输出流的抽象类,所有该类的方法都返回一个void值并在出错的条件下引发 IOException
。Writer
类中的方法有:
方 法 | 作 用 |
---|---|
Writer append(char c) | 将制定字符添加到此writer |
Writer append(CharSequence csq) | 将制定字符序列添加到此writer |
Writer append(CharSequence csq, int start, int end) | 将指定字符序列的子序列添加到此writer.Appendable |
abstract void close() | 关闭此流,但要先flush()它 |
abstract void flush() | 刷新该流的缓冲 |
void write(char[] cbuf) | 将cbuf中的内容写入文件 |
abstract void write(char[] cbuf, int off, int len) | 将字符数组cbuf中从off开始的len个字节写入文件 |
void write(int c) | 写入单个字符到文件中 |
void write(String str) | 写入字符串到文件中 |
void write(String str, int off, int len) | 写入str从off位置开始的len个字符到文件中 |
4.8 FileReader和FileWriter
FileReader
类创建了一个可以读取文件内容的Reader类,最常用的构造方法是:
FileReader(String fileName)
FileReader(File file)
FileWriter创建了一个可以写文件的Writer类,最常用的构造方法是:
FileWriter(String fileName)
FileWriter(String fileName, boolean append)
FileWriter(File file)
其中第二个构造方法,如果append为true,那么输出是追加到文件结尾的。FileWriter类的创建不依赖文件是否存在,在创建文件之前,FileWriter将在创建对象时打开它来作为输出。如果试图打开一个只读文件,那么将引发一个IOException。看一下FileWriter和FileReader的使用,现在D盘FIles文件夹下没有”writer.txt”:
1 | public class FileReaderWriterTest { |
4.9 字符流和字节流的区别
字符流和字节流最大的区别在于,字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流操作时使用了缓冲区,通过缓冲区再操作文件。这也解释了上面程序的那个问题,为什么不对资源进行 close()
就无法写入文件的原因。因为在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果没有关闭,缓冲区中的内容是无法输出的。
什么是缓冲区?简单理解,缓冲区就是一块特殊的内存区域。
为什么要使用缓冲区?因为如果一个程序频繁操作一个资源(文件或数据库),则性能会很低,为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域读取数据即可,因为读取内存的速度要快于读取磁盘中文件内容的速度。
在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区。
如果不想在关闭时再输出字符流的内容也行,使用 Writer
的 flush()
方法就可以了。
Java支持字符流和字节流,字符流本身就是一种特殊的字节流,之所以要专门有字符流,是因为Java中有大量对于字符的操作,所以专门有字符流。字节流和字符流的转换是以 InputStreamReader
和 OutputStreamWriter
为媒介的,InputStreamReader
可以将一个字节流中的字节解码成字符, OutputStreamWriter
可以将写入的字符编码成自节后写入一个字节流。
InputStreamReader中的解码字节,是由 StreamDecoder
完成的,StreamDecoder是Reader的实现类,定义在InputStreamReader的开头:
1 | public class InputStreamReader extends Reader { |
同样,OutputStreadWriter中的编码字节,是由 StreamEncoder
完成的,StreamEncoder是Writer的实现类,定义在OutputStreamWriter的开头:
1 | public class OutputStreamWriter extends Writer { |
假如不对StreamDecoder和StreamEncoder指定 Charset
编码格式,将使用本地环境中的默认字符集,例如中文环境中将使用 GBK编码
。
InputStreamReader有两个主要的构造函数:
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, String charsetName)
OutputStreamWriter也有两个主要的构造函数:
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, String charsetName)
从构造函数就可以看出,字符流是利用字节流实现的。InputStreamReader和OutputStreamWriter的两个构造函数的区别在于,一个是使用的默认字符集,一个可以指定字符集名称。
其实FileReader和FileWriter可以看一下源码,很简单,只有构造函数,里面都是分别根据传入的文件绝对路径或者传入的File实例,new出FileInputStream和FileOutputStream,在调用InputStreamReader和OutputStreamWriter的构造方法。这么做,帮助开发者省去了实例化FileInputStream和FileOutputStream的过程,让开发者可以直接以fileName或file作为构造函数的参数。
4.10 BufferedWriter和BufferedReader
为了达到最高的效率,避免频繁地进行字符与字节之间的相互转换,最好不要直接使用 FileReader
和 FileWriter
这两个类进行读写,而使用 BufferedWriter
包装 OutputStreamWriter
,使用 BufferedReader
包装 InputStreamReader
。同样,在D盘Files文件夹下没有”buffered”这个文件,代码示例为:
1 | public class BufferedWriterReaderTest { |
运行一下,首先D盘Files文件夹下多出了”buffered.txt”这个文件,并查看文件中的内容。
然后看一下控制台的输出结果:
1 | 1234 |
没什么问题,输出了文件中的内容。注意两点:
利用
BufferedWriter
进行写操作,写入的内容会放在缓冲区内,直到遇到close()
、flush()
的时候才会将内容一次性写入文件。另外注意close()
的顺序,一定要先关闭BufferedWriter
,再关闭Writer
,不可以倒过来,因为BufferedWriter
的写操作是通过Writer
的write()
方法写的,如果先关闭Writer
的话,就无法将缓冲区内的数据写入文件了,会抛出异常。利用
BufferedReader
进行读操作,不可以用父类Reader指向它,因为readLine()
这个方法是BufferedReader
独有的,readLine()
的作用是逐行读取文件中的内容。
参考:
🔗《Java核心技术 卷Ⅱ》
🔗 Serializable和Externalizable浅析