Java 中的 IO 流

Java 中的 IO 流


内容概要:

File 类

File 概述

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。具体功能如下:

具体功能:
创建一个文件/文件夹 删除文件/文件夹 获取文件/文件夹 判断文件/文件夹是否存在 对文件夹进行遍历 获取文件的大小

输出路径分隔符和文件名称分隔符的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 01-路径分隔符【例如环境变量里的分隔符】 windows系统使用“分号”,linux系统使用“冒号”
String pathSeparator = File.pathSeparator;
System.out.println(pathSeparator);

char pathSeparatorChar = File.pathSeparatorChar;
System.out.println(pathSeparatorChar);

// 02-文件名称分隔符 windows系统使用反斜杠\ linux系统使用正斜杠/
String separator = File.separator;
System.out.println(separator);

char separatorChar = File.separatorChar;
System.out.println(separatorChar);
}

构造方法

构造方法:
public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File 实例。 public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File 实例。 public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File 实例。

构造方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 01-public File(String pathname)
File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
System.out.println(file01); // 重写了 toString 方法,否则会打印内存地址


// 02-public File(String parent, String child)
File file02 = new File("F:\\", "00-BLOG-HOME");
System.out.println(file02);


// 03-public File(File parent, String child)
File parent = new File("F:\\");
File file03 = new File(parent, "00-BLOG-HOME");
System.out.println(file03);
}

小贴士: 一个 File 对象代表硬盘中实际存在的一个文件或者目录,无论该路径下是否存在文件或者目录,都不影响 File 对象的创建。

常用方法

获取功能的方法

获取功能的方法
public String getAbsolutePath() :返回此 File 的绝对路径名字符串。 public String getPath() :将此 File 转换为路径名字符串。 public String getName() :返回由此 File 表示的文件或目录的名称。 public long length() :返回由此 File 表示的文件的长度。

方法演示,代码如下:

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
public static void main(String[] args) {
// 01-public String getAbsolutePath()
File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
System.out.println(file01.getAbsolutePath());

File file02 = new File("00-BLOG-HOME");
System.out.println(file02.getAbsolutePath());


// 02-public String getPath()
System.out.println(file01.getPath());
System.out.println(file02.getPath());

System.out.println(file01);
System.out.println(file01.toString()); // 看源码知:toString 调用的就是 getPath() 方法


// 03-public String getName() ,获取的就是构造方法传递路径的结尾部分(文件/文件夹)
System.out.println(file01.getName());
System.out.println(file02.getName());


/**
* 04-public long length() :返回由此 File 表示的文件的长度。获取的是构造方法指定的文件的大小,以字节为单位。
* 注意 : 1、文件夹是没有大小概念的,不能获取文件夹的大小。2、如果构造方法中给出的路径不存在,那么length方法返回0
*/
// 获取文件大小,不存在返回 0
File file04 = new File("F:\\00-BLOG-HOME\\source\\about\\index.md");
System.out.println(file04.length());

// 获取文件夹大小,直接返回 0 ,因为文件夹没有大小,只有文件有大小
File file05 = new File("F:\\00-BLOG-HOME\\source\\about");
System.out.println(file05.length());
}

判断功能的方法

判断功能的方法
public boolean exists() :此 File 表示的文件或目录是否实际存在。 public boolean isDirectory() :此 File 表示的是否为目录。 public boolean isFile() :此 File 表示的是否为文件。

方法演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
// 01-public boolean exists() :用于判断构造方法中的路径是否存在
File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
System.out.println(file01.exists());

File file02 = new File("00-BLOG-HOME");
System.out.println(file02.exists());


/**
* 02-public boolean isDirectory() :用于判断构造方法中给定的路径是否以文件夹结尾
* 03-public boolean isFile() :用于判断构造方法中给定的路径是否以文件结尾
* 注意 : 1、电脑的硬盘中只有文件/文件夹,两个方法是互斥。2、这两个方法使用前提,路径必须是存在的,否则都返回 false
*/
if(file01.exists()){
System.out.println(file01.isDirectory());
System.out.println(file01.isFile());

System.out.println(file02.isDirectory());
System.out.println(file02.isFile());
}
}

创建和删除功能的方法

创建和删除功能的方法
public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。 public boolean delete() :删除由此 File 表示的文件或目录。 public boolean mkdir() :创建由此 File 表示的目录。 public boolean mkdirs() :创建由此 File 表示的目录,包括任何必需但不存在的父目录。

方法演示,代码如下:

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
public static void main(String[] args) throws IOException {
//demo01();
//demo02();
//demo03();
}

private static void demo03() {
// 04-public boolean delete() :可以删除构造方法路径中给出的文件/文件夹
// 注意:文件/文件夹删除成功,返回 true 。2、文件夹中有内容,不会删除返回 false ; 构造方法中路径不存在 false 。
// 注意:delete 方法是直接在硬盘删除文件/文件夹,不走回收站,删除要谨慎。
File file05 = new File("F:\\home\\111");
System.out.println(file05.delete());
}

private static void demo02() {
// 02-public boolean mkdir() :创建单级空文件夹
File file03 = new File("F:\\home\\啊啊啊");
System.out.println(file03.mkdir());

// 03-public boolean mkdirs() :既可以创建单级空文件夹,也可以创建多级文件夹,创建文件夹的路径和名称在构造方法中给出(构造方法的参数)
File file04 = new File("F:\\home\\111\\啊啊啊");
System.out.println(file04.mkdirs());

// 注意 : 1.此方法只能创建文件夹,不能创建文件。2、文件夹不存在,创建文件夹,返回 true 。
// 3、文件夹存在,不会创建,返回 false;构造方法中给出的路径不存在返回 false
}

private static void demo01() throws IOException {
// 01-public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。创建文件的路径和名称在构造方法中给出(构造方法的参数)
// 注意 : 1、此方法只能创建文件,不能创建文件夹。2、创建文件的路径必须存在,否则会抛出异常。3、文件不存在,创建文件,返回 true ,文件存在,不会创建,返回 false 。
File file01 = new File("F:\\home\\01.txt");
System.out.println(file01.createNewFile());

File file02 = new File("F:\\blog-home\\01.txt");
System.out.println(file02.createNewFile()); // 路径不存在,抛异常:java.io.IOException: 系统找不到指定的路径。
}

目录的遍历

详情看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 01-public String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。
File file01 = new File("F:\\home");
String[] list = file01.list();
for (String str : list) {
System.out.println(str);
}

// 02-public File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。
File file02 = new File("F:\\home");
File[] listFiles = file02.listFiles();
for (File file : listFiles) {
System.out.println(file);
}

// 03-注意事项:
// 1、list 方法和 listFiles 方法遍历的是构造方法中给出的目录,如果构造方法中给出的目录的路径不存在,会抛出空指针异常。
// 2、如果构造方法中给出的路径不是一个目录,也会抛出空指针异常。

}

调用 listFiles 方法的 File 对象,表示的必须是实际存在的目录,否则返回 null,无法进行遍历。

递归

递归概述

递归: 指在当前方法内调用自己的这种现象。
递归的分类: 递归分为两种,直接递归和间接递归。
直接递归: 方法自身调用自己。
间接递归: 可以理解为 A 方法调用 B 方法,B 方法调用 C 方法,C 方法调用 A 方法。
注意事项:
1、递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
2、在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
3、构造方法,禁止递归。

递归分析图解:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
recursion(1);
}

