Java基础:IO流

目录

一、定义

1.引言

2.分类

(1)按照流的方向分

(2)按操作文件的类型分

3.体系结构 

二、字节流(以操作本地文件为例)

1. FileOutputStream 类

(1)定义

(2)写数据的三种方式

Ⅰ. write(int b)

Ⅱ. write(byte[] b) 

Ⅲ. write(byte[] b, int off, int len) 

(3)换行和续写

Ⅰ. 换行

Ⅱ. 续写

2. FileInputStream 类 

(1)定义

(2)循环读取

(3)读数据的两种方式

3. IO流中捕获异常的三种方式

(1)基础写法

(2)JDK7方案 

(3)JDK9方案

三、字符集

1.ASCII 字符集

2.GBK字符集 

3. Unicode字符集

Unicode 的三种编码方案

(1)UTF-16

(2)UTF-32

(3)UTF-8(最常用)

4.乱码

 5. Java 中的编码与解码

四、字符流(以操作本地文件为例)

1. FileReader 类 

(1)定义

(2)读数据的两种方式 

Ⅰ. 空参 read 方法

Ⅱ. 有参 read 方法 

(3)底层原理

2. FileWriter 类

(1)定义

 (2)写数据的五种方式

I. write(int c) 

​编辑

 Ⅱ. write(string str)  最常用

Ⅲ. write(char[] cbuf)

(3)底层原理

五、缓冲流 

1.定义

2.字节缓冲流

(1)使用

(2)底层原理

3.字符缓冲流

六、转换流 

1.引言

2.定义 

3.使用

(1)利用转换流按照指定字符编码读取

(2)利用转换流按照指定字符编码写出

七、序列化流和反序列化流

1.序列化流 / 对象操作输出流 

2.反序列化流 / 对象操作输入流 

3.细节

(1)序列号

(2)transient 关键字

(3)反序列化多个对象 

八、打印流

1.字节打印流

(1)构造方法

(2)成员方法

 2.字符打印流

(1)构造方法

(2)成员方法

3.标准输出流分析 

 九、解压缩流和压缩流

1.解压缩流 

2.压缩流

(1)压缩单个文件

(2)压缩文件夹

十、常用工具包

1.Commonis-io

2.Hutool


一、定义

1.引言

在执行程序时,数据都存放在内存中,并不能永久化存储。

程序一旦停止,数据就会丢失。

所以为了持久化存储数据,需要将数据进行存档,存在硬盘中的文件中。


① File:表示系统中的文件或则文件夹的路径

缺点:File 类只能对文件本身进行操作,不能读写文件里面存储的数据

② IO流:存储和读取数据的解决方案

I:input(输入、读取)        O:output(输出、写出)

作用:用于读写文件中的数据(可以是本地文件,也可以是网络中的数据)

2.分类

(1)按照流的方向分

输入流:文件  -->  程序

输出流:程序  -->  文件

(2)按操作文件的类型分

字节流:可以操作所有类型的文件

字符流:只能操作纯文本文件(纯文本文件:Windows系统的记事本打开显示正常的文件)

3.体系结构 

二、字节流(以操作本地文件为例)

1. FileOutputStream 类

(1)定义

作用:操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。

步骤:

① 创建字节输出流对象

② 写数据

③ 释放资源

public class FileDemo1 {
    public static void main(String[] args) throws IOException {
        //需求:写出一段文字到本地文件中(不含中文文字)

        //1.创建对象
        FileOutputStream fos = new FileOutputStream("IO\\a.txt");
        //2.写数据
        fos.write(97);
        //3.释放资源
        fos.close();

    }
}

运行结果:

字符输出流的细节:

Ⅰ. 创建字节输出流对象:

① 参数可以是 字符串表示的路径 或者是 File 对象

② 如果文件不存在,会创建一个新的文件,但是要保证父级路径是存在的。 

③ 如果文件已经存在,则会清空该文件中的数据,重写写出新的数据。

Ⅱ. 写数据:

 write 方法的参数是整数,但实际上写到文件中的是整数在 ASCII 码表中对应的字符。

Ⅲ. 释放资源:

每次使用完流之后都必须释放资源,否则程序会一直占用该文件

如果不解除资源的占用,此时该文件将无法进行其他操作(手动删除等)。

(2)写数据的三种方式

Ⅰ. write(int b)

Ⅱ. write(byte[] b) 

Ⅲ. write(byte[] b, int off, int len) 

细节:

参数一:字节数组

参数二 off:数组的起始索引

参数三 len:写出数据的个数


Question:如果写出的数据是"Hello World",如何实现?难道一个个查ASCII码表吗?

public class WriteDemo4 {
    public static void main(String[] args) throws IOException {
        //1.创建对象
        FileOutputStream fos = new FileOutputStream("IO\\a.txt");
        //2.写数据
        String str="Hello World";
        //调用getBytes方法将字符串变成字节数组
        byte[] bytes = str.getBytes();
        fos.write(bytes);
        //3.释放资源
        fos.close();
    }
}

通过 String 类的 getBytes 方法就可以将字符串变成字节数组,再调用 write 方法即可。

(3)换行和续写

Ⅰ. 换行

在不同的操作系统当中,换行符是不一样的:

Windows:\r\n

Linux:      \n

Mac:        \r

注意:在 Windows 操作系统当中, Java 对回车换行进行了优化。

虽然完整的是 \r\n ,但只写其中一个 \r 或者 \n 都可以实现换行,因为 Java 会在底层进行补全。

Ⅱ. 续写

在 FileOutputStream 的构造方法中,会调用另一个重载的构造方法,这个方法有一个参数 append

这个 append 表示的就是 是否续写,默认传递的是 false,所以每次都会清空数据进行写出。

如果想要续写,我们只要调用这个构造方法,给 append 传递一个 true 即可。

2. FileInputStream 类 

(1)定义

作用:操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来。

步骤:

① 创建字节输入流对象

② 读数据

③ 释放资源

细节:

Ⅰ. 创建字节输入流对象:

如果文件不存在,则直接报错

程序中最重要的是:数据。所以文件不存在,数据就读取不到。

即使像 字节输出流那样,创建一个新的文件,也毫无意义,因为新文件没有数据。

Ⅱ. 读数据

① read 方法负责读取文件中的数据,它会一个字节一个字节的去读,返回该字符的 ASCII 码。

read 方法读取一个数据,就向后移动一次指针。如果指针到文件末尾了,read 方法就会返回 -1。

Ⅲ. 释放资源:

每次使用完流之后都必须释放资源,否则程序会一直占用该文件

(2)循环读取

通过上面我们发现,可以通过 read 方法的结果,判断其是否等于 -1,来进一步判断文件是否读完

注意:read 表示读取数据,读一次移动一次指针,所以 while 循环中必须用一个变量进行接收。


 Test:实现文件拷贝

public class FileCopy {
    public static void main(String[] args) throws IOException {
        //1.创建对象
        FileInputStream fis = new FileInputStream("IO\\a.txt");
        FileOutputStream fos = new FileOutputStream("IO\\b.txt");
        //2.循环拷贝
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
        //3.释放资源
        fos.close();
        fis.close();
    }
}

细节:

① 拷贝的核心思想:边读边写

② 释放资源的规则:先开的,后关闭

缺点:一次读写一个字节,速度太慢

(3)读数据的两种方式

 空参 read 已经介绍,这里主要介绍一次读多个字节的 read 重载方法。

注意:一次读一个字节数组的数据,每次读取会尽可能的把数组装满。

数组大小虽然越大越好,但是数组本身也是占用内存的,数组长度过大可能程序就会直接崩溃。

一般定义数组的大小为 1024 的整数倍,比如 1024 * 1024 * 5 = 5 Mb

细节:

① 一次读取多个字节数据,具体读多少个,跟数组长度有关,会尽可能的将数组填满。

② 方法的返回值是:本次读取到了多少个字节数据


Question1:为什么第三次、第四次读取到的数据是:ed,第四次读取的长度是 -1?

第一次读取,读取到的数据是:ab,指针向后移动两位,指向 c 。

然后会将 ab 存放到 bytes 数组中,len 返回 2。

第而次读取,读取到的数据是:cd,指针向后移动两位,指向 e 。

然后会将 cd 存放到 bytes 数组中(进行覆盖),len 返回 2。

第三次读取,只能读取到一个 e,指针就移到了文件的末尾。

此时会将 e 存放到 bytes 数组中(进行覆盖),len 返回为 1。

由于只读取了一个 e,所以只覆盖了数组的第一个元素,第二个元素 d 仍在数组中,所以打印结果为 ed 。

同理,第四次读取时,读取不到任何元素,那么数组将不会覆盖,打印结果仍然是 ed。

注意:read 方法只要读不到数据,无论是空参还是带有参数(数组)的,都会返回 -1 。

所以第四次读取时,len 返回为 -1。


Question2:我希望每次显示的都是自己读到的数据,而没有残留数据,如何修改?

 String 类的构造方法中,除了根据 byte 数组创建对象。

还提供了另一个重载的构造方法,可以设置数组的起始索引,和元素的长度。

即从数据的 offset 索引开始,将 length 个元素,变为一个字符串。

3. IO流中捕获异常的三种方式

对于上述代码中产生的异常,我们采用的都是抛出处理 throws。

如果使用捕获异常,try - catch,该如何书写呢?

(1)基础写法

如果将所有代码,都放入 try 中,这会产生一个问题:

因为三个语句都可能发生异常,如果第二条语句 write 方法产生了异常,close 方法将直接跳过。

这时就会造成资源没有释放的问题。

我们可以使用 捕获异常的完整格式:try - catch - finally

特点:finally 中的代码一定会被执行,除非 JVM 停止。


但这样书写,又会产生另一个问题:fos 是一个局部变量,finally 中访问不到

所以我们必须得将 fos 和 fis 定义在 try 外。

同时为了防止 fos 和 fis 未被初始化,还应初始化为 null 。

由于 close 方法本身也会产生异常,所以针对 close 还需要捕获异常,也就是异常嵌套

这时程序虽然没有编译时异常了,但仍存在一个问题:

如果 fos 创建对象时,父级路径不存在,就会创建失败。 

同理 fis 创建对象时,路径不存在,也会创建失败。

此时 fos 和 fis 的值都仍是 null,如果在 finally 中调用 close 方法,就会产生空指针异常

所以完整格式如下: 

但这种书写方式太过麻烦,主要体现在资源释放上。

为此,java 提供了一个 AutoCloseable 接口:

凡是实现了该接口的,在特定的情况下,都可以自动释放资源,无需书写 close 代码。

(2)JDK7方案 

 注意:只有实现了 AutoCloseable 接口的类,才能在 try 的小括号中创建对象。

我们发现,FileInputStream 和 FileOutputStream 都间接实现了 AutoCloseable 接口。

所以,代码可修改为:

(3)JDK9方案

 由于这样小括号中的可阅读性太差,JDK9 中又做了优化。

在 JDK9 开始后,创建对象的代码就可以放在外面写,try 的小括号里只需要写变量名即可。

对于 fos 和 fis 创建对象时出现的异常,抛出处理即可。

三、字符集

在计算机中,任何数据都是以二进制的形式来存储的。

计算机中最小的存储单元是一个字节。

1.ASCII 字符集

在ASCII 码表中,总共制定了 2^7 = 128 种字符, ASCII 码从 0 到 127。

在 ASCII 字符集种,一个英文占一个字节。

一个字节 = 8 bit ,所以后面 7 个 bit 位用于表示不同的字符,即 2^7 = 128 种字符。

在编码时,高位补 0,所以第 1 位必定是 0。

2.GBK字符集 

2000年3月17日发布,收录 21003 个汉字。

