Java基础:IO流

news2024/11/13 12:18:22

目录

一、定义

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/#/

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】Linux编译器(gcc,g++)与动静态链接库

对于一个 C 程序&#xff0c;从源文件到形成可执行程序一共要进行四步&#xff1a;预处理、编译、汇编、链接 。 接下来&#xff0c;我们用 gcc 分别演示这四个过程。 一、预处理(进行宏替换) 预处理中&#xff0c;需要完成头文件的展开、宏替换、去注释、条件编译等工作。 …

说一说三大运营商的流量类型,看完就知道该怎么选运营商了!

说一说三大运营商的流量类型&#xff0c;看完就知道该怎么选运营商了&#xff1f;目前三大运营商的流量类型大致分为通用流量和定向流量&#xff0c;比如&#xff1a; 中国电信&#xff1a;通用流量定向流量 电信推出的套餐通常由通用流量定向流量所组成&#xff0c;通用流量…

《数字图像处理》实验报告一

一、实验任务与要求 1、用 matlab 编写空间域点处理操作处理给定的几幅图像&#xff0c;要求&#xff1a; 使用 imread 读取当前工作目录下的图像设计点处理操作并用代码实现处理用 imnshow 显示处理后的图像用 imwrite 保存处理后的图像 2、提交内容&#xff1a;m文件 实验…

假冒国企现形记:股权变更视角下的甄别分析

启信慧眼-启信宝企业版 假冒国企公告2024-06-07&#xff0c;中粮集团有限公司官网发布《关于冒名中粮企业名单公告》。公告显示&#xff0c;”有不法分子通过伪造相关材料等方式&#xff0c;以我集团子公司名义开展业务&#xff0c;进行虚假宣传。经核实&#xff0c;上述公司假…

【SpringCloud】API网关(Spring Cloud Gateway)

本文基于上一篇http://t.csdnimg.cn/q3YrK 使用抽取的方案使用feign的基础上使用Spring Cloud Gateway。 API网关 API网关&#xff08;简称网关&#xff09;也是一个服务&#xff0c;通常是后端服务的唯一入口。它就像是整个微服务架构的门面&#xff0c;所有的外部客户端访问…

Python学习笔记13:进阶篇(二),类的继承与组合

类的继承 我们在编写一系列的类的时候&#xff0c;会发现这些类很相似&#xff0c;但是又有各自的特点和行为。在编写这些类的时候&#xff0c;我们可以把相同的部分抽象成一个基类&#xff0c;然后根据其他不同的特点和行为&#xff0c;抽象出子类&#xff0c;继承这个基类。…

python实训day4

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

刷题之小欧的平均数(卡码网)

小欧的平均数 这道题不看解析的话完全没有思路&#xff0c;连题目都没读明白&#xff0c;甚至看了评论答出来了还是不知道为什么&#xff0c;有知道的朋友可以教教我 #include<iostream> using namespace std;int main() {int x,y,z;cin>>x>>y>>z;//…

oracle12c到19c adg搭建(六)切换后12c备库服务器安装19c软件在19c主库升级数据字典后尝试同步

一、安装19c软件 参考文章oracle12c到19c adg搭建&#xff08;三&#xff09;oracle19c数据库软件安装 二、原主库尝试通过19c软件启动数据库 2.1复制12c的相关参数文件和密码文件到19c目录 注意:密码文件需要从已切换主库19c传过来 [oracleo12u19p ~]$ cd /u01/app/oracle…

亲测有效!性能压测异常竟能自动化分析!

性能压测是一种评估系统运行效率和稳定性的方法&#xff0c;通过模拟真实的使用场景和负载条件&#xff0c;对系统进行压力测试和负载测试&#xff0c;并对测试结果进行分析&#xff0c;以评估系统的性能&#xff0c;其中性能压测结果分析是性能压测的重要环节。以往的性能压测…

ONLYOFFICE 桌面编辑器 8.1 发布:全新功能齐备的 PDF 编辑器、丰富的幻灯片版式