private static void recursion(int i) {
// 添加一个递归结束的条件,i == 500 的时候结束
// 如果此递归没有结束条件,会报异常:Exception in thread "main" java.lang.StackOverflowError
if(i > 500){
return; // 结束方法
}
System.out.println(i);
recursion(++i);
}

递归累加求和

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int sum = sum(100);
System.out.println(sum);
}

public static int sum(int n) {
if (n == 1) {
return 1;
}
return n + sum(n - 1);
}

递归求阶乘

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
long mul = mul(10);
System.out.println(mul);
}

public static long mul(int n){
if(n == 1){
return 1;
}
return n * mul(n - 1);
}

递归打印多级目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个变量,记录文件数目
static int count = 0;
public static void main(String[] args) {
File file = new File("F:\\home");
getFiles(file);
}

public static void getFiles(File file){
File[] list = file.listFiles();
for (File s : list) {
if(s.isDirectory()){
getFiles(s);
}
System.out.println(s);
System.out.println(++count);
}
}

文件搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 // 定义一个变量,记录文件数目
static int count = 0;

public static void main(String[] args) {
File file = new File("F:\\home");
getFiles(file);
}

public static void getFiles(File file) {
File[] list = file.listFiles();
for (File s : list) {
if (s.isDirectory()) {
getFiles(s);
} else {
if (s.toString().endsWith(".xml")) {
System.out.println(s);
System.out.println(++count);
}
}
}
}

文件搜索之过滤器

文件过滤器:
1、java.io.FileFilter 是一个接口,是 File 的过滤器。该接口的对象可以传递给 File 类的 listFiles(FileFilter) 作为参数。 2、接口中只有一个方法 boolean accept(File pathname) ,测试 pathname 是否应该包含在当前 File 目录中,符合则返回 true。
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
// 定义一个变量,记录文件数目
static int count = 0;

public static void main(String[] args) {
File file = new File("F:\\home");
getFiles(file);
}

public static void getFiles(File file) {
File[] list = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if(pathname.isDirectory()){
return true;
}
return pathname.getName().toLowerCase().endsWith(".xml");
}
});
for (File s : list) {
if (s.isDirectory()) {
getFiles(s);
} else {
if (s.toString().endsWith(".xml")) {
System.out.println(s);
System.out.println(++count);
}
}
}
}

过滤器之 Lambda 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 // 定义一个变量,记录文件数目
static int count = 0;

public static void main(String[] args) {
File file = new File("F:\\home");
getFiles(file);
System.out.println("\n匹配到的合适的文件个数:" + count + "个");
}

public static void getFiles(File file) {
// 使用 Lambda 表达式优化了匿名内部类
File[] list = file.listFiles(pathname-> pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".xml"));
for (File s : list) {
if (s.isDirectory()) {
getFiles(s);
} else {
if (s.toString().endsWith(".xml")) {
System.out.println(s);
count++;
}
}
}
}

IO 流概述

什么是 IO 流

当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。我们把这种数据的传输,可以看做是一种数据的流动,称之为 IO 流 。按照流动的方向,以内存为基准,分为 输入流 input输出流 output ,即流向内存是输入流,流出内存的输出流。如下草图:

Java 中 IO 操作主要是指使用 java.io 包下的内容所进行的输入、输出操作。输入 也叫做读取数据,输出 也叫做作写出数据。

IO 流的分类

根据数据的流向分:
输入流 :把数据从其他设备上读取到内存中的流。 输出流 :把数据从内存中写出到其他设备上的流。
根据数据的类型分:
字节流 :以字节为单位,读写数据的流。 字符流 :以字符为单位,读写数据的流。

根据分类不同,IO 流所对应的类也有所不同,如下表:

输入流 输出流
字节流 字节输入流
InputStream
字节输出流
OutputStream
字符流 字符输入流
Reader
字符输出流
Writer

IO 流的简单介绍就到这里了,下面将详细介绍 Java 中的 IO 流。请往下阅读 👇👇👇

字节流

一切皆为字节

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都是一个一个的字节,那么在传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

字节输出流

java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。

字节输出流的通用方法:
public void close() :关闭此输出流并释放与此流相关联的任何系统资源。 public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。 public void write(byte[] b) :将 b.length 字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。 public abstract void write(int b) :将指定的字节输出流。

FileOutputStream 类

OutputStream 有很多子类,我们从最简单的一个子类 java.io.FileOutputStream 开始。这个类是文件输出流,用于将数据写出到文件,也就是从内存把数据写出到硬盘上。

写出数据的原理(内存 --> 硬盘):
java 程序 --> JVM(java 虚拟机) --> OS(操作系统) --> OS 调用写数据的方法 --> 把数据写入到文件中
字节输出流的使用步骤(重点):
1、创建一个 FileOutputStream 对象,构造方法中传递写入数据的目的地 2、调用 FileOutputStream 对象中的方法 write ,把数据写入到文件中 3、释放资源(IO 流操作会占用一定的内存,使用完毕要把内存清空,提供程序的效率)

构造方法

构造方法:
public FileOutputStream(File file) : 创建文件输出流以写入由指定的 File 对象表示的文件。 public FileOutputStream(String name) : 创建文件输出流以指定的名称写入文件。

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。上述两种构造方法举例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
// 01-创建文件对象
File file = new File("F:\\home\\02.txt");

// 02-创建输出流对象的第一种构造方法
FileOutputStream fos = new FileOutputStream(file);

// 03-创建输出流对象的第二种构造方法
FileOutputStream fileOutputStream = new FileOutputStream("F:\\home\\03.txt");

// 04-关闭流
fileOutputStream.close();
fos.close();
}

写出字节数据

通过 write(int b) 方法,每次可以写出一个字节数据,代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws IOException {
// 01-使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("F:\\home\\fos.txt");

// 02-写出数据
fos.write(97); // 写出第1个字节
fos.write(98); // 写出第2个字节
fos.write(99); // 写出第3个字节

// 03-关闭资源
fos.close();
}

// 输出结果 :abc

小贴士:

1、虽然参数为 int 类型四个字节,但是只会保留一个字节的信息写出。
2、流操作完毕后,必须释放系统资源,调用 close 方法,千万记得。

写出字节数组

通过 write(byte[] b) 方法,每次可以写出数组中的数据,代码使用演示:

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
public static void main(String[] args) throws Exception {
public static void main(String[] args) throws Exception {
/**
* 一次写多个字节的方法:
* public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。
* public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
*/
FileOutputStream fileOutputStream = new FileOutputStream("F:\\home\\03.txt");
FileOutputStream fos = new FileOutputStream("F:\\home\\04.txt");

// 01-如果写的第一个字节是正数(0 —— 127),那么显示的时候会查询 ASCII 表
byte[] arr = {65, 66, 67, 68, 69}; // ABCDE

// 02-向文件中写入数据
fileOutputStream.write(arr);

// 03-如果写的第一个字节是负数,那第一个字节会和第二个字节,两个字节组成一个中文显示,查询系统默认码表(GBK)
byte[] bytes = {-65, -66, -67, 68, 69}; // 烤紻E
fos.write(bytes);

// 04-public void write(byte[] b, int off, int len
fos.write(arr, 1, 2); // 04.txt文件中:烤紻EBC

// 05-写入字符串的方法 : 可以使用 String 类中的 getBytes() 方法把字符串,转换为字节数组
byte[] str = "你好".getBytes();
System.out.println(Arrays.toString(str)); // [-28, -67, -96, -27, -91, -67]
fos.write(str); // 04.txt文件中:烤紻EBC浣犲ソ

// 06-关闭流
fileOutputStream.close();
fos.close();
}

写出指定长度字节数组

通过 write(byte[] b, int off, int len) 方法,每次写出从 off 索引开始,len 个字节,代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("F:\\home\\06.txt");

// 字符串转换为字节数组
byte[] b = "abcde".getBytes();

// 写出从索引 2 开始,2 个字节。索引 2 是 c,两个字节,也就是 cd
fos.write(b, 2, 2);

// 关闭资源
fos.close();
}

