Java进阶_05_IO流


本文介绍Java中的IO流机制。

1. 概述

输入就是将数据从硬盘读取到内存中,输出就是将数据从内存传送到硬盘中。其实输入输出就是相对于内存来说的,数据流向内存,就是输入(读);数据流出内存就是输出(写)。

image-20220707151917660

除了上面按照流的方向分为输入流、输出流,还可以按照读取方式不同进行分类,如

  1. 一次读取一个字节(一次读取8个二进制位),这种字节流是万能的,可以读取任何类型的文件,如视频、音频、文件等等。
  2. 一个读取一个字符,这种字符流是为了方便读取普通文本文件而存在的(如txt),不能读取声音、图片等有格式的文件,连word都不能。比如英文字符占1个字节,中文占2个字节。字符流可以区分字符,直接将其读取出来。

综上所述,流按照方向分为两种,按照读取方式分为两种。此时输入流可分为字节输入流和字符输入流,输出流也分为字节输出流和字符输出流。有如下四大家族【都是抽象类】:

java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流

只要以Stream结尾的,都是字节流;只要以Reader、Writer结尾的都是字符流。

  1. 上面四个类,都实现了java.io.Closeable接口,都是可关闭的。因此,这些流在使用完毕之后,一定要关闭。可看成是管道,都是占用资源的,需要需要关闭。
  2. 上面的输出流,无论是字节流和字符流,都实现了java.io.Flushable接口,都是可刷新的。输出流在用完之后,一定要刷新,刷新表示将管道中剩余未输出的数据强行输出完,即清空管道。【一般情况下,只有管道满了之后才会自动刷新】。

Java中提供的具体流有如下16个。

java.io.FileInputStream 文件专属
java.io.FileOutputStream 文件专属
java.io.FileReader 文件专属
java.io.FileWriter 文件专属
java.io.BufferedInputStream 缓冲流专属
java.io.BufferedOutputStream 缓冲流专属
java.io.BufferedReader 缓冲流专属
java.io.BufferedWriter 缓冲流专属
java.io.DataInputStream 数据流专属
java.io.DataOutputStream 数据流专属
java.io.ObjectInputStream 对象专属流
java.io.ObjectOutputStream 对象专属流
java.io.PrintWriter 标准输出流
java.io.PrintStream 标准输出流
java.io.InputStreamReader 将字节流转换为字符流(输入)
java.io.OutputStreamWriter 将字节流转换为字符流(输出)

2. 文件专属流

2.1 FileInputStream(重点)

2.1.1 read()

文件字节输入流,是万能的,可以读取任意文件。FileInputStream类有一个read方法,一次可读取一个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class IOTest01 {

public static void main(String[] args) {

FileInputStream fis = null;
try {
fis = new FileInputStream("D:\\others\\study_source\\Java\\test\\src\\io\\test.txt");

// 该方法,读取一个字节,返回读取到的字节的ascii
// 相当于迭代器,有一个游标
// 当到达文件结尾时,返回-1
// 这种一次读取一个字节,效率有点低,大部分时间都浪费在管道中。

int read;

while((read = fis.read()) != -1) {
System.out.println(read);
}

} catch (IOException e) {
e.printStackTrace();
} finally {

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

}
}
}

