【JavaSE】15- IO流

十五、 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 个文件:

image-20220421092056529

@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 文件:

image-20220421092436301

image-20220421092532687

随便输入:

image-20220421092606543

@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 文件删除后,再次运行。

image-20220421093204498

输出:

true

修改成功,结果返回 true 。且原来在 ch15 模块下创建 hi.txt 文件移动到 file2 所指定的路径,文件名称由 hi.txt 修改为 file2 指定的 hello.txt

image-20220421093512812

打开 hello.txt 文件,里面的内容和原来在 ch15 模块下创建 hi.txt 文件的内容是一致的:

image-20220421093754576

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()创建文件目录。若上层文件目录不存在,就一并创建了

注意事项:如果你创建文件或者文件目录没写盘符路径,则默认创建在项目路径下。

① 创建文件:

执行前:

image-20220425205216593

@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

执行后:

image-20220425205303238

② 创建文件目录:

执行前:

image-20220425205803155

@Test
public void test6() throws IOException {
    //创建不存在的文件目录
    File file2 = new File("G:\\io\\documents");//不存在的,但上层存在
    boolean isMkdir = file2.mkdir();
    if (isMkdir){
        System.out.println("成功创建文件目录");
    }
}

输出:

成功创建文件目录

执行后:

image-20220425205843614

③ 创建上层文件目录不存在的文件目录:

执行前:

image-20220425210214498

@Test
public void test6() throws IOException {
    //创建上层文件目录不存在的文件目录
    File file3 = new File("G:\\io\\pic\\photos");//不存在的,且上层不存在
    boolean isMkdirs = file3.mkdirs();
    if (isMkdirs) {
        System.out.println("成功创建文件目录");
    }
}

输出:

成功创建文件目录

执行后:

image-20220425210309213

连同上级文件目录 pic 都一同创建出来了。

5.File类的删除功能

方法作用
boolean delete()删除文件或者文件夹

注意事项:Java 的删除不走回收站。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

例子:

删除前:

image-20220425210309213

比方说我想删除文件目录 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("删除失败");
    }
}

输出:

删除失败

要想删除成功,要删除的文件目录内不能包含文件或文件目录。即只能删除文件和空的文件目录。

【内存解析】

image-20220426090509828

15.1.3 File类练习

题目1:

判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称。

image-20220425212823607

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 :删除指定文件目录及其下的所有文件。

image-20220425215453656

我的首次答案:

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

image-20220426102321267

@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("失败");
    }
}

输出:

成功

image-20220426102430101

15.2 IO流原理及流的分类

15.2.1 IO流原理

由于输入输出具有相对性,因此,当我们在 Java 中讲 IO 流时,是从程序 (即内存) 的角度出发来判定输入输出。

  • 从硬盘传输到内存中,称为“输入流”;
  • 从内存传输到硬盘中,称为“输出流”。

image-20220426110246297

15.2.2 IO流分类

  • 按操作数据单位的不同分为:字节流 (8 bit)、字符流 (16 bit);
  • 按数据流的流向不同分为:输入流、输出流;
  • 按流的角色的不同分为:节点流、处理流。
    • 节点流:指的是承担数据和程序 (硬盘和内存) 之间数据传输的流。
    • 处理流:是包裹在节点流之上,用于加速、控制节点流的一类流。

image-20220426110627112

15.2.3 IO流的体系结构

抽象基类字节流字符流
输入流InputStreamReader
输出流OutputStreamWrite
  • Java 的 IO 流共涉及 40 多个类,实际上非常简单,都是从如上 4 个抽象基类派生的。
  • 由上面这 4 个类派生出来的子类名称都是以其父类名作为子类名后缀。

image-20220426111929421

  • 各自列的第一行 抽象基类 是列中其他行的父类。
  • 标浅蓝色底的是需要熟练掌握的。
  • 其中,第三行 访问文件 是可以直接访问硬盘的节点流;其他都是处理流。

15.3 节点流(或文件流)

节点流字节流字符流
输入流FileInputStreamFileReader
输出流FileInputStreamFileWriter

15.3.1 节点字符输入流

例子1

在 Module ch15 下创建了一个文件:hello.txt ,其内容为 “HelloWorld!”

