二、IO流(字节流)
2.1 IO流概述
各位小伙伴,在前面我们已经学习过File类。但是我们知道File只能操作文件,但是不能操作文件中的内容。我们也学习了字符集,不同的字符集存字符数据的原理是不一样的。有了前面两个知识的基础,接下来我们再学习IO流,就可以对文件中的数据进行操作了。
IO流的作用:就是可以对文件或者网络中的数据进行读、写的操作。如下图所示
-
把数据从磁盘、网络中读取到程序中来,用到的是输入流。
-
把程序中的数据写入磁盘、网络中,用到的是输出流。
-
简单记:输入流(读数据)、输出流(写数据)
-

IO流在Java中有很多种,不同的流来干不同的事情。
Java把各种流用不同的类来表示,这些流的分类和继承体系如下图所示:
IO流分为两大派系:
1.字节流:字节流又分为字节输入流、字节输出流
2.字符流:字符流由分为字符输入流、字符输出流


总结流的四大类:
字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流
字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流。
字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。
总结:
1. IO流的作用?
读写文件数据的
2. IO流是怎么划分的,大体分为几类,各自的作用?
字节输入流 InputStream(读字节数据的)
字节输出流 OutputStream(写字节数据出去的)
字符输入流 Reader(读字符数据的)
字符输出流 Writer(写字符数据出去的)

2.2 FileInputStream读取一个字节
同学们,在上节课认识了什么是IO流,接下来我们学习字节流中的字节输入流,用InputStream来表示。但是InputStream是抽象类,因为抽象类不能被实例化对象,我们用的是它的子类,叫FileInputStream。

需要用到的方法如下图所示:有构造方法、成员方法

使用FileInputStream读取文件中的字节数据,步骤如下
第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read()方法开始读取文件的字节数据(一个字节一个字节读取)。
第三步:调用close()方法释放资源
代码如下:
/**
* 目标:掌握文件字节输入流,每次读取一个字节。
*/
public class FileInputStreamTest1 {
public static void main(String[] args) throws Exception {
// 创建文件字节输入流管道, 与源文件接通
// InputStream is = new FileInputStream(new File("day11_io\\src\\sy.txt"));
// 简化写法(推荐使用)
InputStream is = new FileInputStream(("day11_io\\src\\sy.txt"));
// 开始读取文件的字节数据
// read() 每次读取一个字节返回,如果没有数据了,返回-1.
/*int b1 = is.read(); // 读第一个字节
System.out.println((char) b1); // 转字符
int b2 = is.read(); // 读第二个字节
System.out.println((char) b2);
int b3 = is.read(); // 如果没有数据了,返回-1
System.out.println(b3);*/
// 如果文件中数据太大, 以上代码实现比较臃肿, 可以使用循环来读取
int b; // 用于记住读取的字节
while ((b = is.read()) != -1){
System.out.print((char) b);
}
/**
* 注意事项:
* 1. 读取数据的性能很差
* 2. 读取汉字输出会乱码, 无法避免的
* 3. 流使用完毕之后,必须关闭!释放系统资源!
*/
is.close();
}
}
总结:
这里需要注意三个问题:
1. 此方式在读取数据的性能很差,因为每次读取一个字节,读取文件的字节数据时,文件是在硬盘中,需要去硬盘中找文件读取,由于程序在内存中执行的,是不能直接去找硬盘中的文件,每次读取文件的字节时,程序实际是调用相关的硬件资源去读取硬盘文件的数据,程序调用硬件资源的开销以及去硬盘中读取数据的性能,相对内存来说是很慢的,因此在开发中让程序尽量减少调用硬件相关的资源,读取数据的频次,这种方案每次读取一个字节,假如文件中有十万个字节,一百万个字节,就意味着程序要调用十万次硬件相关资源,性能肯定会很差的。
2. 由于一个中文在UTF-8编码方案中是占3个字节,采用一次读取一个字节的方式,读一个字节就相当于读了1/3个汉字,此时将这个字节转换为字符,是会有乱码的。
3. 释放资源
2.3 FileInputStream读取多个字节
各位同学,在上一节我们学习了FileInputStream调用read()方法,可以一次读取一个字节。但是这种读取方式效率太太太太慢了。 为了提高效率,我们可以使用另一个read(byte\[\] bytes)的重载方法,可以一次读取多个字节,至于一次读多少个字节,就在于你传递的数组有多大。
使用FileInputStream一次读取多个字节的步骤如下
第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read(byte[] bytes)方法开始读取文件的字节数据。
第三步:调用close()方法释放资源
代码如下:
/**
* 目标:掌握使用FileInputStream每次读取多个字节。
*/
public class FileInputStreamTest2 {
public static void main(String[] args) throws Exception {
// 创建一个字节输入流对象代表字节输入流管道与源文件接通。
InputStream is = new FileInputStream("day11_io\\src\\sy01.txt");
// 开始读取文件的字节数据: 每次读取多个字节
// 每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1.
/*byte[] buffer = new byte[3];
int len1 = is.read(buffer);
String str1 = new String(buffer);
System.out.println(str1);
System.out.println("当次读取的字节数量: " + len1);
*//**
* buffer = [abc]
* buffer = [66c]
*//*
int len2 = is.read(buffer);
// String str2 = new String(buffer);
// 注意: 读取多少, 倒出多少 buffer[66]
String str2 = new String(buffer, 0, len2);
System.out.println(str2);
System.out.println("当次读取的字节数量: " + len2);
int len3 = is.read(buffer);
System.out.println(len3); // -1*/
// 使用循环改造
byte[] buffer = new byte[3];
int len; // 记住每次读取了多少个字节
while ((len = is.read(buffer)) != -1){
// 注意: 读取多少, 倒出多少
String str = new String(buffer, 0, len);
System.out.print(str);
}
// 释放资源
is.close();
/**
* 性能得到了明显的提升!!
* 这种方案也不能避免读取汉字输出乱码的问题!!
*/
}
}
- 需要我们注意的是:read(byte\[\] bytes)它的返回值,表示当前这一次读取的字节个数。
假设有一个a.txt文件如下:
abc66
每次读取过程如下:
也就是说,并不是每次读取的时候都把数组装满,比如数组是 byte[] bytes = new byte[3];
第一次调用read(bytes)读取了3个字节(分别是97,98,99),并且往数组中存,此时返回值就是3
第二次调用read(bytes)读取了2个字节(分别是99,100),并且往数组中存,此时返回值是2
第三次调用read(bytes)文件中后面已经没有数据了,此时返回值为-1
- 还需要注意一个问题:采用一次读取多个字节的方式,也是可能有乱码的。因为也有可能读取到半个汉字的情况。
问题: 每次读取一个字节数组有什么好处? 存在什么问题?
读取的性能得到了提升
读取中文字符输出无法避免乱码问题。
2.4 FileInputStream读取全部字节
同学们,前面我们到的读取方式,不管是一次读取一个字节,还是一次读取多个字节,都有可能有乱码。那么接下来我们介绍一种,不出现乱码的读取方式。
我们可以一次性读取文件中的全部字节,然后把全部字节转换为一个字符串,就不会有乱码了。