数据追加续写

当我们创建输出流对象时,都会清空目标文件中的数据。那么如何保留目标文件中数据,并且还能继续添加新数据呢?使用以下两个构造方法便可以解决我们苦恼的问题。构造方法如下:

构造方法:
public FileOutputStream(File file, boolean append) : 创建文件输出流以写入由指定的 File 对象表示的文件。 public FileOutputStream(String name, boolean append) : 创建文件输出流以指定的名称写入文件。

这两个构造方法的参数中,都需要传入一个boolean 类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
// 这样创建流对象,可以在 05.txt 文件中追加数据
FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt"true);

// 字符串转换为字节数组
byte[] bytes = "abcde".getBytes();

// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(bytes);

// 关闭资源
fos.close();
}

写出换行

Windows 系统里,换行符号是 \r\n ,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt");

// 定义字节数组
byte[] words = {97, 98, 99, 100, 101};

// 遍历数组
for (int i = 0; i < words.length; i++) {
// 写出一个字节
fos.write(words[i]);
// 写出一个换行, 换行符号转成数组写出
fos.write("\r\n".getBytes());
}

// 关闭资源
fos.close();
}
回车符和换行符:
回车符:回到一行的开头(return)。 换行符:下一行(newline)。
各种系统的回车换行:
1、Windows 系统里,每行结尾是【回车 + 换行】 ,即 \r\n 2、Unix/Linux 系统里,每行结尾只有【换行】 ,即 \n 3、Mac 系统里,每行结尾是【回车】 ,即 \r 。从 Mac OS X 开始与 Linux 统一

字节输入流

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

基本共性功能方法:
public void close() :关闭此输入流并释放与此流相关联的任何系统资源。 public abstract int read(): 从输入流读取数据的下一个字节。 public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

FileInputStream 类

java.io.FileInputStream 类是文件输入流,从文件中读取字节。

读入数据的原理(硬盘-->内存):
java 程序 --> JVM --> OS --> OS读取数据的方法 --> 读取文件
字节输入流的使用步骤(重点) :
1、创建 FileInputStream 对象,构造方法中绑定要读取的数据源 2、使用 FileInputStream 对象中的方法 read , 读取文件 3、释放资源

构造方法

构造方法:
FileInputStream(File file) : 创建一个 FileInputStream ,该文件由文件系统中的 File 对象 file 命名。 FileInputStream(String name) : 创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名。

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出 FileNotFoundException 。构造举例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException{
// 01-使用 File 对象创建流对象
File file = new File("F:\\home\\05.txt");
FileInputStream fos = new FileInputStream(file);

// 02-使用文件名称创建流对象
FileInputStream fileInputStream = new FileInputStream("F:\\home\\06.txt");

// 03-关闭资源
fileInputStream.close();
fos.close();
}

读取字节数据

通过 read 方法,每次可以读取一个字节的数据,提升为 int 类型,读取到文件末尾,返回 -1 ,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
// 01-创建输入流对象
FileInputStream fileInputStream = new FileInputStream("F:\\home\\04.txt");

// 02-记录读取到的字节
int read = fileInputStream.read();

// 03-循环输出内容
while (read != -1){
System.out.print(read + " ");
read = fileInputStream.read();
}

// 04-关闭资源
fileInputStream.close();
}

小贴士:

1、虽然读取了一个字节,但是会自动提升为 int 类型。
2、流操作完毕后,必须释放系统资源,调用 close 方法,千万记得。

使用字节数组读取

通过 read(byte[] b) ,每次读取 b 个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回 -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
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("F:\\home\\06.txt"); // 文件中为 abcde

// 定义变量,作为有效个数
int len;

// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];

// 循环读取
while (( len = fis.read(b)) != -1) {
// 每次读取后,把数组变成字符串打印
System.out.println(new String(b));
}

// 关闭资源
fis.close();
}

// 输出结果:
// ab
// cd
// ed

最后一次读取的 ed 是错误数据 ,原因是:在最后一次读取中,上次读取到的数据 c 被 e 替换了,而 d 没有被替换,只读取一个字节 e ,从而产生了错误。所以要通过 len 来获取有效的字节。代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws IOException {
// 01-使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("F:\\home\\03.txt"); // 文件中为 abcde

// 02-定义变量,作为有效个数
int len;

// 03-定义字节数组,作为装字节数据的容器
byte[] b = new byte[1024];

// 04-循环读取
while ((len = fis.read(b)) != -1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b, 0, len)); // len 每次读取的有效字节个数
System.out.println(len);
}

// 05-关闭资源
fis.close();
}

// 输出结果:
// abcde
// 5

小贴士: 使用数组读取,每次读取多个字节,减少了系统间的 IO 操作次数,从而提高了读写的效率,建议开发中使用。

图片复制练习

复制原理图解

案例实现

复制图片文件,代码演示如下:

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 static void main(String[] args) throws IOException {
// 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("F:\\home\\01.png");

// 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
FileOutputStream fos = new FileOutputStream("F:\\01.png");

// 一次读取一个字节写入一个字节的方式
// 3、使用字节输入流对象中的方法read读取文件
/*int len = 0;
while((len = fis.read())!=-1){
// 4、使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
fos.write(len);
}*/

// 3、使用数组缓冲读取多个字节,写入多个字节
byte[] bytes = new byte[1024 * 8];

// 4、使用字节输入流对象中的方法 read 读取文件
int len; // 每次读取的有效字节个数
while ((len = fis.read(bytes)) != -1) {
// 5、使用字节输出流中的方法 write , 把读取到的字节写入到目的地的文件中
fos.write(bytes, 0, len);
}

// 6、释放资源(先关写的,后关闭读的;如果写完了,肯定读取完毕了)
fos.close();
fis.close();
}

小贴士: 流的关闭原则:先开后关,后开先关。

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以 Java 提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

使用字节流读取中文文件,1个中文所占用字节如下:
GBK: 占用 2 字节
UTF-8: 占用 3 个字节

字符输入流

java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

基本共性功能方法:
public void close() :关闭此流并释放与此流相关联的任何系统资源。 public int read() : 从输入流读取一个字符。 public int read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中 。

FileReader 类

java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

小贴士:

1、字符编码:字节与字符的对应规则。Windows 系统的中文编码默认是 GBK 编码表。在 IDEA 中是 UTF-8 编码。
2、字节缓冲区:一个字节数组,用来临时存储字节数据。

构造方法

构造方法:
FileReader(File file) : 创建一个新的 FileReader ,给定要读取的File对象。 FileReader(String fileName) : 创建一个新的 FileReader ,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径。类似于 FileInputStream 。构造举例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args)  throws IOException{
// 01-使用 File 对象创建流对象
File file = new File("F:\\home\\02.txt");
FileReader fr = new FileReader(file);

// 02-使用文件名称创建流对象
FileReader fileReader = new FileReader("F:\\home\\03.txt");

// 03-关闭资源
fileReader.close();
fr.close();
}

