十五、 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() {//构造器1File 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);//构造器2File file3 = new File("F:\\OneDrive - stu.ouc.edu.cn\\MarkDown\\2-后端开发\\IdeaProject", "JavaSE");System.out.println(file3);//构造器3File 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());//获取上层文件目录路径。若无,返回nullSystem.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码值;如果达到文件末尾,则返回-1int 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

例子2read() 方法的升级操作,使用 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数组中的字符的个数;如果到达文件末尾,则返回-1char[] 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.提供流的实例化对象,FileInputStreamfis = 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);
//        }//方式二:使用StringString 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);
//      }//方式二:使用StringString 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置1map.put(len, 1);} else {//已经存在,把value值加1map.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置1map.put(c, 1);} else {//已经存在,把value值加1map.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//转换输出流的字符集采用GBKisr = 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
请输入字符串:EXitProcess 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;}//读入intpublic static int readInt() {return Integer.parseInt(readString());}//读入doublepublic static double readDouble() {return Double.parseDouble(readString());}//读入floatpublic static float readFloat() {return Float.parseFloat(readString());}//读入booleanpublic static boolean readBoolean() {return Boolean.parseBoolean(readString());}//读入shortpublic static short readShort() {return Short.parseShort(readString());}//读入bytepublic 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;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic 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;}@Overridepublic 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[]数组转换成Stringsb.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;
}

测试:

@Testpublic 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 {//方式二:BufferedReaderBufferedReader 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路径

15.11.4 常用方法

【JavaSE】15- IO流相关推荐

  1. 08 - JavaSE之IO流

    IO流 JAVA流式输入输出原理:可以想象成一根管道怼到文件上,另一端是我们程序,然后流的输入输出都是按照程序本身作为第一人称说明的.比如 input,对于我们程序来说就是有数据输入我们程序,outp ...

  2. JavaSE(十七)——IO流之字节流

    文章目录 1. 概述 2. FileOutputStream写出数据 3. FileInputStream读取数据 4. 字节流复制文本文件 5. 字节流复制MP3 6. 高效的字节输入输出流 1. ...

  3. 进阶15 IO流+字节字符输入输出+IO异常处理+属性集+缓冲流+各种编码+序列化

    IO概述 什么是IO 生活中,你肯定经历过这样的场景.当你编辑一个文本文件,忘记了ctrl+s ,可能文件就白白编辑了.当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里.那么数据都是在哪些 ...

  4. JAVASE——2.IO流

    JAVASE-IO 文章目录 JAVASE-IO 2.1 IO流概述和分类. Java定义了两个超类(抽象类): java将流分为两类:节点流与处理流: 实际应用中,我们可以通过串联一组高级流到某个低 ...

  5. 【JavaSE】IO流(下)

    文章目录 1. 缓冲流 1.1 缓冲流概述 1.2 字节缓冲流的使用 1.3 字节缓冲流的性能分析 1.4 字符缓冲流 1.5 出师表排序 2. 转换流 2.1 问题引出 2.2 字符输入转换流 2. ...

  6. JAVASE基础模块三十四( 菜鸡版简单登录验证模块系统IO流文件写入)

    JAVASE基础模块三十四( 菜鸡版简单登录验证模块系统IO流文件写入) 开发流程:需求文档 接口文档 效果图 开发环境统一:JDK1.8 IDEA win10 GIT SSM SSH SpringB ...

  7. JavaSE IO流 Vol.2 节点流 - 输入流 入门

    JavaSE IO流 Vol.2 节点流 - 输入流 入门 1. 前言 2. 节点流 - 输入流 结构体系 3. 节点流 - 输入流 ● InputStream · FileInputStream ● ...

  8. JavaSE进阶26 - IO流概述、字节流、字符流、转换流、缓冲流

    目录 IO概述 FileInputStream的使用 FileOutputStream使用 文件复制 文件的复制原理 代码 FileReader FileWriter 复制普通文本文件 Buffere ...

  9. JavaSE(字符流、IO资源的处理、属性集、ResourceBundle工具类、缓冲流、转换流、序列化、打印流、装饰设计模式、commons-io工具包)

    JavaSE 字符流 字符输入流[Reader] 字符输入流Reader类的概述 字符输入流Reader类的常用方法 FileReader类 FileReader类的概述 FileReader类的构造 ...

  10. JavaSE从头再来(七)——增强IO流

    JavaSE从头再来(七)--增强IO流 从头再来第七弹,主要涉及到缓冲流.转换流.序列化流和打印流 一.缓冲流 能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等.这些功能 ...

最新文章

  1. LeetCode简单题之重新排列数组
  2. 完全相同的4个小矩形如图所示放置_3个数学难题——走进传奇数学天才的数字游戏...
  3. 回归模型和时间序列模型中的MAPE指标是什么?MAPE指标解读、MAPE越大越好还是越小越好、使用MAPE指标的注意事项
  4. html/jquery 常用的输入与获取参数
  5. nyoj99(欧拉路)
  6. Zabbix 3.2.6 升级到 Zabbix 3.4.3
  7. 软件开发---全套规范
  8. Microsoft AI - Custom Vision in C#
  9. C语言中的三目运算符是啥?有何用处?
  10. Unity3D_(API)Quaternion四元数中的Quaternion.LookRotation()
  11. 行为型设计模式(4)—— 观察者模式(Observer Pattern)
  12. 【设计鉴赏】精选字体设计鉴赏(三)
  13. 畅捷支付显示连接服务器失败,畅捷支付POS机常见错误码解决方法!
  14. vue实现非同源图片的下载功能--跨域问题(解决浏览器打开图片,而不是下载)
  15. [Java] 身份证号码验证
  16. About Contest and Trainning
  17. 实战 .Net 数据访问层 - 19
  18. Python文本文件的输入输出
  19. 腾讯云Linux服务器 centos7 Lampp环境搭建 vsftp搭建 ssl证书安装 所遇到的问题
  20. C# Label 通过Panel中的ScrollBar实现滑动条

热门文章

  1. 今天我给大家分享几款珍藏已久的指标公式,都是简单好用的指标
  2. PageSpeed Insights网页性能测试工具
  3. [机缘参悟-63]:《兵者,诡道也》-4-三十六计解读-攻战计
  4. 微信小程序与公众号的区别与联系
  5. springboot 多模块项目构建【创建√ + 启动√ 】
  6. 归因分析笔记4:PCA逆变换
  7. sweetalert swal 简单使用示例详解
  8. 微信小说电子书阅读系统设计与实现
  9. 材料科学基础学习指导-吕宇鹏-名词和术语解释-第二章晶体缺陷
  10. 51单片机STC89C52控制LED闪烁(将延时程序写成子函数有形参)