本文介绍Java中的IO流机制。
1. 概述
输入就是将数据从硬盘读取到内存中,输出就是将数据从内存传送到硬盘中。其实输入输出就是相对于内存来说的,数据流向内存,就是输入(读);数据流出内存就是输出(写)。
除了上面按照流的方向分为输入流、输出流,还可以按照读取方式不同进行分类,如
- 一次读取一个字节(一次读取8个二进制位),这种字节流是万能的,可以读取任何类型的文件,如视频、音频、文件等等。
- 一个读取一个字符,这种字符流是为了方便读取普通文本文件而存在的(如txt),不能读取声音、图片等有格式的文件,连word都不能。比如英文字符占1个字节,中文占2个字节。字符流可以区分字符,直接将其读取出来。
综上所述,流按照方向分为两种,按照读取方式分为两种。此时输入流可分为字节输入流和字符输入流,输出流也分为字节输出流和字符输出流。有如下四大家族【都是抽象类】:
类 | 流 |
---|---|
java.io.InputStream | 字节输入流 |
java.io.OutputStream | 字节输出流 |
java.io.Reader | 字符输入流 |
java.io.Writer | 字符输出流 |
只要以Stream结尾的,都是字节流;只要以Reader、Writer结尾的都是字符流。
- 上面四个类,都实现了java.io.Closeable接口,都是可关闭的。因此,这些流在使用完毕之后,一定要关闭。可看成是管道,都是占用资源的,需要需要关闭。
- 上面的输出流,无论是字节流和字符流,都实现了java.io.Flushable接口,都是可刷新的。输出流在用完之后,一定要刷新,刷新表示将管道中剩余未输出的数据强行输出完,即清空管道。【一般情况下,只有管道满了之后才会自动刷新】。
Java中提供的具体流有如下16个。
类 | 流 |
---|---|
java.io.FileInputStream | 文件专属 |
java.io.FileOutputStream | 文件专属 |
java.io.FileReader | 文件专属 |
java.io.FileWriter | 文件专属 |
java.io.BufferedInputStream | 缓冲流专属 |
java.io.BufferedOutputStream | 缓冲流专属 |
java.io.BufferedReader | 缓冲流专属 |
java.io.BufferedWriter | 缓冲流专属 |
java.io.DataInputStream | 数据流专属 |
java.io.DataOutputStream | 数据流专属 |
java.io.ObjectInputStream | 对象专属流 |
java.io.ObjectOutputStream | 对象专属流 |
java.io.PrintWriter | 标准输出流 |
java.io.PrintStream | 标准输出流 |
java.io.InputStreamReader | 将字节流转换为字符流(输入) |
java.io.OutputStreamWriter | 将字节流转换为字符流(输出) |
2. 文件专属流
2.1 FileInputStream(重点)
2.1.1 read()
文件字节输入流,是万能的,可以读取任意文件。FileInputStream类有一个read方法,一次可读取一个字节。
1 | public class IOTest01 { |
除了读取一个字节,可传入一个byte数组,一次最多读取byte数组长度的字节数量,效率较高。此时该方法返回值为读取到的字节数量,而读取到的内容则保存在byte数组中。后面的读取会覆盖数组中前面读取的内容。如果到文件末尾,那么仍然返回-1。代码如下所示:
1 | public class IOTest02 { |
2.1.2 skip()
跳过几个字节不读。
2.2.3 available()
返回流中剩余的没有读到的字节数量。
2.2 FileOutputStream(重点)
FileOutputStream是字节输出流。
2.2.1 write()
该方法是写入文件,参数是byte数组,也可以是指定byte数组中的指定区间。
注意,下面方式是将原文件清空再写入。
1 | public class IOTest04 { |
要想以追加的形式写入内容,则需要在创建输出流对象的时候,传入参数true,表示append为true。
1 | fos = new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\testOut2.txt", true); |
对于字符串等内容,需要将其转换成byte数组,才能写入。注意,最后输出流要flush。
2.3 文件拷贝
可利用输入流读取到内存,然后获取到byte数组,调用输出流,将byte数组的内容输出到文件中【注意,输出流对象以追加形式创建】。
代码如下所示:
1 | public class IOTest05 { |
2.4 FileReader
文本字符输入流,只能读取普通文本。和FileInputStream类似,只不过参数不是byte数组,而是char数组,每次读取的内容都存储到char数组中。
2.5 FileWriter
文本字符输出流,只能写入普通文本。该方法可以写入char数组,也可以是字符串,而无需是byte数组。
3. 缓冲专属流
3.1 BufferedReader
这种字符输入流,自带缓冲区,在读取的时候,无需再传入数组参数存放读取的内容。可以查看源码,该类内部有一个数组,保存读取的数据。并且读取方法有多种,可直接读取一行【不带换行符】。
注意,该类创建对象,需要传入Reader对象,而FileReader等就是继承了该抽象类,可作为参数传入。传入的流就是要读取的文件,被称为节点流。而创建的BufferedReader对象,则被称为包装流/处理流。
注意,关闭的时候只需要关闭包装流即可,因为包装流会关闭节点流。
另外,因为对于FileInputStream,因为不是Reader类型,不能传入BufferedReader,但是可通过InputStreamReader将FileInputStream转换为Reader类型,然后传入。
3.2 BufferedWriter
同样,该类对象创建需要Writer流,FileWriter可作为参数传入。可直接写入字符串。也可利用OutputStreamWriter将OutputStream转换为Writer后传入。
4. 数据流(了解)
这两个流DataInputStream、DataOutputStream是数据专属的流,可将数据连同数据类型一并写入文件。生成的文件不是普通文本文档,用记事本打不开。文件只能由DataInputStream读取文件。
5. 标准输出流(重点)
注意,标准输出流不需要手动关闭。前面最经常使用的语句就是:System.out.println()
,那么这个表达式,System.out
就是获取到的是PrintStream类型对象,通过该对象调用println方法来实现输出。
另外,可通过标准输出流来改变JVM的输出流方向,如下所示【日志工具】:
1 | // 设置标准输出流不再指向控制台,而是指向log文件。 |
6. 对象流(重点,序列化)
我们知道,在Java程序运行过程中,会创建对象,而前面的IO流仅仅是将普通数据存储到文件中,那能不能将对象存储到硬盘中呢?当然可以,将Java对象存储到硬盘的过程称为序列化(Serialize),将对象拆分成小部分,每部分都有一个编号,转换成流,输入到硬盘中。将硬盘中的对象数据重新恢复到内存中,使其成为Java对象,这被称为反序列化(DeSerialize)。
注意,如果一个类要序列化,必须实现Serializable接口。这个接口其实什么方法也没有,只是起到一个标识的作用,当JVM看到这个类实现这个接口后,会对这个类进行特殊待遇,为该类自动生成一个序列化版本号。
那么序列化版本号有什么用呢?
ObjectOutputStream类用于序列化,ObjectInputStream类用于反序列化。
序列化如下所示:
1 | public class IOTest09 { |
反序列化如下所示,因为已经实现了toString方法,所以可直接输出,输出的属性和创建的对象是一样的:
1 | public class IOTest10 { |
可将对象放入到集合中,直接序列化集合,这样就可一次性序列化多个对象。另外,如果序列化对象的时候,不希望某个属性值序列化,那么可在该属性上加入transient关键字修饰。之后反序列化的时候获取到的属性值就是null。
注意,本质上说,反序列化时,是依据的class文件。那么如果在反序列化之前,将java文件修改,重新编译之后,生成新的class文件以及新的序列化版本号。反序列化的时候就会依据新的class文件,此时就会反序列化失败,因为两个序列化版本号不一样【java.io.InvalidClassException】。
1 Exception in thread "main" java.io.InvalidClassException: io.Student; local class incompatible: stream classdesc serialVersionUID = -218909040601832639, local class serialVersionUID = -423644352464462558Java语言中采用什么机制来区分类的?
- 首先通过类名,如果类名不一样,肯定不是同一个类。
- 如果类名一样,就通过序列化版本号进行区分。
JVM在编译的时候会自动为该类生成序列化版本号。因为是自动生成的,此时如果后续修改了该类之后,之前序列化后的数据此时无法反序列化。
因此,如果想要后续反序列化,为了避免修改源码后自动生成新的序列化版本号。可手动生成版本号【相当于静态常量】,这样,后续无论怎么修改都是这个手动生成的版本号。
1 private static final long serialVersionUID = 1L;注意,变量名只能是serialVersionUID。
7. java.io.File
File类是文件和目录路径名的抽象表示形式,该类和流没有关系,不能完成文件的读和写,仅仅能够表示路径名【可是文件夹,也可以是文件】。
通过File,可以得到目录,进而可通过IO流读取文件,实现文件夹的文件读取。简单代码如下所示,其中exists()方法表示判断file所表示的路径名是否存在:
1 | public class IOTest08 { |
7.1 常用方法
方法名 | 作用 |
---|---|
File() | 构造方法,参数为路径名(文件名)的字符串形式 |
exists() | 判断该对象所表示的路径名是否存在 |
createNewFile() | 以文件形式创建该对象所表示的路径名 |
mkdir() | 以目录形式创建该对象所表示的路径名 |
mkdirs() | 上面只是最后一级目录不存在可创建,本方法是允许多级目录不存在,然后创建 |
getParent() | 以字符串形式获取该路径的父路径 |
getParentFile() | 以对象形式获取父路径 |
getAbsolutePath() | 获取该对象的绝对路径 |
delete() | 删除该对象所表示的路径/文件 |
getName() | 获取该对象所表示的名字 |
isDirectory() | 判断该对象是否是目录 |
isFile() | 判断该对象是否是文件 |
File类和String一样,有很多方法,比如该目录/文件的最后一次修改时间、文件大小、重命名、获取所有的子目录(listFiles())等等。