读取字符数据

通过 read 方法,每次可以读取一个字符的数据,提升为 int 类型,读取到文件末尾,返回 -1 ,循环读取,代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("F:\\home\\02.txt");

// 定义变量,保存数据
int b;

// 循环读取
while ((b = fr.read()) != -1) {
System.out.println((char)b);
}

// 关闭资源
fr.close();
}

小贴士: 虽然读取了一个字符,但是会自动提升为 int 类型。

通过 read(char[] cbuf) 方法,每次读取 b 个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回 -1 ,代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IOException {
// 1、创建 FileReader 对象,构造方法中绑定要读取的数据源
FileReader fr = new FileReader("F:\\home\\01.txt");

// 2、int read(char[] cbuf) 一次读取多个字符,将字符读入数组。
char[] cs = new char[2]; // 存储读取到的多个字符
int len;
while((len = fr.read(cs)) != -1){
// String类的构造方法:String(char[] value) 把字符数组转换为字符串
System.out.println(new String(cs);
}

// 3、释放资源
fr.close();
}

// 输出结果:
// 我在
// 这里
// 呀里

获取有效的字符改进,避免读取到无效的字符,代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IOException {
// 1、创建 FileReader 对象,构造方法中绑定要读取的数据源
FileReader fr = new FileReader("F:\\home\\01.txt");

// 2、int read(char[] cbuf) 一次读取多个字符,将字符读入数组。
char[] cs = new char[2]; // 存储读取到的多个字符
int len;
while((len = fr.read(cs)) != -1){
// String(char[] value, int offset, int count) 把字符数组的一部分转换为字符串 offset 数组的开始索引 count 转换的个数
System.out.println(new String(cs, 0, len));
}

// 3、关闭资源
fr.close();
}

// 输出结果:
// 我在
// 这里
// 呀

字符输出流

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。以下是字符输出流的基本共性功能方法。

基本共性功能方法:
void write(int c) ,写入单个字符。 void write(char[] cbuf) ,写入字符数组。 abstract void write(char[] cbuf, int off, int len) ,写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
void write(String str) ,写入字符串。 void write(String str, int off, int len) ,写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
void flush() ,刷新该流的缓冲。 void close() ,关闭此流,但要先刷新它。

FileWriter 类

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

构造方法

构造方法:
FileWriter(File file) : 创建一个新的 FileWriter,给定要读取的 File 对象。 FileWriter(String fileName) : 创建一个新的 FileWriter,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径,类似于 FileOutputStream 。构造举例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {
// 使用 File 对象创建流对象
File file = new File("F:\\home\\05.txt");
FileWriter fw = new FileWriter(file);

// 使用文件名称创建流对象
FileWriter fileWriter = new FileWriter("F:\\home\\06.txt");

// 03-关闭资源
fileWriter.close();
fw.close();
}

基本写出数据

通过 write(int b) 方法,每次可以写出一个字符数据,代码使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("F:\\home\\07.txt");

// 写出数据
fw.write(97); // 写出第1个字符
fw.write('b'); // 写出第2个字符
fw.write('C'); // 写出第3个字符
fw.write(30000); // 写出第4个字符,中文编码表中30000对应一个汉字。

// 【注意】关闭资源时,与 FileOutputStream 不同。如果不关闭,数据只是保存到缓冲区,并未保存到文件。
fw.close();
}

// 输出结果:
// abC田

小贴士:

1、虽然参数为 int 类型四个字节,但是只会保留一个字符的信息写出。
2、未调用 close 方法,数据只是保存到了缓冲区,并未写出到文件中。

关闭和刷新

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要使用 flush() 方法了。代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* flush 方法和 close 方法的区别:
* - flush :刷新缓冲区,流对象可以继续使用。
* - close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
*/
public static void main(String[] args) throws IOException {
// 1、创建 FileWriter 对象,构造方法中绑定要写入数据的目的地
FileWriter fw = new FileWriter("F:\\home\\08.txt");

// 2、使用 FileWriter 中的方法 write , 把数据写入到内存缓冲区中(字符转换为字节的过程)
// void write(int c) 写入单个字符。
fw.write(97);

// 3、使用 FileWriter 中的方法 flush , 把内存缓冲区中的数据,刷新到文件中
fw.flush();
fw.write(98); // 刷新之后流可以继续使用

// 4、释放资源(会先把内存缓冲区中的数据刷新到文件中)
fw.close();

// close 方法之后流已经关闭了,已经从内存中消失了,流就不能再使用了
fw.write(99); // IOException: Stream closed
}

小贴士: 即便是 flush 方法写出了数据,操作的最后还是要调用 close 方法,释放系统资源。

写出其他数据

写出字符数组 :通过 write(char[] cbuf) 方法和 write(char[] cbuf, int off, int len) 方法 ,每次可以写出字符数组中的数据,用法类似于 FileOutputStream ,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("F:\\home\\09.txt");

// 字符串转换为字节数组
char[] chars = "井冈山大学".toCharArray();

// 写出字符数组
fw.write(chars); // 井冈山大学

// 写出从索引3开始,写2个字节。索引3是'大',两个字节,也就是 大学
fw.write(b, 3, 2); // 大学

// 关闭资源
fos.close();
}

写出字符串:通过 write(String str) 方法和 write(String str, int off, int len) 方法,每次可以写出字符串中的数据,更为方便,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException {
// 01-使用文件名称创建流对象
FileWriter fw = new FileWriter("F:\\home\\10.txt");

// 02-定义字符串
String msg = "井冈山大学";

// 03-写出字符串
fw.write(msg); // 井冈山大学

// 04-写出从索引3开始,写2个字节。索引3是'大',两个字节,也就是 大学
fw.write(msg, 3, 2); // 大学

// 05-关闭资源
fw.close();
}

数据追加续写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IOException {
// 01-使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("F:\\home\\11.txt"true);

// 02-写出字符串
fw.write("井冈山");

// 03-写出换行
fw.write("\r\n");

// 04-写出字符串
fw.write("大学");

// 05-关闭资源
fw.close();
}

// 输出结果:
// 井冈山
// 大学

小贴士:

1、字符流只能操作文本文件,不能操作图片、视频等非文本文件。
2、当我们单纯读或者写文本文件时可以使用字符流,其他情况使用字节流。

IO 异常的处理

JDK7 前处理

