File类以及IO流

File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹。现在要学习的File类,它的就用来表示当前系统下的文件(也可以是文件夹),通过File类提供的方法可以获取文件大小、判断文件是否存在、创建文件、创建文件夹等。

但是需要我们注意:File对象只能对文件进行操作,不能操作文件中的内容。

所以,我们需要IO流来对文件中的数据进行读取并且写入。

File: 代表文本
IO流: 读写数据(可以读写文件,或网络中的数据

File类的使用

File类的创建对象用的构造方法:

构造器 说明
public File(string pathname 根据文件路径创建文件对象
public File(String parent,String child) 根据父路径和子路径名字创建文件对象
public File(File parent,String child) 根据父路径对应文件对象和子路径名字创建文件对象

下面我们演示一下,File类创建对象的代码

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
/**
* 目标:掌握File创建对象,代表具体文件的方案。
*/
public class FileTest1 {
public static void main(String[] args) {
// 1、创建一个File对象,指代某个具体的文件。
// 路径分隔符
// File f1 = new File("D:/resource/ab.txt");
// File f1 = new File("D:\\resource\\ab.txt");
File f1 = new File("D:" + File.separator +"resource" + File.separator + "ab.txt");
System.out.println(f1.length()); // 文件大小

File f2 = new File("D:/resource");
System.out.println(f2.length());

// 注意:File对象可以指代一个不存在的文件路径
File f3 = new File("D:/resource/aaaa.txt");
System.out.println(f3.length());
System.out.println(f3.exists()); // false

// 我现在要定位的文件是在模块中,应该怎么定位呢?
// 绝对路径:带盘符的
// File f4 = new File("D:\\code\\javasepromax\\file-io-app\\src\\omiga.txt");
// 相对路径(重点):不带盘符,默认是直接去工程下寻找文件的。
File f4 = new File("file-io-app\\src\\omiga.txt");
System.out.println(f4.length());
}
}

File判断和获取方法

刚才我们创建File对象的时候,会传递一个文件路径过来。但是File对象封装的路径是存在还是不存在,是文件还是文件夹其实是不清楚的。好在File类提供了方法可以帮我们做判断。

方法名称 说明
public boolean exists() 判断当前文件对象,对应的文件路径是否存在,存在返回true
public boolean isFile() 判断当前文件对象指代的是否是文件,是文件返回true,反之返回false
public boolean isDirectory() 判断当前文件对象指代的是否是文件,是文件返回true,反之返回false
public boolean isDirectory() 判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之返回false
public String getName() 获取文件的名称(包含后缀)
public long length() 获取文件的大小,返回字节个数
public long lastModified() 获取文件的最后修改时间。
public String getPath() 获取创建文件对象时,使用的路径
public String getAbsolutePath() 获取绝对路径

话不多少,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
目标:掌握File提供的判断文件类型、获取文件信息功能
*/
public class FileTest2 {
public static void main(String[] args) throws UnsupportedEncodingException {
// 1.创建文件对象,指代某个文件
File f1 = new File("D:/resource/ab.txt");
//File f1 = new File("D:/resource/");

// 2、public boolean exists():判断当前文件对象,对应的文件路径是否存在,存在返回true.
System.out.println(f1.exists());

// 3、public boolean isFile() : 判断当前文件对象指代的是否是文件,是文件返回true,反之。
System.out.println(f1.isFile());

// 4、public boolean isDirectory() : 判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之。
System.out.println(f1.isDirectory());
}
}

除了判断功能还有一些获取功能,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
File f1 = new File("D:/resource/ab.txt");

// 5.public String getName():获取文件的名称(包含后缀)
System.out.println(f1.getName());

// 6.public long length():获取文件的大小,返回字节个数
System.out.println(f1.length());

// 7.public long lastModified():获取文件的最后修改时间。
long time = f1.lastModified();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
System.out.println(sdf.format(time));

// 8.public String getPath():获取创建文件对象时,使用的路径
File f2 = new File("D:\\resource\\ab.txt");
File f3 = new File("file-io-app\\src\\omiga.txt");
System.out.println(f2.getPath());
System.out.println(f3.getPath());

// 9.public String getAbsolutePath():获取绝对路径
System.out.println(f2.getAbsolutePath());
System.out.println(f3.getAbsolutePath());

创建和删除方法

Java可以代码创建一个文件或者文件夹,不光可以创建还可以删除。

File类提供了创建和删除文件的方法,话不多少,看代码。

File类创建文件的功能

方法名称 说明
public boolean createNewFile() 创建一个新的空的文件
public boolean mkdir() 只能创建一级文件夹
public boolean mkdirs() 可以创建多级文件夹

File类删除文件的功能

方法名称 说明
public boolean delete() 删除文件、空文件夹

注意: delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站

需要注意的是:

  1. mkdir(): 只能创建单级文件夹、
  2. mkdirs(): 才能创建多级文件夹
  3. delete(): 文件可以直接删除,但是文件夹只能删除空的文件夹,文件夹有内容删除不了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 目标:掌握File创建和删除文件相关的方法。
*/
public class FileTest3 {
public static void main(String[] args) throws Exception {
// 1、public boolean createNewFile():创建一个新文件(文件内容为空),创建成功返回true,反之。
File f1 = new File("D:/resource/omiga.txt");
System.out.println(f1.createNewFile());

// 2、public boolean mkdir():用于创建文件夹,注意:只能创建一级文件夹
File f2 = new File("D:/resource/aaa");
System.out.println(f2.mkdir());

// 3、public boolean mkdirs():用于创建文件夹,注意:可以创建多级文件夹
File f3 = new File("D:/resource/bbb/ccc/ddd/eee/fff/ggg");
System.out.println(f3.mkdirs());

// 3、public boolean delete():删除文件,或者空文件,注意:不能删除非空文件夹。
System.out.println(f1.delete());
System.out.println(f2.delete());
File f4 = new File("D:/resource");
System.out.println(f4.delete());
}
}

遍历文件夹方法

有人说,想获取到一个文件夹中的内容,有没有方法呀?也是有的,下面我们就学习两个这样的方法。

方法名称 说明
public String[] list() 获取当前目录下所有的”一级文件名称”到一个字符串数组中去返回
public Filel] istFiles() 获取当前目录下所有的”一级文件对象”到一个文件对象数组中去返回(重点)

话不多少上代码,演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 目标:掌握File提供的遍历文件夹的方法。
*/
public class FileTest4 {
public static void main(String[] args) {
// 1、public String[] list():获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。
File f1 = new File("D:\\course\\待研发内容");
String[] names = f1.list();
for (String name : names) {
System.out.println(name);
}

// 2、public File[] listFiles():(重点)获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点)
File[] files = f1.listFiles();
for (File file : files) {
System.out.println(file.getAbsolutePath());
}

File f = new File("D:/resource/aaa");
File[] files1 = f.listFiles();
System.out.println(Arrays.toString(files1));
}
}

这里需要注意几个问题:

  1. 当主调是文件时,或者路径不存在时,返回null
  2. 当主调是空文件夹时,返回一个长度为0的数组
  3. 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹路径放在File数组中,并把数组返回
  4. 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中,包含隐藏文件
  5. 当主调是一个文件夹,但是没有权限访问时,返回null

目前方法还只能获取当前目录下所有的”一级文件对象”,需要获取到下面一级目录的话,需要用到递归的算法来进行。

递归

  • 什么是递归?

    递归是一种算法,从形式上来说,方法调用自己的形式称之为递归。

  • 递归的形式:有直接递归、间接递归,如下面的代码。

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
/**
* 目标:认识一下递归的形式。
*/
public class RecursionTest1 {
public static void main(String[] args) {
test1();
}

// 直接方法递归
public static void test1(){
System.out.println("----test1---");
test1(); // 直接方法递归
}

// 间接方法递归
public static void test2(){
System.out.println("---test2---");
test3();
}

public static void test3(){
test2(); // 间接递归
}
}

如果直接执行上面的代码,会进入死循环,最终导致栈内存溢出

内存溢出报错

递归算法的执行流程

为了弄清楚递归的执行流程,接下来我们通过一个案例来学习一下。

案例需求:计算n的阶乘,比如5的阶乘 = 1 * 2 * 3 * 4 * 5 ; 6 的阶乘 = 1 * 2 * 3 * 4 * 5 * 6

分析需求用递归该怎么做

1
2
3
4
5
6
7
8
9
10
假设f(n)表示n的阶乘,那么我们可以推导出下面的式子
f(5) = 1+2+3+4+5
f(5) = f(4)+5
f(4) = f(3)+4
f(3) = f(2)+3
f(2) = f(1)+2
f(1) = 1
总结规律:
除了f(1) = 1; 出口
其他的f(n) = f(n-1)+n

我们可以把f(n)当做一个方法,那么方法的写法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 目标:掌握递归的应用,执行流程和算法思想。
*/
public class RecursionTest2 {
public static void main(String[] args) {
System.out.println("5的阶乘是:" + f(5));
}

//求n个数的阶乘
public static int f(int n){
// 终结点
if(n == 1){
return 1;
}else {
return f(n - 1) * n;
}
}
}

这个代码的执行流程,我们用内存图的形式来分析一下,该案例中递归调用的特点是:一层一层调用,再一层一层往回返。

递归的执行流程

递归文件搜索

  1. 先调用文件夹的listFiles方法,获取文件夹的一级内容,得到一个数组
  2. 然后再遍历数组,获取数组中的File对象
  3. 因为File对象可能是文件也可能是文件夹,所以接下来就需要判断
    1. 判断File对象如果是文件,就获取文件名,如果文件名是QQ.exe则打印,否则不打印
    2. 判断File对象如果是文件夹,就递归执行1,2,3步骤
  4. 所以:把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
36
37
38
39
40
41
42
43
/**
* 目标:掌握文件搜索的实现。
*/
public class RecursionTest3 {
public static void main(String[] args) throws Exception {
searchFile(new File("D:/") , "QQ.exe");
}

/**
* 去目录下搜索某个文件
* @param dir 目录
* @param fileName 要搜索的文件名称
*/
public static void searchFile(File dir, String fileName) throws Exception {
// 1、把非法的情况都拦截住
if(dir == null || !dir.exists() || dir.isFile()){
return; // 代表无法搜索
}

// 2、dir不是null,存在,一定是目录对象。
// 获取当前目录下的全部一级文件对象。
File[] files = dir.listFiles();

// 3、判断当前目录下是否存在一级文件对象,以及是否可以拿到一级文件对象。
if(files != null && files.length > 0){
// 4、遍历全部一级文件对象。
for (File f : files) {
// 5、判断文件是否是文件,还是文件夹
if(f.isFile()){
// 是文件,判断这个文件名是否是我们要找的
if(f.getName().contains(fileName)){
System.out.println("找到了:" + f.getAbsolutePath());
Runtime runtime = Runtime.getRuntime();
runtime.exec(f.getAbsolutePath());
}
}else {
// 是文件夹,继续重复这个过程(递归)
searchFile(f, fileName);
}
}
}
}
}

字符集

前面我们已经了解了File类,通过File类的对象可以对文件进行操作,但是不能操作文件中的内容。要想操作文件中的内容,我们还得学习IO流。但是在正式学习IO流之前,我们还需要学习一个前置知识叫做字符集,只有我们把字符集搞明白了,再学习IO流才会更加丝滑。

字符集的来历

我们知道计算机是美国人发明的,由于计算机能够处理的数据只能是0和1组成的二进制数据,为了让计算机能够处理字符,于是美国人就把他们会用到的每一个字符进行了编码(所谓编码,就是为一个字符编一个二进制数据),如下图所示:

标准ASCII字符集

美国人常用的字符有英文字母、标点符号、数字以及一些特殊字符,这些字符一共也不到128个,所以他们用1个字节来存储1字符就够了。 美国人把他们用到的字符和字符对应的编码总结成了一张码表,这张码表叫做ASCII码表(也叫ASCII字符集)。

标准ASCII字符集:ASCII(American Standard Code for lnformation Interchange): 美国信息交换标准代码,包括了英文、符号等
标准ASCII使用1个字节存储一个字符,首尾是0,总共可表示128个字符,对美国人来说完全够用。

其实计算机只在美国用是没有问题的,但是计算机慢慢的普及到全世界,当普及到中国的时候,在计算机中想要存储中文,那ASCII字符集就不够用了,因为中文太多了,随便数一数也有几万个字符。

于是中国人为了在计算机中存储中文,也编了一个中国人用的字符集叫做GBK字符集,这里面包含2万多个汉字字符,GBK中一个汉字采用两个字节来存储,为了能够显示英文字母,GBK字符集也兼容了ASCII字符集,在GBK字符集中一个字母还是采用一个字节来存储

GBK(汉字内码扩展规范,国标)
汉字编码字符集,包含了2万多个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。

注意: GBK兼容了ASCII字符集。

Unicode字符集(统一码,也叫万国码)
Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。

UTF-8
是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。

  1. UTF-8是一种可变长的编码方案,工分为4个长度区
  2. 英文字母、数字占1个字节兼容(ASCII编码)
  3. 汉字字符占3个字节
  4. 极少数字符占4个字节

UTF-8字符集存储数据的样式

字符集小结

最后,我们将前面介绍过的字符集小结一下

  • ASCII字符集:《美国信息交换标准代码》,包含英文字母、数字、标点符号、控制字符
    • 特点:1个字符占1个字节
  • GBK字符集:中国人自己的字符集,兼容ASCII字符集,还包含2万多个汉字
    • 特点:1个字母占用1个字节;1个汉字占用2个字节
  • Unicode字符集:包含世界上所有国家的文字,有三种编码方案,最常用的是UTF-8
    • UTF-8编码方案:英文字母、数字占1个字节兼容(ASCII编码)、汉字字符占3个字节

注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
注意2:英文,数字一般不会乱码,因为很多字符集都兼容了ASCII编码

编码和解码

接下来,我们来使用Java代码完成编码和解码的操作。

其实String类类中就提供了相应的方法,可以完成编码和解码的操作。

  • 编码:把字符串按照指定的字符集转换为字节数组
  • 解码:把字节数组按照指定的字符集转换为字符串

java代码完成对字符的编码

string提供了如下方法 说明
byte[] getBytes() 使用平台的默认字符集将该 string编码为一系列字节,将结果存储到新的字节数组中
byte[] getBytes(String charsetName) 使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中

java代码完成对字符的解码

string提供了如下方法 说明
String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的 string
String(byte[] bytes,String charsetName) 通过指定的字符集解码指定的字节数组来构造新的 string

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 目标:掌握如何使用Java代码完成对字符的编码和解码。
*/
public class Test {
public static void main(String[] args) throws Exception {
// 1、编码
String data = "a我b";
byte[] bytes = data.getBytes(); // 默认是按照平台字符集(UTF-8)进行编码的。
System.out.println(Arrays.toString(bytes));

// 按照指定字符集进行编码。
byte[] bytes1 = data.getBytes("GBK");
System.out.println(Arrays.toString(bytes1));

// 2、解码
String s1 = new String(bytes); // 按照平台默认编码(UTF-8)解码
System.out.println(s1);

String s2 = new String(bytes1, "GBK");
System.out.println(s2);
}
}

IO流(字节流)

IO流的作用:就是可以对文件或者网络中的数据进行读、写的操作。如下图所示

  • 把数据从磁盘、网络中读取到程序中来,用到的是输入流。
  • 把程序中的数据写入磁盘、网络中,用到的是输出流。
  • 简单记:输入流(读数据)、输出流(写数据)

IO流概述

IO流在Java中有很多种,不同的流来干不同的事情。Java把各种流用不同的类来表示,这些流的继承体系如下图所示:

IO流分为两大派系:

  1. 字节流:字节流又分为字节输入流、字节输出流
  2. 字符流:字符流由分为字符输入流、字符输出流

IO流的分类

总结流的四大类
字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流
字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流
字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流
字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。

IO流的体系

FileInputStream读取一个字节

字节流中的字节输入流,用InputStream来表示。但是InputStream是抽象类,我们用的是它的子类,叫FileInputStream

FileInputStream的工作原理

构造方法、成员方法如下图所示:

构造器 说明
public FileInputStream(File file) 创建字节输入流管道与源文件接通
public FileInputStream(String pathname) 创建字节输入流管道与源文件接通
方法名称 说明
public int read() 每次读取一个字节返回,如果发现没有数据可读会返回-1
public int read(byte[] buffer) 每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1

使用FileInputStream读取文件中的字节数据,步骤如下

第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read()方法开始读取文件的字节数据。
第三步:调用close()方法释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 目标:掌握文件字节输入流,每次读取一个字节。
*/
public class FileInputStreamTest1 {
public static void main(String[] args) throws Exception {
// 1、创建文件字节输入流管道,与源文件接通。
InputStream is = new FileInputStream(("file-io-app\\src\\omiga01.txt"));

// 2、开始读取文件的字节数据。
// public int read():每次读取一个字节返回,如果没有数据了,返回-1.
int b; // 用于记住读取的字节。
while ((b = is.read()) != -1){
System.out.print((char) b);
}

//3、流使用完毕之后,必须关闭!释放系统资源!
is.close();
}
}

这里需要注意一个问题:由于一个中文在UTF-8编码方案中是占3个字节,采用一次读取一个字节的方式,读一个字节就相当于读了1/3个汉字,此时将这个字节转换为字符,是会有乱码的。

FileInputStream读取多个字节

上面我们用了FileInputStream调用read()方法,可以一次读取一个字节。但是这种读取方式效率太太太太慢了。 为了提高效率,我们可以使用另一个read(byte[] bytes)的重载方法,可以一次读取多个字节,至于一次读多少个字节,就在于你传递的数组有多大。

使用FileInputStream一次读取多个字节的步骤如下

第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read(byte[] bytes)方法开始读取文件的字节数据。
第三步:调用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
/**
* 目标:掌握使用FileInputStream每次读取多个字节。
*/
public class FileInputStreamTest2 {
public static void main(String[] args) throws Exception {
// 1、创建一个字节输入流对象代表字节输入流管道与源文件接通。
InputStream is = new FileInputStream("file-io-app\\src\\omiga02.txt");

// 2、开始读取文件中的字节数据:每次读取多个字节。
// public int read(byte b[]) throws IOException
// 每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1.

// 3、使用循环改造。
byte[] buffer = new byte[3];
int len; // 记住每次读取了多少个字节。 abc 66
while ((len = is.read(buffer)) != -1){
// 注意:读取多少,倒出多少。
String rs = new String(buffer, 0 , len);
System.out.print(rs);
}
// 性能得到了明显的提升!!
// 这种方案也不能避免读取汉字输出乱码的问题!!

is.close(); // 关闭流
}
}
  • 需要我们注意的是:read(byte[] bytes)它的返回值,表示当前这一次读取的字节个数。

假设有一个a.txt文件如下:abcde

每次读取过程如下

也就是说,并不是每次读取的时候都把数组装满,比如数组是 byte[] bytes = new byte[3];
第一次调用read(bytes)读取了3个字节(分别是97,98,99),并且往数组中存,此时返回值就是3
第二次调用read(bytes)读取了2个字节(分别是99,100),并且往数组中存,此时返回值是2
第三次调用read(bytes)文件中后面已经没有数据了,此时返回值为-1

注意事项:

  • 还需要注意一个问题:采用一次读取多个字节的方式,也是可能有乱码的。因为也有可能读取到半个汉字的情况。

FileInputStream读取全部字节

前面我们到的读取方式,不管是一次读取一个字节,还是一次读取多个字节,都有可能有乱码。那么接下来我们介绍一种,不出现乱码的读取方式。

我们可以一次性读取文件中的全部字节,然后把全部字节转换为一个字符串,就不会有乱码了。

  • 方式一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("file-io-app\\src\\omiga03.txt");

// 2、准备一个字节数组,大小与文件的大小正好一样大。
File f = new File("file-io-app\\src\\omiga03.txt");
long size = f.length();
byte[] buffer = new byte[(int) size];

int len = is.read(buffer);
System.out.println(new String(buffer));

//3、关闭流
is.close();
  • 方式二:
1
2
3
4
5
6
7
8
9
10
// 1、一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("file-io-app\\src\\omiga03.txt");

//2、调用方法读取所有字节,返回一个存储所有字节的字节数组。
byte[] buffer = is.readAllBytes();
System.out.println(new String(buffer));

//3、关闭流
is.close();

最后,还是要注意一个问题:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。

FileOutputStream写字节

往文件中写数据需要用到OutputStream下面的一个子类FileOutputStream。写输入的流程如下图所示

FileOutputStream工作原理

使用FileOutputStream往文件中写数据的步骤如下:

第一步:创建FileOutputStream文件字节输出流管道,与目标文件接通。
第二步:调用wirte()方法往文件中写数据
第三步:调用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
27
28
29
30
/**
* 目标:掌握文件字节输出流FileOutputStream的使用。
*/
public class FileOutputStreamTest4 {
public static void main(String[] args) throws Exception {
// 1、创建一个字节输出流管道与目标文件接通。
// 覆盖管道:覆盖之前的数据
// OutputStream os =
// new FileOutputStream("file-io-app/src/Omiga04out.txt");

// 追加数据的管道
OutputStream os =
new FileOutputStream("file-io-app/src/Omiga04out.txt", true);

// 2、开始写字节数据出去了
os.write(97); // 97就是一个字节,代表a
os.write('b'); // 'b'也是一个字节
// os.write('磊'); // [ooo] 默认只能写出去一个字节

byte[] bytes = "我爱你中国abc".getBytes();
os.write(bytes);

os.write(bytes, 0, 15);

// 换行符
os.write("\r\n".getBytes());

os.close(); // 关闭流
}
}

字节流复制文件

下面做一个文件复制的综合案例。

比如:我们要复制一张图片,从磁盘D:/resource/meinv.png的一个位置,复制到C:/data/meinv.png位置。

  1. 需要创建一个FileInputStream流与源文件接通,创建FileOutputStream与目标文件接通
  2. 然后创建一个数组,使用FileInputStream每次读取一个字节数组的数据,存如数组中
  3. 然后再使用FileOutputStream把字节数组中的有效元素,写入到目标文件中

代码如下:

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
/**
* 目标:使用字节流完成对文件的复制操作。
*/
public class CopyTest5 {
public static void main(String[] args) throws Exception {
// 需求:复制照片。
// 1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("C:/data/meinv.png");

System.out.println(10 / 0);
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}

os.close();
is.close();
System.out.println("复制完成!!");
}
}