除了读取一个字节,可传入一个byte数组,一次最多读取byte数组长度的字节数量,效率较高。此时该方法返回值为读取到的字节数量,而读取到的内容则保存在byte数组中。后面的读取会覆盖数组中前面读取的内容。如果到文件末尾,那么仍然返回-1。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class IOTest02 {

public static void main(String[] args) {

FileInputStream fis = null;
try {
fis = new FileInputStream("D:\\others\\study_source\\Java\\test\\src\\io\\test.txt");

// 此时read保存的值为读取的字节数量,bytes数组存储的是内容
byte[] bytes = new byte[10];
int read;

while((read = fis.read(bytes)) != -1) {

// 输出读取到的内容
// for (int i = 0; i < read; i++) {
// System.out.print(bytes[i] + "\t");
// }
// System.out.println();

System.out.println(new String(bytes, 0, read));
}

} catch (IOException e) {
e.printStackTrace();
} finally {

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

2.1.2 skip()

跳过几个字节不读。

2.2.3 available()

返回流中剩余的没有读到的字节数量。

2.2 FileOutputStream(重点)

FileOutputStream是字节输出流。

2.2.1 write()

该方法是写入文件,参数是byte数组,也可以是指定byte数组中的指定区间。

注意,下面方式是将原文件清空再写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class IOTest04 {

public static void main(String[] args) {

FileOutputStream fos = null;

try {
fos = new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\testOut2.txt");

byte[] bytes = {97, 98, 99, 100};

fos.write(bytes, 0, 2);

fos.flush();

} catch (IOException e) {
e.printStackTrace();
} finally {

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

要想以追加的形式写入内容,则需要在创建输出流对象的时候,传入参数true,表示append为true。

1
fos = new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\testOut2.txt", true);

对于字符串等内容,需要将其转换成byte数组,才能写入。注意,最后输出流要flush。

2.3 文件拷贝

可利用输入流读取到内存,然后获取到byte数组,调用输出流,将byte数组的内容输出到文件中【注意,输出流对象以追加形式创建】。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class IOTest05 {

public static void main(String[] args) {
System.out.println();

FileInputStream fis = null;
FileOutputStream fos = null;

try {
fis = new FileInputStream("D:\\others\\study_source\\Java\\test\\src\\io\\testOut2.txt");

fos = new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\testOut3.txt", true);

byte[] bytes = new byte[2];
int read;

// 循环读写
while((read = fis.read(bytes)) != -1) {
fos.write(bytes, 0, read);
}

fos.flush();

} catch (IOException e) {
e.printStackTrace();
} finally {

if (fis != null) {

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

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

2.4 FileReader

文本字符输入流,只能读取普通文本。和FileInputStream类似,只不过参数不是byte数组,而是char数组,每次读取的内容都存储到char数组中

2.5 FileWriter

文本字符输出流,只能写入普通文本。该方法可以写入char数组,也可以是字符串,而无需是byte数组。

3. 缓冲专属流

3.1 BufferedReader

这种字符输入流,自带缓冲区,在读取的时候,无需再传入数组参数存放读取的内容。可以查看源码,该类内部有一个数组,保存读取的数据。并且读取方法有多种,可直接读取一行【不带换行符】。

注意,该类创建对象,需要传入Reader对象,而FileReader等就是继承了该抽象类,可作为参数传入。传入的流就是要读取的文件,被称为节点流。而创建的BufferedReader对象,则被称为包装流/处理流。

注意,关闭的时候只需要关闭包装流即可,因为包装流会关闭节点流。

另外,因为对于FileInputStream,因为不是Reader类型,不能传入BufferedReader,但是可通过InputStreamReader将FileInputStream转换为Reader类型,然后传入。

3.2 BufferedWriter

同样,该类对象创建需要Writer流,FileWriter可作为参数传入。可直接写入字符串。也可利用OutputStreamWriter将OutputStream转换为Writer后传入。

4. 数据流(了解)

这两个流DataInputStream、DataOutputStream是数据专属的流,可将数据连同数据类型一并写入文件。生成的文件不是普通文本文档,用记事本打不开。文件只能由DataInputStream读取文件。

5. 标准输出流(重点)

注意,标准输出流不需要手动关闭。前面最经常使用的语句就是:System.out.println(),那么这个表达式,System.out就是获取到的是PrintStream类型对象,通过该对象调用println方法来实现输出。

另外,可通过标准输出流来改变JVM的输出流方向,如下所示【日志工具】:

1
2
3
4
5
6
7
8
9
// 设置标准输出流不再指向控制台,而是指向log文件。
PrintStream ps = new PrintStream(new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\log"));

// 修改系统的输出方向为该标准输出流
System.setOut(ps);

// 再次进行系统输出
System.out.println("hello world");
System.out.println("hello kitty");

6. 对象流(重点,序列化)

我们知道,在Java程序运行过程中,会创建对象,而前面的IO流仅仅是将普通数据存储到文件中,那能不能将对象存储到硬盘中呢?当然可以,将Java对象存储到硬盘的过程称为序列化(Serialize),将对象拆分成小部分,每部分都有一个编号,转换成流,输入到硬盘中。将硬盘中的对象数据重新恢复到内存中,使其成为Java对象,这被称为反序列化(DeSerialize)。

注意,如果一个类要序列化,必须实现Serializable接口。这个接口其实什么方法也没有,只是起到一个标识的作用,当JVM看到这个类实现这个接口后,会对这个类进行特殊待遇,为该类自动生成一个序列化版本号。

那么序列化版本号有什么用呢?

ObjectOutputStream类用于序列化,ObjectInputStream类用于反序列化。

序列化如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class IOTest09 {

public static void main(String[] args) throws IOException {

Student s = new Student("张三", 19, 175);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\others\\study_source\\Java\\test\\src\\io\\student"));

// 序列化
oos.writeObject(s);

// 刷新
oos.flush();

// 关闭
oos.close();
}
}

class Student implements Serializable {
private String name;
private int age;
private int height;

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
}

反序列化如下所示,因为已经实现了toString方法,所以可直接输出,输出的属性和创建的对象是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IOTest10 {

public static void main(String[] args) throws Exception {
System.out.println();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\others\\study_source\\Java\\test\\src\\io\\student"));

Object o = ois.readObject();

System.out.println(o);

ois.close();

}
}

可将对象放入到集合中,直接序列化集合,这样就可一次性序列化多个对象。另外,如果序列化对象的时候,不希望某个属性值序列化,那么可在该属性上加入transient关键字修饰。之后反序列化的时候获取到的属性值就是null。

注意,本质上说,反序列化时,是依据的class文件。那么如果在反序列化之前,将java文件修改,重新编译之后,生成新的class文件以及新的序列化版本号。反序列化的时候就会依据新的class文件,此时就会反序列化失败,因为两个序列化版本号不一样【java.io.InvalidClassException】。

1
Exception in thread "main" java.io.InvalidClassException: io.Student; local class incompatible: stream classdesc serialVersionUID = -218909040601832639, local class serialVersionUID = -423644352464462558

Java语言中采用什么机制来区分类的?

  1. 首先通过类名,如果类名不一样,肯定不是同一个类。
  2. 如果类名一样,就通过序列化版本号进行区分。

JVM在编译的时候会自动为该类生成序列化版本号。因为是自动生成的,此时如果后续修改了该类之后,之前序列化后的数据此时无法反序列化。

因此,如果想要后续反序列化,为了避免修改源码后自动生成新的序列化版本号。可手动生成版本号【相当于静态常量】,这样,后续无论怎么修改都是这个手动生成的版本号。

1
private static final long serialVersionUID = 1L;

注意,变量名只能是serialVersionUID。

7. java.io.File

File类是文件和目录路径名的抽象表示形式,该类和流没有关系,不能完成文件的读和写,仅仅能够表示路径名【可是文件夹,也可以是文件】。

通过File,可以得到目录,进而可通过IO流读取文件,实现文件夹的文件读取。简单代码如下所示,其中exists()方法表示判断file所表示的路径名是否存在:

1
2
3
4
5
6
7
8
9
10
public class IOTest08 {

public static void main(String[] args) {
System.out.println();

File file = new File("D:\\others\\study_source\\Java\\test\\src\\io\\loging");

System.out.println(file.exists());
}
}

7.1 常用方法

方法名 作用
File() 构造方法,参数为路径名(文件名)的字符串形式
exists() 判断该对象所表示的路径名是否存在
createNewFile() 以文件形式创建该对象所表示的路径名
mkdir() 以目录形式创建该对象所表示的路径名
mkdirs() 上面只是最后一级目录不存在可创建,本方法是允许多级目录不存在,然后创建
getParent() 以字符串形式获取该路径的父路径
getParentFile() 以对象形式获取父路径
getAbsolutePath() 获取该对象的绝对路径
delete() 删除该对象所表示的路径/文件
getName() 获取该对象所表示的名字
isDirectory() 判断该对象是否是目录
isFile() 判断该对象是否是文件

File类和String一样,有很多方法,比如该目录/文件的最后一次修改时间、文件大小、重命名、获取所有的子目录(listFiles())等等。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录