image-20220426131825105

@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

例子2read() 方法的升级操作,使用 read() 重载的方法 read(char[] cbuf)

image-20220426135822888

  • 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) 。如下图所示:

image-20220426145754128

执行前:

image-20220426150251861

@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();
            }
        }
    }
}

执行后:

image-20220426150336711

15.3.3 使用节点输入输出流实现复制文件操作

通过把 FileReader 和 FileWriter 结合起来,把 hello.txt 读进来,再写出去成 hello1.txt 文件,实现复制操作。

执行前:

image-20220426153959569

@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();
        }

    }
}

执行后:

image-20220426154112217

【注意事项】

  • 字符流只能处理文本文件,处理图片、视频文件还是得用字节流。否则图片和视频会格式错误打不开。

15.3.4 节点流字节输入输出流

【结论】

  • 对于文本文件 (.txt .java .cpp) ,使用字符流处理;
  • 对于非文本文件 (.doc .ppt .jpg .mp4 .mp3 .avi) ,使用字节流处理。

例子1:通过结合字节输入输出流,实现对图片的复制操作

执行前:

image-20220426165741725
@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();
        }
    }
}

执行后:
image-20220426165856408

例子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 左右的视频,计算执行时间

image-20220426183829958

@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)512102420485120102401024002048005120001048576文件大小
所需时间 (ms)12434679240232309175512201102125112875840

从表格上看的话,以后 buffer 还是设定为 204800 会比较快。

15.4 缓冲流

缓冲流属于处理流的一种,用来包装节点流,提高文件读写效率。

能提高读写速度的原因是:内部提供了一个 8192 Byte 的缓冲区。有一个刷新缓冲区的方法:Writer.flush() ,无论缓冲区是否装满,都会手动把缓冲区中的数据弹出到硬盘中。

缓冲流字节流字符流
输入流BufferedInputStreamBufferedReader
输出流BufferedInputStreamBufferedWriter

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)512102420485120102401024002048005120001048576文件大小
字节流所需时间 (ms)12434679240232309175512201102125112875840
缓冲流所需时间 (ms)1545221017702205215814911742133116265638

从上面表格可以看出,缓冲流并不总是起加速作用的。只有在 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();
        }
    }
}

复制的文件是不换行的,如下图所示:

image-20220426202346762

若想换行,则要么手动 + "\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();
        }
    }
}

文件输出效果:

image-20220426203027155

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();
        }
    }
}

输出的加密图片是打不开的:

image-20220426205757802

② 图片的解密操作:解密操作代码不变,把输入改成加密图片即可。

@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();
        }
    }
}

输出的图片恢复:

image-20220426210025354

题目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();
        }
    }
}

输出:

image-20220426214046661

出现的问题:我没考虑空格、换行和制表符。

② 老师的优化代码:

@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();
        }
    }
}

输出:

image-20220426215559907

15.5 转换流

转换流也是处理流的一种。其功能是提供了字节流和字符流之间的互相转换的功能。

image-20220427084556185

功能
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();
        }
    }
}

输出:

image-20220427211315863

第 10 行代码:转换流的构造器第二个形参是字符集,不写采用系统默认。具体使用哪个字符集,取决于文件保存时使用的字符集。

image-20220427205754310

如果把 UTF-8 改为 GBK

isr = new InputStreamReader(fis, "GBK");

则输出为:

image-20220427211442513

字符集不匹配会出现乱码问题。

15.5.2 OutputStreamWriter的使用

OutputStreamWriter 实现了字符输出流到字节输出流的转换。

例子:综合 InputStreamReader 和OutputStreamWriter 实现字符集从 UTF-8 转换到 GBK 的操作。

image-20220427084556185

@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();
        }
    }
}

输出:

image-20220427214848294

输出的 dbcp-gbk.txt 在 IDEA 中打开是乱码,是因为 IDEA 设置的字符集是用 UTF-8 打开的。用 UTF-8 打开 GBK 当然会乱码。在 Windows 文件管理系统中打开就会正常了:

image-20220427215044396

