1 IO 是指的什么?
1.1 基本概念
IO 流也称为输入、输出流,就是用来读写数据的:
- I 表示
intput
,把硬盘文件中的数据读入到内存的过程,称之输入,负责读。、 - O 表示
output
,把内存中的数据写出到硬盘文件的过程,称之输出,负责写。
1.2 IO 流的一般分类
1.3 字符集
字符集基本:
- 计算机底层不可以直接存储字符。计算机中底层智能存储二进制(0,1);
- 二进制是可以转换成十进制的;
- 计算机底层可以表示十进制编号。计算机可以给人类字符进行编号存储,这套编号规则称为字符集;
2 代码实践
字节流主要分为 InputStream 和 OutputStream;前者是输入流,用于读取文件中的数据;后者是文件中的输出流,用于向文件中写入数据;两者的相似之处是,一次性对一个字节进行操作
2.1 FileInputStream
作用:以内存为基准,把磁盘文件中的数据以字节的形式读取到内存中去。
package cn.edu.njust.ioandfile;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* 一次读取一个字节
*/
public class FileInputStreamDemo01 {
public static void main(String[] args) throws IOException {
// 通过文件路径获取文件输入流对象
FileInputStream fis = new FileInputStream("./src/main/resources/io/test01.txt");
// 一次读取一个字符,每次Read完自动跳到下一个
int i1 = fis.read();
int i2 = fis.read();
System.out.println((char) i1);
System.out.println((char) i2);
// 将字节读取到数组中
byte[] buffer = new byte[1024];
int i = fis.read(buffer);
System.out.println("当前读取位置:" + i);
System.out.println("读取内容:" + Arrays.toString(buffer));
// 记得最后释放资源
fis.close();
}
}
package cn.edu.njust.ioandfile;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 循环读取
*/
public class FileInputStreamDemo02 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("./src/main/resources/io/test01.txt");
int code;
while ((code = fis.read()) != -1) {
System.out.print((char) code);
}
fis.close();
}
}
2.2 FileOutputStream
作用:以内存为基准,把内存中的数据以字节的形式写出到磁盘文件中去的流。
package cn.edu.njust.ioandfile;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 不追加数据
* 一次写一个数据
*/
public class FileOutputStreamDemo01 {
public static void main(String[] args) throws IOException {
/*
* 通过路径获取文件流对象,对该文件进行写操作
* 如果文件不存在,会创建一个新的文件
* */
// 使用该方法,会在获取文件流对象的时候清空文件
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/test02.txt");
fos.write(97);
// 记得关闭流,好习惯
fos.close();
}
}
package cn.edu.njust.ioandfile;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 追加文件
* 一次写多个
*/
public class FileOutputStreamDemo02 {
public static void main(String[] args) throws IOException {
/*
* 该方法后面有一个参数可以设置是否追加数据,默认是false
* 可以将其设置为 true,这样创建对象的时候不会清空文件
* */
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/test02.txt", true);
// 一次写入多个数据
byte[] buffer = {48, 49, 52, 56, 52, 53, 54, 56, 55};
fos.write(buffer);
fos.close();
}
}
package cn.edu.njust.ioandfile;
import java.io.FileOutputStream;
public class FileOutputStreamDemo03 {
public static void main(String[] args) throws Exception {
// 追加数据
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/test02.txt", true);
/*
* 在早期的DOS中,换行符需要两步:
* \r 表示回车,会见光标移动至当前行的行首
* \n 将光标移动至下一行
* 实际上在Java中会对我们的换行符进行处理,就算直接写 \n 也达到效果
* */
// windows中的换行符
String wrap = "\r\n";
String data = "haohaohao";
fos.write(wrap.getBytes());
fos.write(data.getBytes());
fos.close();
}
}
2.3 文件拷贝
package cn.edu.njust.ioandfile;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/**
* 小文件的拷贝
* 利用循环读取的方式实现文件的拷贝
*/
public class FileCopyDemo01 {
public static void main(String[] args) throws Exception {
// 输入流读取源文件,输出流写出拷贝文件
// 源文件
FileInputStream fis = new FileInputStream("./src/main/resources/io/test01.txt");
// 目标文件
FileOutputStream fos = new FileOutputStream("src/main/resources/io/copy_test01.txt");
int code;
while ((code = fis.read()) != -1) {
fos.write(code);
}
// 关闭资源
fos.close();
fis.close();
}
}
package cn.edu.njust.ioandfile;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 大文件拷贝
* 将文件使用字节数组存储,进行读写操作
*/
public class FileCopyDemo02 {
public static void main(String[] args) throws Exception {
// 源文件
FileInputStream fis = new FileInputStream("./src/main/resources/io/big.mp4");
// 目标文件
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/copy_big.mp4");
int code;
// 申请一个 5M 大小的缓冲区
byte[] buffer = new byte[1024 * 1024 * 5];
while ((code = fis.read(buffer)) != -1) {
fos.write(buffer, 0, code);
}
fos.close();
fis.close();
}
}
2.4 字符编码解码
package cn.edu.njust.ioandfile;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
/**
* 体验编码和解码
*/
public class CharsetDemo01 {
public static void main(String[] args) throws UnsupportedEncodingException {
/*
* 1. 编码:空参方法是使用默认的编码方式,IDEA使用UTF-8,Eclipse使用GBK
* byte[] getBytes() 使用平台的默认字符集将该 String编码为一系列字节,将结果存储到新的字节数组中
* byte[] getBytes(String charsetName) 使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中
*
* 2. 解码
* String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的 String
* String(byte[] bytes, String charsetName) 通过指定的字符集解码指定的字节数组来构造新的 String
*
* */
String str = "测试test";
// 使用默认的编码方式,IDEA中是UTF-8
byte[] strBytes1 = str.getBytes();
System.out.println("UTF-8编码:" + Arrays.toString(strBytes1));
// 使用GBK编码
byte[] strBytes2 = str.getBytes("GBK");
System.out.println("GBK编码:" + Arrays.toString(strBytes2));
/*
* 解码
* strBytes1 是 UTF-8 编码
* strBytes2 是 GBK 编码
* */
// 使用默认字符集,这里是UTF-8
String str1 = new String(strBytes1);
System.out.println("str1 = " + str1);
// 使用默认方式解码strBytes2,即编码是GBK,而使用UTF-8解码,应该会乱码
String str2 = new String(strBytes2);
System.out.println("默认解码:str2 = " + str2);
// 使用指定字符集解码
String str3 = new String(strBytes2, "GBK");
System.out.println("指定字符集:str3 = " + str3);
}
}
/*
UTF-8编码:[-26, -75, -117, -24, -81, -107, 116, 101, 115, 116]
GBK编码:[-78, -30, -54, -44, 116, 101, 115, 116]
str1 = 测试test
默认解码:str2 = ����test
指定字符集:str3 = 测试test
* */
2.5 FileReader
字符读入流,底层是基于字节输入流的,但是当遇到中文等占用多个字节的编码字符时,会一次读取相应的字节数,即一次读入一个字符,而不是字节。
package cn.edu.njust.ioandfile;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
/**
* 字符读取,一次读取一个
*/
public class FileReaderDemo01 {
public static void main(String[] args) throws IOException {
// 获取一个读取字符的对象
Reader fr = new FileReader("./src/main/resources/io/reader.txt");
/*
* 使用无参的 reader() 得到的返回值是Java将字符处理后得到的数字
* 即返回值就是我们的目标值
*
* */
int code;
while ((code = fr.read()) != -1) {
System.out.print((char) code);
}
// 关闭资源
fr.close();
}
}
package cn.edu.njust.ioandfile;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
/**
* 一次读取多个
*/
public class FileReaderDemo02 {
public static void main(String[] args) throws IOException {
Reader fr = new FileReader("./src/main/resources/io/reader.txt");
/*
* 使用有参的 reader() 读取到的数据,实际数据存储在字符数组中
* 返回值是本次读的实际数据在数据中的长度
* */
char[] buffer = new char[2];
int len;
while ((len = fr.read(buffer)) != -1) {
// len 是返回里面的字符长度,不一定每次读取都会将buffer读满
System.out.print(new String(buffer, 0, len));
}
// 关闭资源
fr.close();
}
}
2.6 FileWriter
字节输出流是每次只输出一个字节,当遇到一些字符的编码需要多个字节的时候,使用字节输出流就会出现问题。而使用字符输出流,就会在编码的时候使用相应的编码方式将多个字节进行编码,将编码后的数据写出。
package cn.edu.njust.ioandfile;
import java.io.FileWriter;
import java.io.Writer;
/**
* 字符输出流的基本使用
*/
public class FileWriterDome01 {
public static void main(String[] args) throws Exception {
/*
* void write(int c) 写一个字符
* void write(char[] cbuf) 写入一个字符数组
* void write(char[] cbuf, int off, int len) 写入字符数组的一部分
* void write(String str) 写一个字符串
* void write(String str, int off, int len) 写一个字符串的一部分\
* flush() 刷新流,还可以继续写数据
* close() 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
* */
Writer fr = new FileWriter("./src/main/resources/io/writer.txt");
// 一次写入一个字符
fr.write(48);
// 一次写入一个字符数组
char[] chars = {'a', 'b', 'c'};
fr.write(chars);
// 一次写入一个字符串
String str = "写入数据";
fr.write(str);
fr.close();
}
}
3 常用方法汇总
3.1 FileInputStream
构造器 | 说明 |
---|---|
public FileInputStream(File file) | 创建字节输入流管道与源文件对象接通 |
public FileInputStream(String pathname) | 创建字节输入流管道与源文件路径接通 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字节返回,如果字节已经没有可读的返回-1 |
public int read(byte[] buffer) | 每次读取一个字节数组返回,如果字节已经没有可读的返回-1 |
public byte[] readAllBytes() throws IOException | 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
3.2 FileOutputStream
构造器 | 说明 |
---|---|
public FileOutputStream(File file) | 创建字节输出流管道与源文件对象接通 |
public FileOutputStream(File file,boolean append) | 创建字节输出流管道与源文件对象接通,可追加数据 |
public FileOutputStream(String filepath) | 创建字节输出流管道与源文件路径接通 |
public FileOutputStream(String filepath,boolean append) | 创建字节输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
public void write(int a) | 写一个字节出去 |
public void write(byte[] buffer) | 写一个字节数组出去 |
public void write(byte[] buffer , int pos , int len) | 写一个字节数组的一部分出去。 |
流的关闭与刷新
方法 | 说明 |
---|---|
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据 |
3.3 字符集的解码、解码操作
String编码:
方法名称 | 说明 |
---|---|
byte[] getBytes() | 使用平台的默认字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
byte[] getBytes(String charsetName) | 使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
String解码:
构造器 | 说明 |
---|---|
String(byte[] bytes) | 通过使用平台的默认字符集解码指定的字节数组来构造新的 String |
String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来构造新的 String |
3.4 FileReader
构造器 | 说明 |
---|---|
public FileReader(File file) | 创建字符输入流管道与源文件对象接通 |
public FileReader(String pathname) | 创建字符输入流管道与源文件路径接通 |
public int read() | 每次读取一个字符返回,如果字符已经没有可读的返回-1 |
public int read(char[] buffer) | 每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字符返回,如果字符已经没有可读的返回-1 |
public int read(char[] buffer) | 每次读取一个字符数组,返回读取的字符数,如果字符已经没有可读的返回-1 |
3.5 FileWriter
构造器 | 说明 |
---|---|
public FileWriter(File file) | 创建字符输出流管道与源文件对象接通 |
public FileWriter(File file,boolean append) | 创建字符输出流管道与源文件对象接通,可追加数据 |
public FileWriter(String filepath) | 创建字符输出流管道与源文件路径接通 |
public FileWriter(String filepath,boolean append) | 创建字符输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
void write(int c) | 写一个字符 |
void write(char[] cbuf) | 写入一个字符数组 |
void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
void write(String str) | 写一个字符串 |
void write(String str, int off, int len) | 写一个字符串的一部分 |
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据 |
4 异常处理
package cn.edu.njust.exceptionlearn;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* JDK7 之前的异常处理
*/
public class ExceptionDemo01 {
public static void main(String[] args) {
// 如果直接定义在 try 块内,那时局部变量,就不能在 finally 中释放资源
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 源文件
fis = new FileInputStream("./src/main/resources/io/big.mp4");
// 目标文件
fos = new FileOutputStream("./src/main/resources/io/copy_big.mp4");
int code;
// 申请一个 5M 大小的缓冲区
byte[] buffer = new byte[1024 * 1024 * 5];
while ((code = fis.read(buffer)) != -1) {
fos.write(buffer, 0, code);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 必须判断是否为空
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
package cn.edu.njust.exceptionlearn;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* JDK7 的异常处理
*/
public class ExceptionDemo02 {
public static void main(String[] args) {
/*
* JDK7 开始,可以将资源定义在try的小括号内,不用手动释放资源,会自动释放
* 但是这有个弊端就是,资源太多可能代码看起看来不爽
* */
try (
FileInputStream fis = new FileInputStream("./src/main/resources/io/big.mp4");
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/copy_big.mp4");
) {
int code;
// 申请一个 5M 大小的缓冲区
byte[] buffer = new byte[1024 * 1024 * 5];
while ((code = fis.read(buffer)) != -1) {
fos.write(buffer, 0, code);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package cn.edu.njust.exceptionlearn;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* JDK9 的异常处理
*/
public class ExceptionDemo03 {
public static void main(String[] args) throws FileNotFoundException {
/*
* JDK9 开始,觉得在JDK7中那样代码不易阅读,所以将资源定义在外部,直接将资源传入
* 但是这样会存在编译时异常,所以必须在方法上抛异常
* 个人感觉不如JDK7的方式
* */
FileInputStream fis = new FileInputStream("./src/main/resources/io/big.mp4");
FileOutputStream fos = new FileOutputStream("./src/main/resources/io/copy_big.mp4");
try (fis; fos) {
int code;
// 申请一个 5M 大小的缓冲区
byte[] buffer = new byte[1024 * 1024 * 5];
while ((code = fis.read(buffer)) != -1) {
fos.write(buffer, 0, code);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
5 字符输入流和字节输出流的底层
5.1 字符输入流 FileReader
底层有一个大小为 8192 字节大小的缓冲区(8KB),每次读入数据的时候
- 首先检查缓冲区是否有数据
- 如果有,就从缓冲区中读取数据
- 如果没有,尽可能的将文件中的数据读入缓冲区
- 不满 8192 字节的全部读入
- 超过 8192 字节的多余部分暂时不读入,等到本次缓冲区全部处理完毕的时候,在将剩余的文件字符尽可能多的一次性读取缓冲区,缓冲区冲第一个位置开始覆盖,不做清空处理
- 根据缓冲区中的数据,每次读一个字符输出
5.2 字符输出流 FileWriter
底层同样有一个大小为 8192 字节的缓冲区,每次先将数据写入到缓冲区,再根据情况写入磁盘
- 首先读入缓冲区
- 如果未满,为调用 flush()方法,为调用 close()方法,则不会输出到磁盘,继续读取数据,将数据继续写入缓冲区
- 缓冲区满,一次性将所有数据写入磁盘;
- 调用 flush() 方法,将当前缓冲区中的所有数据写入磁盘,并将缓冲区的游标指向最初的位置
- 调用 close() 方法,直接将所有缓冲区的内容写入磁盘,并关闭流,关闭流后,不能再进行写操作