包含国家标准 GB130000-1 中的全部中日韩汉字,和 BIG5 编码(台湾地区繁体中文标准字符集)中的所有汉字。

注意:

① 简体中文版 windows 系统中默认使用的就是GBK。

② 在 GBK 字符集种,是完全兼容 ASCII 字符集的:

一个英文占一个字节,二进制第一位是 0 ;

一个中文占两个字节,二进制高位字节的第一位是 1(为了和ASCII 中的做区分)。

GBK 种,每个汉字由 2 个字节存储,2个字节 = 16 bit,也就是 2^16 = 65536 种。

由于要兼容ASCII 字符集,所以汉字的高位字节一定是以 1 开头,转成十进制之后也就是负数。

第一个字节的高位是 1,所以一定是中文汉字,看两个字节。

第三个字节的高位是 0,服从 ASCII 编码规范,是英文字符,所以看一个字节。

GBK在编码汉字时,不需要做任何变动。所以在解码汉字时,也不需要做任何变动。

3. Unicode字符集

Unicode :万国码(包含绝大多数国家的语言)

由统一码联盟(Unicode 组织)在1994年发布1.0版本,期间不断添加新的文字,最新版本是2022年9月3日发布的15.0版本。

Unicode 的三种编码方案

(1)UTF-16

规则:用 2 - 4个字节进行存储,高位全部补 0 

(2)UTF-32

规则:固定采用 4 个字节进行存储,高位全部补 0

(3)UTF-8(最常用)

 由于前两种编码方案都存在大量的补0,会造成内存空间的浪费,所以推出了 UTF-8。

规则:用 1 - 4个字节进行存储,英文占一个字节,中文占三个字节。

 红色的表示固定格式,甚于部分再用字符的二进制进行填补,如:

4.乱码

原因1:读取数据时未读完整个汉字

对于 UTF-8编码的 "ai你哟",如果通过字节流的方式,每次读一个字节。

那么在读到第三个字节时,就是 11100100,转成十进制是 -28。

-28 在 ASCII 码表中对应的字符并不存在。在不同的操作系统中,就会显示 ?或者 □


原因2:编码和解码方式不统一

对于使用 UTF-8 方式进行编码的汉字,也应用 UTF-8 进行解码。

如果采用 GBK 方式进行解码,就会产生错误。

高位字节是 1,GBK 方式认为其是汉字,所以会读 2 个字节,十进制是 59057,-119。

在通过查找 GBK 字符集,获得 "姹"。

后面还有一个字节的数据,但也以 1 开头,所以 GBK 也会认为其是汉字,读取 2 个字节。

但字节数不够,就会乱码,显示 "�"。

 5. Java 中的编码与解码

注意:在 IDEA编译器中,默认方式是 UTF-8;在 eclipse 编译器中,默认方式是 GBK。

public class Code {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String str = "汉";
        //1.编码
        //1.1 使用默认方式进行编码
        byte[] bytes1 = str.getBytes();
        System.out.println(Arrays.toString(bytes1));//[-26, -79, -119]

        //1.2 使用指定方式进行编码
        byte[] bytes2 = str.getBytes("GBK");
        System.out.println(Arrays.toString(bytes2));//[-70, -70]

        //2.解码
        //2.1 使用默认方式进行解码
        String str1 = new String(bytes1);
        System.out.println(str1);//汉

        //2.2 使用指定方式进行编码
        String str2 = new String(bytes1,"GBK");//编码和解码方式不一致:乱码
        System.out.println(str2);//姹�

        String str3 = new String(bytes2,"GBK");
        System.out.println(str3);//汉
    }
}

四、字符流(以操作本地文件为例)

由上可知,乱码的原因有两个,编码和解码方式统一这个很好实现。

所以,主要原因在于读取数据不完整,如何解决?


字符流的底层其实就是字节流 ,只不过在字节流的基础上又添加了字符集。

特点:

输入流:一次读一个字节,如果遇到中文,一次读多个字节。

输出流:底层会把数据按照指定的编码方式进行编码,变成字节在写到文件中。

使用场景:对于纯文本文件进行读写操作。

1. FileReader 类 

(1)定义

 步骤:

① 创建字符输入流对象

② 读取数据

③ 释放资源

(2)读数据的两种方式 

Ⅰ. 空参 read 方法

细节: 

① 虽然字符流的 read 和 write 方法的操作单位都是一个字符,但本质上: 

字符流的底层也是字节流,默认是一个一个字节读取的。

但如果遇到中文就会一次读取多个, GBK 一次读两个字节,UTF-8一次读三个字节。

② read 方法在读取之后,方法底层还会进行解码并转成十进制,最终将这个十进制作为返回值。

英文:a --> 文件中的数据:0110 0001 --> read 方法进行读取,解码并转成十进制:97

中文:汉 -->  文件中的数据:11100110 10110001 10001001 --> read 方法进行读取,解码并转成十进制:22721

所以,需要使用强制转换,将十进制数转换为字符型。

Ⅱ. 有参 read 方法 

细节:

read(char[] buffer):读取数据,解码,强转 三步合并在一起,把强转之后的字符放到数组中。 

即:有参的 rea的方法,相当于 空参的 read 方法 + 强制类型转换。

(3)底层原理

① 创建字符输入流对象:

底层:关联文件,并创建缓冲区长度为8192的字节数组,默认初始化 0

② 读取数据

底层:判断缓冲区中是否有数据可以读

        Ⅰ. 缓冲区中没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区

                                          如果文件中也没有未获取的数据了,就返回 -1。

        Ⅱ. 缓冲区中有数据:就从缓冲区中读取 (等缓冲区中全部读完,再次去文件中获取数据装入)

           空参的 read 方法:一次读一个字节,遇到中文一次读多个,把字节解码后转成十进制返回

           有参的 read 方法:把读取字节,解码,强转三步合并了,强转之后的字符放到字符数组中


 Test:我们知道,输出流对象 fw 一旦创建,就会清空文件中的数据。针对以下代码,fr 是否能读取到数据呢?