IO流资源释放

在我们之前写的代码中,可能会遇到一些bug,导致流无法被正常关闭。

关闭流可能会出现的bug

我们现在知道这个问题了,那这个问题怎么解决呢? 在JDK7以前,和JDK7以后分别给出了不同的处理方案。

JDK7以前的资源释放

在JDK7版本以前,我们可以使用try...catch...finally语句来处理。格式如下

1
2
3
4
5
6
7
8
try{
//有可能产生异常的代码
}catch(异常类 e){
//处理异常的代码
}finally{
//释放资源的代码
//finally里面的代码有一个特点,不管异常是否发生,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
public class Test2 {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
System.out.println(10 / 0);
// 1、创建一个字节输入流管道与源文件接通
is = new FileInputStream("file-io-app\\src\\Omiga03.txt");
// 2、创建一个字节输出流管道与目标文件接通。
os = new FileOutputStream("file-io-app\\src\\Omiga03copy.txt");

System.out.println(10 / 0);

// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源的操作
try {
if(os != null) os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(is != null) is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

代码写到这里,就变得非常冗余了。是的,我也看不下去,本来几行代码就写完了的,加上try...catch...finally之后代码多了十几行,而且阅读性并不高。

JDK7以后的资源释放

在了解了try...catch...finally处理异常,并释放资源代码比较繁琐。Java在JDK7版本为我们提供了一种简化的是否资源的操作,它会自动是否资源。代码写起来也想当简单。

格式如下:

1
2
3
4
5
6
7
try(资源对象1; 资源对象2;){
使用资源的代码
}catch(异常类 e){
处理异常的代码
}

//注意:注意到没有,这里没有释放资源的代码。它会自动是否资源

代码如下:

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
/**
* 目标:掌握释放资源的方式:try-with-resource
*/
public class Test3 {
public static void main(String[] args) {
try (
// 1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("C:/data/meinv.png");
){
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println(conn);
System.out.println("复制完成!!");

} catch (Exception e) {
e.printStackTrace();
}
}
}

字符流

前面我们学习了字节流,使用字节流可以读取文件中的字节数据。但是如果文件中有中文使用字节流来读取,就有可能读到半个汉字的情况,这样会导致乱码。虽然使用读取全部字节的方法不会出现乱码,但是如果文件过大又不太合适。

所以Java专门为我们提供了另外一种流,叫字符流,可以字符流是专门为读取文本数据而生的。

FileReader类

先类学习字符流中的FileReader类,这是字符输入流,用来将文件中的字符数据读取到程序中来。

FileReader读取文件的步骤如下:

第一步:创建FileReader对象与要读取的源文件接通
第二步:调用read()方法读取文件中的字符
第三步:调用close()方法关闭流

需要用到的方法:先通过构造器创建对象,再通过read方法读取数据(注意:两个read方法的返回值,含义不一样

构造器 说明
public FileReader(File file) 创建字符输入流管道与源文件接通
public FileReader(String pathname) 创建字符输入流管道与源文件接通
方法名称 说明
public int read() 每次读取一个字符返回,如果发现没有数据可读会返回-1。
public int read(char[] buffer) 每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,如果发现没有数据可读会返回-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
/**
* 目标:掌握文件字符输入流。
*/
public class FileReaderTest1 {
public static void main(String[] args) {
try (
// 1、创建一个文件字符输入流管道与源文件接通
Reader fr = new FileReader("io-app2\\src\\Omiga.txt");
){
// 2、一个字符一个字符的读(性能较差)
// int c; // 记住每次读取的字符编号。
// while ((c = fr.read()) != -1){
// System.out.print((char) c);
// }
// 每次读取一个字符的形式,性能肯定是比较差的。

// 3、每次读取多个字符。(性能是比较不错的!)
char[] buffer = new char[3];
int len; // 记住每次读取了多少个字符。
while ((len = fr.read(buffer)) != -1){
// 读取多少倒出多少
System.out.print(new String(buffer, 0, len));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

FileWriter类

学习了FileReader,它可以将文件中的字符数据读取到程序中来。接下来,我们就要学习FileWriter了,它可以将程序中的字符数据写入文件。

FileWriter往文件中写字符数据的步骤如下:

第一步:创建FileWirter对象与要读取的目标文件接通
第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符
第三步:调用close()方法关闭流

需要用到的方法如下:构造器是用来创建FileWriter对象的,有了对象才能调用write方法写数据到文件。

构造器 说明
public FileWriter(File file) 创建字节输出流管道与源文件对象接通
public Filewriter(String filepath) 创建字节输出流管道与源文件路径接通
public FileWriter(File file,boolean append) 创建字节输出流管道与源文件对象接通,可追加数据
public FileWriter(String filepath,boolean append) 创建字节输出流管道与源文件路径接通,可追加数据
构造器 说明
void write(int c) 写一个字符
void write(String str) 写一个字符串
void write(String str, int off, int len) 写一个字符串的一部分
void write(char[] cbuf) 写入一个字符数组
void write(char[] cbuf, int off, int 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 目标:掌握文件字符输出流:写字符数据出去
*/
public class FileWriterTest2 {
public static void main(String[] args) {
try (
// 0、创建一个文件字符输出流管道与目标文件接通。
// 覆盖管道
// Writer fw = new FileWriter("io-app2/src/Omiga02out.txt");
// 追加数据的管道
Writer fw = new FileWriter("io-app2/src/Omiga02out.txt", true);
){
// 1、public void write(int c):写一个字符出去
fw.write('a');
fw.write(97);
//fw.write('磊'); // 写一个字符出去
fw.write("\r\n"); // 换行

// 2、public void write(String c)写一个字符串出去
fw.write("我爱你中国abc");
fw.write("\r\n");

// 3、public void write(String c ,int pos ,int len):写字符串的一部分出去
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");

// 4、public void write(char[] buffer):写一个字符数组出去
char[] buffer = {'N', 'P', 'a', 'b', 'c'};
fw.write(buffer);
fw.write("\r\n");

// 5、public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
fw.write(buffer, 0, 2);
fw.write("\r\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}

FileWriter写的注意事项

刚才我们已经学习了FileWriter字符输出流的基本使用。但是,这里有一个小问题需要和同学们说下一:FileWriter写完数据之后,必须刷新或者关闭,写出去的数据才能生效。

比如:下面的代码只调用了写数据的方法,没有关流的方法。当你打开目标文件时,是看不到任何数据的。

方法名称 说明
public void flush() throws IOException 刷新流,就是将内存中缓存的数据立即写到文件中去生效!
public void close() throws IOException 关闭流的操作,包含了刷新!
1
2
3
4
5
6
7
//1.创建FileWriter对象
Writer fw = new FileWriter("io-app2/src/Omiga03out.txt");

//2.写字符数据出去
fw.write('a');
fw.write('b');
fw.write('c');

而下面的代码,加上了flush()方法之后,数据就会立即到目标文件中去。

1
2
3
4
5
6
7
8
9
10
//1.创建FileWriter对象
Writer fw = new FileWriter("io-app2/src/Omiga03out.txt");

//2.写字符数据出去
fw.write('a');
fw.write('b');
fw.write('c');

//3.刷新
fw.flush();

下面的代码,调用了close()方法,数据也会立即到文件中去。因为close()方法在关闭流之前,会将内存中缓存的数据先刷新到文件,再关流。

1
2
3
4
5
6
7
8
9
10
//1.创建FileWriter对象
Writer fw = new FileWriter("io-app2/src/Omiga03out.txt");

//2.写字符数据出去
fw.write('a');
fw.write('b');
fw.write('c');

//3.关闭流
fw.close(); //会先刷新,再关流

但是需要注意的是,关闭流之后,就不能在对流进行操作了。否则会出异常。

字节流、字符流的使用场景小结
字节流适合做一切文件数据的拷贝(音视频,文本);字节流不适合读取中文内容输出
字符流适合做文本文件的操作(读,写)。

缓冲流

学习完字符流之后,接下来我们学习一下缓冲流。我们还是先来认识一下缓存流,再来说一下它的作用。缓冲流有四种,如下图所示

IO流的体系2

缓冲流的作用:可以对原始流进行包装,提高原始流读写数据的性能。

缓冲字节流

我们先来学习字节缓冲流是如何提高读写数据的性能的,原理如下图所示。是因为在缓冲流的底层自己封装了一个长度为8KB(8129byte)的字节数组,但是缓冲流不能单独使用,它需要依赖于原始流。

  • 读数据时:它先用原始字节输入流一次性读取8KB的数据存入缓冲流内部的数组中(ps: 先一次多囤点货),再从8KB的字节数组中读取一个字节或者多个字节(把消耗屯的货)。

缓冲流-读数据时

  • 写数据时: 它是先把数据写到缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始的字节输出流,一次性写到目标文件中去(把囤好的货,一次性运走)。

缓冲流-写数据时

在创建缓冲字节流对象时,需要封装一个原始流对象进来。构造方法如下

构造器 说明
public BufferedInputStream(InputStream is) 把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读数据的性能
public BufferedOutputStream(OutputStream os) 把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能

用缓冲流复制文件,代码写法如下:

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 class BufferedInputStreamTest1 {
public static void main(String[] args) {
try (
InputStream is = new FileInputStream("io-app2/src/Omiga01.txt");
// 1、定义一个字节缓冲输入流包装原始的字节输入流
InputStream bis = new BufferedInputStream(is);

OutputStream os = new FileOutputStream("io-app2/src/Omiga01_bak.txt");
// 2、定义一个字节缓冲输出流包装原始的字节输出流
OutputStream bos = new BufferedOutputStream(os);
){

byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1){
bos.write(buffer, 0, len);
}
System.out.println("复制完成!!");

} catch (Exception e) {
e.printStackTrace();
}
}
}

字符缓冲流

字符缓冲流。它的原理和字节缓冲流是类似的,它底层也会有一个8KB的数组,但是这里是字符数组。字符缓冲流也不能单独使用,它需要依赖于原始字符流一起使用。

BufferedReader读数据时:它先原始字符输入流一次性读取8KB的数据存入缓冲流内部的数组中(ps: 先一次多囤点货),再从8KB的字符数组中读取一个字符或者多个字符(把消耗屯的货)。

创建BufferedReader对象需要用到BufferedReader的构造方法,内部需要封装一个原始的字符输入流,我们可以传入FileReader.

构造器 说明
public BufferedReader(Reader r) 把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读字符数据的性能

而且BufferedReader还要特有的方法,一次可以读取文本文件中的一行

方法 说明
public String readLine() 读取一行数据返回,如果没有数据可读了,会返回nu11

使用BufferedReader读取数据的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BufferedReaderTest2 {
public static void main(String[] args) {
try (
Reader fr = new FileReader("io-app2\\src\\Omiga04.txt");
// 创建一个字符缓冲输入流包装原始的字符输入流
BufferedReader br = new BufferedReader(fr);
){

String line; // 记住每次读取的一行数据
while ((line = br.readLine()) != null){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

BufferedWriter写数据时: 它是先把数据写到字符缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始的字符输出流,一次性写到目标文件中去(把囤好的货,一次性运走)

创建BufferedWriter对象时需要用到BufferedWriter的构造方法,而且内部需要封装一个原始的字符输出流,我们这里可以传递FileWriter

构造器 说明
public Bufferedwriter(Wrter r) 把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能

而且BufferedWriter新增了一个功能,可以用来写一个换行符

方法 说明
public void newLine() 换行

接下来,用代码演示一下,使用BufferedWriter往文件中写入字符数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BufferedWriterTest3 {
public static void main(String[] args) {
try (
Writer fw = new FileWriter("io-app2/src/Omiga05out.txt", true);
// 创建一个字符缓冲输出流管道包装原始的字符输出流
BufferedWriter bw = new BufferedWriter(fw);
){

bw.write('a');
bw.write(97);
bw.write('磊');
bw.newLine();

bw.write("我爱你中国abc");
bw.newLine();

} catch (Exception e) {
e.printStackTrace();
}
}
}

缓冲流性能分析

我们说缓冲流内部多了一个数组,可以提高原始流的读写性能。但是缓冲流不一定能提高性能

下面我们用一个比较大文件(889MB)复制,做性能测试,分别使用下面四种方式来完成文件复制,并记录文件复制的时间。

  1. 使用低级流一个字节一个字节的复制

  2. 使用低级流按照字节数组的形式复制

  3. 使用缓冲流一个字节一个字节的复制

  4. 使用缓冲流按照字节数组的形式复制

1
2
3
4
5
低级流一个字节复制: 慢得简直让人无法忍受
低级流按照字节数组复制(数组长度1024): 12.117s
缓冲流一个字节复制: 11.058s
缓冲流按照字节数组复制(数组长度1024): 2.163s
【注意:这里的测试只能做一个参考,和电脑性能也有直接关系】

经过上面的测试,我们可以得出一个结论:默认情况下,采用一次复制1024个字节,缓冲流完胜。

但是,缓冲流就一定性能高吗?我们采用一次复制8192个字节试试

1
2
低级流按照字节数组复制(数组长度8192): 2.535s
缓冲流按照字节数组复制(数组长度8192): 2.088s

经过上面的测试,我们可以得出一个结论:一次读取8192个字节时,低级流和缓冲流性能相当。相差的那几毫秒可以忽略不计。

继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*32个字节数据试试

1
2
低级流按照字节数组复制(数组长度8192): 1.128s
缓冲流按照字节数组复制(数组长度8192): 1.133s

经过上面的测试,我们可以得出一个结论:数组越大性能越高,低级流和缓冲流性能相当。相差的那几秒可以忽略不计。

继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*6个字节数据试试

1
2
低级流按照字节数组复制(数组长度8192): 1.039s
缓冲流按照字节数组复制(数组长度8192): 1.151s

此时你会发现,当数组大到一定程度,性能已经提高了多少了,甚至缓冲流的性能还没有低级流高。

最终总结一下:缓冲流的性能不一定比低级流高,其实低级流自己加一个数组,性能其实是不差。只不过缓冲流帮你加了一个相对而言大小比较合理的数组 。

转换流

前面我们学习过FileReader读取文件中的字符,但是我们需要注意,FileReader默认只能读取UTF-8编码格式的文件。如果使用FileReader读取GBK格式的文件,可能存在乱码,因为FileReader它遇到汉字默认是按照3个字节来读取的,而GBK格式的文件一个汉字是占2个字节,这样就会导致乱码。

Java给我们提供了另外两种流InputStreamReaderOutputStreamWriter,这两个流我们把它叫做转换流。它们可以将字节流转换为字符流,并且可以指定编码方案。

InputStreamReader类

接下来,我们先学习InputStreamReader类,你看这个类名就比较有意思,前面是InputStream表示字节输入流,后面是Reader表示字符输入流,合在一起意思就是表示可以把InputStream转换为Reader,最终InputStreamReader其实也是Reader的子类,所以也算是字符输入流。

InputStreamReader也是不能单独使用的,它内部需要封装一个InputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。

需求:我们可以先准备一个GBK格式的文件,然后使用下面的代码进行读取,看是是否有乱码。

1
2
3
4
5
//GBK格式
1小声用俄语呢喃
2来掩饰娇羞的
3阿丽亚同学c
abcde
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InputStreamReaderTest2 {
public static void main(String[] args) {
try (
// 1、得到文件的原始字节流(GBK的字节流形式)
InputStream is = new FileInputStream("io-app2/src/Omiga06.txt");
// 2、把原始的字节输入流按照指定的字符集编码转换成字符输入流
Reader isr = new InputStreamReader(is, "GBK");
// 3、把字符输入流包装成缓冲字符输入流
BufferedReader br = new BufferedReader(isr);
){
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}


} catch (Exception e) {
e.printStackTrace();
}
}
}

执行完之后,你会发现没有乱码。

OutputStreamWriter类

接下来,我们学习OutputStreamWriter类,你看这个类名也比较有意思,前面是OutputStream表示字节输出流,后面是Writer表示字符输出流,合在一起意思就是表示可以把OutputStream转换为Writer,最终OutputStreamWriter其实也是Writer的子类,所以也算是字符输出流。

OutputStreamReader也是不能单独使用的,它内部需要封装一个OutputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。

需求:我们可以先准备一个GBK格式的文件,使用下面代码往文件中写字符数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OutputStreamWriterTest3 {
public static void main(String[] args) {
// 指定写出去的字符编码。
try (
// 1、创建一个文件字节输出流
OutputStream os = new FileOutputStream("io-app2/src/Omiga07out.txt");
// 2、把原始的字节输出流,按照指定的字符集编码转换成字符输出转换流。
Writer osw = new OutputStreamWriter(os, "GBK");
// 3、把字符输出流包装成缓冲字符输出流
BufferedWriter bw = new BufferedWriter(osw);
){
bw.write("我是中国人abc");
bw.write("我爱你中国123");

} catch (Exception e) {
e.printStackTrace();
}
}
}

打印流

打印流,这里所说的打印其实就是写数据的意思,它和普通的write方法写数据还不太一样,一般会使用打印流特有的方法叫print(数据)或者println(数据),它打印啥就输出啥。

打印流有两个,一个是字节打印流PrintStream,一个是字符打印流PrintWriter,如下图所示

IO流体系3

PrintStream和PrintWriter的用法是一样的,所以这里就一块演示了。

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 class PrintTest1 {
public static void main(String[] args) {
try (
// 1、创建一个打印流管道
// PrintStream ps =
// new PrintStream("io-app2/src/Omiga08.txt", Charset.forName("GBK"));
// PrintStream ps =
// new PrintStream("io-app2/src/Omiga08.txt");
PrintWriter ps =
new PrintWriter(new FileOutputStream("io-app2/src/Omiga08.txt", true));
){
ps.print(97); //文件中显示的就是:97
ps.print('a'); //文件中显示的就是:a
ps.println("我爱你中国abc"); //文件中显示的就是:我爱你中国abc
ps.println(true);//文件中显示的就是:true
ps.println(99.5);//文件中显示的就是99.5

ps.write(97); //文件中显示a,发现和前面println方法的区别了吗?

} catch (Exception e) {
e.printStackTrace();
}
}
}

重定向输出语句

其实我们开学java的时候,就讲过System.out.println()这句话表示打印输出,但是至于为什么能够输出,其实我们一直不清楚。

以前是因为知识储备还不够,无法解释,到现在就可以给同学们揭晓谜底了,因为System里面有一个静态变量叫out,out的数据类型就是PrintStream,它就是一个打印流,而且这个打印流的默认输出目的地是控制台,所以我们调用System.out.pirnln()就可以往控制台打印输出任意类型的数据,而且打印啥就输出啥。

而且System还提供了一个方法,可以修改底层的打印流,这样我们就可以重定向打印语句的输出目的地了。我们玩一下, 直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrintTest2 {
public static void main(String[] args) {
System.out.println("老骥伏枥");
System.out.println("志在千里");

try ( PrintStream ps = new PrintStream("io-app2/src/Omiga09.txt"); ){
// 把系统默认的打印流对象改成自己设置的打印流
System.setOut(ps);

System.out.println("烈士暮年");
System.out.println("壮心不已");
} catch (Exception e) {
e.printStackTrace();
}
}
}

此时打印语句,将往文件中打印数据,而不在控制台。

数据流

接下我们再学习一种流,这种流在开发中偶尔也会用到。比如,我们想把数据和数据的类型一并写到文件中去,读取的时候也将数据和数据类型一并读出来。这就可以用到数据流,有两个DataInputStreamDataOutputStream

IO流体系4

DataOutputStream类

我们先学习DataOutputStream类,它也是一种包装流,创建DataOutputStream对象时,底层需要依赖于一个原始的OutputStream流对象。然后调用它的wirteXxx方法,写的是特定类型的数据。

构造器 说明
public DataOutputstream(Outputstream out) 创建新数据输出流包装基础的字节输出流
方法 说明
public final void writeByte(int v) throws IOException 将double类型的数据写入基础的字节输出流
public final void writeInt(int v) throws IOException 将byte类型的数据写入基础的字节输出流
public final void writeDouble(Double v) throws IOException 将int类型的数据写入基础的字节输出流
public final void writeUTF(String str) throws IOException 将字符串数据以UTF-8编码成字节写入基础的字节输出流
public void write(int/byte[]/byte[]一部分) 支持写字节数据出去

代码如下:往文件中写整数、小数、布尔类型数据、字符串数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DataOutputStreamTest1 {
public static void main(String[] args) {
try (
// 1、创建一个数据输出流包装低级的字节输出流
DataOutputStream dos =
new DataOutputStream(new FileOutputStream("io-app2/src/Omiga10out.txt"));
){
dos.writeInt(97);
dos.writeDouble(99.5);
dos.writeBoolean(true);
dos.writeUTF("啊哈哈哈666!");

} catch (Exception e) {
e.printStackTrace();
}
}
}

DataInputStream类

学习完DataOutputStream后,再学习DataIntputStream类,它也是一种包装流,创建DataInputStream对象时,底层需要依赖于一个原始的InputStream流对象。然后调用它的readXxx()方法就可以读取特定类型的数据。

构造器 说明
public DataInputstream(Inputstream is) 创建新数据输入流包装基础的字节输入流
方法 说明
public final byte peadByte() throws IOException 读取字节数据返回
public final int readInt() throws IOException 读取int类型的数据返回
public final double readDouble() throws IOException 读取double类型的数据返回
public final String readuTF() throws IOException 读取字符串数(UTF-8)据返回
public int readInt()/read(byte[]) 支持读字节数据进来

代码如下:读取文件中特定类型的数据(整数、小数、字符串等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DataInputStreamTest2 {
public static void main(String[] args) {
try (
DataInputStream dis =
new DataInputStream(new FileInputStream("io-app2/src/Omiga10out.txt"));
){
int i = dis.readInt();
System.out.println(i);

double d = dis.readDouble();
System.out.println(d);

boolean b = dis.readBoolean();
System.out.println(b);

String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}
}
}

序列化流

目前最后一个流要学习,叫做序列化流。 我们知道字节流是以字节为单位来读写数据、字符流是按照字符为单位来读写数据、而对象流是以对象为单位来读写数据。也就是把对象当做一个整体,可以写一个对象到文件,也可以从文件中把对象读取出来。

IO流体系5

序列化的解释:

序列化:意思就是把对象写到文件或者网络中去。(简单记:写对象)
反序列化:意思就是把对象从文件或者网络中读取出来。(简单记:读对象)

ObjectOutputStraem类

接下来,先学习ObjectOutputStream流,它也是一个包装流,不能单独使用,需要结合原始的字节输出流使用。

代码如下:将一个User对象写到文件中去

  • 第一步:先准备一个User类,必须让其实现Serializable接口。
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
// 注意:对象如果需要序列化,必须实现序列化接口。
public class User implements Serializable {
private String loginName;
private String userName;
private int age;
// transient 这个成员变量将不参与序列化。
private transient String passWord;

public User() {
}

public User(String loginName, String userName, int age, String passWord) {
this.loginName = loginName;
this.userName = userName;
this.age = age;
this.passWord = passWord;
}

@Override
public String toString() {
return "User{" +
"loginName='" + loginName + '\'' +
", userName='" + userName + '\'' +
", age=" + age +
", passWord='" + passWord + '\'' +
'}';
}
}
  • 第二步:再创建ObjectOutputStream流对象,调用writeObject方法对象到文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test1ObjectOutputStream {
public static void main(String[] args) {
try (
// 2、创建一个对象字节输出流包装原始的字节 输出流。
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("io-app2/src/Omiga11out.txt"));
){
// 1、创建一个Java对象。
User u = new User("admin", "张三", 32, "666888xyz");

// 3、序列化对象到文件中去
oos.writeObject(u);
System.out.println("序列化对象成功!!");

} catch (Exception e) {
e.printStackTrace();
}
}
}

注意:写到文件中的对象,是不能用记事本打开看的。因为对象本身就不是文本数据,打开是乱码

怎样才能读懂文件中的对象是什么呢?这里必须用反序列化,自己写代码读。

补充知识:IO框架

最后,再补充讲解一个知识,叫做IO框架。现在如果让我们自己写复制文件夹的代码需要用到递归,还是比较麻烦的。为了简化对IO操作,由apache开源基金组织提供了一组有关IO流小框架,可以提高IO流的开发效率。

这个框架的名字叫commons-io:其本质是别人写好的一些字节码文件(class文件),打包成了一个jar包。我们只需要把jar包引入到我们的项目中,就可以直接用了。

这里介绍一个jar包中提供的工具类叫FileUtils,它的部分功能如下,很方便,你一看名字就知道怎么用了。

FileUtils类提供的部分方法展示 说明
public static void copyFile(File srcFile,File destFile) 复制文件
public static void copyDirectory(File srcDir,File destDir) 复制文件夹
public static void deleteDirectory(File directory) 删除文件夹
public static String readFileToString(File file,String encoding) 读数据
public static void writestringToFile(File file, String data, String charname, boolean append) 写数据
louUtils类提供的部分方法展示 说明
public static int copy(InputStream inputStream, OutputStream outputStream) 复制文件
public static int copy(Reader reader, Writer writer) 复制文件
public static void write(String data, OutputStream output, String charsetName) 写数据

在写代码之前,先需要引入jar包,具体步骤如下

  1. 在模块的目录下,新建一个lib文件夹
  2. 把jar包复制粘贴到lib文件夹下
  3. 选择lib下的jar包,右键点击Add As Library,然后就可以用了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CommonsIOTest1 {
public static void main(String[] args) throws Exception {
//1.复制文件
FileUtils.copyFile(new File("io-app2\\src\\Omiga01.txt"), new File("io-app2/src/a.txt"));

//2.复制文件夹
FileUtils.copyDirectory(new File("D:\\resource\\实用技术"), new File("D:\\resource\\实用技术3"));

//3.删除文件夹
FileUtils.deleteDirectory(new File("D:\\resource\\实用技术3"));

// Java提供的原生的一行代码搞定很多事情
Files.copy(Path.of("io-app2\\src\\Omiga01.txt"), Path.of("io-app2\\src\\b.txt"));
System.out.println(Files.readString(Path.of("io-app2\\src\\Omiga01.txt")));
}
}

链接

封面图来源:https://www.pixiv.net/artworks/101462757