前言 在当前数字化迅速发展的时代&#xff0c;办公软件套件作为企业日常工作的核心工具&#xff0c;其功能的全面性和使用的便捷性直接影响着工作效率和团队协作的效果。在众多办公软件中&#xff0c;ONLYOFFICE凭借其卓越的性能和丰富的功能&#xff0c;备受用户青睐。近日&a…

深入了解 GPT-4 和 ChatGPT 的 API---OpenAI Playground

文章目录 基本概念OpenAI API 提供的可用模型在 OpenAI Playground 中使用 GPT 模型 掌握GPT-4 和 ChatGPT 的 API 的使用方法&#xff0c;以便有效地将它们集成到 Python 应用程序中。首先&#xff0c;需要了解 OpenAI Playground。这将使你在编写代码之前更好地了解模型。接着…

React尚硅谷115-126(setState、Hooks、Fragment、context、组件优化、renderprops

122&#xff0c;context 只能用value传&#xff0c;可以传对象&#xff0c;字符串 一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信 使用&#xff1a; 创建Context容器对象&#xff1a; const XxxContext React.createContext() 渲染子组时&#xff0c;外面包…

MIT6.s081 2021 Lab Utilities

Boot xv6 按照示例切换到 util 分支后&#xff0c;看到目录下包含 Makefile 文件&#xff0c;执行 make qemu 即可。 sleep 思路 借助系统调用 sleep 实现一个命令行程序&#xff0c;关键是要找到封装了系统调用的 C 函数的位置&#xff0c;根据提示&#xff1a; … user/u…

软件测试--花了我一周时间整理各种测试模板

2024软件测试面试刷题&#xff0c;这个小程序&#xff08;永久刷题&#xff09;&#xff0c;靠它快速找到工作了&#xff01;&#xff08;刷题APP的天花板&#xff09;-CSDN博客跳槽涨薪的朋友们有福了&#xff0c;今天给大家推荐一个软件测试面试的刷题小程序。https://blog.c…

北邮《计算机网络》MAC子层笔记

文章目录 缩写复习MAC层所在层次动态分配信道算法们的简要介绍信道的五条基本假设多路访问的协议&#xff08;理论上的协议&#xff09;aloha协议CSMA协议其他冲突避免协议无线局域网协议 &#xff0c;MACA 以太网协议802.3&#xff08;实际协议&#xff0c;刚刚是理论&#xf…

光猫BOB 功率调测误差分析与校验指南

DDM&#xff08;Digital Diagnostic Monitoring&#xff09;数字诊断监控技术&#xff0c;是光模块中使用的技术&#xff0c;以便用户能够监控光模块的实时参数。这些参数包括工作温度、工作电压、工作电流、发射和接收光功率等&#xff0c;还可以显示模块的常规波长、速率、传…

在Vue表单中设置缺省值

有个需求&#xff0c;在新增记录的时候&#xff0c;打开新增页面&#xff0c;员工姓名处获取到当前登录用户的用户名&#xff0c;并将其设置为缺省值。 /** 新增按钮操作 */handleAdd() {this.reset();this.open true;// this.form.employeeName this.$store.state.user.name…

计算机网络 DHCP以及防护

一、理论知识 1.DHCP&#xff1a;用于在网络中自动分配IP地址及其他网络参数&#xff08;如DNS、默认网关&#xff09;给客户端设备。 2.VLAN&#xff1a;逻辑上的局域网分段&#xff0c;用于隔离和管理不同的网络流量。 3.DHCP地址池&#xff1a;为每个VLAN配置不同的DHCP地…

【语义分割系列】基于cityscape的DDRNet算法

基于cityscape的DDRNet算法 前言 DDRNet是专门为实时语义分割设计的高效主干。该模型由两个深度分支组成,在这两个分支之间执行多次双边融合,并且还设计了一个新的上下文信息抽取器,名为深度聚合金字塔池模块(DAPPM),用于扩大有效的接受域,并基于低分辨率特征映射融合…