答案是可以的,但最多只能读到前 8192个字节的数据,文件中剩余的数据是无法读取到的。

因为 fr 先调用 read 方法进行读取,它会将文件中的数据尽可能装满缓冲区(最多8192个字节)。

之后 fw 对象创建,这时才会立即清空文件中的数据。

所以之后 read 方法继续读取时,fr 中的缓冲区仍存在数据,可以读取。

但一旦缓冲区中数据全部读完,再次去文件中获取未读取的数据时,就获取不到了。

原则:IO 流随用随创建,不用则关闭。(为了防止提前创建 fw 而清空文件数据)

2. FileWriter 类

(1)定义

步骤:

① 创建字符输出流对象

② 写数据 

③ 释放资源

 (2)写数据的五种方式

这里只演示其中三种,剩余两种和上面类似,不在过多介绍。

I. write(int c) 

细节:

write 方法会在底层根据字符集的编码方式进行编码,把编码之后的字节数据写到文件中去。

IDEA 的默认编码方式是 UTF-8,所以编码后的这个汉字应该占 3个字节。

 Ⅱ. write(string str)  最常用

细节:

 方法的底层也会根据当前的编码方式和码表进行编码,将对应的字节数据写到文件中去。

“你好” 一共占 2*3=6个字节,三个感叹号是英文状态下的,所以占 3*1=3 个字节,一共占 9 个字节。

Ⅲ. write(char[] cbuf)

注:如果想要续写,构造方法中 append 参数传递为 true 即可。 

(3)底层原理

① 创建字符输出流对象:

底层:关联文件,并创建缓冲区长度为8192的字节数组,默认初始化 0

注意:fr 和 fw 的缓冲区不是同一个缓冲区,都有各自对应缓冲区(字节数组) 

② 写出数据

底层:会将数据都写到缓冲区中

当以下三种情况发生时,会将缓冲区中的数据全部写进目标文件中:

Ⅰ.  往缓冲区中添加数据,发现缓冲区已经满了时

Ⅱ. 调用 flush 方法手动刷新

Ⅲ. 调用 close 方法释放资源

flush 和 close 的区别 :

flush 刷新:刷新之后,还可以继续往文件中写出数据。

close 关流:断开通道,无法再往文件中写出数据(如果继续写,则报错) 。

五、缓冲流 

1.定义

缓冲流:就是在基本流的基础之上进行包装,增加了缓冲区 ,用来提高读写的效率。

(由于 FileReader 和 FileWriter 已经有缓冲区,所以提升并不明显,但包装后提供了很多新的方法)

2.字节缓冲流

(1)使用

 原理:底层自带了长度为 8192 的 缓冲区提高性能

public class CopyDemo {
    public static void main(String[] args) throws IOException {
        //1.创建缓冲流对象
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(("IO\\a.txt")));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("IO\\b.txt"));
        //2.循环读取并写出数据
        int b;
        while ((b=bis.read())!=-1){
            bos.write(b);
        }
        //3.释放资源
        bos.close();
        bis.close();
    }
}

 Question1:增加的缓冲区在哪里呢?

 BufferedInputStream 调用构造方法时,底层创建了一个大小为 DEFAULT_BUFFER_SIZE 的字节数组。

 而这个 DEFAULT_BUFFER_SIZE 的值为 8192。

 BufferedOutputStream 在调用构造方法时,也会创建一个长度为 8192 的字节数组(缓冲区)。


Question2:为什么关流的时候,不需要关基本流呢?

在 close 方法中,其实也将基本输出流进行了关闭。

同理,在 close 方法中,也将基本输入流进行了关闭。

(2)底层原理

① 首先基本流会从硬盘中的文件中读取数据,将数据放进缓冲区当中,一次性会读 8192 个字节的数据。

② 再通过变量 b 从输入缓冲区中拿数据,写到数据输出缓冲区中。

③ 当输出缓冲区已经满了时,会将缓冲区中的数据通过基本流一次性写到文件中去。

注意:

① 虽然这种方式也是一个一个字节从输入缓冲区传到输出缓冲区中,但这个操作是在内存中进行的

在内存中的运算速度是非常快的,可以忽略不计。

真正节约的是读和写的时候,跟硬盘之间操作的时间

read 方法和 write 方法都是对缓冲区进行读取和写出的,真正对文件进行操作的还是基本流

③ 从硬盘中读取数据到缓冲区中,和 FileReader 一样,也是在调用 read 方法时进行的。

从缓冲区中写出数据到硬盘中,和 FileWriter 一样,也是在装满,flush,close三种情况时发生的

④ 如果是有参的 read 方法,区别仅仅是:每次对缓冲区进行读写数据,不是一个一个字节,而是一次一个数组的大小。--> 只是中间的内存中的运算过程更快而已。

3.字符缓冲流

原理:底层自带了长度为 8192 的缓冲区提高性能。

由于字符基本流中已经存在缓冲区,所以提升效果不大,但包装后提供了很多特有的方法。

细节:

① readLine 方法在读数据时,一次读一整行。

在缓冲区中遇到回车换行符结束,但不会把回车换行读到内存中。

②  readLine 方法在读到文件末尾时,不会返回 -1,而是返回 null。

③ 以上所有的构造方法,续写:append 参数都是在基本流的构造方法中,而不是在高级流中。

④ 在字节缓冲流中,底层的缓冲区是 8192 的字节数组,即 8 KB

而在字符缓冲流中,底层的缓冲区是 8192 的字符数组,即 16 KB。

(在 Java 中,1 个字符 = 2 个字节)

六、转换流 

1.引言

前面说过,IDEA 默认的编码方式是 UTF-8,所以无论是在读取还是写出数据时,都是用 UTF-8 方式进行编码和解码的

Question:如果现在有一个 GBK 的文件,想要成功读取其中的数据,如何实现?

可以通过字节流进行读取,然后对字节通过 GBK 方式进行解码

但有一个缺点,字节数组的大小是不确定的。