15.5.3 多种字符编码集的说明

  • ASCII:美国标准信息交换码。用一个字节的7位可以表示。
  • ISO8859-1:拉丁码表。欧洲码表,用一个字节的8位表示。
  • GB2312:中国的中文编码表。最多两个字节编码所有字符。
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
  • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

下图很好地解释了困惑我多年的一个问题,就是二进制编码到底怎么区分多少个字节为一个整体,去代表一个字符呢?第一个字节,开头第一个 0 之前有多少个 1 ,就是由多少个字节作为一个整体代表一个字符。

image-20220428154432796

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 打印流(了解)

打印流只有输出,没有输入。

字节打印流字符打印流
PrintStreamPrintWriter

其实 System.out.println() 就是 PrintStream 类中各种重载的方法。如下图所示:

image-20220428173023208

例子:把控制台输出通过 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 构造器的第二个形参是自动刷新模式。如下图所示:

image-20220428173615907

输出:

image-20220428175214770

15.8 数据流(了解)

数据流,是用于读取或写出基本数据类型的变量或字符串。

数据输入流数据输出流
DataInputStreamDataOutputStream

数据流的方法都是针对不同的基本数据类型的变量或字符串。其中字符串的和字节数组的名称比较特殊,需要留意:

方法功能
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 行代码:

image-20220428180335525

输出:

image-20220428181315356

打开后是乱码,是因为要用 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 中的对象写入到数据源中,也能把对象从数据源中还原回来。

对象输入流对象输出流
ObjectInputStreamObjectOutputStream

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 对象:

image-20220428210436490

输出:

image-20220428211120318

② 反序列化过程:

@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();
        }
    }
}

输出:

image-20220428213244032

③ 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 的接口,意味着这个类既可以当作输入流,也可以当作输出流。

image-20220429210743287

  • 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:对文本文件的覆盖还是追加的研究。

执行前:

image-20220429220832776

@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[] 数组。

执行后:

image-20220429221430217

结论:如果 RandomAccessFile 类 作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。如果写出到的文件如果已经存在,在 “rw” 模式下,RandomAccessFile 类对文本文件的写出操作是覆盖原文件的 (默认情况下,从头覆盖,能覆盖多少就多少)。

15.10.2 RandomAccessFile 实现数据插入

  • RandomAccessFile 有一个很特殊的功能:就是能通过指针跳到文件的任意地方来读写。类似于 Windows 里的光标。

    • 【数据插入思路】指针跳到文件指定地方后,在指针后面写入数据是覆盖,而不是插入。要想完成插入操作,思路是先把指针后的数据用一个容器装起来 (如文本数据使用 StringBuilder 装起来) ,然后在该指针后写入数据,覆盖。最后再把容器中的数据填补到末尾,就完成了数据的插入操作。
  • RandomAccessFile 对象用来自由移动记录指针的方法:

    方法作用
    long getFilePointer()获取文件记录指针的当前位置
    void seek(long pos)将文件记录指针定位到pos位置

例子:在 hello.txt 中第一行的 “Hello” 后插入 “Deep Dark Fantasies” 。

执行前:

image-20220430091731225

@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();
        }
    }
}

执行后:

image-20220430095124017

这种方法有个缺点,就是如果文件很大,在文件比较靠前的地方插入的话,需要把后面大量数据装到容器中缓存起来,这种效率是比较差的。一般开发中倾向于直接在文件末尾追加内容。

15.10.3 ByteArraayOutputStream的使用

将上一节中的 StringBuilder 替换为 ByteArrayOutputStream。

访问数组流字节流字符流
输入流ByteArrayInputStreamCharArrayReader
输出流ByteArrayOutputStreamCharArrayWriter

① 方式一:直接使用 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();
            }
        }
    }

输出:

image-20220430181751217

如图所示,输出有异常,估计是 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;
}

测试代码同上。

输出:

image-20220501105135241

输出正常。

③ 使用 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();
}

测试代码同上。

输出:

image-20220501105714351

输出正常。

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处理本地文件
    SocketChannelTCP网络编程的客户端的 Channel
    ServerSocketChannelTCPTCP网络编程的服务器端的 Channel
    DatagramChannelUDP网络编程中发送端和接收端的 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 常用方法

image-20220501123717012

image-20220501123751475

image-20220501123824426

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡皮巴拉不躺平

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值