public class FileInputStreamTest3 {
public static void main(String[] args) throws Exception {
// 一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("day11_io\\src\\sy02.txt");
// 准备一个字节数组,大小与文件的大小正好一样大。
File f = new File("day11_io\\src\\sy02.txt");
long size = f.length();
byte[] buffer = new byte[(int) size];
// 读取文件信息
int len = is.read(buffer);
System.out.println(new String(buffer));
// 打印文件大小和文件的字节信息
System.out.println(size);
System.out.println(len);
// 关闭流
is.close();
}
}

public class FileInputStreamTest4 {
public static void main(String[] args) throws Exception {
// 一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("day11_io\\src\\sy02.txt");
// 调用方法读取所有字节,返回一个存储所有字节的字节数组。
byte[] buffer = is.readAllBytes();
System.out.println(new String(buffer));
// 关闭流
is.close();
}
}
最后,还是要注意一个问题:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。
总结:
1. 如何使用字节输入流读取中文内容输出时不乱码呢?
一次性读取完全部字节。
可以定义与文件一样大的字节数组读取,也可以使用官方API.
2. 直接把文件数据全部读取到一个字节数组可以避免乱码,是否存在问题?
如果文件过大,定义的字节数组可能引起内存溢出。
2.5 FileOutputStream写字节
各位同学,前面我们学习了使用FileInputStream读取文件中的字节数据。然后有同学就迫不及待的想学习往文件中写入数据了。
往文件中写数据需要用到OutputStream下面的一个子类FileOutputStream。写输入的流程如下图所示

