跳到正文
返回

Java IO 流基础

发表于 更新于
浏览量: --

本文参考:尚硅谷 宋红康 Java 零基础教程 P577-P619

1. File 类的使用

1.1 路径分隔符

路径中的每级目录之间用一个 路径分隔符 隔开。

路径分隔符与系统有关:

Java 程序支持跨平台运行,因此路径分隔符要 慎用!

为了解决这个隐患,File 类提供了一个常量:

public static final String separator

使用这个常量可以根据操作系统,动态的提供分隔符。比如:

File file1 = new File("D:\\mofan.txt");
File file2 = new File("D:" + File.separator + "mofan.txt");
File file3 = new File("d:/mofan");

这里出现了路径的一种写法:绝对路径,既然有绝对路径,那么也有相对路径。

相对路径的注意要点

IDEA 中:

如果开发使用 JUnit 中的单元测试方法进行测试,相对路径即为当前 Module 下。

如果使用 main() 进行测试,相对路径即为当前 Project 下。

public static void main(String[] args) {
    // 相当于当前工程
    File file = new File("hello.txt");
    // D:\Code\IdeaCode\JavaSE\hello.txt
    System.out.println(file.getAbsolutePath());

    File file1 = new File("IO-Stream\\hello.txt");
    // D:\Code\IdeaCode\JavaSE\IO-Stream\hello.txt
    System.out.println(file1.getAbsolutePath());
}

Eclipse 中:

不管使用单元测试方法还是使用 main() 方法,相对路径都是当前 Project 下。

1.2 常用构造器

第一种

 public File(String pathname)

以 pathname 为路径创建 File 对象,可以是 绝对路径或相对路径 ,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir 中存储。

第二种

public File(String parent, String child)

以 parent 为父路径,child 为子路径创建 File 对象

第三种

public File(File parent, String child) 

根据一个父 File 对象和子文件路径创建 File 对象

1.3 常用方法

File 类的获取功能

public String getAbsolutePath():获取绝对路径

public String getPath():获取路径

public String getName():获取名称

public String getParent():获取上层文件目录路径。若无,返回 null

public long length():获取文件长度 (即:字节数),但不能获取目录的长度。

public long lastModified():获取最后一次的修改时间,毫秒值

测试代码:

@Test
public void test2() {
    File file1 = new File("hello.txt");
    File file2 = new File("D:\\io\\hi.txt");

    System.out.println(file1.getAbsoluteFile());
    System.out.println(file1.getPath());
    System.out.println(file1.getName());
    System.out.println(file1.getParent());
    System.out.println(file1.length());
    // 返回毫秒级别的时间戳
    System.out.println(new Date(file1.lastModified()));

    System.out.println();

    System.out.println(file2.getAbsoluteFile());
    System.out.println(file2.getPath());
    System.out.println(file2.getName());
    System.out.println(file2.getParent());
    System.out.println(file2.length());
    System.out.println(file2.lastModified());
}

如下两个方法适用于文件目录:

public String[] list():获取指定目录下的所有文件或者文件目录的名称数组

public File[] listFiles():获取指定目录下的所有文件或者文件目录的 File 数组

测试代码:

@Test
public void test3() {
    File file = new File("D:\\io");
    String[] list = file.list();
    for (String s : list) {
        // 打印出文件名
        System.out.println(s);
    }

    File[] files = file.listFiles();
    for (File f : files) {
        // 打印出绝对路径
        System.out.println(f);
    }
}

File 类的重命名功能

public boolean renameTo(File dest):把文件重命名为指定的文件路径。

针对 file1.renameTo(file2) 来说,需要 file1 存在,但是 file2 不存在,将 file1 移动到 file2 并改名为 file2 (相当于剪切与重命名)。

测试代码:

@Test
public void test4() {
    // hello.txt 存在
    File file1 = new File("hello.txt");
    // hi.txt 并不存在
    File file2 = new File("D:\\io\\hi.txt");

    boolean renameTo = file1.renameTo(file2);
    // 要想保证返回 true,file1 要存在,file2 要不存在
    System.out.println(renameTo);
}

File 类的判断功能

public boolean isDirectory():判断是否是文件目录

public boolean isFile():判断是否是文件

public boolean exists():判断是否存在

public boolean canRead():判断是否可读

public boolean canWrite():判断是否可写

public boolean isHidden():判断是否隐藏

测试代码:

@Test
public void test5() {
    File file1 = new File("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());

    System.out.println("======================");

    File file2 = new File("D:\\io");
    System.out.println(file2.isDirectory());
    System.out.println(file2.isFile());
    System.out.println(file2.exists());
    System.out.println(file2.canRead());
    System.out.println(file2.canWrite());
    System.out.println(file2.isHidden());
}

FIle 类的创建功能

public boolean createNewFile():创建文件。若文件存在,则不创建并返回 false。

public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建。如果此文件目录的上层目录不存在,也不创建。

public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建。

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

测试代码:

// 文件目录的创建
@Test
public void test7() {
    // io1 并不存在
    File file1 = new File("D:\\io\\io1\\io3");
    boolean mkdir1 = file1.mkdir();
    if (mkdir1) {
        System.out.println("创建成功1"); // 不会打印
    }

    // io1 并不存在
    File file2 = new File("D:\\io\\io1\\io4");
    boolean mkdir2 = file1.mkdirs();
    if (mkdir2) {
        System.out.println("创建成功2"); // 最终打印
    }
}

当硬盘中真有一个真实的文件或目录存在,创建 File 对象的时候,各个属性会被显式赋值。

当硬盘中 没有 真实的文件或目录存在时,那么创建 File 对象时,除了指定的目录和路径之外,其他的属性都是取 成员变量的默认值 。

File 类的删除功能

public boolean delete():删除文件或者文件夹

删除注意事项:

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

1.4 总结

使用 File 类时,我们需要明白:

1、File 类的一个对象,代表一个文件或一个文件目录

2、File 类声明在 java.io 包下

3、File 类中设计到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,但是并未设计到写入或读取文件内容的操作。如果需要读取或写入文件内容,就必须使用 IO 流来完成。

4、后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的「终点」。

2. IO 流概述

2.1 Java IO 原理

I/O 是 Input / Output 的缩写,I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。😮

Java 程序中,对于数据的输入 / 输出操作以「流(stream)」的方式进行。

java.io 包下提供了各种「流」类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中

输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

输入与输出针对不同的对象,会得到不同的结果。比如站在文件的角度来看,将文件中的数据写入到内存中,那这就是输出,但从内存的角度来看,这就是输入。因为我们是编写代码的人,因此我们需要 站在内存(程序)的角度 来看待 IO。

2.2 流的分类

按操作数据单位不同可分为:字节流(8 bit)、字符流(16 bit)

按数据流的流向不同分为:输入流、输出流

按流的角色不同分为:节点流、处理流

抽象基类字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

Java 的 IO 流共涉及到 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。


当我们操作文本数据时,里面的数据都是一个一个的字符,所以用 字符流;当我们操作视频、音频或图片时,就需要用 字节流。

针对数据流的流向,我们是一直站在程序(内存)的角度出发的,写入内存叫 输入流,从内存中输出到外部存储就叫 输出流。

如果流直接作用在文件上,那么这叫 节点流;如果当前流作用在已有的流的基础之上,那么当前流叫做 处理流(可以理解成:水流等价于节点流,管道等价于处理流)。😵

关于节点流和处理流还可以这样理解:自己就可以操作的叫 节点流,依赖于别的流操作的叫 处理流。😰

IO 流体系图

Java IO 流体系图参考:Java IO 流体系图

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管理PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOuputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
特殊流DataInputStreamDataOutputStream

重点放在:抽象基类、访问文件、缓冲流、转换流和对象流上。

3. 字符流

3.1 数据读入

使用 FileReader 读取文件中的数据,测试代码如下:

/*
* 将 IO-Stream 下的 hello.txt 文件内容输出到控制台
* 1. 异常的处理:为了保证资源一定可以执行关闭操作,不建议使用 throws ,采用 try-catch 更好
* 2. 读入的文件一定要存在,否则会抛出 FileNotFoundException 异常
* */
@Test
public void testFileReader() {
    FileReader fileReader = null;
    try {
        // 1. 实例化 File,指明要操作的文件
        File file = new File("hello.txt"); // 当前 Module 下存在 hello.txt 文件
        // 2. 提供具体的流
        fileReader = new FileReader(file);
        // 3. 数据的流入
        // read(): 返回读入的一个字符(ASCII码)。如果达到文件末位,返回 -1
        /*int data = fileReader.read();
        while (data != -1) {
            System.out.print(((char) data));
            data = fileReader.read();
        }*/

        // 优化一下
        int data;
        while ((data = fileReader.read()) != -1) {
            System.out.print(((char) data));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileReader != null) {
                // 4. 关闭流
                fileReader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,FileReader 的构造方法、read() 和 close() 都会抛出异常,为了保证资源一定可以执行关闭操作,不建议使用 throws,采用 try-catch 更好。

被读取文件一定要存在,否则会抛出 FileNotFoundException 异常。

在上述代码中,读取文件中的内容使用了 read() 方法,这个方法可以 读取单个字符 ,如果纯文本文件中有内容的话,就会返回 ASCII 码表中对应的数字值,如果 ASCII 表中不存在,就会去查找 Unicode 表。将字符读取完毕后,会将读取目标指向下一个字符,因此常与循环一起使用。

如果纯文本文件没有内容,或者前面的内容已经被读取完了,read() 返回的就是 -1,由于我们不知道文本内容有多少,所以我们需要使用一个循环。

read() 方法只能读取一个字符,如果读取的文本很长,那效率也太低了。😳

因此,我们想可不可以一次性多读取一点,然后放到数组或字符串中,那样多奈斯!😏

JDK 提供了一个 read() 的重载方法,可以一次性多读取一点,并将其放到 char[] 中。测试代码如下:

// 对 read() 的操作升级,使用 read() 的重载方法
@Test
public void testFileReader1(){
    FileReader fileReader = null;
    try {
        // 1. File 类的实例化
        File file = new File("hello.txt");
        // 2. FileReader流的实例化
        fileReader = new FileReader(file);
        // 3. 读入的操作
        // read(char[] cBuf):返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1
        char[] cBuf = new char[5];
        int len;
        while ((len = fileReader.read(cBuf)) != -1) {
            // 方式一
            // 错误的写法
//                for (int i = 0; i < cBuf.length; i++) {
//                    System.out.print(cBuf[i]);
//                }
            // 正确的写法
//                for (int i = 0; i < len; i++) {
//                    System.out.print(cBuf[i]);
//                }

            // 方式二
            // 错误的写法,对应方式一错误的写法
//                String str = new String(cBuf);
//                System.out.print(str);
            // 正确的写法
            String str = new String(cBuf, 0, len);
            System.out.print(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4. 资源关闭
        try {
            if (fileReader != null) {
                fileReader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

read(char[] cBuf):返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1。

使用 read(char[] cBuf) 方法时,将读取字符数组。假设字符数组的长度是 2,那么一口气读的是 2 个字符,但由于文本内容只有 3 个字符,那么第二次读取的时候,可读取的有效长度就是 1,那么存放在字符数组的第 1 位就会被覆盖掉,但是第 2 位没有被覆盖,就会保留。

如果我们直接读取字符串,而不指定有效长度的话,会按字符数组的长度读取,并将没覆盖的字符读取出来,而不是只读有效长度。

所以我们在创建一个 String 对象的时候,要如此使用。

String str = new String(a, 0, len);

a 代表的是字符数组,0 代表起始位置,len 代表读取的长度。

参考链接:FileReader用法和问答

3.2 数据写出

有了读入,当然还有写出,字符流写出将用到 FileWriter,测试代码如下:

// 从内存中写出数据到硬盘文件中
@Test
public void testFileWriter() {
    FileWriter fileWriter = null;
    try {
        // 1. 提供 File 类的对象,指明写出到的文件
        File file = new File("hello1.txt");
        // 2. 提供 FileWriter 的对象,用于数据的写出
        fileWriter = new FileWriter(file);
        // 3. 写出的操作
        fileWriter.write("I have a dream!\n");
        fileWriter.write("You need to have a dream!\n");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileWriter != null) {
                // 4. 流资源的关闭
                fileWriter.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用字符流写出数据时,有几点需要进行说明:

1、使用字符流进行写出操作时,内容写入的目标文件可以不存在,且不会报异常。

2、File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建文件。

3、File 对应的硬盘中的文件如果存在:

3.3 读入与写出

前面列举了数据读入和写出,但是读入是将文本文件内容输出到控制台上,写入是使用代码的方式直接写入到文本文件中,那能否将两者结合一下?

先读入 A 文件的内容,然后将该内容写入到 B 文件中呢?(这就是传说中的文本复制!😂)

其实也很简单,直接上测试代码:

@Test
public void testFileReaderFileWriter() {
    FileReader fileReader = null;
    FileWriter fileWriter = null;
    try {
        // 1. 创建 File 类的对象,指明读入和写出的文件
        File srcFile = new File("hello.txt"); // 该文件存在当前 Module 中
        File destFile = new File("hello2.txt"); // 该文件并不存在当前 Module 中
        // 2. 创建输出流和输入流的对象
        fileReader = new FileReader(srcFile);
        fileWriter = new FileWriter(destFile);
        // 3. 数据的读入和写出操作
        char[] cBuf = new char[5];
        int len; // 记录每次写入到 cBuf 数组中的字符的个数
        while ((len = fileReader.read(cBuf)) != -1) {
            // 每次写出 len 个字符
            fileWriter.write(cBuf, 0 ,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4. 关闭流资源
        try {
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.4 拓展

我们前面编写的代码都是处理文本文件的,那么那些代码可以处理图片吗?

是否只需要将 File 构造器内的文件名修改一下就可以了呢?比如:

// 1. 创建 File 类的对象,指明读入和写出的文件
File srcFile = new File("pic.jpg");
File desFile = new File("pic2.jpg");

其实测试一下就知道,压根是不行的,虽然 pic2.jpg 会出现在我们的 Module 下,但并不能打开该文件。

原因也很简单,FileReader 和 FileWriter 都是处理字符流的,或者说都是处理文本文件的,而图片并不是文本文件,属于二进制文件,要想处理图片,就需要用到字节流!👇

4. 字节流

4.1 字节输入流初体验

上文说到,对于文本文件(.txt, .java, .c, .cpp 等)使用字符流处理,对于非文本文件(.jpg, .mp3, .mp4, .pdf 等)使用字节流处理。我们也验证了使用字符流 FileReader 和 FileWriter 处理非文本文件会出错,那么使用字节流 FileInputStream 和 FileOutputStream 来处理文本文件会怎么样呢?

不多 BB,直接上代码,步骤和格式与前面的操作差不多:

// 使用字节流 FileInputStream 处理文本文件可能出现乱码
@Test
public void testFileInputStream() {
    FileInputStream fileInputStream = null;
    try {
        // 操作文件
        File file = new File("hello.txt"); // 该文件存在当前 Module 中
        // 造流
        fileInputStream = new FileInputStream(file);
        // 读数据
        byte[] bytes = new byte[5];
        int len; // 记录每次读取的字节的个数
        while ((len = fileInputStream.read(bytes)) != -1) {
            String s = new String(bytes, 0, len);
            System.out.print(s);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            // 关闭资源
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

相信聪明的你一定看到贴出代码的首行注释了:使用字节流 FileInputStream 处理文本文件可能出现乱码。于是你一通 cv 把代码拷贝到自己的项目中,然后发现并没有出现乱码,这是怎么回事?

可以看一下 hello.txt 文件的编码,一般来说都是 UTF-8,在 UTF-8 中,英文字符和数字也都是一个字节,使用字节流传输是没有什么问题的,但是如果你加几个中文汉字呢?

于是你又一番操作,结果发现还是没乱码?咋回事?(第一次这么想让汉字乱码 😂)

原因也很简单。 在 UTF-8 编码中,一个汉字占 3 个字节。 我们代码中设置的字节数组长度为 5,然后你编写的文本中的数据恰好也是 5 字节的整数倍,不妨再加一个汉字,看看会不会乱码?

我相信,一定会乱码的!

因此结论也得出来了:使用字节流 FileInputStream 处理文本文件 可能 出现乱码。(这可能二字用得就很妙!)

总的来说,操作文本文件还是使用字符流好,毕竟在字符流中,无论英文、数字,抑或汉字,都算一个字符,如此下来,使用字符流操作文本文件是不会出现乱码的情况的。

扯了这么多,接下来就该进入主题了,使用字节流操作非文本文件。

4.2 操作非文本文件

在本节我们使用字节流来操作一下非文本文件,通俗一点:复制图片。😝

操作步骤和前面的复制文本文件一下,方法也基本一样,只是换几个类:

// 实现对图片的复制
@Test
public void testFileInputOutputStream() {

    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        File srcFile = new File("妖梦.jpg"); // 该文件存在当前 Module 中
        File destFile = new File("魂魄妖梦.jpg"); // 该文件并不存在当前 Module 中

        fileInputStream = new FileInputStream(srcFile);
        fileOutputStream = new FileOutputStream(destFile);

        byte[] bytes = new byte[10];
        int len;
        while ((len = fileInputStream.read(bytes)) != -1) {
            fileOutputStream.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileOutputStream != null) {
                fileOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行后,确实实现了图片的复制,复制得到的图片也可以正常打开。

什么?魂魄妖梦是啥?😇 老二刺猿了!

4.3 文件复制

根据 4.2 操作非文本文件 提供的代码,我们可以改写一下代码,编写一个复制文件的方法 copyFile()。

copyFile() 方法和测试代码如下:

// 实现指定路径下文件复制
public void copyFile(String srcPath, String destPath) {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);

        fileInputStream = new FileInputStream(srcFile);
        fileOutputStream = new FileOutputStream(destFile);

        byte[] bytes = new byte[1024];
        int len;
        while ((len = fileInputStream.read(bytes)) != -1) {
            fileOutputStream.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fileOutputStream != null) {
                fileOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

@Test
public void testCopyFile() {

    long start = System.currentTimeMillis();
    String srcPath = "D:\\01-视频.mp4";
    String destPath = "D:\\02-视频.mp4";

//        String srcPath = "hello.txt";
//        String destPath = "hello3.txt";
    copyFile(srcPath, destPath);
    long end = System.currentTimeMillis();
    // 164MB --> 2085ms
    System.out.println("复制操作花费时间为:" + (end - start));
}

运行上述代码,复制一个大小 164M 的视频文件耗费了 2085 ms。

你会发现提供的代码中,我还尝试复制了文本文件,那么真的可以复制成功吗?不会像前面测试那样乱码吗?

不妨取消注释,自己尝试一下,最终会发现能够复制成功。

总结

针对上面的现象进行进行一个小总结:

1、 使用字节流读入文本文件并显示在控制台,可能会出现乱码,但是使用字节流复制文本文件并不会出现乱码,并且能够成功复制。 (直接看就乱码,不看就不会乱码,这是薛定谔的代码?😆)

2、不能够使用字符流来处理非文本文件,就算是复制也不行!当然对于文本文件来说,还是使用字符流处理更好。

5. 缓冲流

5.1 非文本文件的复制

缓冲流的概述

前面讲的字符流(FileReader、FileWriter)和字节流(FileInputStream、FileOutputStream) 属于节点流,而 缓冲流属于处理流的一种 ,在节点流的基础上包装了一层。那么,缓冲流有什么用?

缓冲流的作用就是为了提高文件的读写效率。因此在设计开发过程中,一般不会使用前面列述的几个类,而是考虑使用缓冲流,因为它们是比较基本的几个流,效率不是很高。

有关缓冲流,存在这四个类:BufferedInputStream、BufferedOuputStream、BufferedReader 和 BufferedWriter。根据前面的说明,我们可以很简单地明白前两个是针对字节流的,后面两个是针对字符流的。

还有一点,缓冲流是不能直接作用在源文件上的,需要作用在节点流之上。

缓冲流就是「套接」在已有流的基础上。

测试代码

编写代码,实现图片的复制:

// 实现非文本文件的复制
@Test
public void BufferedStreamTest(){
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;

    try {
        // 1. 指定原文件与目标文件
        File srcFile = new File("妖梦.jpg");
        File destFile = new File("Youmu.jpg");
        // 2. 造流
        // 2.1 造节点流
        FileInputStream fis = new FileInputStream(srcFile);
        FileOutputStream fos = new FileOutputStream(destFile);
        // 2.2 造缓冲流
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);

        // 3. 复制的细节:读取和写入
        byte[] bytes = new byte[10];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4. 资源的关闭
        // 要求:先关闭外层的流,再关闭内层的
        try {
            if (bos != null) {
                bos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (bis != null) {
                bis.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 说明:关闭外层的流时,内层流也会自动关闭。因此,内层流的关闭可以省略。
//        fos.close();
//        fis.close();
    }
}

运行代码后,可以前往当前 Module 中查看复制得到的图片。

上述代码中,涉及到 4 个类,两种节点流,两种作用于节点流的处理流(缓冲流),那么最后关闭的时候要关闭 4 种?

当然不用。关闭外层的流时,内层流也会自动关闭,因此,内层流的关闭可以省略。

使用缓冲流可以实现非文本文件的复制,但是怎么体现缓冲流的作用(提高了读写效率)呢?

5.2 读写速度对比

和前面一样,提取一个 copyFileWithBuffered() 方法用于复制一个视频,然后使用与前面相同的视频进行测试,比较使用缓冲流所耗费的时间与未使用缓冲流所耗费的时间:

public void copyFileWithBuffered(String srcPath, String destPath) {
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;

    try {
        // 1. 指定原文件与目标文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        // 2. 造流
        // 2.1 造节点流
        FileInputStream fis = new FileInputStream(srcFile);
        FileOutputStream fos = new FileOutputStream(destFile);
        // 2.2 造缓冲流
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);

        // 3. 复制的细节:读取和写入
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 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 testCopyFile() {
    long start = System.currentTimeMillis();
    String srcPath = "D:\\01-视频.mp4";
    String destPath = "D:\\02-视频.mp4";

    copyFileWithBuffered(srcPath, destPath);
    long end = System.currentTimeMillis();
    // 未使用缓冲流: 164MB --> 2085ms
    // 使用缓冲流后: 164MB --> 450ms
    System.out.println("复制操作花费时间为:" + (end - start));

}

运行代码后得出使用缓冲流复制相同视频所用时间为 450 ms,而未使用缓冲流时,所消耗时间为 2085 ms,效率得到了明显的提升(注意测试时将字节数组的大小设置成一样,控制变量嘛)。

那么为什么使用缓冲流后读写效率会得到提升呢?

主要是因为内部提供了一个 8kb 的缓冲区,数据会先写入缓冲区,当缓冲区数据满(达到某一条件)后,刷新缓冲区,将数据进行复写到目标文件中。

缓冲流内部还有一个 flush() 方法,这个方法用于刷新缓冲区。如果每次使用缓冲流写入后,手动调用这个方法(这个时候缓冲区还没达到刷新条件),会发现耗时又增加了(我的环境下耗时为 1431 ms)。

5.3 文本文件的复制

对于字符流也有对应的缓冲流,我们可以测试一下(被复制文本最好是个大文本文件):

// 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
@Test
public void testBufferedReaderBufferedWriter() {

    BufferedReader br = null;
    BufferedWriter bw = null;

    try {
        // 原 Module 中已经存在
        br = new BufferedReader(new FileReader(new File("training-1000.txt")));
        // 原 Module 中并不存在
        bw = new BufferedWriter(new FileWriter(new File("result.txt")));

        // 读写操作
        // 方式一 使用 char[] 数组
        /*char[] chars = new char[1024];
            int len;
            while ((len = br.read(chars)) != -1) {
                bw.write(chars, 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();
        }
    }
}

在上述代码的读写操作中,使用了两种方法:一种使用 char[] 数组,一种使用了 String 类。

使用 char[] 数组时,与使用 byte[] 一样,就不赘述了。

使用 String 类时需要注意实例化的字符串中并 不包含换行符 ,如果直接写入,将无法写入换行符,导致所有内容都在一行。解决方法也有两种:

5.4 技巧练习

图片加密

// 图片的加密
@Test
public void test1() {

    FileInputStream fis = null;
    FileOutputStream fos = null;
    
    try {
        fis = new FileInputStream("妖梦.jpg");
        fos = new FileOutputStream("妖梦-secret.jpg");

        byte[] bytes = new byte[20];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            // 直接数组进行修改
            for (int i = 0; i < len; i++) {
                // 异或的异或就是原数据
                bytes[i] = (byte) (bytes[i] ^ 5);
            }
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (fis != null) {
                fis.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用字节流传输文件时,可以将获取的字节与某个特定的数进行异或,这样就可以实现图片的简单加密。

我们知道一个数异或的异或就是原数据,因此解密时,只需要再进行异或就可以了。

字的统计

给定一个文本,统计该文本中每次字出现的次数。代码如下:

@Test
public void testWordCount() {
    FileReader fr = null;
    BufferedWriter bw = null;

    HashMap<Character, Integer> map = new HashMap<>();
    try {
        // 被统计文本
        fr = new FileReader("training-1000.txt");
        int c = 0;
        while ((c = fr.read()) != -1) {
            // int 还原 char
            char ch = (char) c;
            // 判断 char 是否在 map 中第一次出现
            if (map.get(ch) == null) {
                map.put(ch, 1);
            } else {
                map.put(ch, map.get(ch) + 1);
            }
        }

        // 输出文本
        bw = new BufferedWriter(new FileWriter("wordCount.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 '\r':
                    bw.write("回车= " + entry.getValue());
                    break;
                case '\n':
                    bw.write("换行= " + entry.getValue());
                    break;
                default:
                    bw.write(entry.getKey() + "=" + entry.getValue());
                    break;
            }
            bw.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fr != null) {
            try {
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (bw != null) {
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

小技巧

使用 FileInputStream、FileOutputStream、FileReader 和 FileWriter 的构造方法时,可以直接传入路径,不用再书写 new File() 获取 File 对象。

6. 转换流

6.1 转换流的概述

转换流也是处理流的一种,转换流提供了在字节流和字符流之间的转换。

Java API 提供了两个转换流:

字节流中的数据都是字符时,转换成字符流操作更加高效。

很多时候我们使用转换流来 处理文件乱码 问题,实现编码和解码的功能。

转换流的作用

解码与编码

解码:字节、字节数组 ------> 字符数组、字符串 使用 InputStreamReader

编码:字符数组、字符串 ------> 字节、字节数组 使用 OutputStreamWriter

6.2 InputStreamReader

使用字节流读取一个文本文件,然后将字节流转换成字符流,最后将文本内容输出到控制台:

// 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒)
@Test
public void test1() throws IOException {
    FileInputStream fis = new FileInputStream("hello.txt");
    //        InputStreamReader isr = new InputStreamReader(fis); // 使用默认的字符集
    // 这种方式指明了字符集,具体使用哪个字符集,取决于文件保存时使用的字符集
    // 字节转字符
    InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); 
    char[] chars = new char[5];
    int len;
    while ((len = isr.read(chars)) != -1) {
        String s = new String(chars, 0, len);
        System.out.println(s);
    }

    isr.close();
    fis.close();
}

上述代码中的异常处理仍应该使用 try-catch-finally 来处理,下述代码仅仅是为了简便,或者说偷懒。

6.3 综合使用

综合使用 InputStreamReader 和 OutputStreamWriter 实现文本文件的解码与编码:

// 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒)
// 综合使用 InputStreamReader 和 OutputStreamWriter
@Test
public void test2() throws IOException{
    File file1 = new File("hello.txt");
    File file2 = new File("hello-gbk.txt");

    FileInputStream fis = new FileInputStream(file1);
    FileOutputStream fos = new FileOutputStream(file2);

    InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
    OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");

    char[] chars = new char[5];
    int len;
    while ((len = isr.read(chars)) != -1){
        osw.write(chars, 0, len);
    }

    isr.close();
    osw.close();
}

运行上述代码后,可以生成 helle-gbk.txt 文本文件,这个文本文件的编码是 gbk 的,如果在 IDEA 中直接打开,将会出现乱码,因为默认使用的 UTF-8 的编码打开的,所以会出现乱码。

当然如果使用 Nodepad3、Edit Plus 打开就不会,因为这些软件可以自动匹配字符集。

6.4 字符集说明

编码表的由来

计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。而这就是编码表。

常见的编码表

ASCII:美国标准信息交换码。用一个字节的 7 位可以表示。

IS08859-1:拉丁码表、欧洲码表。用一个字节的 8 位表示。

GB2312:中国的中文编码表。最多两个字节编码所有字符。

GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码。中文是两个字节,但是英文、数字是一个字节的。

Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码,所有的文字都用两个字节来表示。

UTF-8:变长的编码方式,可用 1 - 4 个字节来表示一个字符。

编码的问题

Unicode 虽然融合了人类使用的所有字,但是它不完美,这里就有三个问题,一个是我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别 Unicode 和 ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode 在很长一段时间内无法推广,直到互联网的出现。

面向传输的众多 UTF (UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode 编码是 UTF-8 和 UTF-16。|

补充

ANSI 编码,通常指的是平台的默认编码,例如英文操作系统中是 ISO-8859-1,中文系统是 GBK。

Unicode 字符集只是定义了字符的集合和唯一编号,Unicode 编码这是对 UTF-8、UCS-2 / UTF-16 等具体编码方案的统称而已,并不是具体的编码方案。

参考视频:多种字符编码集的说明

7. 标准的输入、输出流

7.1 概述

System.in 和 System.out 分别代表了系统标准的输入和输出设备

默认输入设备是:键盘,输出设备是:显示器

System.in 的类型是 InputStream,标准的输入流,默认从键盘输入

System.out 的类型是 PrintStream,它是 OutputStream 的子类、FilterOutputStream 的子类,标准的输出流,默认从控制台输出

重定向:通过 System 类的 setln, setOut 方法对默认设备进行改变。

7.2 具体使用

从键盘上输入字符串,要求将读取到的整行字符串转成大写输出,然后继续进行输入操作,直至输入 e 或 exit 时,退出程序:

// System.in 是字节流,BufferedReader 的 readLine() 是字符流,需要使用转换流
public static void main(String[] args) {
    BufferedReader br = null;
    try {
        InputStreamReader isr = new InputStreamReader(System.in);
        br = new BufferedReader(isr);
        // 也可以直接使用 Scanner, Scanner s = new Scanner(System.in)

        while (true) {
            System.out.println("请输入字符串:");
            String data = br.readLine();
            if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
                System.out.println("程序结束!");
                break;
            }

            String upperCase = data.toUpperCase();
            System.out.println(upperCase);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

8. 打印流

打印流概要

实现将 基本数据类型 的数据格式转化为 字符串 输出 。打印流,打印流,肯定是用来打印输出的,因此没有输入。

打印流:PrintStream 和 PrintWriter

提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出

PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常

PrintStream 和 PrintWriter 有自动 flush 功能

PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。

System.out 返回的是 PrintStream 的实例

具体使用

实现使用打印流将 ASCII 码对应的字符打印到指定文件中:

// 打印流 PrintStream 和 PrintWriter
@Test
public void test1() {
    PrintStream ps = null;
    try {
        FileOutputStream fos = new FileOutputStream(new File("D:\\io\\text.txt"));
        // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\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 {
        if (ps != null) {
            ps.close();
        }
    }
}

9. 数据流

为了方便地操作 Java 语言的基本数据类型和 String 的数据,可以使用数据流。

数据流有两个类:(用于读取和写出基本数据类型、String 类的数据)

DataInputStream 中的方法

boolean readBoolean()

byte readByte()

char readChar()

float readFloat()

double readDouble()

short readShort()

long readLong()

int readInt()

String readUTF()

void readFully(byte[] b)

DataOutputStream 中的方法

将上述的方法的 read 改为相应的 write 即可。

具体使用

将内存中的基本数据类型和字符串写入文件:

// 将内存中的基本数据变量和字符串写入文件
@Test
public void test2() throws IOException {
    DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
    dos.writeUTF("默烦");
    dos.flush(); // 刷新操作,将内存中的数据写入文件中
    dos.writeInt(20);
    dos.flush();
    dos.writeBoolean(true);
    dos.flush();

    dos.close();
}

将文件中存储的基本数据类型和字符串读取到内存中,保存到变量:

// 将文件中存储的基本数据类型和字符串读取到内存中,保存到变量
// 读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致
@Test
public void test3() throws IOException{
    DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));

    String name = dis.readUTF();
    int age = dis.readInt();
    boolean isMale = dis.readBoolean();

    System.out.println("name = " + name);
    System.out.println("age = " + age);
    System.out.println("isMale = " + isMale);

    dis.close();
}

10. 对象流

10.1 对象流的概述

对象流使用到的类是:ObjectInputStream 和 OjbectOutputSteam

对象流是用于存储和读取 基本数据类型 数据或 对象 的处理流。它的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。这样就涉及到序列化和反序列化的概念:

序列化:用 ObjectOutputStream 类 保存 基本类型数据或对象的机制

反序列化:用 ObjectInputStream 类 读取 基木类型数据或对象的机制

注意: ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。

对象的序列化

对象序列化机制 允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。

序列化的好处在于可将任何实现了 Serializable 接口的对象转化为 字节数据,使其在保存和传输时可被还原.

序列化是 RMI(Remote Method Invoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下 两个接口之一 ,否则,会抛出 NotSerializableException 异常:

10.2 字符串的序列化

将某个字符串序列化写入文件 object.dat,然后使用反序列化将文件 object.dat 中的数据输出到控制台:

// 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去
@Test
public void testObjectOutputStream() {
    ObjectOutputStream oos = null;
    try {
        // 当前 Module 下并没有 object.dat 文件
        oos = new ObjectOutputStream(new FileOutputStream("object.dat"));

        oos.writeObject(new String("默烦"));
        oos.flush(); // 数据刷新
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (oos != null) {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

// 反序列化
@Test
public void testObjectInputStream() {
    ObjectInputStream ois = null;
    try {
        // 当前 Module 下存在上次序列化的 object.dat 文件
        ois = new ObjectInputStream(new FileInputStream("object.dat"));

        Object obj = ois.readObject();
        String str = (String) obj;
        System.out.println(str);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ois != null) {
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行上述代码后,当前 Module 下生成 object.dat 文件,控制台输出字符串 「默烦」。

10.3 自定义类的序列化

使用对象流序列化对象

若某个类实现了 Serializable 接口,那么该类的对象就是可序列化的。序列化过程如下:

1、创建一个 ObjectOutputStream 对象

2、调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象

3、注意写出一次,就要操作 flush() 一次

那么反序列化:

1、创建一个 ObjectInputStream 对象

2、调用 readObject() 方法读取流中的对象

强调: 如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。

序列化要点

1、如果需要让某个对象支持序列化机制,那么这个对象所属的类 必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。

2、需要被序列化的类必须提供一个全局常量:serialVersionUID。

3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的。(默认情况下,基本数据类型都可序列化)

自定义类序列化示例

创建一个实体类 Person,具体代码如下:

/**
 * @author 默烦 2020/10/12
 */
public class Person implements Serializable {

    private static final long serialVersionUID = 4216546544L;

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() { }
    
    // 省略 Getter、Setter 和 toString 方法

}

在字符串序列化的代码基础上进行修改:

// 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去
@Test
public void testObjectOutputStream() {
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream("object.dat"));

        oos.writeObject(new String("默烦"));
        oos.flush(); // 数据刷新

        oos.writeObject(new Person("默烦", 20));
        oos.flush();
        
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (oos != null) {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

// 反序列化
@Test
public void testObjectInputStream() {
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("object.dat"));

        // 注意反序列化顺序
        Object obj = ois.readObject();
        String str = (String) obj;

        Person person = (Person) ois.readObject();
        System.out.println(str);
        System.out.println(person);
        
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ois != null) {
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行上述代码后,控制台输出:

自定义类的序列化

需要注意的有:

1、每次序列化一个对象后,记得执行 flush() 操作

2、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错

10.4 serialVersionUID

凡是实现 Serializable 接口的类都有一个表示序列化版木标识符的静态变量:

private static final long serialVersionUID;

serialVersionUID 用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。

如果类没有显式定义这个静态变量,它的值是 Java 运行时环境根据类的内部细节自动生成的。 若类的实例变量做了修改,serialVersionUID 可能发生变化。 故建议,显式声明。

简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 InvalidCastException。

10.5 序列化要点

1、如果需要让某个对象支持序列化机制,那么这个对象所属的类 必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。

2、需要被序列化的类必须提供一个全局常量:serialVersionUID。

3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的(默认情况下,基本数据类型都可序列化)。

4、ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。

5、每次序列化一个对象后,记得执行 flush() 操作

6、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错

序列化更多相关内容可以查阅:Lambda 与序列化

11. 随机存取文件流

11.1 RandomAccessFile 类

RandomAccessFile 类的概述

RandomAccessFile 声明在 java.io 包下,但 直接继承 于 java.lang.Object 类,并
且它实现了 Datalnput、DataOutput 这两 个接口,也就意味着这个类既可以读也可以写。

RandomAccessFile 类支持「随机访问」的方式,程序可以直接跳到文件的任意地方来读、写文件,比如:

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:

RandomAccessFile 构造器

public RandomAccessFile(File file, String mode)

public RandomAccessFile(String name, String mode)

创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:

如果模式为只读 r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。如果 模式为 rw 读写,且文件不存在则会去创建文件,反之则不会创建。

RandomAccessFile 实现图片的复制示例

@Test
public void test1() {
    RandomAccessFile raf1 = null;
    RandomAccessFile raf2 = null;
    try {
        // 当前 Module 下存在此文件
        raf1 = new RandomAccessFile(new File("妖梦.jpg"), "r");
        // 当前 Module 下并无此文件,需要实现图片的复制
        raf2 = new RandomAccessFile(new File("魂魄妖梦.jpg"), "rw");

        byte[] bytes = new byte[1024];
        int len;
        while ((len = raf1.read(bytes)) != -1) {
            raf2.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (raf1 != null) {
            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (raf2 != null) {
            try {
                raf2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

RandomAccessFile 作为输出流示例

/*
* RandomAccessFile 作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建
* 如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)
* */
@Test
// 异常处理还是使用 try-catch-finally 处理更好,这里为了简便(偷懒)
public void test2() throws IOException {
    // 当前 Module 下存在 hello.txt 文件
    RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");

    raf1.write("mofan".getBytes());

    raf1.close();
}

我们需要知道:

1、RandomAccessFile 作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建

2、如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)

在当个方法中调用 write() 方法后会记录角标的位置,如果再次调用该方法,将会从记录的角标位置后进行写入。

11.2 实现数据的插入

实现指定位置的复写

在前文代码的基础上指定角标的位置:

@Test
public void test2() throws IOException {
    RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
    raf1.seek(3); // 指针调到角标为 3 的位置
    // 从角标为 4 的位置开始插入(角标从 1 开始计算)
    raf1.write("xyz".getBytes());

    raf1.close();
}

实现指定位置的插入

// 实现数据的插入效果
@Test
public void test3() throws IOException {
    RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
    raf1.seek(3); // 指针调到角标为 3 的位置

    // 保存指针 3 后面的所有数据到 StringBuffer 中
    StringBuffer builder = new StringBuffer((int) new File("hello.txt").length());
    byte[] bytes = new byte[20];
    int len;
    while ((len = raf1.read(bytes)) != -1) {
        builder.append(new String(bytes, 0, len));
    }

    raf1.seek(3);
    raf1.write("xyz".getBytes());

    // 将 StringBuffer 中的数据写入到文件里
    raf1.write(builder.toString().getBytes());
    raf1.close();
}

RandomAccessFile 并没有提供插入方法,我们实现的插入其实就是:复制指定位置后的所有数据,在指定位置后写入数据,最后再将复制的数据写入文件。

那 RandomAccessFile 有什么用呢?

我们可以用 RandomAccessFile 这个类,来实现一个 多线程断点下载 的功能,用过下载工具的朋友们都知道,下 载前都会建立 两个临时文件 ,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。

12. NIO 的简单介绍

12.1 NIO.2

NIO 的简单介绍

Java NIO(New IO,Non-Blocking I0)是从 Java 1.4 版本开始引入的一套新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。

Java API 中提供了两套 NIO,一套是针对标准输入输出 NIO,另一套就是网络编程 NIO。

➢|-----java.nio.channels.Channel
 	|-----FileChannel : 处理本地文件
	|-----SocketChannel : TCP 网络编程的客户端的Channel
	|-----ServerSocketChannel : TCP 网絡编程的服务器端的 Channel
	|-----DatagramChannel : UDP 网络编程中发送端和接收端的 Channel

但是 JDK 1.4 中 NIO 的使用体验并不好,随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称它们为 NIO.2。因为 NIO 提供的一-些功能,NIO 已经成为文件处理中越来越重要的部分。

NIO.2 中 Path、Paths 的使用

早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且 大多数方法在出错时仅返回失败,并不会提供异常信息。

NIO. 2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。

在以前 IO 操作都是这样写的:

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 路径

NIO.2 中 Files 类的使用

java.nio.file.Files 用于操作文件或目录的上具类。

Files 常用方法:

Path copy(Path src, Path dest, CopyOption ... how): 文件的复制
Path createDirectory(Path path, FileAttribute<?> ... attr): 创建一一个目录
Path createFile(Path path, FileAttribute<?> ... arr): 创建一一个文件
void delete(Path path): 删除一个文件/目录,如果不存在,执行报错
void deletelfExists(Path path): Path对应的文件/目录如果存在,执行删除
Path move(Path src, Path dest, CopyOption... how): 将src移动到dest位置
long size(Path path): 返回path指定文件的大小
    
Files常用方法: 用于判断
boolean exists(Path path, LinkOption ... opts): 判断文件是否存在
boolean isDirectory(Path path, LinkOption ... opts): 判断是否是目录
boolean isRegularFile(Path path, LinkOption ... opts): 判断是否是文件
boolean isHidden(Path path): 判断是否是隐藏文件
boolean isReadable(Path path): 判断文件是否可读
boolean isWritable(Path path): 判断文件是否可写
boolean notExists(Path path, LinkOption ... opts): 判断文件是否不存在
    
Files常用方法: 用于操作内容
SeekableByteChannel newByteChannel(Path path, OpenOption..how): 获取与指定文件的连接,how 指定打开方式。
DirectoryStream<Path> newDirectoryStream(Path path): 打开path指定的目录
InputStream newInputStream(Path path, OpenOption... how): 获取InputStream对象
OutputStream newOutputStream(Path path, OpenOpition... how) : 获取OutputStream对象

到此,Java IO 流基础就基本说完了,在实际开发过程中可能会书写这些 IO 流代码,但是大概率是不会使用的,经常使用的是已经封装好的第三方库,比如 Apache 下的 commons-io。


Java IO 流基础完

如果这篇文章对你有帮助,可以通过 赞赏 请我喝杯 Coffee ☕


上一篇
Java 网络编程基础
下一篇
Java 泛型