之前的代码中,我们一直把异常抛出,而实际开发中并不能这样处理,而是建议使用 try...catch...finally 代码块处理异常部分,代码演示如下:

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
/**
* 在jdk1.7之前使用try catch finally 处理流中的异常
* 格式:
* try{
* 可能会产出异常的代码
* }catch(异常类变量 变量名){
* 异常的处理逻辑
* }finally{
* 一定会执行的代码
* 资源释放
* }
*/
public static void main(String[] args) {
// 提高变量 fw 的作用域,让 finally 可以使用。变量在定义的时候,可以没有值,但是使用的时候必须有值
// fw = new FileWriter("09_IOAndProperties\\g.txt",true); 执行失败,fw 没有值,fw.close 会报错
FileWriter fw = null;
try{
// 可能会产出异常的代码
fw = new FileWriter("F:\\home\\11.txt", true);
for (int i = 0; i < 10 ; i++) {
fw.write("HelloWorld " + i + "\r\n");
}
}catch(IOException e){
// 异常的处理逻辑
System.out.println(e);
}finally {
// 一定会执行的代码
// 创建对象失败了,fw的默认值就是 null , null 是不能调用方法的,会抛出 NullPointerException , 需要增加一个判断,不是 null 在把资源释放
if(fw != null){
try {
// fw.close 方法声明抛出了 IOException 异常对象,所以我们就的处理这个异常对象,要么 throws , 要么 try catch
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

JDK7 的处理

还可以使用 JDK7 优化后的 try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。代码演示如下:

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
/**
* JDK7 的新特性
* 在try的后边可以增加一个(),在括号中可以定义流对象
* 那么这个流对象的作用域就在try中有效
* try中的代码执行完毕,会自动把流对象释放,不用写finally
*/
public static void main(String[] args) {
try(// 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("F:\\home\\12.txt");
// 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
FileOutputStream fos = new FileOutputStream("F:\\home\\13.txt");){

// 可能会产出异常的代码
// 一次读取一个字节写入一个字节的方式
// 3、使用字节输入流对象中的方法 read 读取文件
int len = 0;
while((len = fis.read()) != -1){
// 4、使用字节输出流中的方法 write , 把读取到的字节写入到目的地的文件中
fos.write(len);
}

}catch (IOException e){
// 异常的处理逻辑
System.out.println(e);
}
}

JDK9 的改进

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
/**
* JDK9 新特性
* try的前边可以定义流对象
* 在try后边的()中可以直接引入流对象的名称(变量名)
* 在try代码执行完毕之后,流对象也可以释放掉,不用写finally
* 格式:
* A a = new A();
* B b = new B();
* try(a,b){
* 可能会产出异常的代码
* }catch(异常类变量 变量名){
* 异常的处理逻辑
* }
*/
public static void main(String[] args) throws IOException {
// 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("F:\\home\\14.txt");

// 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
FileOutputStream fos = new FileOutputStream("F:\\home\\15.txt");

try(fis; fos){
// 一次读取一个字节写入一个字节的方式
// 3、使用字节输入流对象中的方法 read 读取文件
int len = 0;
while((len = fis.read()) != -1){
// 4、使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
fos.write(len);
}
}catch (IOException e){
System.out.println(e);
}
}

Properties 属性集

属性集概述

java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多 Java 类使用,比如获取系统属性时,System.getProperties 方法就是返回一个 Properties 对象。

Properties 类

构造方法和其他方法

构造方法:
public Properties() : 创建一个空的属性列表。
其他方法:
public Object setProperty(String key, String value) : 保存一对属性。 public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。 public Set stringPropertyNames() :所有键的名称的集合。

上述方法代码演示如下:

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
/**
* 使用Properties 集合存储数据,遍历取出 Properties 集合中的数据。Properties 集合是一个双列集合,key 和 value 默认都是字符串
* Properties集合有一些操作字符串的特有方法
* Object setProperty(String key, String value) 调用 Hashtable 的方法 put。
* String getProperty(String key) 通过 key 找到 value 值,此方法相当于 Map 集合中的 get(key) 方法
* Set<String> stringPropertyNames() 返回此属性列表中的键集,其中该键及其对应值是字符串,此方法相当于 Map 集合中的 keySet 方法
*/
public static void main(String[] args) {
// 01-创建 Properties 集合对象
Properties prop = new Properties();

// 02-使用 setProperty 往集合中添加数据
prop.setProperty("赵丽颖", "168");
prop.setProperty("迪丽热巴", "165");
prop.setProperty("古力娜扎", "160");

// 03-使用 stringPropertyNames 把 Properties 集合中的键取出,存储到一个 Set 集合中
Set<String> set = prop.stringPropertyNames();

// 04-遍历 Set 集合,取出 Properties 集合的每一个键
for (String key : set) {
// 05-使用 getProperty 方法通过 key 获取 value
String value = prop.getProperty(key);
System.out.println(key + " = " + value);
}
}

与流相关的方法

这是保存的方法,知识点都在代码的注释里面了,请看代码演示:

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
/**
* 可以使用 Properties 集合中的方法 store,把集合中的临时数据,持久化写入到硬盘中存储
* void store(OutputStream out, String comments)
* void store(Writer writer, String comments)
* 参数:
* OutputStream out: 字节输出流,不能写入中文
* Writer writer: 字符输出流,可以写中文
* String comments: 注释,用来解释说明保存的文件是做什么用的,但是不能使用中文,会产生乱码,默认是 Unicode 编码,一般使用""空字符串
*/
public static void main(String[] args) throws IOException {
// 1、创建 Properties 集合对象并添加数据
Properties prop = new Properties();
prop.setProperty("赵丽颖", "168");
prop.setProperty("迪丽热巴", "165");
prop.setProperty("古力娜扎", "175");

// 2、创建字符输出流对象,构造方法中绑定要输出的目的地,不要使用字节输出流,会乱码
FileWriter fw = new FileWriter("F:\\home\\14.txt");

// 3、使用 Properties 集合中的方法 store,把集合中的临时数据,持久化写入到硬盘中存储
prop.store(fw,"save my data");

// 4、释放资源
fw.close();
}

这是读取的方法,知识点都在代码的注释里面了,请看代码演示:

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
/**
* 可以使用 Properties 集合中的方法 load,把硬盘中保存的文件(键值对),读取到集合中使用
* void load(InputStream inStream)
* void load(Reader reader)
* 参数:
* InputStream inStream : 字节输入流,不能读取含有中文的键值对
* Reader reader : 字符输入流,能读取含有中文的键值对
* 注意事项:
* 1、存储键值对的文件中,键与值默认的连接符号可以使用=,空格(其他符号)
* 2、存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
* 3、存储键值对的文件中,键与值默认都是字符串,不用再加引号
*/
public static void main(String[] args) throws IOException {
// 1、创建 Properties 集合对象
Properties prop = new Properties();

// 2、使用 Properties 集合对象中的方法 load 读取保存键值对的文件
prop.load(new FileReader("F:\\home\\14.txt"));

// 3、遍历 Properties 集合
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
String value = prop.getProperty(key);
System.out.println(key + " = " + value);
}
}

小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

缓冲流

上面介绍了基本的一些流,作为 IO 流的入门。现在我们要见识一些更强大的流,比如能够高效读写的缓冲流、能够转换编码的转换流、能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。

缓冲流概述

缓冲流,也叫高效流,是对 4 个基本的 FileXxx 流的增强。缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统 IO 操作的次数,从而提高读写的效率。以下是缓冲流的分类:

按照数据类型分类:
字节缓冲流:BufferedInputStream 和 BufferedOutputStream 字符缓冲流:BufferedReader 和 BufferedWriter

字节缓冲流

构造方法

构造方法:
public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。 public BufferedOutputStream(OutputStream out) : 创建一个新的缓冲输出流。

构造方法举例如下:

1
2
3
4
5
// 01-创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\05.txt"));

// 02-创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\home\\05.txt"));

字节缓冲输出流

字节缓冲输出流【BufferedOutputStream】实际案例如下:

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
/**
* 继承自父类的共性成员方法:
* public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
* public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
* public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
* public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
* public abstract void write(int b) :将指定的字节输出流。
* 构造方法:
* BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
* BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。
* 参数:
* OutputStream out:字节输出流,我们可以传递FileOutputStream,缓冲流会给FileOutputStream增加一个缓冲区,提高FileOutputStream的写入效率
* int size:指定缓冲流内部缓冲区的大小,不指定默认
*/
public static void main(String[] args) throws IOException {
// 1、创建 FileOutputStream 对象,构造方法中绑定要输出的目的地
FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt");

// 2、创建 BufferedOutputStream 对象,构造方法中传递 FileOutputStream 对象,提高效率
BufferedOutputStream bos = new BufferedOutputStream(fos);

// 3、使用 BufferedOutputStream 对象中的方法 write , 把数据写入到内部缓冲区中
bos.write("井冈山大学".getBytes());

// 4、使用 BufferedOutputStream 对象中的方法 flush , 把内部缓冲区中的数据,刷新到文件中
bos.flush();

// 5、释放资源(会先调用 flush 方法刷新数据,第4部可以省略)
bos.close();
}

字节缓冲输入流

字节缓冲输入流【BufferedInputStream】实际案例如下:

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
/**
* 继承自父类的成员方法:
* int read()从输入流中读取数据的下一个字节。
* int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
* void close() 关闭此输入流并释放与该流关联的所有系统资源。
* 构造方法:
* BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
* BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
* 参数:
* InputStream in:字节输入流,我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的读取效率
* int size:指定缓冲流内部缓冲区的大小,不指定默认
*/
public static void main(String[] args) throws IOException {
// 1、创建 FileInputStream 对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("F:\\home\\05.txt");

// 2、创建 BufferedInputStream 对象,构造方法中传递 FileInputStream 对象,提高读取效率
BufferedInputStream bis = new BufferedInputStream(fis);

// 3、使用 BufferedInputStream 对象中的方法 read ,读取文件
// int read() 从输入流中读取数据的下一个字节,也就是一个字节一个字节地读,效率低
// int len; // 记录每次读取到的字节
// while((len = bis.read()) != -1){
// System.out.println(len);
// }

// int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
byte[] bytes = new byte[1024 * 8];//存储每次读取的数据
int len; // 记录每次读取的有效字节个数
while ((len = bis.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
}

// 4、释放资源
bis.close();
}

效率测试

查询 API,缓冲流读写方法与基本的流是一致的。那我们通过复制大文件(229MB),来测试它的效率。

基本流的复制,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
FileInputStream fis = new FileInputStream("F:\\home\\01.pdf");
FileOutputStream fos = new FileOutputStream("F:\\01.pdf")
) {
// 读写数据
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("普通流复制时间: " + (end - start) + " 毫秒");
}

// 普通流复制时间: 1278098 毫秒

缓冲流的复制,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\01.pdf"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\01.pdf"))
) {
// 读写数据
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流复制时间: " + (end - start) + " 毫秒");
}