使用FileOutputStream往文件中写数据的步骤如下:
第一步:创建FileOutputStream文件字节输出流管道,与目标文件接通。
第二步:调用wirte()方法往文件中写数据
第三步:调用close()方法释放资源
代码如下:
/**
* 目标:掌握文件字节输出流FileOutputStream的使用。
*/
public class FileOutputStreamTest1 {
public static void main(String[] args) throws Exception {
// 创建一个字节输出流管道与目标文件接通。
// 覆盖管道:覆盖之前的数据
// OutputStream os = new FileOutputStream("day11_io\\src\\sy03.txt.txt");
// 追加数据的管道
OutputStream os = new FileOutputStream("day11_io\\src\\sy03.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(); // 关闭流
}
}
2.6 字节流复制文件
同学们,我们在前面已经把字节输入流和字节输出流都学习完了。
现在我们就可以用这两种流配合起来使用,做一个文件复制的综合案例。
比如:我们要复制一张图片,从磁盘D:/resource/meinv.png的一个位置,复制到C:/data/meinv.png位置。
复制文件的思路如下图所示:
1.需要创建一个FileInputStream流与源文件接通,创建FileOutputStream与目标文件接通
2.然后创建一个数组,使用FileInputStream每次读取一个字节数组的数据,存如数组中
3.然后再使用FileOutputStream把字节数组中的有效元素,写入到目标文件中

代码如下:
/**
* 目标:使用字节流完成对文件的复制操作。
*/
public class CopyDemo {
public static void main(String[] args) throws Exception {
// 需求:复制照片。
// 1. 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2. 创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("E:/data/meinv.png");
// 3. 创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4. 从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
// 5. 释放资源
os.close();
is.close();
System.out.println("复制完成!!");
}
}
字节流非常适合做一切文件的复制操作:
任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!
三、IO流资源释放
各位同学,上面我们已经学习了字节流,也给同学们强调过,流使用完之后一定要释放资源。如下图一旦程序出现问题输入流和输出流得不到释放,这样系统的性能会受到一定的影响,所以我们之前的代码并不是很专业。

我们现在知道这个问题了,那这个问题怎么解决呢? 在JDK7以前,和JDK7以后分别给出了不同的处理方案。
3.1 JDK7以前的资源释放
在JDK7版本以前,我们可以使用try...catch...finally语句来处理。格式如下
try{
// 有可能产生异常的代码
}catch(异常类 e){
// 处理异常的代码
}finally{
// 释放资源的代码
// finally里面的代码有一个特点,无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。
// 作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)
}
目标对finally的认识:
public class FinallyTest {
public static void main(String[] args) {
try {
System.out.println(10 / 2);
// System.out.println(10 / 0);
// return; // 跳出方法的执行 finally也会执行
// System.exit(0); // 只要终止虚拟机finally就不再执行了
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("-------finally-------");
}
// 思考: 如果不添加finally 放在try...catch...外边是否可以呢? 一旦出现return就不行了
// System.out.println("-------Finally-------");
System.out.println(chu(10, 5));
}
public static int chu(int a, int b){
try {
return a / b;
} catch (Exception e){
e.printStackTrace();
return -1; // 代表的是出现异常
} finally {
// 千万不要在finally中返回数据
// return 111;
}
}
}
改造上面的代码:
public class CopyDemo1 {
public static void main(String[] args) {
// 需求:复制照片。
InputStream is = null;
OutputStream os = null;
try {
// 1、创建一个字节输入流管道与源文件接通
is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
os = new FileOutputStream("E:/data//meinv.png");
System.out.println(10 / 0);
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024];
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len;
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
/**
* 在释放资源时需要考虑两个问题:
* 1. 在try代码块中是否存在关闭流的情况
* 2. 在创建流对象时是否存在异常的情况
*/
try {
if (os != null) os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (is != null) is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
代码写到这里,有很多同学就已经看不下去了。是的,我也看不下去,本来几行代码就写完了的,加上try...catch...finally之后代码多了十几行,而且阅读性并不高。难受....
3.2 JDK7以后的资源释放
刚才很多同学已经发现了try...catch...finally处理异常,并释放资源代码比较繁琐,臃肿,不优雅。
Java在JDK7版本为我们提供了一种简化的释放资源的操作,它会自动释放资源。代码写起来也相当简单。
格式如下:
try(资源对象1; 资源对象2;){
可能出现异常的代码;
}catch(异常类 e){
处理异常的代码
}
() 中只能放置资源,否则报错
什么是资源呢?
资源一般指的是最终实现了AutoCloseable接口。
public abstract class InputStream implements Closeable{ }
public abstract class OutputStream implements Closeable, Flushable { }
public interface Closeable extends AutoCloseable { }
//注意:这里没有释放资源的代码。该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
代码如下:
/**
* 目标:掌握释放资源的方式:try-with-resource
*/
public class CopyDemo2 {
public static void main(String[] args) {
// 需求:复制照片。
try (
// 1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("E:/data//meinv.png");
){
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024];
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
四、字符流
同学们,我们学习了字节流,使用字节流可以读取文件中的字节数据。但是如果文件中有中文,使用字节流来读取,就有可能读到半个汉字的情况,这样会导致乱码。虽然使用读取全部字节的方法不会出现乱码,但是如果文件过大又不太合适。
所以Java专门为我们提供了另外一种流,叫字符流,字符流是专门为读取文本数据而生的。
4.1 FileReader类
先类学习字符流中的FileReader类,这是字符输入流,用来将文件中的字符数据读取到程序中来。

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

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

/**
* 目标:掌握文件字符输入流。
*/
public class FileReaderTest {
public static void main(String[] args) {
try (
// 1、创建一个文件字符输入流管道与源文件接通
Reader fr = new FileReader("day11_io\\src\\sy01.txt");
){
// 2、一个字符一个字符的读(性能较差)
/*int len; // 记住每次读取的字符编号。
while ((len = fr.read()) != -1){
System.out.print((char) len);
}*/
/**
* 每次读取一个字符的形式,性能肯定是比较差的。
* 3、每次读取多个字符。(性能是比较不错的!)
*/
char[] buffer = new char[3];
int len; // 记住每次读取了多少个字符。
while ((len = fr.read(buffer)) != -1){
// 读取多少倒出多少
String str = new String(buffer, 0, len);
System.out.print(str);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2 FileWriter类
我们学习了FileReader,它可以将文件中的字符数据读取到程序中来。接下来,我们就要学习FileWriter了,它可以将程序中的字符数据写入文件。

FileWriter往文件中写字符数据的步骤如下:
第一步:创建FileWirter对象与要读取的目标文件接通
第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符
第三步:调用close()方法关闭流
需要用到的方法如下:构造器是用来创建FileWriter对象的,有了对象才能调用write方法写数据到文件。

接下来,用代码演示一下:
/**
* 目标:掌握文件字符输出流:写字符数据出去
*/
public class FileWriterTest {
public static void main(String[] args) {
try (
// 0、创建一个文件字符输出流管道与目标文件接通。
// 覆盖管道
//Writer fw = new FileWriter("day11_io\\src\\sy02.txt");
// 追加数据的管道
Writer fw = new FileWriter("day11_io\\src\\sy02.txt", true);
){
// 1、写一个字符出去
fw.write('a');
fw.write(97);
//fw.write('东'); // 写一个字符出去
fw.write("\r\n"); // 换行
// 2、写一个字符串出去
fw.write("我爱你中国abc");
fw.write("\r\n");
// 3、写字符串的一部分出去
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");
// 4、写一个字符数组出去
char[] buffer = {'胜', '雅', 'a', 'b', 'c'};
fw.write(buffer);
fw.write("\r\n");
// 5、写字符数组的一部分出去
fw.write(buffer, 0, 2);
fw.write("\r\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3 FileWriter写的注意事项
刚才我们已经学习了FileWriter字符输出流的基本使用。但是,这里有一个小问题需要和同学们说下一:FileWriter写完数据之后,必须刷新或者关闭,写出去的数据才能生效。
比如:下面的代码只调用了写数据的方法,没有关流的方法。当你打开目标文件时,是看不到任何数据的。
public class FileWriterTest2 {
public static void main(String[] args) throws Exception {
// 1. 创建一个文件字符输出流管道与目标文件接通。
Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
// 2. 写字符出去
fw.write('a');
fw.write('b');
fw.write('c');
fw.write('d');
fw.write("\r\n"); // 换行
fw.write("我爱你中国abc");
fw.write("\r\n");
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");
}
}
而下面的代码,加上了flush()方法之后,数据就会立即到目标文件中去。
public class FileWriterTest3 {
public static void main(String[] args) throws Exception {
// 1. 创建一个文件字符输出流管道与目标文件接通。
Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
// 2. 写字符出去
fw.write('a');
fw.write('b');
fw.write('c');
fw.write('d');
fw.write("\r\n"); // 换行
fw.write("我爱你中国abc");
fw.write("\r\n");
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");
// 3. 刷新 每写入数据时都要进行刷新
fw.flush();
fw.write("柳岩");
fw.write("\r\n");
fw.flush();
}
}
下面的代码,调用了close()方法,数据也会立即到文件中去。因为close()方法在关闭流之前,会将内存中缓存的数据先刷新到文件,再关流。
public class FileWriterTest4 {
public static void main(String[] args) throws Exception {
// 1. 创建一个文件字符输出流管道与目标文件接通。
Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
// 2. 写字符出去
fw.write('a');
fw.write('b');
fw.write('c');
fw.write('d');
fw.write("\r\n"); // 换行
fw.write("我爱你中国abc");
fw.write("\r\n");
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");
// 3. 刷新
// fw.flush();
// 或者关闭流 关闭流是包含刷新操作
fw.close();
// 注意: 如果关闭流后再写入数据就会报错
// 可能会有同学问: 如果缓存区的内存不够怎么办? 之前写的数据会自动同步到文件中去
}
}
但是需要注意的是,关闭流之后,就不能在对流进行操作了。否则会出异常
