Java IO 流基础
封面来源:碧蓝航线 北境序曲 活动CG
本文参考:尚硅谷 宋红康 Java 零基础教程 P577-P619
1. File 类的使用
1.1 路径分隔符
路径中的每级目录之间用一个 路径分隔符 隔开。
路径分隔符与系统有关:
- windows 和 DOS 系统默认使用 “\” 来表示
- UNIX 和 URL 使用 “/” 来表示
Java 程序支持跨平台运行,因此路径分隔符要 慎用 !
为了解决这个隐患,File 类提供了一个常量:
1 | public static final String separator |
使用这个常量可以根据操作系统,动态的提供分隔符。比如:
1 | File file1 = new File("D:\\mofan.txt"); |
这里出现了路径的一种写法:绝对路径,既然有绝对路径,那么也有相对路径。
- 相对路径:相较于某个路径,指明的路径
- 绝对路径:包含盘符在内的文件或文件目录的路径
相对路径的注意要点
IDEA 中:
如果开发使用 JUnit 中的单元测试方法进行测试,相对路径即为当前 Module 下。
如果使用 main()
进行测试,相对路径即为当前 Project 下。
1 | public static void main(String[] args) { |
Eclipse 中:
不管使用单元测试方法还是使用 main()
方法,相对路径都是当前 Project 下。
1.2 常用构造器
第一种
1 | public File(String pathname) |
以 pathname
为路径创建 File
对象,可以是 绝对路径或相对路径 ,如果 pathname
是相对路径,则默认的当前路径在系统属性 user.dir 中存储。
- 绝对路径:是一个固定的路径,从盘符开始
- 相对路径:相对于某个位置开始
第二种
1 | public File(String parent, String child) |
以 parent
为父路径,child
为子路径创建 File
对象
第三种
1 | public File(File parent, String child) |
根据一个父 File
对象和子文件路径创建 File
对象
1.3 常用方法
File
类的获取功能
public String getAbsolutePath()
:获取绝对路径
public String getPath()
:获取路径
public String getName()
:获取名称
public String getParent()
:获取上层文件目录路径。若无,返回null
public long length()
:获取文件长度 (即:字节数) ,但不能获取目录的长度。
public long lastModified()
:获取最后一次的修改时间,毫秒值
测试代码:
1 |
|
如下两个方法适用于文件目录:
public String[] list()
:获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles()
:获取指定目录下的所有文件或者文件目录的 File
数组
测试代码:
1 |
|
File
类的重命名功能
public boolean renameTo(File dest)
:把文件重命名为指定的文件路径。
针对 file1.renameTo(file2)
来说,需要 file1 存在,但是 file2 不存在,将 file1 移动到 file2 并改名为 file2 (相当于剪切与重命名)。
测试代码:
1 |
|
File
类的判断功能
public boolean isDirectory()
:判断是否是文件目录
public boolean isFile()
:判断是否是文件
public boolean exists()
:判断是否存在
public boolean canRead()
:判断是否可读
public boolean canWrite()
:判断是否可写
public boolean isHidden()
:判断是否隐藏
测试代码:
1 |
|
FIle
类的创建功能
public boolean createNewFile()
:创建文件。若文件存在,则不创建并返回 false 。
public boolean mkdir()
:创建文件目录。如果此文件目录存在,就不创建。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs()
:创建文件目录。如果上层文件目录不存在, 一并创建。
注意:如果你创建文件或者文件目录没有写盘符路径,那么默认在项目路径下。
测试代码:
1 | // 文件目录的创建 |
当硬盘中真有一个真实的文件或目录存在,创建 File
对象的时候,各个属性会被显式赋值。
当硬盘中 没有 真实的文件或目录存在时,那么创建 File
对象时,除了指定的目录和路径之外,其他的属性都是取成员变量的默认值 。
File
类的删除功能
public boolean delete()
:删除文件或者文件夹
删除注意事项:
Java 中的删除不走回收站 。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
1.4 总结
使用 File
类时,我们需要明白:
1、File
类的一个对象,代表一个文件或一个文件目录
2、File
类声明在 java.io
包下
3、File
类中设计到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,但是并未设计到写入或读取文件内容的操作。如果需要读取或写入文件内容,就必须使用 IO 流来完成。
4、后续 File
类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”。
2. IO 流概述
2.1 Java IO 原理
I/O 是 Input / Output 的缩写,I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。😮
Java 程序中,对于数据的输入 / 输出操作以“流(stream)”的方式进行。
java.io
包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
输入与输出针对不同的对象,会得到不同的结果。比如站在文件的角度来看,将文件中的数据写入到内存中,那这就是输出,但从内存的角度来看,这就是输入。因为我们是编写代码的人,因此我们需要 站在内存(程序)的角度 来看待 IO。
2.2 流的分类
按操作数据单位不同可分为:字节流(8 bit)、字符流(16 bit)
按数据流的流向不同分为:输入流、输出流
按流的角色不同分为:节点流、处理流
抽象基类 | 字节流 | 字符流 |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
Java 的 IO 流共涉及到 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
当我们操作文本数据时,里面的数据都是一个一个的字符,所以用字符流;当我们操作视频、音频或图片时,就需要用字节流。
针对数据流的流向,我们是一直站在程序(内存)的角度出发的,写入内存叫输入流,从内存中输出到外部存储就叫输出流。
如果流直接作用在文件上,那么这叫节点流;如果当前流作用在已有的流的基础之上,那么当前流叫做处理流(可以理解成:水流等价于节点流,管道等价于处理流)。😵
关于节点流和处理流还可以这样理解:自己就可以操作的叫节点流,依赖于别的流操作的叫处理流。😰
IO 流体系图
Java IO 流体系图参考:Java IO 流体系图
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管理 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOuputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
FilterInputStream | FilterOutputStream | FilterReader | FilterWriter | |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
重点放在:抽象基类、访问文件、缓冲流、转换流和对象流上。
3. 字符流
3.1 数据读入
使用 FileReader
读取文件中的数据,测试代码如下:
1 | /* |
在上述代码中,FileReader
的构造方法、read()
和 close()
都会抛出异常,为了保证资源一定可以执行关闭操作,不建议使用 throws
,采用 try-catch
更好。
被读取文件一定要存在,否则会抛出 FileNotFoundException
异常。
在上述代码中,读取文件中的内容使用了 read()
方法,这个方法可以读取单个字符 ,如果纯文本文件中有内容的话,就会返回 ASCII 码表中对应的数字值,如果 ASCII 表中不存在,就会去查找 Unicode 表。将字符读取完毕后,会将读取目标指向下一个字符,因此常与循环一起使用。
如果纯文本文件没有内容 ,或者前面的内容已经被读取完了,read()
返回的就是 -1,由于我们不知道文本内容有多少,所以我们需要使用一个循环。
read()
方法只能读取一个字符,如果读取的文本很长,那效率也太低了。😳
因此,我们想可不可以一次性多读取一点,然后放到数组或字符串中,那样多奈斯!😏
JDK 提供了一个 read()
的重载方法,可以一次性多读取一点,并将其放到 char[]
中。测试代码如下:
1 | // 对 read() 的操作升级,使用 read() 的重载方法 |
read(char[] cBuf)
:返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1 。
使用 read(char[] cBuf)
方法时,将读取字符数组。假设字符数组的长度是 2,那么一口气读的是 2 个字符,但由于文本内容只有 3 个字符,那么第二次读取的时候,可读取的有效长度就是 1 ,那么存放在字符数组的第 1 位就会被覆盖掉,但是第 2 位没有被覆盖,就会保留。
如果我们直接读取字符串,而不指定有效长度的话,会按字符数组的长度读取,并将没覆盖的字符读取出来,而不是只读有效长度。
所以我们在创建一个 String 对象的时候,要如此使用。
1 | String str = new String(a, 0, len); |
a 代表的是字符数组,0 代表起始位置,len 代表读取的长度。
参考链接:FileReader用法和问答
3.2 数据写出
有了读入,当然还有写出,字符流写出将用到 FileWriter
,测试代码如下:
1 | // 从内存中写出数据到硬盘文件中 |
使用字符流写出数据时,有几点需要进行说明:
1、使用字符流进行写出操作时,内容写入的目标文件可以不存在,且不会报异常。
2、File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建文件。
3、File 对应的硬盘中的文件如果存在:
-
如果流使用构造器是
FileWriter(file, false)
或FileWriter(file)
将会对原有文件进行覆盖 -
如果流使用构造器是
FileWriter(file, true)
不会对原有文件进行覆盖,而是原有文件基础上追加内容
3.3 读入与写出
前面列举了数据读入和写出,但是读入是将文本文件内容输出到控制台上,写入是使用代码的方式直接写入到文本文件中,那能否将两者结合一下?
先读入 A 文件的内容,然后将该内容写入到 B 文件中呢?(这就是传说中的文本复制! 😂)
其实也很简单,直接上测试代码:
1 |
|
3.4 拓展
我们前面编写的代码都是处理文本文件的,那么那些代码可以处理图片吗?
是否只需要将 File 构造器内的文件名修改一下就可以了呢?比如:
1 | // 1. 创建 File 类的对象,指明读入和写出的文件 |
其实测试一下就知道,压根是不行的,虽然 pic2.jpg 会出现在我们的 Module 下,但并不能打开该文件。
原因也很简单,FileReader
和 FileWriter
都是处理字符流的,或者说都是处理文本文件的,而图片并不是文本文件,属于二进制文件,要想处理图片,就需要用到字节流! 👇
4. 字节流
4.1 字节输入流初体验
上文说到,对于文本文件(.txt, .java, .c, .cpp 等)使用字符流处理,对于非文本文件(.jpg, .mp3, .mp4, .pdf 等)使用字节流处理。我们也验证了使用字符流 FileReader
和 FileWriter
处理非文本文件会出错,那么使用字节流 FileInputStream
和 FileOutputStream
来处理文本文件会怎么样呢?
不多 BB,直接上代码,步骤和格式与前面的操作差不多:
1 | // 使用字节流 FileInputStream 处理文本文件可能出现乱码 |
相信聪明的你一定看到贴出代码的首行注释了:使用字节流 FileInputStream
处理文本文件可能出现乱码。于是你一通 cv 把代码拷贝到自己的项目中,然后发现并没有出现乱码,这是怎么回事?
可以看一下 hello.txt
文件的编码,一般来说都是 UTF-8 ,在 UTF-8 中,英文字符和数字也都是一个字节,使用字节流传输是没有什么问题的,但是如果你加几个中文汉字呢?
于是你又一番操作,结果发现还是没乱码?咋回事?(第一次这么想让汉字乱码😂)
原因也很简单。 在 UTF-8 编码中,一个汉字占 3 个字节。 我们代码中设置的字节数组长度为 5,然后你编写的文本中的数据恰好也是 5 字节的整数倍,不妨再加一个汉字,看看会不会乱码?
我相信,一定会乱码的!
因此结论也得出来了:使用字节流 FileInputStream
处理文本文件可能出现乱码。(这可能二字用得就很妙!)
总的来说,操作文本文件还是使用字符流好,毕竟在字符流中,无论英文、数字,抑或汉字,都算一个字符,如此下来,使用字符流操作文本文件是不会出现乱码的情况的。
扯了这么多,接下来就该进入主题了,使用字节流操作非文本文件。
4.2 操作非文本文件
在本节我们使用字节流来操作一下非文本文件,通俗一点:复制图片。😝
操作步骤和前面的复制文本文件一下,方法也基本一样,只是换几个类:
1 | // 实现对图片的复制 |
运行后,确实实现了图片的复制,复制得到的图片也可以正常打开。
什么?魂魄妖梦是啥?😇 老二刺猿了!
4.3 文件复制
根据 4.2 操作非文本文件 提供的代码,我们可以改写一下代码,编写一个复制文件的方法 copyFile()
。
copyFile()
方法和测试代码如下:
1 | // 实现指定路径下文件复制 |
运行上述代码,复制一个大小 164M 的视频文件耗费了 2085ms。
你会发现提供的代码中,我还尝试复制了文本文件,那么真的可以复制成功吗?不会像前面测试那样乱码吗?
不妨取消注释,自己尝试一下,最终会发现能够复制成功。
总结
针对上面的现象进行进行一个小总结:
1、 使用字节流读入文本文件并显示在控制台,可能会出现乱码,但是使用字节流复制文本文件并不会出现乱码,并且能够成功复制。 (直接看就乱码,不看就不会乱码,这是薛定谔的代码?😆)
2、不能够使用字符流来处理非文本文件,就算是复制也不行!当然对于文本文件来说,还是使用字符流处理更好。
5. 缓冲流
5.1 非文本文件的复制
缓冲流的概述
前面讲的字符流(FileReader、FileWriter)和字节流(FileInputStream、FileOutputStream) 属于节点流,而缓冲流属于处理流的一种 ,在节点流的基础上包装了一层。那么,缓冲流有什么用?
缓冲流的作用就是为了提高文件的读写效率。因此在设计开发过程中,一般不会使用前面列述的几个类,而是考虑使用缓冲流,因为它们是比较基本的几个流,效率不是很高。
有关缓冲流,存在这四个类:BufferedInputStream
、 BufferedOuputStream
、 BufferedReader
和 BufferedWriter
。根据前面的说明,我们可以很简单地明白前两个是针对字节流的,后面两个是针对字符流的。
还有一点,缓冲流是不能直接作用在源文件上的,需要作用在节点流之上。
缓冲流就是“套接”在已有流的基础上。
测试代码
编写代码,实现图片的复制:
1 | // 实现非文本文件的复制 |
运行代码后,可以前往当前 Module 中查看复制得到的图片。
上述代码中,涉及到 4 个类,两种节点流,两种作用于节点流的处理流(缓冲流),那么最后关闭的时候要关闭 4 种?
当然不用。关闭外层的流时,内层流也会自动关闭,因此,内层流的关闭可以省略。
使用缓冲流可以实现非文本文件的复制,但是怎么体现缓冲流的作用(提高了读写效率)呢?
5.2 读写速度对比
和前面一样,提取一个 copyFileWithBuffered()
方法用于复制一个视频,然后使用与前面相同的视频进行测试,比较使用缓冲流所耗费的时间与未使用缓冲流所耗费的时间:
1 | public void copyFileWithBuffered(String srcPath, String destPath) { |
运行代码后得出使用缓冲流复制相同视频所用时间为 450ms,而未使用缓冲流时,所消耗时间为 2085ms,效率得到了明显的提升(注意测试时将字节数组的大小设置成一样,控制变量嘛)。
那么为什么使用缓冲流后读写效率会得到提升呢?
主要是因为内部提供了一个 8kb 的缓冲区,数据会先写入缓冲区,当缓冲区数据满(达到某一条件)后,刷新缓冲区,将数据进行复写到目标文件中。
缓冲流内部还有一个 flush()
方法,这个方法用于刷新缓冲区。如果每次使用缓冲流写入后,手动调用这个方法(这个时候缓冲区还没达到刷新条件),会发现耗时又增加了(我的环境下耗时为 1431ms)。
5.3 文本文件的复制
对于字符流也有对应的缓冲流,我们可以测试一下(被复制文本最好是个大文本文件):
1 | // 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制 |
在上述代码的读写操作中,使用了两种方法:一种使用 char[]
数组,一种使用了 String
类。
使用 char[]
数组时,与使用 byte[]
一样,就不赘述了。
使用 String
类时需要注意实例化的字符串中并 不包含换行符 ,如果直接写入,将无法写入换行符,导致所有内容都在一行。解决方法也有两种:
- 在字符串末尾拼接
\n
再写入 - 依旧按照原方式写入,但是后续调用
readLine()
方法
5.4 技巧练习
图片加密
1 | // 图片的加密 |
使用字节流传输文件时,可以将获取的字节与某个特定的数进行异或,这样就可以实现图片的简单加密。
我们知道一个数异或的异或就是原数据,因此解密时,只需要再进行异或就可以了。
字的统计
给定一个文本,统计该文本中每次字出现的次数。代码如下:
1 |
|
小技巧
使用 FileInputStream
、 FileOutputStream
、 FileReader
和 FileWriter
的构造方法时,可以直接传入路径,不用再书写 new File()
获取 File 对象。
6. 转换流
6.1 转换流的概述
转换流也是处理流的一种,转换流提供了在字节流和字符流之间的转换。
Java API 提供了两个转换流:
InputStreamReader
:将InputStream
转换为Reader
,将一个字节的输入流转换成字符的输入流OutputStreamWriter
:将Writer
转换为OutputStream
,将一个字符的输出流转换成字节的输出流
字节流中的数据都是字符时,转换成字符流操作更加高效。
很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
解码与编码
解码:字节、字节数组 ------> 字符数组、字符串 使用 InputStreamReader
编码:字符数组、字符串 ------> 字节、字节数组 使用 OutputStreamWriter
6.2 InputStreamReader
使用字节流读取一个文本文件,然后将字节流转换成字符流,最后将文本内容输出到控制台:
1 | // 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒) |
上述代码中的异常处理仍应该使用 try-catch-finally
来处理,下述代码仅仅是为了简便,或者说偷懒。
6.3 综合使用
综合使用 InputStreamReader
和 OutputStreamWriter
实现文本文件的解码与编码:
1 | // 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒) |
运行上述代码后,可以生成 helle-gbk.txt 文本文件,这个文本文件的编码是 gbk 的,如果在 IDEA 中直接打开,将会出现乱码,因为默认使用的 UTF-8 的编码打开的,所以会出现乱码。
当然如果使用 Nodepad3 、Edit Plus 打开就不会,因为这些软件可以自动匹配字符集。
6.4 字符集说明
编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。而这就是编码表。
常见的编码表
ASCII:美国标准信息交换码。用一个字节的 7 位可以表示。
IS08859-1:拉丁码表、欧洲码表。用一个字节的 8 位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符。
GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码。中文是两个字节,但是英文、数字是一个字节的。
Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码, 所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用 1 - 4 个字节来表示一个字符。
编码的问题
Unicode 虽然融合了人类使用的所有字,但是它不完美,这里就有三个问题,一个是我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别 Unicode 和 ASCII ?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode 在很长一段时间内无法推广,直到互联网的出现。
面向传输的众多UTF (UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode 编码是 UTF-8 和 UTF-16 。|
补充
ANSI 编码,通常指的是平台的默认编码,例如英文操作系统中是 ISO-8859-1,中文系统是 GBK。
Unicode 字符集只是定义了字符的集合和唯一编号,Unicode 编码这是对 UTF-8、UCS-2 / UTF-16 等具体编码方案的统称而已,并不是具体的编码方案。
参考视频:多种字符编码集的说明
7. 标准的输入、输出流
7.1 概述
System.in
和 System.out
分别代表了系统标准的输入和输出设备
默认输入设备是:键盘,输出设备是:显示器
System.in
的类型是 InputStream
,标准的输入流,默认从键盘输入
System.out
的类型是 PrintStream
,它是 OutputStream
的子类、FilterOutputStream
的子类,标准的输出流,默认从控制台输出
重定向:通过 System
类的 setln
, setOut
方法对默认设备进行改变。
-
public static void setln(InputStream in)
-
public static void setOut(PrintStream out)
7.2 具体使用
从键盘上输入字符串,要求将读取到的整行字符串转成大写输出,然后继续进行输入操作,直至输入 e 或 exit 时,退出程序:
1 | // System.in 是字节流,BufferedReader 的 readLine() 是字符流,需要使用转换流 |
8. 打印流
打印流概要
实现将基本数据类型的数据格式转化为字符串 输出 。打印流,打印流,肯定是用来打印输出的,因此没有输入。
打印流:PrintStream
和 PrintWriter
提供了一系列重载的 print()
和 println()
方法, 用于多种数据类型的输出
PrintStream
和 PrintWriter
的输出不会抛出 IOException
异常
PrintStream
和 PrintWriter
有自动 flush
功能
PrintStream
打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter
类。
System.out
返回的是 PrintStream
的实例
具体使用
实现使用打印流将 ASCII 码对应的字符打印到指定文件中:
1 | // 打印流 PrintStream 和 PrintWriter |
9. 数据流
为了方便地操作 Java 语言的基本数据类型和 String 的数据, 可以使用数据流。
数据流有两个类:(用于读取和写出基本数据类型、String 类的数据)
-
DatalnputStream
和DataOutputStream
-
分别“套接”在
InputStream
和OutputStream
子类的流上
DataInputStream
中的方法
boolean readBoolean()
byte readByte()
char readChar()
float readFloat()
double readDouble()
short readShort()
long readLong()
int readInt()
String readUTF()
void readFully(byte[] b)
DataOutputStream
中的方法
将上述的方法的 read 改为相应的 write 即可。
具体使用
将内存中的基本数据类型和字符串写入文件:
1 | // 将内存中的基本数据变量和字符串写入文件 |
将文件中存储的基本数据类型和字符串读取到内存中,保存到变量:
1 | // 将文件中存储的基本数据类型和字符串读取到内存中,保存到变量 |
10. 对象流
10.1 对象流的概述
对象流使用到的类是: ObjectInputStream
和 OjbectOutputSteam
对象流是用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。这样就涉及到序列化和反序列化的概念:
序列化:用 ObjectOutputStream
类 保存 基本类型数据或对象的机制
反序列化:用 ObjectInputStream
类 读取 基木类型数据或对象的机制
注意: ObjectOutputStream
和 ObjectInputStream
不能序列化 static
和 transient
修饰的成员变量。
对象的序列化
对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
序列化的好处在于可将任何实现了 Serializable
接口的对象转化为字节数据 ,使其在保存和传输时可被还原.
序列化是RMI(Remote Method Invoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下 两个接口之一 ,否则,会抛出 NotSerializableException
异常:
-
Serializable
(常用) -
Externalizable
10.2 字符串的序列化
将某个字符串序列化写入文件 object.dat ,然后使用反序列化将文件 object.dat 中的数据输出到控制台:
1 | // 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去 |
运行上述代码后,当前 Module 下生成 object.dat 文件,控制台输出字符串 “默烦” 。
10.3 自定义类的序列化
使用对象流序列化对象
若某个类实现了 Serializable
接口,那么该类的对象就是可序列化的。序列化过程如下:
1、创建一个 ObjectOutputStream
对象
2、调用 ObjectOutputStream
对象的 writeObject(对象)
方法输出可序列化对象
3、注意写出一次,就要操作 flush()
一次
那么反序列化:
1、创建一个 ObjectInputStream
对象
2、调用 readObject()
方法读取流中的对象
强调: 如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。
序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable
接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID
。
3、除了保证需要被序列化的类需要实现 Serializable
接口之外,还必须保证其内部所有属性都必须是可序列化的。(默认情况下,基本数据类型都可序列化)
自定义类序列化示例
创建一个实体类 Person
,具体代码如下:
1 | /** |
在字符串序列化的代码基础上进行修改:
1 | // 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去 |
运行上述代码后,控制台输出:
需要注意的有:
1、每次序列化一个对象后,记得执行 flush()
操作
2、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
10.4 serialVersionUID
凡是实现 Serializable
接口的类都有一个表示序列化版木标识符的静态变量:
1 | private static final long serialVersionUID; |
serialVersionUID
用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
如果类没有显式定义这个静态变量,它的值是 Java 运行时环境根据类的内部细节自动生成的。 若类的实例变量做了修改,serialVersionUID 可能发生变化。 故建议,显式声明。
简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID
来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID
与本地相应实体类的 serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 InvalidCastException
。
10.5 序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable
接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID
。
3、除了保证需要被序列化的类需要实现 Serializable
接口之外,还必须保证其内部所有属性都必须是可序列化的(默认情况下,基本数据类型都可序列化)。
4、 ObjectOutputStream
和 ObjectInputStream
不能序列化 static
和 transient
修饰的成员变量。
5、每次序列化一个对象后,记得执行 flush()
操作
6、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
序列化更多相关内容可以查阅:Lambda 与序列化
11. 随机存取文件流
11.1 RandomAccessFile 类
RandomAccessFile 类的概述
RandomAccessFile
声明在 java.io
包下, 但直接继承于 java.lang.Object
类,并
且它实现了 Datalnput
、DataOutput
这两 个接口,也就意味着这个类既可以读也可以写。
RandomAccessFile
类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件,比如:
-
支持只访问文件的部分内容
-
可以向已存在的文件后追加内容
RandomAccessFile
对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile
类对象可以自由移动记录指针:
-
long getFilePointer()
:获取文件记录指针的当前位置 -
void seek(long pos)
:将文件记录指针定位到 pos 位置
RandomAccessFile 构造器
1 | public RandomAccessFile(File file, String mode) |
创建 RandomAccessFile
类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile
的访问模式:
-
r:以只读方式打开
-
rw:打开以便读取和写入
-
rwd:打开以便读取和写入,同步文件内容的更新
-
rws:打开以便读取和写入,同步文件内容和元数据的更新
如果模式为只读 r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。如果 模式为 rw 读写,且文件不存在则会去创建文件,反之则不会创建。
RandomAccessFile 实现图片的复制示例
1 |
|
RandomAccessFile 作为输出流示例
1 | /* |
我们需要知道:
1、RandomAccessFile
作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建
2、如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)
在当个方法中调用 write()
方法后会记录角标的位置,如果再次调用该方法,将会从记录的角标位置后进行写入。
11.2 实现数据的插入
实现指定位置的复写
在前文代码的基础上指定角标的位置:
1 |
|
实现指定位置的插入
1 | // 实现数据的插入效果 |
RandomAccessFile
并没有提供插入方法,我们实现的插入其实就是:复制指定位置后的所有数据,在指定位置后写入数据,最后再将复制的数据写入文件。
那 RandomAccessFile
有什么用呢?
我们可以用 RandomAccessFile
这个类,来实现一个 多线程断点下载 的功能,用过下载工具的朋友们都知道,下 载前都会建立 两个临时文件 ,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。
12. NIO 的简单介绍
12.1 NIO.2
NIO 的简单介绍
Java NIO(New IO,Non-Blocking I0)是从 Java 1.4 版本开始引入的一套新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。
Java API 中提供了两套 NIO,一套是针对标准输入输出 NIO,另一套就是网络编程NIO。
1 | ➢|-----java.nio.channels.Channel |
但是 JDK 1.4 中 NIO 的使用体验并不好,随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称它们为 NIO.2 。因为 NIO 提供的一-些功能,NIO 已经成为文件处理中越来越重要的部分。
NIO.2 中 Path、Paths 的使用
早期的 Java 只提供了一个 File
类来访问文件系统,但 File
类的功能比较有限,所提供的方法性能也不高。而且大多数方法在出错时仅返回失败,并不会提供异常信息。
NIO. 2 为了弥补这种不足,引入了 Path
接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path
可以看成是 File
类的升级版本,实际引用的资源也可以不存在。
在以前IO操作都是这样写的:
1 | import java.io.File; |
但在Java7中,我们可以这样写:
1 | import java.nio.file.Path; |
同时,NIO.2 在 java.nio.file
包下 还提供了 Files
、Paths
工具类,Files
包含了大量静态的工具方法来操作文件,Paths
则包含 了两个返回 Path
的静态工厂方法。
Paths
类提供的静态 get()
方法用来获取 Path
对象:
1 | static Path get(String first, String ... more): 用于将多个字符串串连成路径 |
NIO.2 中 Files 类的使用
java.nio.file.Files
用于操作文件或目录的上具类。
Files 常用方法:
1 | Path copy(Path src, Path dest, CopyOption ... how): 文件的复制 |
到此,Java IO 流基础就基本说完了,在实际开发过程中可能会书写这些 IO 流代码,但是大概率是不会使用的,经常使用的是已经封装好的第三方库,比如 Apache 下的 commons-io
。
Java IO 流基础完