当数组的字节数 >= 文件中数据的字节数,结果是正常的。

如果数组的字节数 < 文件中数据的字节数,就有可能发生问题。

比如,文件中的数据 "你好我好大家好" 是GBK 方式编码的,所以共占 2*7=14个字节。

如果数组长度小于 14个字节,且是一个奇数,就会存在读取字节不完整的情况,导致乱码。

所以为了解决字节读取不完整的问题,我们可以采用字符流进行读取。

字符流每次的操作单位都是一整个字符,在底层:遇到一个汉字,会读多个字节。

但如果用字符流读取,那么方法底层就会用 IDEA的默认编码方式 UTF-8 进行解码。

这时编码(GBK)和解码(UTF-8)方式不统一,就会产生乱码。

所以真正的解决方式,应该是:用字符流进行读取,但读取的同时要限定为根据 GBK 进行解码。 

2.定义 

转换流:是字符流和字节流之间的桥梁

作用:

① 可以根据字符集一次读取多个字节,使得读取数据不会产生乱码(指定字符集进行读写)。

字节流想要使用字符流中的方法(比如:readLine 方法)。

细节:

① InputStreamReader 可以将 字节流 转换成 字符流

    OutputStreamWriter 可以将 字符流 转换成 字节流

两者本身都属于 字符流 中的一员父类是 Reader。

3.使用

(1)利用转换流按照指定字符编码读取

注意:

① 字符集是在转换流 InputStreamReader 的构造方法中添加,不是字节流 FileInputStream的构造方法中。(转换流本身就是字符流中的一员)

② 这种方式了解即可,因为在 JDK11 后,被淘汰了。

替代方案:JDK11后,FileReader 中定义了一个新的构造方法,可以限定字符集来读取数据

 而 FileReader 的父类,正是 InputStreamReader。

替代写法:

(2)利用转换流按照指定字符编码写出

public class WriteDemo1 {
    public static void main(String[] args) throws IOException {
        //1.创建转换流的对象
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\Users\\24285\\Desktop\\www.txt"), "GBK");
        //2.写出数据
        osw.write("你好你好");
        //3.释放资源
        osw.close();
    }
}

运行结果:

注意:

① 简体中文的 windows 系统中,ANSI 就代表 GBK 字符集。

② 同理,该方式也需了解即可,在 JDK11 后被淘汰了。

替代方案:JDK11后,FileWriter 中定义了一个新的构造方法,可以限定字符集来写出数据

public class WriteDemo2 {
    public static void main(String[] args) throws IOException {
        //1.创建字符流的对象并指定字符编码
        FileWriter fw = new FileWriter("C:\\Users\\24285\\Desktop\\www.txt", Charset.forName("GBK"));
        //2.写出数据
        fw.write("你好你好");
        //3.释放资源
        fw.close();
    }
}

Test1:将本地文件中的 GBK 文件,转成 UTF-8


Test2:利用字节流读取文件中的数据,每次读一整行,且不能出现乱码

① 字节流在读取中文的时候,由于读不完整,是会出现乱码的,但是字符流可以实现。

解决办法:字节流 通过 InputStreamReader 变成 字符(转换)流

② 字节流里面是没有读一整行的方法的,只有字符缓冲流才可以实现。

解决办法:字符(转换)流 通过 BufferedReader 变成 字符缓冲流。

BufferedReader 的构造方法中的参数是一个 Reader 接口,可以利用接口多态传递子类对象。

public class ReadDemo3 {
    public static void main(String[] args) throws IOException {
        //1.创建字节流
        FileInputStream fis = new FileInputStream("IO\\a.txt");
        //2.字节流变成字符流
        InputStreamReader isr = new InputStreamReader(fis);
        //3.字符流变成字符缓冲流
        BufferedReader br = new BufferedReader(isr);
        //读数据
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        //释放资源
        br.close();
    }
}

转变过程可以一步写完:

BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("IO\\a.txt")));

七、序列化流和反序列化流

在实际应用中,会创建很多对象,我们需要将这些对象的信息保存到文件中以持久化保存。

且保证存到文件中的数据,用户无法看懂,即无法修改。

在下次启动程序时,可以从文件中再次读取对象的信息,并加载到程序中。

1.序列化流 / 对象操作输出流 

作用:可以把 Java中的对象写到本地文件中

注意:直接使用对象输出流将对象保存到文件中,会出现 NotSerializableException 异常。

解决方案:需要让 JavaBean 类实现 Serializable 接口。

在 Serializable 接口中,没有任何抽象方法,这种接口被称为:标记型接口。(如:Cloneable)

一旦实现该接口,就表示当前类的对象可以被序列化。

运行结果:

注意:序列化流写到文件中的数据,是不能修改的。

        不论修改什么,一旦修改,就无法再次读取回来了。

2.反序列化流 / 对象操作输入流 

作用:可以把序列化到本地文件中的对象,读取到程序中来。

3.细节

(1)序列号

如果一个类实现了 Serializable 接口,就表示这个类的对象是可序列化的。

Java 会在底层根据这个类的所有内容(成员变量,静态变量,构造方法,成员方法等)进行计算,计算出一个 long 类型的序列号(版本号)。

此时,如果创建对象,在对象中,就包含了这个版本号。

 用序列化流将这个对象写进本地文件中时,也会将版本号写进本地文件当中。

但如果此时,修改了 JavaBean类, Java 在底层就会重新计算版本号。

这时在进行反序列化,将文件中的对象数据进行读取,就会产生错误。

错误原因: 文件中的版本号,和 JavaBean 中的版本号不匹配。

由于在开发中,JavaBean 类不可能不会修改,所以办法只有一个:保证版本号唯一。

解决方案:给 JavaBean 类添加一个静态常量 serialVersionUID(版本号、序列号)

这时,重新将对象数据序列化保存到文件中,无论 JavaBean 类怎么修改,版本号都是该静态常量的值,不会再发生改变。

(2)transient 关键字

