十五、 IO流
本章涉及到 Java 操纵硬盘持久化数据。把内存中的数据输出到计算机硬盘中,叫输出流;把计算机硬盘中的数据输入到内存中,叫输入流。
15.1 File类的使用
15.1.1 File类的实例化
1.File的概念
在 Java 中,基于万事万物皆对象的思想,硬盘中的文件要想加载到 Java 中,首先要有一个对象作为容器来盛装文件。这个对象就是 File 类创建的对象。File 类创建的对象既可以指的是硬盘中的单个文件,如 XXX.txt、XXX.docx,也可以指的是整个文件目录 (文件夹) 。
2.File的4种构造器
File的4种构造器 |
---|
File(String pathname):直接传入文件路径 |
File(String parent, String child):传入父路径和子路径 |
File(File parent, String child):传入父File和子路径 |
File(URI uri):传入统一资源标识符(Uniform Resource Identifier,URI) |
@Test
public void test1() {
//构造器1
File file1 = new File("F:\\OneDrive - stu.ouc.edu.cn\\MarkDown\\2-后端开发\\IdeaProject\\JavaSE\\ch15\\hello.txt");
File file2 = new File("hi.txt");
System.out.println(file1);
System.out.println(file2);
//构造器2
File file3 = new File("F:\\OneDrive - stu.ouc.edu.cn\\MarkDown\\2-后端开发\\IdeaProject", "JavaSE");
System.out.println(file3);
//构造器3
File file4 = new File(file3, "ch15\\hello.txt");
System.out.println(file4);
}
输出:
F:\OneDrive - stu.ouc.edu.cn\MarkDown\2-后端开发\IdeaProject\JavaSE\ch15\hello.txt
hi.txt
F:\OneDrive - stu.ouc.edu.cn\MarkDown\2-后端开发\IdeaProject\JavaSE
F:\OneDrive - stu.ouc.edu.cn\MarkDown\2-后端开发\IdeaProject\JavaSE\ch15\hello.txt
此时硬盘中还没有 hello.txt
文件,但是并没有报错,输出的 File 对象是指定的路径。是因为这些对象还仅仅是在内存中创建的对象,并没有与硬盘中实际的文件关联起来作增删改查操作。
3.绝对路径和相对路径
-
绝对路径:带有盘符的完整文件路径。
File file1 = new File("F:\\OneDrive - stu.ouc.edu.cn\\MarkDown\\2-后端开发\\IdeaProject\\JavaSE\\ch15\\hello.txt");
-
相对路径:如果是在 Junit 单元测试中创建,就相对于当前 java 文件的 module 下的路径。如果是在 main 方法中创建,就相对于当前工程文件下的路径。
File file2 = new File("hi.txt");
4.不同系统下的路径分割符
-
在 Windows 系统下,Java 中的路径分割符是
\
;由于 Java 中\
本来就有转义字符如换行\n
的意思,因此要多加一个\
,变成\\
。 -
在 Unix 或 Linux 系统下,Java 中的路径分割符是
/
; -
Java 为了增强代码中路径分隔符的通用性,在 File 类中定义了分隔符常量来统一不同系统下的路径分隔符
separator
。File files = new File("d:" + File.separator + "code" + File.separator + "JavaSE" + File.separator + "hello.txt")
15.1.2 File类的常用方法
File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。
1.File 类的获取功能
方法 | 作用 |
---|---|
String getAbsolutePath() | 获取绝对路径 |
String getPath() | 获取路径 |
String getName() | 获取文件名称 |
String getParent() | 获取上层文件目录路径。若无,返回null |
long length() | 获取文件长度(即字节数)。不能获取目录的长度 |
long lastModified() | 获取最后一次的修改时间,毫秒值 |
String[] list() | 获取指定目录下的所有文件或者文件目录的名称数组,只适用于文件目录 |
File[] listFiles() | 获取指定目录下的所有文件或者文件目录的File数组,只适用于文件目录 |
如下图所示创建了 2 个文件:
@Test
public void test2() {
File file = new File("G:\\io\\hello.txt");
//获取绝对路径
System.out.println(file.getAbsoluteFile());
//获取路径
System.out.println(file.getPath());
//获取文件名称
System.out.println(file.getName());
//获取上层文件目录路径。若无,返回null
System.out.println(file.getParent());
//获取文件长度(即字节数)。不能获取目录的长度
System.out.println(file.length());
//获取最后一次的修改时间,毫秒值
System.out.println(new Date(file.lastModified()));
}
输出:
G:\io\hello.txt
G:\io\hello.txt
hello.txt
G:\io
8
Wed Apr 20 16:39:18 CST 2022
针对文件目录的 2 个方法:
@Test
public void test3() {
File file = new File("G:\\io");
//获取指定目录下的所有文件或者文件目录的名称数组,只适用于文件目录
String[] list = file.list();
for (String s : list) {
System.out.println(s);
}
//获取指定目录下的所有文件或者文件目录的File数组,只适用于文件目录
File[] files = file.listFiles();
for (File f : files) {
System.out.println(f);
}
}
输出:
hello.txt
project.txt
G:\io\hello.txt
G:\io\project.txt
2.File类的重命名
方法 | 作用 |
---|---|
boolean renameTo(File dest) | 把文件名重命名为指定的文件路径,注意不是重命名文件名 |
在 ch15 模块下创建 hi.txt
文件:
随便输入:
@Test
public void test4() {
File file1 = new File("hi.txt");//相对路径
File file2 = new File("G:\\io\\hello.txt");//绝对路径
//把文件名重命名为指定的文件路径
boolean isRename = file1.renameTo(file2);
System.out.println(isRename);
}
输出:
false
可见,重命名失败了。要想返回 true ,需要保证 file1
在硬盘中是存在的,且 file2
不能在硬盘中。
把硬盘中的 file2
文件 hello.txt
文件删除后,再次运行。
输出:
true
修改成功,结果返回 true 。且原来在 ch15 模块下创建 hi.txt
文件移动到 file2
所指定的路径,文件名称由 hi.txt
修改为 file2
指定的 hello.txt
:
打开 hello.txt
文件,里面的内容和原来在 ch15 模块下创建 hi.txt
文件的内容是一致的:
3.File类的判断功能
方法 | 作用 |
---|---|
boolean isDirectory() | 判断是否是文件目录 |
boolean isFile() | 判断是否是文件 |
boolean exists() | 判断是否存在在硬盘中 |
boolean canRead() | 判断是否可读 |
boolean canWrite() | 判断是否可写 |
boolean isHidden() | 判断是否隐藏 |
@Test
public void test5() {
File file1 = new File("hi.txt");
File file2 = new File("G:\\io\\hello.txt");
//判断是否是文件目录
System.out.println(file1.isDirectory());
//判断是否是文件
System.out.println(file1.isFile());
//判断是否存在在硬盘中
System.out.println(file1.exists());
//判断是否可读
System.out.println(file1.canRead());
//判断是否可写
System.out.println(file1.canWrite());
//判断是否隐藏
System.out.println(file1.isHidden());
}
输出:
false
true
true
true
true
false
经验:可以先调用 exist()
判断文件在不在,再去进行下一步的工作。
4.File类的创建功能
以下方法是真的可以在硬盘下创建文件的方法。
方法 | 作用 |
---|---|
boolean createNewFile() | 创建文件。若文件存在,则不创建,返回false |
boolean mkdir() | 创建文件目录。若文件目录存在,就不创建了 |
boolean mkdirs() | 创建文件目录。若上层文件目录不存在,就一并创建了 |
注意事项:如果你创建文件或者文件目录没写盘符路径,则默认创建在项目路径下。
① 创建文件:
执行前:
@Test
public void test6() throws IOException {
//创建一个当前ch15模块下不存在的File对象
File file1 = new File("hello.txt");
//如文件不存在硬盘中,则创建该文件
if (!file1.exists()){
file1.createNewFile();
System.out.println("成功创建hello.txt");
}
}
输出:
成功创建hello.txt
执行后:
② 创建文件目录:
执行前:
@Test
public void test6() throws IOException {
//创建不存在的文件目录
File file2 = new File("G:\\io\\documents");//不存在的,但上层存在
boolean isMkdir = file2.mkdir();
if (isMkdir){
System.out.println("成功创建文件目录");
}
}
输出:
成功创建文件目录
执行后:
③ 创建上层文件目录不存在的文件目录:
执行前:
@Test
public void test6() throws IOException {
//创建上层文件目录不存在的文件目录
File file3 = new File("G:\\io\\pic\\photos");//不存在的,且上层不存在
boolean isMkdirs = file3.mkdirs();
if (isMkdirs) {
System.out.println("成功创建文件目录");
}
}
输出:
成功创建文件目录
执行后:
连同上级文件目录 pic
都一同创建出来了。
5.File类的删除功能
方法 | 作用 |
---|---|
boolean delete() | 删除文件或者文件夹 |
注意事项:Java 的删除不走回收站。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
例子:
删除前:
比方说我想删除文件目录 pic
,但它里面包含另一个文件目录 photos
。这样子删除是不成功的。
@Test
public void test7() {
//删除操作
File file = new File("G:\\io\\pic");
boolean isDelete = file.delete();
if (isDelete) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
}
输出:
删除失败
要想删除成功,要删除的文件目录内不能包含文件或文件目录。即只能删除文件和空的文件目录。
【内存解析】
15.1.3 File类练习
题目1:
判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称。
public class JpgTest {
public static void main(String[] args) {
File file = new File("F:\\图片\\头像\\迪迦");
String[] nameList = file.list();//名称数组
for (String s : nameList) {
boolean isContains = s.contains(".jpg");
if (isContains) {
System.out.println(s);
} else {
System.out.println("此文件目录不包含.jpg文件");
}
}
}
}
输出:
迪迦复合型.jpg
迪迦复合型2.jpg
迪迦复合型3.jpg
题目2:
遍历指定目录所有文件名称,包括子文件目录中的文件。
拓展1 :并计算指定目录占用空间的大小;
拓展2 :删除指定文件目录及其下的所有文件。
我的首次答案:
public class Ergodic {
//递归操作实现
public static void ergodic(File file) {
//获取File类对象的数组
File[] files = file.listFiles();
//遍历每个File对象并判断
for (File f : files) {
if (f.isFile()) {//如果是文件,直接输出文件名称
System.out.println(f.getName());
} else if (f.isDirectory()) {//如果是文件目录,递归调用
ergodic(f);//递归
}
}
}
public static void main(String[] args) {
File file = new File("F:\\1-海大学习\\3-计算机经典");
ergodic(file);
}
}
输出:
Java编程思想第4版.pdf
尚硅谷Java数据结构和算法【最新版】.pptx
深入理解Java虚拟机-第二版.pdf
数据结构与算法分析——Java语言描述.pdf
深入理解计算机操作系统.pdf
《算法图解》.pdf
数据结构与算法分析——Java语言描述.pdf
算法导论_原书第3版_CHS.pdf
算法笔记-上机训练实战指南-胡凡 完整.pdf
算法笔记.胡凡.pdf
计算机网络 自顶向下方法(第七版).pdf
拓展1:统计所有文件的占用空间。我用递归法死活想不出来,因为如果把统计变量写在递归方法内,递归又会重复调用,然后归 0 再统计。达不到累计的效果,如下代码所示:
public class Ergodic {
//递归操作实现
public static void ergodic(File file) {
int space = 0;
//获取File类对象的数组
File[] files = file.listFiles();
//遍历每个File对象并判断
for (File f : files) {
if (f.isFile()) {//如果是文件,直接输出文件名称
System.out.println(f.getName());
space += f.length();
//累计占用空间
} else if (f.isDirectory()) {//如果是文件目录,递归调用
ergodic(f);//递归
}
}
System.out.println("所占空间:" + space);
}
public static void main(String[] args) {
File file = new File("F:\\1-海大学习\\3-计算机经典");
ergodic(file);
}
}
输出:
Java编程思想第4版.pdf
尚硅谷Java数据结构和算法【最新版】.pptx
深入理解Java虚拟机-第二版.pdf
所占空间:85910926 //反复统计1
数据结构与算法分析——Java语言描述.pdf
深入理解计算机操作系统.pdf
《算法图解》.pdf
数据结构与算法分析——Java语言描述.pdf
算法导论_原书第3版_CHS.pdf
算法笔记-上机训练实战指南-胡凡 完整.pdf
算法笔记.胡凡.pdf
所占空间:224083623 //反复统计2
所占空间:140145152 //反复统计3
计算机网络 自顶向下方法(第七版).pdf
所占空间:619844389 //反复统计4
【拓展1老师的解题】
老师的解决方法非常巧妙,依然是使用递归。但是却把每层递归都分为文件和文件目录来考虑,真是妙啊!
// 拓展1:求指定目录所在空间的大小
// 求任意一个目录的总大小
public static long getDirectorySize(File file) {
// file是文件,那么直接返回file.length()
// file是目录,把它的下一级的所有大小加起来就是它的总大小
long size = 0;
if (file.isFile()) {
size += file.length();
} else {
File[] all = file.listFiles();// 获取file的下一级
// 累加all[i]的大小
for (File f : all) {
size += getDirectorySize(f);//此处用递归求f的大小,妙啊;
}
}
return size;
}
//主函数
public static void main(String[] args) {
File file = new File("F:\\1-海大学习\\3-计算机经典");
long size = getDirectorySize(file);
System.out.println(size + " Bytes");
}
输出:
1069984090 Bytes
这次的占用空间是正确的。
拓展2 :删除指定文件目录及其下的所有文件。
//拓展2 :删除指定文件目录及其下的所有文件
public static void deleteAll(File file) {
//如果是文件,则直接删除
//如果是文件目录,先删除里面的东西,再删除自己
if (file.isDirectory()) {
File[] files = file.listFiles();
//循环删除的是file的下一级
for (File f : files) {//f代表file的每一个下级
deleteAll(f);
}
}
file.delete();//删除自己
}
题目3:创建一个与 a.txt
文件同目录下的另一个文件 b.txt
。
@Test
public void test1() throws IOException {
File file1 = new File("G:\\io\\a.txt");
File file2 = new File(file1.getParent(), "b.txt");
boolean isCreate = file2.createNewFile();
if (isCreate) {
System.out.println("成功");
} else {
System.out.println("失败");
}
}
输出:
成功
15.2 IO流原理及流的分类
15.2.1 IO流原理
由于输入输出具有相对性,因此,当我们在 Java 中讲 IO 流时,是从程序 (即内存) 的角度出发来判定输入输出。
- 从硬盘传输到内存中,称为“输入流”;
- 从内存传输到硬盘中,称为“输出流”。
15.2.2 IO流分类
- 按操作数据单位的不同分为:字节流 (8 bit)、字符流 (16 bit);
- 按数据流的流向不同分为:输入流、输出流;
- 按流的角色的不同分为:节点流、处理流。
- 节点流:指的是承担数据和程序 (硬盘和内存) 之间数据传输的流。
- 处理流:是包裹在节点流之上,用于加速、控制节点流的一类流。
15.2.3 IO流的体系结构
抽象基类 | 字节流 | 字符流 |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Write |
- Java 的 IO 流共涉及 40 多个类,实际上非常简单,都是从如上 4 个抽象基类派生的。
- 由上面这 4 个类派生出来的子类名称都是以其父类名作为子类名后缀。
- 各自列的第一行
抽象基类
是列中其他行的父类。 - 标浅蓝色底的是需要熟练掌握的。
- 其中,第三行
访问文件
是可以直接访问硬盘的节点流;其他都是处理流。
15.3 节点流(或文件流)
节点流 | 字节流 | 字符流 |
---|---|---|
输入流 | FileInputStream | FileReader |
输出流 | FileInputStream | FileWriter |
15.3.1 节点字符输入流
例子1
在 Module ch15
下创建了一个文件:hello.txt
,其内容为 “HelloWorld!”
@Test
public void test1() {
FileReader fr = null;
try {
//1.实例化File类对象,指明要操作的文件
File file = new File("hello.txt");//相较于当前Module下
//2.提供具体的流
fr = new FileReader(file);
//3.数据的读入
//read(): 返回读入的一个字符,返回的是字符的ASCII码值;如果达到文件末尾,则返回-1
int data;
while ((data = fr.read()) != -1) {//没有到文件末尾就一直循环读取
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流的关闭操作,保证在finally内
try {
if (fr != null)//为了避免第一行创建的fr出现空指针异常
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
HelloWorld!
【注意事项】
-
read()
: 返回读入的一个字符,返回的是字符的ASCII码值;如果达到文件末尾,则返回-1。要想读取文件全部的信息,则需要使用 while 循环。 -
close()
关闭流操作是一定要执行的重要操作。否则就会像水龙头一样流个不停,造成内存泄漏,从而出现信息安全问题。为了使close()
操作不受其他异常对象的影响,因此要使用 try-catch-finally 操作,把close()
放入 finally 中,确保close()
操作一定被执行。 -
读入的文件一定要存在,否则就会报
FileNotFoundException
。
例子2:read()
方法的升级操作,使用 read()
重载的方法 read(char[] cbuf)
- char[] 数组
cbuf
是缓存缓冲区 (cache buffer) 的简写。数组的长度就是每次输入内存的字符数量,即每次从硬盘一次性取多个字符到内存中,减少 IO 的次数,从而提高读取数据的效率。
【我的首次代码】
@Test
public void test2() {
FileReader fr = null;
try {
//1.实例化File类对象
File file = new File("hello.txt");
//2.FileReader的实例化
fr = new FileReader(file);
//3.数据的读入
//read(char[] cbuf): 返回每次读入cbuf数组中的字符的个数;如果到达文件末尾,则返回-1
char[] cbuf = new char[(int) file.length()];//我根据文件的大小创建char[]数组
int len;
while ((len = fr.read(cbuf)) != -1) {
for (char c : cbuf) {//直接用增强for循环输出字符结果
System.out.print(c);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流的关闭操作,保证在finally内
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
HelloWorld!
15.3.2 节点字符输出流
【说明】
- 输出操作,对应的 File 文件可以不存在。并不会报异常,而是自动在输出的过程中自动创建此文件。
- File 对应的硬盘中的文件如果存在:
- 如果输出流使用的构造器是 `FileWriter(file, false) / FileWriter(file) :则对原有的文件进行覆盖操作;
- 如果输出流使用的构造器是 `FileWriter(file, true) :则不会覆盖原有文件,而是在原有文件的基础上,进行追加写入操作;
write()
中传入的参数可以是 char 的 ASCII 编码值;也可以是 String 字符串;也可以是 cbuf (cache buffer) 。如下图所示:
执行前:
@Test
public void testFileWriter() {
FileWriter fw = null;
try {
//1.提供File类的对象,指明写出到的文件
File file = new File("write.txt");
//2.提供FileWriter的对象,用于数据的输出
fw = new FileWriter(file);
//3.写出的操作
fw.write("I have a dream!\n");
fw.write("You need to have a dream!");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流资源的关闭
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行后:
15.3.3 使用节点输入输出流实现复制文件操作
通过把 FileReader 和 FileWriter 结合起来,把 hello.txt
读进来,再写出去成 hello1.txt
文件,实现复制操作。
执行前:
@Test
public void testFileReaderAndWriter() {
FileReader fr = null;
FileWriter fw = null;
try {
//1.创建File类的对象,指明读入和写出到的文件
File srcFile = new File("hello.txt");
File destFile = new File("hello1.txt");
//2.创建输入流和输出流的对象
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);
//3.数据的读入和写出操作
char[] cbuf = new char[(int) srcFile.length()];
int len;//记录每次读入到cbuf数组中的字符的个数
while ((len = fr.read(cbuf)) != -1) {
//每次写出len个字符
fw.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
//即时上面fr.close()出现异常,下面也会被执行,因为try-catch是真正已经把异常处理掉了
try {
if (fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行后:
【注意事项】
- 字符流只能处理文本文件,处理图片、视频文件还是得用字节流。否则图片和视频会格式错误打不开。
15.3.4 节点流字节输入输出流
【结论】
- 对于文本文件 (.txt .java .cpp) ,使用字符流处理;
- 对于非文本文件 (.doc .ppt .jpg .mp4 .mp3 .avi) ,使用字节流处理。
例子1:通过结合字节输入输出流,实现对图片的复制操作
执行前:
@Test
public void testFileInputStream() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.创建File类的对象
File srcFile = new File("迪迦复合型.jpg");
File destFile = new File("迪迦复合型1.jpg");
//2.提供流的实例化对象,FileInputStream
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//3.对字节流进行读写操作
byte[] buffer = new byte[(int) srcFile.length()];
int len;
while ((len = fis.read(buffer)) != -1) {//字节输入流读取原始图片
fos.write(buffer, 0, len);//字节输出流写出目标图片
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行后:
例子2:把上述复制操作封装成一个通用的方法
//指定路径下文件的复制非文本文件
public void copyFile(String srcPath, String destPath) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File srcFile = new File(srcPath);
File destFile = new File(destPath);
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
byte[] buffer = new byte[(int) srcFile.length()];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试:传入一个800 MB 左右的视频,计算执行时间
@Test
public void testCopyFile() {
long start = System.currentTimeMillis();
//来复制视频文件
String srcPath = "G:\\特利迦奥特曼17.mp4";
String destPath = "G:\\特利迦奥特曼17-1.mp4";
copyFile(srcPath, destPath);
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("执行时间:" + time + " ms");
}
输出:
执行时间:25445 ms
在 copyFile()
方法中,我把 buffer
的大小设定为文件占用空间的大小,即一次性把所有数据写入写出。这是搬一次的执行时间。下面,我把第 12 行代码 buffer
改小一点,设置成 1024 ,看看执行时间:
byte[] buffer = new byte[1024];
输出:
执行时间:7127 ms
令我没想到的是,把 buffer
改小一点一点竟然能比原来快了 3.57 倍!
因此,只搬运一次虽然看起来可以减小硬盘与内存直接的 IO 次数,但是却增长了数据装载和卸载的时间。所以,buffer
并不是越大越好的。我还做了不同 buffer
大小所需要的执行时间,整理成表格如下:
buffer大小 (Byte) | 512 | 1024 | 2048 | 5120 | 10240 | 102400 | 204800 | 512000 | 1048576 | 文件大小 |
---|---|---|---|---|---|---|---|---|---|---|
所需时间 (ms) | 12434 | 6792 | 4023 | 2309 | 1755 | 1220 | 1102 | 1251 | 1287 | 5840 |
从表格上看的话,以后 buffer
还是设定为 204800
会比较快。
15.4 缓冲流
缓冲流属于处理流的一种,用来包装节点流,提高文件读写效率。
能提高读写速度的原因是:内部提供了一个 8192 Byte 的缓冲区。有一个刷新缓冲区的方法:Writer.flush()
,无论缓冲区是否装满,都会手动把缓冲区中的数据弹出到硬盘中。
缓冲流 | 字节流 | 字符流 |
---|---|---|
输入流 | BufferedInputStream | BufferedReader |
输出流 | BufferedInputStream | BufferedWriter |
15.4.1 缓冲流字节输入输出流
例子:依然是复制上节提到的视频文件,比较和节点流的传输速度
【封装的 Buffered 流复制方法】
public void bufferedCopyFile(String srcPath, String destPath) {
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.创建File类的对象,指明读入和写出到的文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2.创建输入流和输出流的对象
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//3.创建输入缓冲流和输出缓冲流的对象,并把节点流放进去
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
//4.数据的读入和写出操作
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//5.关闭流资源
//先关闭外层的流,再关闭内层的流,就像穿衣服脱衣服一样
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
//说明:在关闭外层流的同时,内层流会自动关闭,因此只需要关闭bis和bos即可
// try {
// if (fis != null)
// fis.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// try {
// if (fos != null)
// fos.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
测试:
@Test
public void testBufferedCopyFile() {
long start = System.currentTimeMillis();
String srcPath = "G:\\特利迦奥特曼17.mp4";
String destPath = "G:\\特利迦奥特曼17-1.mp4";
bufferedCopyFile(srcPath, destPath);
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("执行时间:" + time + " ms");
}
输出:
执行时间:2210 ms
当 buffer
同为 1024 字节时,缓冲流的执行时间仅为 2210 ms ,而节点流的执行时间为 6792 ms 。因此可以看出,缓冲流确实起到加速数据传输效率的作用。
buffer大小 (Byte) | 512 | 1024 | 2048 | 5120 | 10240 | 102400 | 204800 | 512000 | 1048576 | 文件大小 |
---|---|---|---|---|---|---|---|---|---|---|
字节流所需时间 (ms) | 12434 | 6792 | 4023 | 2309 | 1755 | 1220 | 1102 | 1251 | 1287 | 5840 |
缓冲流所需时间 (ms) | 1545 | 2210 | 1770 | 2205 | 2158 | 1491 | 1742 | 1331 | 1626 | 5638 |
从上面表格可以看出,缓冲流并不总是起加速作用的。只有在 buffer
比较小的时候,加速作用才越明显。而在 buffer
超过 5120 之后,缓冲流的加速作用其实已经几乎消失了,甚至还比不加缓冲流要慢。
15.4.2 缓冲流字符输入输出流
例子:依然是实现对文本文件 .txt 的复制操作。
@Test
public void testBufferedReaderAndWriter() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(new File("dbcp.txt")));
bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));
char[] cbuf = new char[1024];
int len;
while ((len = br.read(cbuf)) != -1) {
bw.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null)
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null)
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在读写操作中,除了用char[] 数组,还可以使用 br.readLine()
方法,一行一行地读入:
@Test
public void testBufferedReaderAndWriter() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(new File("dbcp.txt")));
bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));
//读写操作
//方式一:使用char[]数组
// char[] cbuf = new char[1024];
// int len;
// while ((len = br.read(cbuf)) != -1) {
// bw.write(cbuf, 0, len);
// }
//方式二:使用String
String data;
while ((data=br.readLine())!=null){//一行一行地读入
bw.write(data);//data中不包含换行符
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null)
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null)
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制的文件是不换行的,如下图所示:
若想换行,则要么手动 + "\n"
,要么使用 bw.newLine()
方法,如下代码所示:
@Test
public void testBufferedReaderAndWriter() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(new File("dbcp.txt")));
bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));
//读写操作
//方式一:使用char[]数组
// char[] cbuf = new char[1024];
// int len;
// while ((len = br.read(cbuf)) != -1) {
// bw.write(cbuf, 0, len);
// }
//方式二:使用String
String data;
while ((data = br.readLine()) != null) {//一行一行地读入
//换行方法一:
// bw.write(data + "\n");//data中不包含换行符
//换行方法二:
bw.write(data);
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null)
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null)
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件输出效果:
15.4.3 练习题
题目1:图片的加密操作。主要就是采用与 5 的异或运算,与 5 的异或运算一次就成了加密图片,再把加密图片输入,再与 5 的异或运算一次,就能恢复回去。
① 图片的加密操作
@Test
//图片的加密
public void test() {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream("迪迦复合型.jpg"));
bos = new BufferedOutputStream(new FileOutputStream("迪迦复合型-加密.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
//对字节数据数组进行修改,加密
for (int i = 0; i < len; i++) {
buffer[i] = (byte) (buffer[i] ^ 5);//与5做异或运算
}
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出的加密图片是打不开的:
② 图片的解密操作:解密操作代码不变,把输入改成加密图片即可。
@Test
//图片的解密
public void test2() {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream("迪迦复合型-加密.jpg"));
bos = new BufferedOutputStream(new FileOutputStream("迪迦复合型2.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
//再进行一次与 5 的异或运算,解密
for (int i = 0; i < len; i++) {
buffer[i] = (byte) (buffer[i] ^ 5);//与5做异或运算
}
bos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出的图片恢复:
题目2:
获取文本上每个字符出现的次数。
提示:遍历文本的每一个字符;字符及出现的次数保存在Map 中;将 Map 中数据写入文件。
① 我的首次答案:
@Test
public void test() {
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader("dbcp.txt");
fw = new FileWriter("count.txt");
int len;
//保存统计次数的Map,key是字符编码,不可重复;value是出现的次数
HashMap<Integer, Integer> map = new HashMap<>();
while ((len = fr.read()) != -1) {
if (!map.containsKey(len)) {//不存在时,添加新字符,value置1
map.put(len, 1);
} else {//已经存在,把value值加1
map.put(len, map.get(len) + 1);
}
}
//把Map写出保存到count.txt文件中
//转换成集合再遍历
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
Iterator<Map.Entry<Integer, Integer>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator.next();
int k = entry.getKey();
char key = (char) k;
Integer value = entry.getValue();
fw.write(key + "=" + value + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
出现的问题:我没考虑空格、换行和制表符。
② 老师的优化代码:
@Test
public void test2() {
FileReader fr = null;
BufferedWriter bw = null;
try {
fr = new FileReader("dbcp.txt");
bw = new BufferedWriter(new FileWriter("count.txt"));
int len;
//保存统计次数的Map,key是字符编码,不可重复;value是出现的次数
HashMap<Character, Integer> map = new HashMap<>();
while ((len = fr.read()) != -1) {
char c = (char) len;
if (!map.containsKey(c)) {//不存在时,添加新字符,value置1
map.put(c, 1);
} else {//已经存在,把value值加1
map.put(c, map.get(c) + 1);
}
}
//把Map写出保存到count.txt文件中
//转换成集合再遍历
Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) {
switch (entry.getKey()) {
case ' ':
bw.write("空格=" + entry.getValue());
break;
case '\t':
bw.write("tab键=" + entry.getValue());
break;
case '\n':
bw.write("换行=" + entry.getValue());
break;
case '\r':
bw.write("回车=" + entry.getValue());
break;
default:
bw.write(entry.getKey() + "=" + entry.getValue());
break;
}
bw.newLine();//换行
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null)
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
15.5 转换流
转换流也是处理流的一种。其功能是提供了字节流和字符流之间的互相转换的功能。
类 | 功能 |
---|---|
InputStreamReader (字符输入流) | 将字节输入流 InputStream 转换为字符输入流 Reader |
OutPutStreamWriter (字符输出流) | 将字符输出流 Writer 转换为字节输出流 OutPutStream |
- 解码:字节、字节数组 --> 字符、字符数组
- 编码:字符、字符数组 --> 字节、字节数组
15.5.1 InputStreamReader的使用
InputStreamReader 实现字节输入流到字符输入流的转换。
例子1:把 Module ch15
下的 dbcp.txt
文件用 InputStreamReader 读入内存。
@Test
public void testInputStreamReader() {
InputStreamReader isr = null;
try {
//1.创建字节流
FileInputStream fis = new FileInputStream("dbcp.txt");
//2.创建转换流,构造器第二个形参是字符集,不写采用系统默认
//具体使用哪个字符集,取决于文件保存时使用的字符集
isr = new InputStreamReader(fis, "UTF-8");
//3.以字符流读入内存
char[] cbuf = new char[32];
int len;
while ((len = isr.read(cbuf)) != -1) {
String str = new String(cbuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
第 10 行代码:转换流的构造器第二个形参是字符集,不写采用系统默认。具体使用哪个字符集,取决于文件保存时使用的字符集。
如果把 UTF-8
改为 GBK
,
isr = new InputStreamReader(fis, "GBK");
则输出为:
字符集不匹配会出现乱码问题。
15.5.2 OutputStreamWriter的使用
OutputStreamWriter 实现了字符输出流到字节输出流的转换。
例子:综合 InputStreamReader 和OutputStreamWriter 实现字符集从 UTF-8
转换到 GBK
的操作。
@Test
public void testOutputStreamWriter() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
//1.创建字节输入输出流
FileInputStream fis = new FileInputStream("dbcp.txt");
FileOutputStream fos = new FileOutputStream("dbcp-gbk.txt");
//2.创建转换输入输出流,转换输入流的字符集是UTF-8
//转换输出流的字符集采用GBK
isr = new InputStreamReader(fis, "UTF-8");
osw = new OutputStreamWriter(fos, "GBK");
//3.转换操作
char[] cbuf = new char[32];
int len;
while ((len = isr.read(cbuf)) != -1) {
osw.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (osw != null)
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
输出的 dbcp-gbk.txt
在 IDEA 中打开是乱码,是因为 IDEA 设置的字符集是用 UTF-8 打开的。用 UTF-8 打开 GBK 当然会乱码。在 Windows 文件管理系统中打开就会正常了:
15.5.3 多种字符编码集的说明
- ASCII:美国标准信息交换码。用一个字节的7位可以表示。
- ISO8859-1:拉丁码表。欧洲码表,用一个字节的8位表示。
- GB2312:中国的中文编码表。最多两个字节编码所有字符。
- GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
- Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
- UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
下图很好地解释了困惑我多年的一个问题,就是二进制编码到底怎么区分多少个字节为一个整体,去代表一个字符呢?第一个字节,开头第一个 0
之前有多少个 1
,就是由多少个字节作为一个整体代表一个字符。
15.6 标准输入输出流(了解)
15.6.1 标准输入输出流
名称 | 作业 |
---|---|
System.in | 标准的输入流,默认从键盘输入 |
System.out | 标准的输出流,默认从控制台输出 |
15.6.2 采用方法
方法 | 作用 |
---|---|
System.setIn(InputStream is) | 重新指定输入流 |
System.setOut(PrintStream ps) | 重新指定输出流 |
例子:从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。
思路:System.in是字节流 --> 转换流转换成字符流 --> BufferedReader 的 readLine() 。
public static void main(String[] args) {
BufferedReader br = null;
try {
//1.转换流,字节流转成字符流
InputStreamReader isr = new InputStreamReader(System.in);
//2.缓冲流,把转换流放在缓冲流当中
br = new BufferedReader(isr);
//3.转换大写操作
while (true) {
System.out.print("请输入字符串:");
//读一行数据
String data = br.readLine();
//如果碰到忽略大小写的“e”或者“exit”时,退出程序
//为了避免空指针的问题,要把变量写到括号里面
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
break;
}
System.out.println(data.toUpperCase());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (br != null)
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
请输入字符串:Deep dark fantasies
DEEP DARK FANTASIES
请输入字符串:I'm so horny
I'M SO HORNY
请输入字符串:EXit
Process finished with exit code 0
15.6.3 练习
题目:创建一个名为MyInput类的程序,包含一个方法:该方法可以从键盘上读入int, double, float, boolean, short, byte和String类型的数据。
public class MyInput {
//怎么判断输入字符串中的是什么类型呢?分为不同的方法就好了
public static String readString() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = "";
try {
str = br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return str;
}
//读入int
public static int readInt() {
return Integer.parseInt(readString());
}
//读入double
public static double readDouble() {
return Double.parseDouble(readString());
}
//读入float
public static float readFloat() {
return Float.parseFloat(readString());
}
//读入boolean
public static boolean readBoolean() {
return Boolean.parseBoolean(readString());
}
//读入short
public static short readShort() {
return Short.parseShort(readString());
}
//读入byte
public static byte readByte() {
return Byte.parseByte(readString());
}
}
15.7 打印流(了解)
打印流只有输出,没有输入。
字节打印流 | 字符打印流 |
---|---|
PrintStream | PrintWriter |
其实 System.out.println()
就是 PrintStream 类中各种重载的方法。如下图所示:
例子:把控制台输出通过 System.setOut(PrintStream ps)
转化为存储到新文件。
@Test
public void test() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream("PrintStream.txt");
//1.创建打印输出流,设置为自动刷新模式(写入换行符或字节'\n'时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {//把标准输出流(控制台输出)改成文件
System.setOut(ps);
}
for (int i = 0; i <= 255; i++) {//输出ASCII字符
System.out.print((char) i);
if (i % 50 == 0) {//每50个数据一行
System.out.println();//换行
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
assert ps != null;
ps.close();
}
}
第 7 行代码:PrintStream 构造器的第二个形参是自动刷新模式。如下图所示:
输出:
15.8 数据流(了解)
数据流,是用于读取或写出基本数据类型的变量或字符串。
数据输入流 | 数据输出流 |
---|---|
DataInputStream | DataOutputStream |
数据流的方法都是针对不同的基本数据类型的变量或字符串。其中字符串的和字节数组的名称比较特殊,需要留意:
方法 | 功能 |
---|---|
String readUTF() | 读入字符串 |
void readFully(byte[] b) | 读入字节数组 |
DataOutputStream 的方法把上述的 read
改成相应的 write
即可。
例子:先把内存的基本数据类型的变量或字符串写出到硬盘中,形成文件,从而持久化数据。再从硬盘中读入到内存中。
① 写出到文件操作:
@Test
public void test() {
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new FileOutputStream("DataStream.txt"));
//写出
dos.writeUTF("Tom");
dos.flush();//手动地刷新缓冲区,强迫让数据写出到文件
dos.writeInt(23);
dos.writeBoolean(true);
dos.writeDouble(13.14);
dos.flush();//手动地刷新缓冲区,强迫让数据写出到文件
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dos != null)
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第 8 行代码:
输出:
打开后是乱码,是因为要用 DataInputStream 来读。
② 读入到内存操作:将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中。
【注意】读入操作,必须按写出的顺序来读入,否则报错。
@Test
public void testRead() {
DataInputStream dis = null;
try {
dis = new DataInputStream(new FileInputStream("DataStream.txt"));
//读入操作,必须按写出的顺序来读入,否则报错
String str = dis.readUTF();
int i = dis.readInt();
boolean b = dis.readBoolean();
double d = dis.readDouble();
System.out.println("name=" + str + "\nage=" + i + "\nisMan=" + b + "\nbalance=" + d);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null)
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
name=Tom
age=23
isMan=true
balance=13.14
15.9 对象流
同上一节的数据流相似,对象流是处理流的一种。用来传输 Java 中的对象。强大之处是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。
对象输入流 | 对象输出流 |
---|---|
ObjectInputStream | ObjectOutputStream |
15.9.1 序列化和反序列化
-
序列化:Java 中对象的序列化,指的是把对象转换为与平台无关的二进制流。该二进制流可以存储到硬盘中进行持久化,也可以通过网络进行传输。
-
反序列化:把硬盘中或网络中的二进制字节流读入到内存中,恢复成原来的 Java 对象的过程。就叫反序列化。
-
序列化的好处在于可将任何实现了
Serializable
接口的对象转化为字节数据,使其在保存和传输时可被还原。 -
序列化是 RMI (Remote Method Invoke: 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
-
如果需要让某个对象支持序列化机制,则 必须 让对象所属的类及其属性是可序列化的,为了让某个类是 可序列化的,该类必须实现如下两个接口之一。否则,会抛出 NotSerializableException 异常。
Serializable Externalizable -
注意:ObjectInputStream 和 ObjectOutputStream 不能序列化 static 和 transient 修饰的成员变量。
15.9.2 对象的序列化和反序列化代码
① 序列化过程:
@Test
public void testObjectTest() {
ObjectOutputStream oos = null;
try {
//1.序列化
oos = new ObjectOutputStream(new FileOutputStream("Object.dat"));
//2.序列化对象
oos.writeObject(new String("我爱Java"));
oos.flush();//刷新操作
} catch (IOException e) {
e.printStackTrace();
} finally {
//3.关闭流资源
try {
if (oos != null)
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第 7 行代码:.dat
格式文件不是双击打开看的,是一种 data 数据文件。
第 10 行代码:可以写出 Object
对象:
输出:
② 反序列化过程:
@Test
public void testObjectInputStreamTest() {
ObjectInputStream ois = null;
try {
//1.创建反序列化输入流
ois = new ObjectInputStream(new FileInputStream("Object.dat"));
//2.反序列化对象
Object obj = ois.readObject();
String str = (String) obj;
System.out.println(str);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
//3.关闭流资源
try {
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
我爱Java
15.9.3 自定义对象的序列化和反序列化代码
【注意】
第一,要想序列化,自定义的类必须实现 Serializable 接口或 Externalizable 接口。在开发中更常用 Serializable 接口,源代码如下所示:
public interface Serializable {
}
像 Serializable 接口这样,没有抽象方法的接口,可以称作标识接口。意思是只要你实现了该接口,程序就认为你是可以序列化的。
第二,实现完 Serializable 接口后,还必须声明一个 long 型的全局常量序列版本号 (serialVersionUID) 常量,在网络传输的时候供校验用:
private static final long serialVersionUID = 42L;//具体的值随便设
(这个操作在自定义异常类的时候也出现过)
第三,声明完序列版本号 (serialVersionUID) 后,还必须保证该类内部的所有属性也必须是可序列化的。默认情况下,基本数据类型也是可序列化的。
第四,ObjectInputStream 和 ObjectOutputStream 不能序列化 static 和 transient 修饰的成员变量。比如银行卡密码等重要的属性信息就必须声明为 transient 的,不让其序列化成二进制流传输。
① 自定义 Person 类:
public class Person implements Serializable {//要想序列化,第一,自定义的类必须实现Serializable接口
//要想序列化,第二,自定义的类必须声明序列版本号全局常量
private static final long serialVersionUID = 42545642218L;
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (age != person.age) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
② Person 类的序列化:
@Test
public void testObjectTest() {
ObjectOutputStream oos = null;
try {
//1.序列化
oos = new ObjectOutputStream(new FileOutputStream("Person.dat"));
//2.序列化对象
oos.writeObject(new Person("Tom", 23));
oos.flush();//刷新操作
} catch (IOException e) {
e.printStackTrace();
} finally {
//3.关闭流资源
try {
if (oos != null)
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
③ Person 类的反序列化:
@Test
public void testObjectInputStreamTest() {
ObjectInputStream ois = null;
try {
//1.创建反序列化输入流
ois = new ObjectInputStream(new FileInputStream("Person.dat"));
//2.反序列化对象
Object obj = ois.readObject();
Person p = (Person) obj;
System.out.println(p.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
//3.关闭流资源
try {
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
Person{name='Tom', age=23}
15.9.4 SerialVersionUID的理解
序列版本号 (SerialVersionUID) ,我的理解是给 Java 中每一个类都指定一个唯一标识的“身份证号码”。这样在Java 类被序列化成二进制字节流的时候,每个二进制字节都带着这个类的唯一”身份证号码“。当反序列化时,大量的二进制字节流就能准确地恢复回原来地类别,而不会造成混乱。
为什么要求手动地添加序列版本号 (SerialVersionUID) ?其实就算你不手动添加,Java 也会自动根据自定义类的属性和方法生成一个序列版本号 (SerialVersionUID) 。但这样的问题是,如果你已经序列化输出了一个二进制流的 .dat
文件,但是中途你修改了这个自定义的属性或方法,那么这个类的序列版本号 (SerialVersionUID) 就会跟着改变。从而导致硬盘中的 .dat
文件因为序列版本号不一致,无法再反序列化恢复成对象了。
15.10 随机存取文件流RandomAccessFile
15.10.1 RandomAccessFile基本概念
查看 Java 8 文档,可以看到 RandomAccessFile 类与前面所有 IO 流的类都不一样。
- 前面的所有流都是继承于 InputStream、OutputStream、Reader 和 Writer 四个基类。但是 RandomAccessFile 类却是直接继承 Object 类。
- 并且 RandomAccessFile 类同时实现了 DataInput 和 DataOutput 的接口,意味着这个类既可以当作输入流,也可以当作输出流。
-
RandomAccessFile 类的构造器,第一个形参是 File 类的对象,指明读取或写出的文件的路径;第二个 String 类型的形参指明打开的模式,如下表所示:
模式 描述 “r” 以只读模式打开 “rw” 以可读可写模式打开 “rws” 以可读可写模式打开,同步文件内容和元数据的更新 “rwd” 以可读可写模式打开,同步文件内容的更新
例子1:用 RandomAccessFile 类实现图片的复制。这个类既可以当作输入流,也可以当作输出流。
@Test
public void testRandomAccessFile() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
//1.读
raf1 = new RandomAccessFile(new File("迪迦复合型.jpg"), "r");
//2.写
raf2 = new RandomAccessFile(new File("迪迦复合型3.jpg"), "rw");
//3.复制
byte[] buffer = new byte[32];
int len;
while ((len = raf1.read(buffer)) != -1) {
raf2.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
assert raf1 != null;
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
assert raf2 != null;
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
例子2:对文本文件的覆盖还是追加的研究。
执行前:
@Test
public void testRandomAccessFile2() {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(new File("hello.txt"), "rw");
//可以通过.getBytes()方法来获得字符串的byte[]数组
raf.write("DeepDark".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (raf != null)
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第 8 行代码:可以通过 .getBytes()
方法来获得字符串的 byte[] 数组。
执行后:
结论:如果 RandomAccessFile 类 作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。如果写出到的文件如果已经存在,在 “rw” 模式下,RandomAccessFile 类对文本文件的写出操作是覆盖原文件的 (默认情况下,从头覆盖,能覆盖多少就多少)。
15.10.2 RandomAccessFile 实现数据插入
-
RandomAccessFile 有一个很特殊的功能:就是能通过指针跳到文件的任意地方来读写。类似于 Windows 里的光标。
- 【数据插入思路】指针跳到文件指定地方后,在指针后面写入数据是覆盖,而不是插入。要想完成插入操作,思路是先把指针后的数据用一个容器装起来 (如文本数据使用 StringBuilder 装起来) ,然后在该指针后写入数据,覆盖。最后再把容器中的数据填补到末尾,就完成了数据的插入操作。
-
RandomAccessFile 对象用来自由移动记录指针的方法:
方法 作用 long getFilePointer() 获取文件记录指针的当前位置 void seek(long pos) 将文件记录指针定位到pos位置
例子:在 hello.txt
中第一行的 “Hello” 后插入 “Deep Dark Fantasies” 。
执行前:
@Test
public void testRandomAccessFileInsert() {
RandomAccessFile raf = null;
try {
//1.创建RandomAccessFile类对象
raf = new RandomAccessFile(new File("hello.txt"), "rw");
//2.将指针定位到第1行索引为5的位置,即"Hello"后
raf.seek(5);
//3.把指针5后的数据存储到StringBuilder中
byte[] buffer = new byte[20];
int len;
//为了避免StringBuilder扩容,在构造器中指明容量为File文件的长度
StringBuilder sb = new StringBuilder((int) new File("hello.txt").length());
while ((len = raf.read(buffer)) != -1) {
//StringBuilder的append方法形参没有byte[]数组,要把byte[]数组转换成String
sb.append(new String(buffer, 0, len));
}
//4.插入新数据
raf.seek(5);//把指针放回"Hello"后
//raf.write()方法形参只能传入byte[]数组, 要把String转换成byte[]数组
raf.write("Deep Dark Fantasies".getBytes());
//5.末尾加上原指针后数据
//raf.write()方法形参只能传入byte[]数组,因此要把StringBuilder转换成String,再转换成byte[]数组
// raf.write(new String(sb).getBytes());//下面两种变String的方法都可以
raf.write(sb.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//6.关闭流资源
try {
if (raf != null)
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行后:
这种方法有个缺点,就是如果文件很大,在文件比较靠前的地方插入的话,需要把后面大量数据装到容器中缓存起来,这种效率是比较差的。一般开发中倾向于直接在文件末尾追加内容。
15.10.3 ByteArraayOutputStream的使用
将上一节中的 StringBuilder 替换为 ByteArrayOutputStream。
访问数组流 | 字节流 | 字符流 |
---|---|---|
输入流 | ByteArrayInputStream | CharArrayReader |
输出流 | ByteArrayOutputStream | CharArrayWriter |
① 方式一:直接使用 String 的拼接操作:
private String readStringFromInputStream(FileInputStream fis) throws IOException {
//方式一:可能出现乱码
String content = "";
byte[] buffer = new byte[128];
int len;
while ((len = fis.read(buffer)) != -1) {
content += new String(buffer);
}
return content;
}
测试:
@Test
public void test() {
FileInputStream fis = null;
try {
fis = new FileInputStream("hello.txt");
String info = readStringFromInputStream(fis);
System.out.println(info);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
如图所示,输出有异常,估计是 buffer 设得太长导致的。
② 使用字符缓冲流 BufferedReader
private String readStringFromInputStream(FileInputStream fis) throws IOException {
//方式二:BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
char[] cbuf = new char[16];
int len;
String str = "";
while ((len = br.read(cbuf)) != -1) {
str += new String(cbuf, 0, len);
}
return str;
}
测试代码同上。
输出:
输出正常。
③ 使用 ByteArrayOutputStream
private String readStringFromInputStream(FileInputStream fis) throws IOException {
//方法三:使用 ByteArrayOutputStream,避免出现乱码
//创建ByteArrayOutputStream类对象,是字节输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[16];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toString();
}
测试代码同上。
输出:
输出正常。
15.11 NIO.2中Path、Paths、Files类的使用
15.11.1 NIO概述
-
NIO (New IO, Non-Blocking IO) 是 JDK 1.4 发布的新的可替代标准的 java.io 的API 。NIO 与原来的 IO 的目的和作用相同,但是使用方式完全不同。
-
NIO 支持面向缓冲区 ( IO 是面向流的) 、基于通道的 IO 操作。NIO 将以更高效的方式进行文件的读写操作。
-
Java 中提供了两套 NIO ,一套是针对标准输入输出的 NIO ,另一套是网络编程NIO。
java.nio.channels.Channel:
API 作用 FileChannel 处理本地文件 SocketChannel TCP网络编程的客户端的 Channel ServerSocketChannel TCPTCP网络编程的服务器端的 Channel DatagramChannel UDP网络编程中发送端和接收端的 Channel
15.11.2 NIO.2
NIO 刚出来的时候写得不太方便使用,于是 JDK 7 发布了新的 NIO ,进行了极大的扩展,增强了对文件处理和文件系统特性的支持。
15.11.3 Path、Paths和Files
-
早期 的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。
-
NIO.2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。 Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。
-
在以前 IO 操作都是这样写的:
import java.io.File; File file = new File("index.html");
-
但在 Java7 及以后,我们可以这样写:
import java.nio.file.Path; import java.nio.file.Paths; Path path = Paths.get("index.html")
-
同时, NIO.2 在 java.nio.file 包下还 提供了 Files 、Paths 工具类,Files 包含了大量静态的工具方法来操作文件;Paths 则包含了两个返回 Path 的静态工厂方法:Paths 类提供的静态 get() 方法用来获取 Path 对象:
方法 作用 static Path get(String first, String … more) 用于将多个字符串串联成路径 static Path get(URI uri) 返回指定 uri 对应的Path路径