// 缓冲流复制时间: 10480 毫秒

那么如何做才能更快呢?答案是:使用数组的方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\01.pdf"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\02.pdf"))
) {
// 读写数据
int len;
byte[] bytes = new byte[8 * 1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间: " + (end - start) + " 毫秒");
}

// 缓冲流使用数组复制时间: 566 毫秒

字符缓冲流

构造方法

构造方法:
public BufferedReader(Reader in) :创建一个新的缓冲输入流。 public BufferedWriter(Writer out) : 创建一个新的缓冲输出流。

构造方法举例如下:

1
2
3
4
5
// 01-创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));

// 02-创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\05.txt"));

特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

特有方法:
BufferedReader:public String readLine() : 读一行文字。 BufferedWriter:public void newLine() : 相当于换行分隔符,由系统属性定义符号。

public String readLine() 方法演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));

// 定义字符串,保存读取的一行文字
String line;

// 循环读取,读取到最后返回 null
while ((line = br.readLine()) != null) {
System.out.print(line);
System.out.println("---");
}

// 释放资源
br.close();
}

public void newLine() 方法演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws IOException  {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\edu.txt"));

// 写出数据
bw.write("井冈山");

// 写出换行
bw.newLine();
bw.write("大学");
bw.newLine();
bw.write("欢迎你");
bw.newLine();

// 释放资源
bw.close();
}

// 输出效果:
// 井冈山
// 大学
// 欢迎你

字符缓冲输出流

字符缓冲输出流【BufferedWriter】实际案例如下:

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
/**
* 继承自父类的共性成员方法:
* void write(int c) 写入单个字符。
* void write(char[] cbuf)写入字符数组。
* abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分, off 数组的开始索引, len 写的字符个数。
* void write(String str)写入字符串。
* void write(String str, int off, int len) 写入字符串的某一部分,off 字符串的开始索引, len 写的字符个数。
* void flush()刷新该流的缓冲。
* void close() 关闭此流,但要先刷新它。
*
* 构造方法:
* BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流。
* BufferedWriter(Writer out, int sz) 创建一个使用给定大小输出缓冲区的新缓冲字符输出流。
* 参数:
* Writer out:字符输出流,我们可以传递FileWriter,缓冲流会给FileWriter增加一个缓冲区,提高FileWriter的写入效率
* int sz:指定缓冲区的大小,不写默认大小
*/
public static void main(String[] args) throws IOException {
// 1、创建字符缓冲输出流对象,构造方法中传递字符输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\edu.txt"));

// 2、调用字符缓冲输出流中的方法 write , 把数据写入到内存缓冲区中
for (int i = 0; i < 10 ; i++) {
bw.write("井冈山大学~~~");
// bw.write("\r\n");
bw.newLine();
}

// 3、调用字符缓冲输出流中的方法 flush ,把内存缓冲区中的数据,刷新到文件中
bw.flush();

// 4、释放资源
bw.close();
}

字符缓冲输入流

字符缓冲输入流【BufferedReader】实际案例如下:

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
/**
* 继承自父类的共性成员方法:
* int read() 读取单个字符并返回。
* int read(char[] cbuf)一次读取多个字符,将字符读入数组。
* void close() 关闭该流并释放与之关联的所有资源。
* 构造方法:
* BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。
* BufferedReader(Reader in, int sz) 创建一个使用指定大小输入缓冲区的缓冲字符输入流。
* 参数:
* Reader in:字符输入流,我们可以传递FileReader,缓冲流会给FileReader增加一个缓冲区,提高FileReader的读取效率
* 特有的成员方法:
* String readLine() 读取一个文本行。读取一行数据
* 行的终止符号:通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行(\r\n)。
* 返回值:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null
*/
public static void main(String[] args) throws IOException {
// 1、创建字符缓冲输入流对象,构造方法中传递字符输入流
BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));

// 2、使用字符缓冲输入流对象中的方法read/readLine读取文本
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

// 3、释放资源
br.close();
}

练习:文本排序

练习描述

请将下列文本信息恢复顺序。

1
2
3
4
5
6
7
8
9
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。

案例分析

  • 1、逐行读取文本信息。
  • 2、解析文本信息到集合中。
  • 3、遍历集合,按顺序,写出文本信息。

代码实现

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 static void main(String[] args) throws IOException {
// 1、创建一个 HashMap 集合对象,存储每行文本的序号(1,2,3,..);value:存储每行的文本
HashMap<String, String> map = new HashMap<>();

// 2、创建字符缓冲输入流对象,构造方法中绑定字符输入流
BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));

// 3、创建字符缓冲输出流对象,构造方法中绑定字符输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\05.txt"));