Question:如果一个对象的某个成员变量的值,不想被序列化(即不想被写道文件中),如何实现?

解决方案:给该成员变量添加  transient 关键字,该关键字标记的成员变量,不参与序列化过程。

将 stu 进行序列化保存到文件中后,这时在进行反序列化,结果如下: 

(3)反序列化多个对象 

Question:如果自定了多个对象进行序列化到文件中,现在想要反序列化,但不知道对象个数,如何操作?

public class WriteDemo2 {
    public static void main(String[] args) throws IOException {
        //创建学生对象
        Student s1 = new Student("zhangsan",23,"上海");
        Student s2 = new Student("lisi",23,"北京");
        Student s3 = new Student("wangwu",23,"南京");

        //1.创建序列化流对象/对象操作输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("IO\\a.txt"));
        //2.写出数据
        oos.writeObject(s1);
        oos.writeObject(s2);
        oos.writeObject(s3);
        //3.释放资源
        oos.close();
    }
}

我们知道,一次 readObject 方法会读取一个对象

如果我们读到文件末尾,该方法是否会返回 -1或者 null 呢?

结果发现:当读到文件末尾时,readObject 方法会返回一个 EOFException 异常


在书写代码过程中,我们不能主动的去制造异常(一直读取,直到遇到异常)。

所以,在将多个对象序列化到本地文件时,规定:

将这些所有的对象,统一存放到 ArrayList 集合当中,然后序列化 ArrayList 对象。

在 ArrayList 类本身,同样实现了 Serializable 接口,并定义了静态常量 serialVersionUID。

这时,在反序列化读取所有对象时,就不需要考虑对象个数,直接对 list 进行增强 for 遍历即可。

八、打印流

 特点:

① 打印流只操作文件目的地,不操作数据源(只能写,不能读)。

特有的写出方法可以实现:数据原样写出。

特有的写出方法,可以实现:自动刷新,自动换行(打印一次数据 = 写出 + 换行 + 刷新)。

1.字节打印流

(1)构造方法

细节:

 ① 即使传递的参数为 File 对象或者 String 字符串所表示的路径,方法底层也会根据所传递的路径,创建一个字节基本流的 FileOutputStream 对象。

② autoFlush 参数表示为是否自动刷新,但字节基本流底层没有缓冲区,开不开自动刷新都一样

(2)成员方法

public class WriteDemo {
    public static void main(String[] args) throws FileNotFoundException {
        //1.创建字节打印流的对象
        PrintStream ps = new PrintStream(new FileOutputStream("IO\\a.txt"), true, Charset.forName("UTF-8"));
        //2.写出数据
        ps.write(97);//常规方法
        //特有方法
        ps.println();
        ps.println(97);
        ps.printf("%s爱上了%s%n", "阿珍", "阿强");//%n是换行占位符
        ps.print(true);
        //3.释放资源
        ps.close();
    }
}

 运行结果:

 2.字符打印流

(1)构造方法

细节:字符基本流底层有缓冲区,想要自动刷新,必须要手动开启(autoFlush 设置为 true)。

(2)成员方法

3.标准输出流分析 

System 类是 Java 已经定义好的一个类,且是最终类,不能再有其他子类。

System 类中定义了一个 PrintStream 类型的静态变量 out 。

这个字节打印流的对象不需要我们手动创建,是 JVM 启动之后,自动创建的。默认指向控制台。

这是一个特殊的字节打印流,也被称为标准输出流,所以代码可写为:

public class Test {
    public static void main(String[] args) {
        //获取标准输出流
        PrintStream ps=System.out;
        //调用打印流中的println方法
        ps.println("123");
    }
}

注意:

标准输出流在系统中是唯一的,是不能被关闭的。

如果关闭,将不能在继续打印数据到控制台,除非程序重新执行。

 九、解压缩流和压缩流

1.解压缩流 

压缩包中的每一个文件或者文件夹,在 Java 中都是一个 ZipEntry 对象。

解压的本质:把每一个文件或者文件夹看成 ZipEntry 对象按照层级拷贝到本地另一个文件夹中。

public class UnzipDemo {
    public static void main(String[] args) throws IOException {
        //解压的本质:把压缩包中的每一个或者文件夹读取出来,按照层级拷贝到目的地中

        //创建一个File对象,表示要解压的压缩包
        File src = new File("D:\\aaa.zip");
        //创建一个File对象,表示解压到目的地
        File dest = new File("D:\\");
        //解压
        unzip(src, dest);
    }

    public static void unzip(File src, File dest) throws IOException {
        //1.创建一个解压缩流,用来读取压缩包中的数据
        ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
        //2.遍历获得压缩包中的每一个zipEntry对象
        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            if (entry.isDirectory()) {
                //是文件夹:需要在目的地dest处创建一个同样的文件夹
                //dest : D:\\
                //entry.toString() : dest中的每一个文件或者文件夹名(如:aa/,bb/)
                File file = new File(dest, entry.toString());
                file.mkdirs();
            } else {
                //是文件:需要读取到压缩包中的文件,并把它们存放到目的地dest文件夹
                //dest : D:\\
                //entry.toString() : dest中的每一个文件或者文件名(如:a.txt,aa/aa.txt)
                FileOutputStream fos = new FileOutputStream(new File(dest, entry.toString()));
                int b;
                //用解压缩流进行读取数据
                while ((b = zip.read()) != -1) {
                    fos.write(b);
                }
                fos.close();
                //表示在压缩包中的一个文件处理完毕了
                zip.closeEntry();
            }
        }
        //3.释放资源
        zip.close();
    }
}

注意:

① 在 Java中,只能识别 .zip 格式的压缩包

② getNextEntry 方法可以依次读取到该压缩包中所有的文件和文件夹,不需要手动递归,读完返回 null。

③ ZipEntry 类中并没有 isFile 方法判断是否是文件,但有 isDirectory 方法判断是否是目录(文件夹)

④ 解压缩流 ZipInputStream 包装自 FileInputStream,可以进行数据读取。

