【JAVA入门】Day38 - IO流
文章目录
IO流是存储和读取数据的解决方案。
File 表示系统中的文件或者文件夹的路径,File类只能对文件本身进行操作,不能读写文件里面存储的数据,因此IO流的出现就是为了读写文件里的数据。
IO流既可以读写文件中的数据,也可以读取网络中的数据。它可以把文件中的数据写入本地(Output),也可以把文件中的数据读入到程序中(Input)。
要明确,读和写都是以程序为参照物,是程序在读,程序在写。
一、IO流的分类
IO流按照流的方向分类,可以分为两种:
IO流按照操作文件类型分类,可以分为两种:
注意,纯文本文件指的是:Windows 自带的记事本打开能读懂的文件,比如:txt、md、xml、lrc 等,它们可以用字符流的方式处理。而 docx、xls 等文件不是纯文本文件,只能用字节流的方式处理。
二、IO流的体系结构
IO流的体系分为字节流和字符流,它们都有自己的输入和输出流,但是这些输入输出流都是抽象类,不能直接创建对象,我们还要分别看它们的子类。
以字节流为例:
三、FileOutputStream
FileOutputStream 是操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。
书写步骤:
① 创建字节输出流对象。
② 写数据。
③ 释放资源。
下面的代码就是把一个字符’a’输出到本地文件 a.txt 中的过程。
package IOByteStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo1 {
public static void main(String[] args) throws IOException {
/*
演示:字节输出流FileOutputStream
需求:写出一段文字到本地文件中(写出)
实现步骤:创建对象、写出数据、释放资源
*/
//1.创建对象
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
//2.写出数据
fos.write(97);
//3.释放资源
fos.close();
}
}
在输出到文件的过程中存在诸多细节:
1.创建字节输出流对象时,参数可以是字符串表示的路径,也可以是 File 对象。
2.如果参数中的文件不存在会创建一个新的文件,但是一定要保证这个文件的父级路径是存在的。
3.如果文件已存在,会先清空这个文件,再写入。
4.write 方法的参数是整数,但是实际上写到本地文件中的是整数在ASCII上对应的字符,97 --> a。
5.每次使用完流之后,都要释放资源。
3.1 FileOutputStream 写数据的3种方式
FileOutputStream 写入数据的方式一共有3种,他们是:
以下演示三种写出数据方式。
package IOByteStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo2 {
public static void main(String[] args) throws IOException {
/*
写入数据
*/
//1.创建一个输出流
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
//2.写出数据
//fos.write(97); //a
//fos.write(98); //b
//一次写多个-->byte数组
//byte[] bytes = {97,98,99,100,101};
//fos.write(bytes);
//一次写一个字节数组的一部分数据 void write(byte[] b, int off, int len)
//参数一:数组 参数二:起始索引 参数三:字节个数
byte[] bytes = {97,98,99,100,101};
fos.write(bytes,1,2); //b c
//3.释放资源
fos.close();
}
}
3.2 FileOutputStream 写数据的两个问题
3.2.1 换行写
package IOByteStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo3 {
public static void main(String[] args) throws IOException {
/*
换行写
Windows换行是 \r\n
Java中可以写 \r \n 任何一个作为换行符,在底层可以自动补全
续写
*/
//1.创建对象
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
//2.写出数据
//kankelaoyezuishuai
String str1 = "kankelaoyezuishuai";
byte[] bytes1 = str1.getBytes();
fos.write(bytes1);
String br = "\r\n";
byte[] bytes2 = br.getBytes();
fos.write(bytes2);
String str2 = "666";
byte[] bytes3 = str2.getBytes();
fos.write(bytes3);
//3.释放资源
fos.close();
}
}
3.2.2 续写
我们在创建 FileOutputStream 对象时,其实有第二个参数——“续写开关”。
package IOByteStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo4 {
public static void main(String[] args) throws IOException {
/*
续写开关 boolean append
在创建对象时,这个变量如果是true,文件就不会清空,可以续写;如果是false,文件就会被覆盖
*/
//1.创建对象
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt",true);
//2.写出数据
//kankelaoyezuishuai
String str1 = "kankelaoyezuishuai";
byte[] bytes1 = str1.getBytes();
fos.write(bytes1);
//3.释放资源
fos.close();
}
}
四、FileInputStream
FileInputStream 字节输入流可以操作本地文件,把里面的数据读取到程序中来。
书写步骤:
① 创建字节输入流对象。
② 读数据。
③ 释放资源。
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ByteStreamDemo5 {
public static void main(String[] args) throws IOException {
/*
字节流去读
*/
//1.创建对象
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
//2.读数据
int b1 = fis.read();
System.out.println(b1); //读取一个数据,得到的是97,是字符'a'的ASCII码值
System.out.println((char)b1); //强转得到'a'
//read() 方法是负责读取文件里的数据的,而且是一个一个地读,如果最后读不到了(文件尾),会返回-1
//3.释放资源
fis.close();
}
}
在输入到程序的过程中,存在诸多细节:
1.如果文件不存在,就直接报错。
2.read 方法一次读一个字节,读出来的是数据在ASCII上对应的数字。
3.每调用一次 read 方法,“指针”往后移动一位。如果读取多次,读到文件末尾了,read 方法返回 -1。
4.每次使用完流必须要释放资源。
4.1 FileInputStream 循环读取
由于 read 方法每次只能读取一个字节,所以我们要学习循环读取,这样才能读完一个文件中所有的内容。
定义一个变量 b ,用来存储读到的每一个字符,然后用一个 while 循环打印每一个字符,循环条件用 read() 方法赋值给 b,每进行一次这个赋值,读取字符的“指针”就前进一步,循环直到 read() 方法返回 -1,此时意味着读到了文件尾,循环结束。
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ByteStreamDemo6 {
public static void main(String[] args) throws IOException {
/*
字节输入流循环读取
*/
//1.创建对象
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
//2.循环读取
int b;
while((b = fis.read()) != -1) {
System.out.print((char)b);
}
//3.释放资源
fis.close();
}
}
五、文件拷贝
如何拷贝一个文件?参考下面的代码。
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo7 {
public static void main(String[] args) throws IOException {
/*
文件拷贝
把D:\IdeaProjects\HelloWord\src\Files\beCopied拷贝到当前模块下
*/
//1.创建对象
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
//2.拷贝
//核心思想:边读边写
int b;
while((b = fis.read()) != -1) {
fos.write(b);
}
//3.释放资源
//规则:先开的流最后再关闭
fos.close();
fis.close();
}
}
文件拷贝存在一个很严肃的问题,如果拷贝的文件过大,速度会不会有影响?会的,速度非常慢。因为 FileInputStream 在拷贝时,一次只读写一个字节,有多少个字节,就要循环多少次。
如果想要提高拷贝的速度,就需要一次读取多个字节,它也有相关的方法,看下面的表格:
read 方法的重载可以实现一次读取多个字节,我们在拷贝时,可以创建一个较大的数组(最好是1024的整数倍),比如:1024 * 1024 * 5,50MB的数组来进行拷贝。
在下面的例子中,我们读取了一个文件中的数据,我们使用的是一个2字节的数组,每次读取2个字节的数据,但在最后文件尾,出现了问题,我们在读取新数据时,读取到的字符会替换原数组中的数据,但是在读取文件尾的"e"时,只替换了数组的0索引,而1索引的数据没有被替换,这就导致倒数第二次读取到"cd",最后一次还是读取到"ed",这是错误的。
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ByteStreamDemo8 {
public static void main(String[] args) throws IOException {
/*
public int read(byte[] buffer) 一次读取一个字节数组的数据
*/
//1.创建对象
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
//2.读取数据
byte[] bytes = new byte[2];
//一次读取多个字节数据,具体读多少,跟数组长度有关
//返回值:本次读取到了多少个字节数据
//文件里是abcde
int len1 = fis.read(bytes);
System.out.println(len1);
String str1 = new String(bytes); //len 2
System.out.println(str1); //数组: a b
int len2 = fis.read(bytes);
System.out.println(len2);
String str2= new String(bytes); //len 2
System.out.println(str2); //数组: c d
int len3 = fis.read(bytes);
System.out.println(len3);
String str3= new String(bytes); //len 1
System.out.println(str3); //数组: e d
int len4 = fis.read(bytes);
System.out.println(len4);
String str4= new String(bytes); //len -1
System.out.println(str4); //数组: e d
//3.释放资源
fis.close();
}
}
因此我们再读取时,要指定 String() 中读取到的字符串长度,使用new String(byte[] b, int n, int m) 构造方法,保证每次生成的字符串和读取到的字节个数保持一致。如下面的代码:
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ByteStreamDemo8 {
public static void main(String[] args) throws IOException {
/*
public int read(byte[] buffer) 一次读取一个字节数组的数据
*/
//1.创建对象
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
//2.读取数据
byte[] bytes = new byte[2];
//一次读取多个字节数据,具体读多少,跟数组长度有关
//返回值:本次读取到了多少个字节数据
//文件里是abcde
int len1 = fis.read(bytes);
System.out.println(len1);
String str1 = new String(bytes,0,len1); //len 2
System.out.println(str1); //数组: a b 生成的字符串:a b
int len2 = fis.read(bytes);
System.out.println(len2);
String str2= new String(bytes, 0,len2); //len 2
System.out.println(str2); //数组: c d 生成的字符串:c d
int len3 = fis.read(bytes);
System.out.println(len3);
String str3= new String(bytes, 0,len3); //len 1
System.out.println(str3); //数组: e d 生成的字符串:e
int len4 = fis.read(bytes); //len -1
System.out.println(len4); //文件尾
//3.释放资源
fis.close();
}
}
通过一次读取 byte[ ] 中多个字节的数据,我们可以改写文件拷贝,让它变得更简洁:
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo9 {
public static void main(String[] args) throws IOException {
/*
"D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied"拷贝到"D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt"
*/
long l1 = System.currentTimeMillis();
//1.创建IO流
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
//2.一次读取多个字节的数据
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1) {
fos.write(bytes,0, len); //将bytes[] 数组中 0索引开始 len 长度的数据写入文件
}
//3.关闭IO流
fis.close();
fos.close();
long l2 = System.currentTimeMillis();
System.out.println(l2 - l1); //用了2毫秒
}
}
六、捕获IO流异常
之前在写IO流文件操作时,我们都是用 throws 抛出异常,如果我们想用 try…catch 结构来捕获异常,应该怎么办?
package IOByteStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo10 {
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
fos.write(97); //如果在这里抛出异常,会导致JVM跳过fos.close()代码
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果还按之前的结构写,在 write() 方法处如果发生异常抛出,就会导致后续的 fos.close() 方法无法执行,导致流无法释放,因此我们需要完善 try…catch 结构,使用它的完整结构 try…catch…finally。
package IOByteStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo10 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
fos.write(97);
} catch (IOException e) {
e.printStackTrace();
} finally {
fos.close();
}
}
}
finally 的特点是:里面的代码一定会被执行,无论是否抛出并捕获异常,最终代码都会执行 { } 里的部分,除非虚拟机被停止(比如 exit() 方法)。
还有一个细节要注意,FileInputStream 或者 FileOutputStream 流对象要在 try…catch 体系外声明并初始化为 null,因为如果写在 try { } 当中声明,生成的是局部变量,它是无法在 finally { } 结构中使用的,因此先声明,再在 try { } 中接收对象。
虽然在套入 try…catch…finally 结构之后,fos.close() 语句被放到 finally { } 中一定会执行,但是我们要意识到,fos.close() 语句也是可能发生异常的,这里的异常可能有两种情况:
- fos = new FileInputStream() 语句中的路径不存在,此时输出通道建立失败,fos 还是 null,如此用 fos 调用 close() 方法就会发生空指针异常。
- fos.close() 方法本身也可能抛出 IOException 异常。
因此我们还要对 fos.close() 语句进行单独判断:使用 if 判断 fos 是否为空,如果 fos 还是 null,代表通道建立失败,此时不必释放资源,也就不用调用 close() 方法;如果 fos 非空,可以调用 close() 方法,此时用另一个 try…catch 结构包裹 fos.close() 语句。
package IOByteStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo10 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\a.txt");
fos.write(97); //如果在这里抛出异常
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null) {
try {
fos.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
经过上面的改进,我们可以写出完整的包含异常抛出的文件拷贝代码:
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo11 {
public static void main(String[] args) {
/*
文件拷贝
*/
//1.创建IO流
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1) {
fos.write(bytes,0, len);
}
} catch(IOException e) {
e.printStackTrace();
} finally {
if(fos != null) {
try {
fos.close();
} catch(IOException e) {
e.printStackTrace();
}
}
if(fis != null) {
try {
fis.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
思考上面的完整代码,代码是否过于繁琐?为此,Java 自己也意识到了,于是提供了新的改进方案。
通过上面的写法,可以让创建的流对象在使用完以后自动释放资源,但要注意,不是所有的流都可以写在“创建流对象”的位置,只有实现了 AutoCloseable 接口的流对象才可以写在这里(比如我们的 FileOutputStream 和 FileInputStream)。当 try…catch 体系执行完毕之后,系统会自动释放资源。
JDK7提供的书写方案如下:
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo12 {
public static void main(String[] args) {
/*
fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
JDK7 写法
*/
try(FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt")){
//拷贝代码
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch(IOException e) {
e.printStackTrace();
}
//无须在finally写释放资源代码,资源会自动释放
}
}
JDK9提供的书写方案如下:
package IOByteStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo13 {
public static void main(String[] args) throws FileNotFoundException {
/*
fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
JDK9 写法 IO流中捕获异常
*/
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\HelloWord\\src\\Files\\beCopied");
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\HelloWord\\src\\IOByteStream\\Copy.txt");
try(fis;fos){
//拷贝代码
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch(IOException e) {
e.printStackTrace();
}
//无须在finally写释放资源代码,资源会自动释放
}
}