// 4、使用字符缓冲输入流中的方法 readline ,逐行读取文本
String line;
while ((line = br.readLine()) != null) {
// 5、对读取到的文本进行切割,获取行中的序号和文本内容
String[] arr = line.split("\\.");

// 6、把切割好的序号和文本的内容存储到 HashMap 集合中(key 序号是有序的,会自动排序 1,2,3,4..)
map.put(arr[0], arr[1]);
}

// 7、遍历 HashMap 集合,获取每一个键值对
for (String key : map.keySet()) {
String value = map.get(key);

// 8、把每一个键值对,拼接为一个文本行
line = key + "." + value;

// 9、把拼接好的文本,使用字符缓冲输出流中的方法 write ,写入到文件中
bw.write(line);
bw.newLine(); // 写换行
}
// 10、释放资源
bw.close();
br.close();
}

转换流

字符编码和字符集

字符编码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为 编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为 解码 。比如说,按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。

各种概念:
编码 : 字符(我们能看懂的) --> 字节(我们看不懂的) 解码 : 字节(我们看不懂的) --> 字符(我们能看懂的) 字符编码【Character Encoding】 : 就是一套自然语言的字符与二进制数之间的对应规则。 编码表 : 生活中文字和计算机中二进制的对应规则

字符集

字符集【Charset】 :也叫编码表,是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有 ASCII 字符集GBK 字符集Unicode 字符 集等。字符编码与字符集对应关系如下图:

可见,当指定了 编码,它所对应的 字符集 自然就指定了,所以 编码 才是我们最终要关心的。接下来我们介绍各种字符集。

ASCII 字符集
1、 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。 2、 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
ISO-8859-1 字符集
拉丁码表,别名 Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。ISO-8859-1 使用单字节编码,兼容 ASCII 编码。
GBxxx 字符集
1、 GB 就是国标的意思,是为了显示中文而设计的一套字符集。 2、 GB2312 :简体中文码表。一个小于 127 的字符的意义与原来相同。但两个大于 127 的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。 3、 GBK :最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。 4、 GB18030 :最新的中文码表。收录汉字 70244 个,采用多字节编码,每个字可以由 1 个、2 个或 4 个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
Unicode字符集
1、 Unicode 编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。它最多使用 4 个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16 和 UTF-32。最为常用的 UTF-8 编码。 2、 UTF-8 编码,可以用来表示 Unicode 标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持 UTF-8 编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则如下:     1、 128 个 US-ASCII 字符,只需一个字节编码。     2、 拉丁文等字符,需要二个字节编码。     3、 大部分常用字(含中文),使用三个字节编码。     4、 其他极少使用的 Unicode 辅助字符,使用四字节编码。

编码引出的问题

在 IDEA 中,可以使用 FileReader 读取项目中的文本文件。由于 IDEA 默认是 UTF-8 编码,所以读取的内容没有任何问题。但是,当读取 Windows 系统中创建的文本文件时,由于 Windows 系统的默认是 GBK 编码,就会出现乱码。代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader("F:\\GBK.txt"); // 井冈山大学
int read;
while ((read = fileReader.read()) != -1) {
System.out.print((char)read);
}
fileReader.close();
}

// 输出结果:
// ����ɽ��ѧ

那么问题来了,我们要如何才能读取 GBK 编码的文件呢?这时就要用到转换流了。接着往下看 👇👇👇

转换流图解

InputStreamReader 类

转换流 java.io.InputStreamReader ,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

构造方法
InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。 InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。

指定编码读取

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
public static void main(String[] args) throws IOException {
read_gbk();
read_utf_8();
}

// 使用 InputStreamReader 读取 GBK 格式的文件
private static void read_gbk() throws IOException {
// 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称
InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\gbk.txt"),"GBK"); // 你好

// 2、使用InputStreamReader对象中的方法 read 读取文件
int len;
while((len = isr.read())!=-1){
System.out.println((char)len);
}
// 3、释放资源
isr.close();
}

// 使用 InputStreamReader 读取 UTF-8 格式的文件
private static void read_utf_8() throws IOException {
// 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称
InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\utf_8.txt")); // 不指定默认使用 UTF-8

// 2、使用 InputStreamReader 对象中的方法 read 读取文件
int len;
while((len = isr.read())!=-1){
System.out.println((char)len);
}
// 3、释放资源
isr.close();
}

OutputStreamWriter 类

转换流 java.io.OutputStreamWriter ,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

构造方法
OutputStreamWriter(OutputStream in) : 创建一个使用默认字符集的字符流。 OutputStreamWriter(OutputStream in, String charsetName) : 创建一个指定字符集的字符流。

指定编码写出

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
public static void main(String[] args) throws IOException {
gbk();
utf_8();
}

// 使用转换流 OutputStreamWriter 写 GBK 格式的文件
private static void gbk() throws IOException {
// 1、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\gbk.txt"),"GBK");

// 2、使用 OutputStreamWriter 对象中的方法 write , 把字符转换为字节存储缓冲区中(编码)
osw.write("你好");

// 3、使用 OutputStreamWriter 对象中的方法 flush , 把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
osw.flush();

// 4、释放资源
osw.close();
}

// 使用转换流 OutputStreamWriter 写 UTF-8 格式的文件
private static void utf_8() throws IOException {
// 1、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\utf_8.txt"),"utf-8"); // 不指定默认使用UTF-8

// 2、使用 OutputStreamWriter 对象中的方法 write , 把字符转换为字节存储缓冲区中(编码)
osw.write("你好");

// 3、使用 OutputStreamWriter 对象中的方法 flush , 把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
osw.flush();

// 4、释放资源
osw.close();
}

练习:转换文件编码

将 GBK 编码的文本文件,转换为 UTF-8 编码的文本文件。代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws IOException {
// 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称 GBK
InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\GBK.txt"),"GBK");

// 2、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称 UTF-8
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\utf_8.txt"),"UTF-8");

// 3、使用 InputStreamReader 对象中的方法 read 读取文件
int len;
while((len = isr.read()) != -1){
// 4、使用 OutputStreamWriter 对象中的方法 write , 把读取的数据写入到文件中
osw.write(len);
}

// 5、释放资源
osw.close();
isr.close();
}

序列化流

Java 提供了一种对象 序列化 的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据对象的类型对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中 持久保存 了一个对象的信息。 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行 反序列化对象的数据对象的类型对象中存储的数据 信息,都可以用来在内存中创建对象。请看下图理解序列化:

ObjectOutputStream 类

java.io.ObjectOutputStream 类,将 Java 对象的原始数据类型写出到文件,实现对象的持久存储。

构造方法

构造方法
public ObjectOutputStream(OutputStream out) : 创建一个指定 OutputStream 的 ObjectOutputStream 。

序列化操作