⑤ closeEntry 方法表示在压缩包中的一个文件处理完毕了。

2.压缩流

压缩的本质:把每一个文件或者文件夹看成 ZipEntry 对象 放到压缩包里。

(1)压缩单个文件

public class ZipDemo1 {
    public static void main(String[] args) throws IOException {
        //创建一个File对象,表示要压缩的文件
        File src = new File("D:\\a.txt");
        //创建一个File对象,表示压缩包放在哪里
        File dest = new File("D:\\");
        //压缩
        tozip(src, dest);
    }

    public static void tozip(File src, File dest) throws IOException {
        //1.创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest, "a.zip")));
        //2.创建zipEntry对象,表示压缩包中的每一个文件或者文件夹
        ZipEntry entry = new ZipEntry("a.txt");
        //3.把zipEntry对象放到压缩包当中(只是创建了文件,没有数据)
        zos.putNextEntry(entry);
        //4.把src文件中的数据写到压缩包中
        FileInputStream fis = new FileInputStream(src);
        int b;
        while ((b = fis.read()) != -1) {
            zos.write(b);
        }
        zos.closeEntry();
        zos.close();
    }
}

注意:

① ZipEntry 构造方法中的参数,表示压缩包中的路径。

② putNextEntry 方法可以将文件或者文件夹放到压缩包中。只会创建文件,但不会将文件中的数据写进去,仍需要拷贝数据。

(2)压缩文件夹

public class ZipDemo2 {
    public static void main(String[] args) throws IOException {
        //创建一个File对象,表示要压缩的文件夹
        File src = new File("D:\\aaa");
        //创建一个File对象,表示压缩包放在哪里(压缩包 D:\\aaa.zip 的父级路径)
        File destParent = src.getParentFile();//D:\\
        //创建一个File对象,表示压缩包的位置
        File dest = new File(destParent, src.getName() + ".zip");//D:\\aaa.zip
        //创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        //压缩
        tozip(src, zos, src.getName());//aaa
        //释放资源
        zos.close();
    }

    public static void tozip(File src, ZipOutputStream zos, String name) throws IOException {
        //1.进入src文件夹
        File[] files = src.listFiles();
        //2.遍历数组
        for (File file : files) {
            if (file.isFile()) {
                //3.判断-文件,变成ZipEntry对象,放入到压缩包当中
                ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
                zos.putNextEntry(entry);
                //读取文件中的数据,写到压缩包
                FileInputStream fis = new FileInputStream(file);
                int b;
                while ((b = fis.read()) != -1) {
                    zos.write(b);
                }
                fis.close();
                zos.closeEntry();
            } else {
                //4.判断-文件夹,递归
                tozip(file, zos, name + "\\" + file.getName());
            }
        }
    }
}

细节:

① zos 构造方法中的路径:

zos 中的路径表示的是压缩包的位置,也就是 D:\\aaa.zip。

但如果以后 源文件夹 名字变成 bbb,那么 zos 中的路径也得变成 D:\\bbb.zip,很不方便。

为此,我们可以通过 src.getParentFile() 获取 源文件夹 的父级路径,也就是 D:\\。

再和源文件夹的名字 src.getName() 进行拼接,就变成 D:\\aaa。

这样如果 源文件夹名字发生改变,zos 中的路径就会自动改变。

② tozip 参数三的原因:

直接输出 file 对象,打印的是绝对路径(D:\\aaa\\aa\\a.txt)

通过 getName 方法,打印的是文件名(a.txt)

而 ZipEntry 构造方法中的参数,表示压缩包中的路径,所以需要的是:aaa\\aa\\a.txt。

所以首次递归传入 src.getName(),也就是 aaa。

后续遇到文件夹,再通过递归传入 name + "\\" + file.getName(),也就是aaa\\aa

③ putNextEntry 将文件放入压缩包中时,如果 entry 的路径中父级路径不存在,会自动创建父级路径。

所以在 tozip 方法中,遍历到文件夹直接递归即可,不用创建文件夹。

十、常用工具包

1.Commonis-io

Commonis-io 是 Apache开源基金组织提供的一组有关 IO 操作的开源工具包。

作用:提高 IO 流的开发效率

public class Test {
    public static void main(String[] args) throws IOException {
        File src = new File("IO\\a.txt");
        File dest = new File("IO\\b.txt");
        //复制文件
        FileUtils.copyFile(src, dest);
    }
}

导入 jar 包后,原先复杂的操作,现在一行即可搞定。

2.Hutool

详细用法请参考以下网址:

官网:https://hutool.cn/

 

API文档:http://​ https://apidoc.gitee.com/dromara/hutool/ ​​​​​​​​

中文使用文档:https://hutool.cn/docs/#/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/740414.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日一题——Python代码实现PAT甲级1059 Prime Factors(举一反三+思想解读+逐步优化)五千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 代码点评 时间复杂度分析 空间复杂度分析 改进建议 我要更强 时间复杂度…

大学生综合能力测评系统(安装+讲解+源码)

【毕设者】大学生综合能力测评系统(安装讲解源码) 分为管理员老师学生端 技术栈 后端: SpringBoot Mysql MybatisPlus 前端: Vue Element 功能截图: 给你安装运行

从WebM到MP3:利用Python和wxPython提取音乐的魔法

前言 有没有遇到过这样的问题&#xff1a;你有一个包含多首歌曲的WebM视频文件&#xff0c;但你只想提取其中的每一首歌曲&#xff0c;并将它们保存为单独的MP3文件&#xff1f;这听起来可能有些复杂&#xff0c;但借助Python和几个强大的库&#xff0c;这个任务变得异常简单。…

开源的网络瑞士军刀「GitHub 热点速览」

上周的开源热搜项目可谓是精彩纷呈&#xff0c;主打的就一个方便快捷、开箱即用&#xff01;这款无需安装、点开就用的网络瑞士军刀 CyberChef&#xff0c;试用后你就会感叹它的功能齐全和干净的界面。不喜欢 GitHub 的英文界面&#xff1f;GitHub 网站汉化插件 github-chinese…

Vite: 关于预构建的毫秒级响应

概述 在我们的项目代码中&#xff0c;我们所说的模块代码其实分为两部分 一部分是源代码&#xff0c;也就是业务代码另一部分是第三方依赖的代码&#xff0c;即 node_modules 中的代码 Vite 是一个提倡 no-bundle 的构建工具&#xff0c;相比于传统的 Webpack能做到开发时的模…

【通用技巧】自动获取日志存放路径,无需手动修改配置文件

我们在部署环境的时候&#xff0c;常常会手动修改一些配置文件的存放地址&#xff0c;比如日志的路径、截图的路径&#xff0c;这是因为我们的环境不一样&#xff0c;部署应用的位置也不一样导致的。如果位置写死了&#xff0c;那么就会造成通用性很差&#xff0c;所以我们经常…

(深度学习记录)第TR5周:Transformer中的位置编码详解

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 &#x1f3e1;我的环境&#xff1a; 语言环境&#xff1a;Python3.11.4编译器&#xff1a;Jupyter Notebooktorcch版本&#xff1a;2.0.…

[FreeRTOS 基础知识] 信号量 概念

文章目录 信号量定义信号量特性 信号量定义 信号量是一个抽象的数据类型&#xff0c;通常包含一个整数值以及一个等待该值变为正数的任务列表&#xff08;也称为等待队列&#xff09;。信号量的整数值代表了系统中某种资源的可用数量。 在操作系统中信号量用于控制对共享资源访…

[FreeRTOS 内部实现] 信号量

文章目录 基础知识创建信号量获取信号量释放信号量信号量 内部实现框图 基础知识 [FreeRTOS 基础知识] 信号量 概念 创建信号量 #define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) #define semSEMAPHORE_QUEUE_ITEM_LENGTH ( ( uint8_t ) 0U ) #define xSe…

elementUI相关知识及搭建使用过程

​​​​​​ 目录 ​​​​​​ 一.elementUI相关的知识 1.什么是elementUI 2.如何在创建的项目中使用elementUI的组件(1)安装 ​ (2)在项目的main.js中引入elementUI (3)使用elementui里的组件 一.elementUI相关的知识 1.什么是elementUI Element&#xff0c;一套为开…

基于Pytorch框架构建LeNet-5模型

Pytorch 一、训练模型1.导入必要的库2.设置超参数3.数据预处理4.读取数据 二、定义卷积神经网络1.定义卷积神经网络2.定义学习率3.实例化模型并且移动到GPU4.选择优化器 三、定义调整学习率的函数1.定义调整学习率的函数 四、训练模型1.设置模型为训练模式2.遍历训练数据加载器…

嵌入式计算器模块实现

嵌入式计算器模块规划 计算器混合算法解析 上面我们的算法理论已经完善, 我们只用给一个混合运算式, 计算器就可以帮助我们计算出结果. 但是存在一个痛点, 每次计算算式,都要重新编译程序, 所以我们想到了, 利用单片机, 读取用户输入的按键, 组成算式, 输入给机器, 这样我们就…

Docker编译nanopc-t4源码流程介绍

官方文档 Android系统编译 vnc加环境变量配置 https://github.com/friendlyarm/docker-cross-compiler-novnc 下载 git clone https://github.com/friendlyarm/docker-ubuntu-lxde-novnc cd docker-ubuntu-lxde-novnc docker build --no-cache -t docker-ubuntu-lxde-novnc …

板凳--------第20章-信号:基本概念1

tlpi_hdr.h头文件使用及设置 liao__ran 于 2020-09-29 15:12:01 发布 阅读量1.6k 收藏 5 点赞数 1 分类专栏&#xff1a; linux系统编程手册 版权 linux系统编程手册 专栏收录该内容 7 篇文章 1 订阅 订阅专栏 使用的头文件&#xff0c;主要如下&#xff1a; ename.c.inc erro…

python实训day4

1、查看数据库的版本 2、查看当前用户 3、查看当前数据库 4、计算表达式的结果; 任何一个数据库,无论大小,都首先是一个超级计算器 5、查看当前MySQL环境中所有的数据库; 系统数据库(只能看)和自定义数据库(任何操作) 6、先建数据库 gaoming 7、如果表已经存在,则创建不能成功 …

【经典算法OJ题讲解】

1.移除元素 经典算法OJ题1&#xff1a; 移除元素 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/remove-element/desc…

【文字+视频教程】在手机上用文生软件平台CodeFlying开发一个整蛊版《Flappy Bird》

前言&#xff1a; 在之前的文章中我们介绍了国内首家文生软件平台码上飞CodeFlying&#xff0c;并且教给了大家如何用它来开发复杂的项目信息管理系统以及恶搞拼图小游戏等。今天就继续给大家带来一起用码上飞开发整蛊版《Flappy Bird》小游戏的教程。 老规矩&#xff0c;咱还…

node.js环境安装以及Vue-CLI脚手架搭建项目教程

目录 ▐ vue-cli 搭建项目的优点 ▐ 安装node.js环境 ▐ 搭建vue脚手架项目 ▐ 项目结构解读 ▐ 常用命令 ▐ 创建组件 ▐ 组件路由 ▐ vue-cli 搭建项目的优点 传统的前端项目架构由多个html文件&#xff0c;且每个html文件都是相互独立的&#xff0c;导入外部组件时需…

【计算机毕业设计】基于Springboot的网页时装购物系统【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…

solidworks安装教程 - 解决安装后服务不能自动启动问题

Solidworks安装教程&#xff0c;有些同学的电脑过于复杂&#xff0c;产生了正常的服务不能启动。 前面的有个重要的操作操作界面有&#xff0c;大家应该是执行了&#xff1a; 那么我们有变通的方法可以让这个服务启动&#xff1a; 1. cmd用管理员启动 2. 测试下如下命令是否…