一、对象要想序列化,必须满足两个条件:
1、该类必须实现 java.io.Serializable 接口,这是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException 。 2、该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。
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
64
65
66
/*
* 序列化和反序列化的时候,会抛出NotSerializableException没有序列化异常
* 类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
* Serializable接口也叫标记型接口
* 要进行序列化和反序列化的类必须实现Serializable接口,就会给类添加一个标记
* 当我们进行序列化和反序列化的时候,就会检测类上是否有这个标记
* 有:就可以序列化和反序列化
* 没有:就会抛出 NotSerializableException异常
* 去市场买肉-->肉上有一个蓝色章(检测合格)-->放心购买-->买回来怎么吃随意
*
*
* static关键字:静态关键字
* 静态优先于非静态加载到内存中(静态优先于对象进入到内存中)
* 被static修饰的成员变量不能被序列化的,序列化的都是对象
* private static int age;
* oos.writeObject(new Person("小美女",18));
* Object o = ois.readObject();
* Person{name='小美女', age=0}
*
* transient关键字:瞬态关键字
* 被transient修饰成员变量,不能被序列化
* private transient int age;
* oos.writeObject(new Person("小美女",18));
* Object o = ois.readObject();
* Person{name='小美女', age=0}
*/
public class Person implements Serializable{
// 防止出现 InvalidClassException
private static final long serialVersionUID = 1L;
private String name;
//private static int age;
//private transient int age;
public int age;

public Person() {
}

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

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
二、写出对象方法:
通过 public final void writeObject (Object obj)` 方法,将指定的对象写出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* java.io.ObjectOutputStream extends OutputStream
* ObjectOutputStream:对象的序列化流
* 作用:把对象以流的方式写入到文件中保存
*
* 构造方法:
* ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。
* 参数:
* OutputStream out:字节输出流
* 特有的成员方法:
* void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。
*/
public static void main(String[] args) throws IOException {
// 1、创建 ObjectOutputStream 对象,构造方法中传递字节输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\person.txt"));

// 2、使用 ObjectOutputStream 对象中的方法 writeObject ,把对象写入到文件中
oos.writeObject(new Person("小美女", 18));

// 3、释放资源
oos.close();
}

ObjectInputStream 类

ObjectInputStream 反序列化流,将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象。

构造方法

构造方法:
public ObjectInputStream(InputStream in) : 创建一个指定 InputStream 的 ObjectInputStream 。

反序列化操作一

反序列化操作一:
如果能找到一个对象的 class 文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的 public final Object readObject() 方法,读取一个对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1、创建 ObjectInputStream 对象,构造方法中传递字节输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\person.txt"));

// 2、使用 ObjectInputStream 对象中的方法 readObject 读取保存对象的文件
Object o = ois.readObject();

// 3、释放资源
ois.close();

// 4、使用读取出来的对象(打印)
System.out.println(o);
Person p = (Person)o;
System.out.println(p.getName() + p.getAge());
}

JVM 可以反序列化对象的条件:

它必须能够找到 class 文件的类,如果找不到该类的 class 文件,则会抛出一个 ClassNotFoundException 异常。

反序列化操作二

还有一种情况,当 JVM 反序列化对象时,能找到 class 文件,但是 class 文件在序列化对象之后发生了修改,那么反序列化操作也会失败,会抛出一个 InvalidClassException 异常。

InvalidClassException 原因如下:
1、该类的序列版本号与从流中读取的类描述符的版本号不匹配 2、该类包含未知数据类型 3、该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。该版本号的目的在于验证序列化的对象和对应类是否版本匹配。只要给上述 Person 类加入下面代码就可以防止该异常发生。代码如下:

1
2
// 防止出现 InvalidClassException
private static final long serialVersionUID = 1L;

练习:序列化集合

  • 1、将存有多个自定义对象的集合序列化操作,保存到 list.txt 文件中。
  • 2、反序列化 list.txt ,并遍历集合,打印对象信息。

案例分析

案例分析:
1、把若干学生对象 ,保存到集合中。 2、把集合序列化。 3、反序列化读取时,只需要读取一次,转换为集合类型。 4、遍历集合,可以打印所有的学生信息。

代码实现

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
64
65
66
67
68
69
70
71
72
73
74
75
76
// 练习:序列化集合
// 当我们想在文件中保存多个对象的时候,可以把多个对象存储到一个集合中,对集合进序列化和反序列化。
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1、定义一个存储 Person 对象的 ArrayList 集合
ArrayList<Person> list = new ArrayList<>();

// 2、往 ArrayList 集合中存储 Person 对象
list.add(new Person("张三",18));
list.add(new Person("李四",19));
list.add(new Person("王五",20));

// 3、创建一个序列化流 ObjectOutputStream 对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\list.txt"));

// 4、使用 ObjectOutputStream 对象中的方法 writeObject ,对集合进行序列化
oos.writeObject(list);

// 5、创建一个反序列化 ObjectInputStream 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\list.txt"));

// 6、使用 ObjectInputStream 对象中的方法 readObject 读取文件中保存的集合
Object o = ois.readObject();

// 7、把 Object 类型的集合转换为 ArrayList 类型
ArrayList<Person> list2 = (ArrayList<Person>)o;

// 8、遍历 ArrayList 集合
for (Person p : list2) {
System.out.println(p);
}

// 9、释放资源
ois.close();
oos.close();
}

class Person implements Serializable{
// 防止出现 InvalidClassException
private static final long serialVersionUID = 1L;
private String name;
//private static int age;
//private transient int age;
public int age;

public Person() {
}

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

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

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

打印流

打印流概述

平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于 java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。接下来我们介绍 PrintStream 类

PrintStream 类

构造方法

构造方法:
public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

改变打印流向

System.out 就是 PrintStream 类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个 “小把戏” ,改变它的流向。示例代码如下:

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
/**
* java.io.PrintStream:打印流
* PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。
* PrintStream特点:
* 1.只负责数据的输出,不负责数据的读取
* 2.与其他输出流不同,PrintStream 永远不会抛出 IOException
* 3.有特有的方法,print,println
* void print(任意类型的值)
* void println(任意类型的值并换行)
* 构造方法:
* PrintStream(File file):输出的目的地是一个文件
* PrintStream(OutputStream out):输出的目的地是一个字节输出流
* PrintStream(String fileName) :输出的目的地是一个文件路径
* PrintStream extends OutputStream
* 继承自父类的成员方法:
* - public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
* - public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
* - public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
* - public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
* - public abstract void write(int b) :将指定的字节输出流。
* 注意:
* 如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
* 如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97
*/
public static void main(String[] args) throws FileNotFoundException {
// 创建打印流 PrintStream 对象,构造方法中绑定要输出的目的地
PrintStream ps = new PrintStream("F:\\print.txt");

// 如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
ps.write(97);

// 如果使用自己特有的方法 print/println 方法写数据,写的数据原样输出 97->97
ps.println(97);
ps.println(8.8);
ps.println('a');
ps.println("HelloWorld");
ps.println(true);

// 释放资源
ps.close();
}

示例代码二如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 可以改变输出语句的目的地(打印流的流向)
* 输出语句,默认在控制台输出
* 使用 System.setOut 方法改变输出语句的目的地改为参数中传递的打印流的目的地
* static void setOut(PrintStream out) : 重新分配“标准”输出流。
*/
public static void main(String[] args) throws FileNotFoundException {
System.out.println("我是在控制台输出");

PrintStream ps = new PrintStream("F:\\print-out.txt");
System.setOut(ps); // 把输出语句的目的地改变为打印流的目的地
System.out.println("我在打印流的目的地中输出"); // 不会在控制台显示,会在 print-out.txt 文件里显示

ps.close();
}

Java 中的 IO 流到此结束了,欢迎评论区留言!!!

    / 

📚 本站推荐文章
  👉 从 0 开始搭建 Hexo 博客
  👉 计算机网络入门教程
  👉 数据结构入门
  👉 算法入门
  👉 IDEA 入门教程

可在评论区留言哦

一言句子获取中...

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×