Java基础(下)之IO流
IO流-存储和读取数据的解决方案


IO流的体系


字节流 以FileOutputStream/FileInputStream为例
FileOutputStream
基本流程
class Main {
public void main(String[] args) throws IOException {
//1.创建字节输出流对象
FileOutputStream fos = new FileOutputStream("data\\a.txt",false);
//2.写入数据
String str = "kankelaoyezuishuai";
fos.write(str.getBytes());
//3.释放资源
fos.close();
}
}
创建字节输出流 new FileOutputStream
注意:
- 参数是字符串表示的路径或File对象均可
- 若文件不存在则会创建该文件,但必须保证父级路径是存在的
- 若文件中已有数据,是否会覆盖文件中的数据,取决于第二个名为append的boolean参数
写数据 write

注意:
- 在不同系统中对换行的字符规定不同,win:\r\n,Linux:\n,Mac:\r。在Windows OS中,Java对换行进行了优化,程序中写\r或\n可实现换行,因为Java在底层会补全。开发中建议写全。
释放资源 close
每次使用完流之后务必释放资源
FileInputStream
基本流程
class Main {
public void main(String[] args) throws IOException {
File f1 = new File("data\\a.txt");
//1.创建输入流对象
FileInputStream fis = new FileInputStream(f1);
StringBuilder sb = new StringBuilder();
//2.循环读取数据,直到文件末尾
int c;
while((c = fis.read()) != -1){
sb.append((char)c);
}
System.out.println(sb);
//3.释放资源
fis.close();
}
}
创建字节输入流 new FileInputStream
注意:
- 若文件不存在,则直接报错
读数据 read

注意:
- 若读到文件末尾,read方法将返回-1
- read()一次读取一个字节,速度较慢,返回的是读取数据本身。read(byte[])一次读取多个字节并放入数组,具体与数组长度有关,返回的是读取的字节数据个数。
释放资源 close
每次使用完流之后务必释放资源
字节流相关的异常处理

class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\video.mp4");
File f2 = new File("data\\video_copy.mp4");
//1.创建输入流对象
long start = System.currentTimeMillis();
try(FileInputStream fis = new FileInputStream(f1);
FileOutputStream fos = new FileOutputStream(f2);){
//2.循环读取数据,直到文件末尾
byte[] buf = new byte[1024 * 1024];
int len;
while((len = fis.read(buf)) != -1){
fos.write(buf,0,len);
}
}catch (IOException e){
System.out.println(e.getMessage());
}
long end = System.currentTimeMillis();
long dur = end - start;
System.out.println("文件拷贝耗时:"+ dur + " ms");
}
}
示例代码:拷贝一个大文件并测量用时
class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\video.mp4");
File f2 = new File("data\\video_copy.mp4");
//1.创建输入流对象
FileInputStream fis = new FileInputStream(f1);
FileOutputStream fos = new FileOutputStream(f2);
long start = System.currentTimeMillis();
//2.循环读取数据,直到文件末尾
byte[] buf = new byte[1024 * 1024];
int len;
while((len = fis.read(buf)) != -1){
fos.write(buf,0,len);
}
//3.释放资源
fis.close();
fos.close();
long end = System.currentTimeMillis();
long dur = end - start;
System.out.println("文件拷贝耗时:"+ dur + " ms");
}
}
字符集

GBK


Unicode 万国码

UTF-16编码规则:用2-4个字节保存
UTF-32编码规则:固定使用4个字节保存
UTF-8编码规则:用1-4个字节保存

注意:UTF-8不是一个字符集,而是Unicode字符集的一种编码方式。
为什么会产生乱码
原因1:读取数据时未读完整个汉字
例如,使用字节流读取数据时一次只能读取一个字节,而汉字一般由多个字节表示。因此不要用字节流读取文本。
原因2:编码和解码的方式不统一

Java中编解码的实现

class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\video.mp4");
File f2 = new File("data\\video_copy.mp4");
String str = "你好,Java!";
byte[] code_utf8 = str.getBytes();//默认使用UTF-8编码
byte[] code_gbk = str.getBytes("GBK");//GBK
System.out.println(Arrays.toString(code_utf8));
System.out.println(Arrays.toString(code_gbk));
System.out.println(new String(code_utf8));//默认使用UTF-8解码
System.out.println(new String(code_gbk));//编解码方式不一致则会乱码
System.out.println(new String(code_gbk,"GBK"));//正确的编解码不会乱码
}
}
字符流 以FileReader和FileWriter为例


FileReader
class Main {
public static void main(String[] args) throws IOException {
// 1.创建字符输入流对象
File f1 = new File("data\\a.txt");
FileReader fr =new FileReader(f1);
// 2.读取数据(逐个读取)
int ch;
while((ch = fr.read()) != -1){
System.out.print((char)ch);
}
//2.读取数据(连续读取)
char[] buf = new char[1 * 1024 * 1024];
int len;
while((len = fr.read(buf)) != -1){
System.out.println(new String(buf,0,len));
}
//3.释放资源
fr.close();
}
}
基本流程
创建字符输入流对象

注意:路径不存在会报错。
读取数据 read

问题:UTF-8中一个汉字占用了3个字节,而Java中char类型变量占用2个字节,为什么1个char变量接收并存储read返回的1个汉字字符?
解释:在 Java 的体系结构中,文件存储的编码格式与 JVM 堆内存中的字符编码格式是完全解耦的。FileReader 之所以能将 3 字节的 UTF-8 汉字放入 2 字节的 char 中,是因为在这个过程中发生了一次解码与重新编码。
- 外部存储:(UTF-8):这是一种变长编码方案,主要用于磁盘存储和网络传输以节省空间。在 UTF-8 中,常用的汉字确实被编码为 3 个字节。
- 内部内存(UTF-16):Java 语言规范规定,JVM 内存中的 char 类型固定采用 UTF-16 编码表示。在 UTF-16 中,基本多语言平面(BMP,包含了绝大多数常用汉字)内的所有字符,都统一使用 2 个字节(16 bits)来表示。
当调用 FileReader.read() 读取一个汉字时,数据流转经历了以下三个关键阶段:
- 字节读取:FileInputStream 从底层操作系统的文件描述符中读取出 3 个连续的字节。
- 查表与解码(关键步骤):FileReader 内部封装的 StreamDecoder 会根据指定的字符集(此处为 UTF-8),识别出这 3 个字节属于同一个逻辑字符,并计算出它的 Unicode 代码点(Code Point)。
- 内存重塑:获取到代码点后,JVM 会将其转换为内部的 UTF-16 格式。对于常用汉字,该代码点的值恰好落在 0x0000 到 0xFFFF 之间,因此完全可以装入一个 16 位的 char 变量中。
释放资源

FileWriter
基本流程
创建字符输出流

注意:
- 参数是字符串表示的路径或File对象均可
- 若文件不存在则会创建该文件,但必须保证父级路径是存在的
- 若文件中已有数据,是否会覆盖文件中的数据,取决于第二个名为append的boolean参数
写入数据

如果write方法的参数是整数(字符),实际上写到文件中的是经过字符集编码后的整数(字符)
释放资源
每次使用完成后务必释放资源
字符输入流底层原理
- 创建字符输入流对象
底层:关联文件,并创建缓冲区(长度为8192的字节数组) - 读取数据
底层:先判断缓冲区中是否有数据可读取,若有数据,直接从缓冲区读取;若没有数据,则从文件中读取数据到缓冲区,每次尽可能装满缓冲区,若文件中也无数据可读,返回-1
问题:文件中含有8192个a和bcdefg,下列代码打印的结果是什么?
class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\a.txt");
FileReader fr = new FileReader(f1);
int c = fr.read();
FileWriter fw = new FileWriter(f1);
while((c = fr.read()) != -1){
System.out.print((char)c);
}
fw.close();
fr.close();
}
}
解答:打印的结果为8192个a,因为int c = fr.read()会读取文件内容直到8192个字节的缓冲区满,接着fw = new FileWriter(f1)清空了文件,while循环中fr.read()一直从缓冲区中读数据,直到缓冲区为空,再从文件中读取时发现无数据可读,返回-1结束
字符输出流底层原理
- 创建字符输出流对象
底层:关联文件,并创建缓冲区(长度为8192的字节数组) - 写入数据
一般情况下,FileWriter的write方法不会立刻将数据写入文件,而是写入缓冲区中,当缓冲区满 or flush手动刷新 or close方法关闭时,才会将缓冲区数据写入文件中。

高级流

缓冲流

字节缓冲流 提高读写数据的性能

基本流程
同基本字节流
public class HelloWorld {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data\\a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("data\\b.txt"));
// FileInputStream bis = new FileInputStream("data\\a.txt");
// FileOutputStream bos = new FileOutputStream("data\\b.txt");
int b;
while((b = bis.read()) != -1){
bos.write(b);
}
bis.close();
bos.close();
long end = System.currentTimeMillis();
System.out.printf("用时:%d ms",end - start);
}
}
原理(缓冲流与基本流的本质区别)
提速的原因在于缓冲流实例内部持有一个名为 buf 的字节数组(默认大小 8192 字节),以及用于记录读取状态的游标变量,极大地减少了硬盘IO操作导致的系统调用的次数,而基本字节流本身不主动持有应用层缓冲区。这也是缓冲流与基本流的本质区别。

但是缓冲流并不总是比基本流快,特别是在代码中已经手动定义了足够大的缓冲区
FileInputStream fis = new FileInputStream("data.mp4");
byte[] myBuf = new byte[8192];
fis.read(myBuf);
在这种情况下,FileInputStream 同样只产生 1 次系统调用。
此时如果你再套上一层 BufferedInputStream,不仅不会提升速度,反而会降低性能。因为数据会经历两次内存拷贝:操作系统 \(\rightarrow\) 缓冲流的 buf 数组 \(\rightarrow\) 你的 myBuf 数组。
基本流VS缓冲流的拷贝速度对比
public class HelloWorld {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\a.txt");
File f2 = new File("data\\b.txt");
FileInputStream fis = new FileInputStream(f1);
FileOutputStream fos = new FileOutputStream(f2);
long start = System.currentTimeMillis();
int data;
while((data = fis.read()) != -1){
fos.write(data);
}
long end = System.currentTimeMillis();
System.out.printf("基本流逐字节读取%d大小的文件,用时:%d\n",f1.length(),end - start);
fis.close();
fos.close();
fis = new FileInputStream(f1);
fos = new FileOutputStream(f2);
start = System.currentTimeMillis();
byte[] buf = new byte[1024 * 1024];
int len;
while((len = fis.read(buf)) != -1){
fos.write(buf,0,len);
}
end = System.currentTimeMillis();
System.out.printf("基本流批量读取%d大小的文件,用时:%d\n",f1.length(),end - start);
fis.close();
fos.close();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f1));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f2));
start = System.currentTimeMillis();
while((data = bis.read()) != -1){
bos.write(data);
}
end = System.currentTimeMillis();
System.out.printf("缓冲流逐字节读取%d大小的文件,用时:%d\n",f1.length(),end - start);
bis.close();
bos.close();
bis = new BufferedInputStream(new FileInputStream(f1));
bos = new BufferedOutputStream(new FileOutputStream(f2));
start = System.currentTimeMillis();
while((len = bis.read(buf)) != -1){
bos.write(buf,0,len);
}
end = System.currentTimeMillis();
System.out.printf("缓冲流批量读取%d大小的文件,用时:%d\n",f1.length(),end - start);
bis.close();
bos.close();
}
}

字符缓冲流

注意:readLine不会将行末的换行符\n读入字符串。
两个特有方法

原理
这两个类在基础字符流之上,额外开辟了一个字符缓冲区(Character Buffer,即 char[] cb,默认 8192 个字符)。
性能差异(不明显)的本质:
在字符流的场景下,BufferedReader 提升的不再仅仅是减少“系统调用”的次数(这部分已经被 StreamDecoder 做了很大一部分),而是极大地减少了“解码操作”的频率与跨类方法调用的开销。它将高频的单字符解码,转换为了低频的批量解码。
转换流:字符流和字节流之间的桥梁


作用1:使用特定字符集编码读写文本文件(现已淘汰)
FileReader 在早期的 API 设计中存在一个广受诟病的缺陷:它没有提供可以传入字符集参数的构造方法。
Java 11 之前:为了强制使用特定编码读取文件,开发者无法使用 FileReader,只能手动组合转换流与字节流:
// 经典且繁琐的写法
InputStreamReader isr = new InputStreamReader(new FileInputStream("data.txt"), "GBK");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(f1),"GBK");
Java 11 及以后:官方修正了这一缺陷,为 FileReader 补充了重载的构造方法。
在严谨的系统级开发中,隐式依赖默认环境配置是产生诡异 BUG 的根源。无论当前的 JDK 版本如何,符合工程规范的做法是永远显式声明字符集:
// Java 11+ 推荐写法
FileReader fr = new FileReader(file, Charset.forName("GBK"));
FileWriter fw = new FileWriter(file,Charset.forName("GBK"));
作用2:字节流使用字符流的特定方法
利用字节流读取文件中数据,每次读一行,且不能出现乱码
public class HelloWorld {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("data\\a.txt");
InputStreamReader isr = new InputStreamReader(fis,"GBK");//字节流读取中文会乱码,因此需要通过转换流包装为Reader字符流
BufferedReader br = new BufferedReader(isr);//为了使用缓冲流特有的读写整行的方法,因此需要包装为字符缓冲流
String line;
while((line = br.readLine()) != null){
System.out.println(line);
}
fis.close();
}
}
练习:将GBK文本文件转换UTF-8
public class HelloWorld {
public static void main(String[] args) throws IOException {
File f1 = new File("data\\b.txt");
File f2 = new File("data\\c.txt");
BufferedReader br = new BufferedReader(new FileReader(f1,Charset.forName("GBK")));
BufferedWriter bw = new BufferedWriter(new FileWriter(f2,Charset.forName("utf-8")));
String line;
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine();
}
br.close();
bw.close();
}
}
序列化流/对象操作输出流:持久化写Java中的对象
序列化流可以把Java的对象写到文件中,属于字节流的一种。
注意:
- 使用序列化流操作对象时,需要让JavaBean类实现Serializable接口。否则会出现NotSerializableException异常
- 在序列化对象时,若不想保存某个属性,可使用transient关键字来修饰该属性

package com.example.helloworld;
import java.io.*;
import java.util.*;
/// Serializable接口中没有抽象方法,属于标记型接口
/// 一旦实现了这个接口,表明当前类可以被序列化
/// 可序列化是类的对象可以使用序列化流的前提
class Student implements Serializable{
String name;
int age;
public Student(String name,int age){
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) throws IllegalArgumentException{
if (age <= 0 || age > 150) {
throw new IllegalArgumentException();
}
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) throws IllegalArgumentException{
if (name.length() < 3 || name.length() > 10) {
throw new IllegalArgumentException();
}
this.name = name;
}
}
class Main {
public static void main(String[] args) throws IOException {
//1.创建对象
Student s1 = new Student("yxc",18);
//2.创建序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data\\stu.txt"));
//3.写入对象到文件
oos.writeObject(s1);
//4.关闭流
oos.close();
}
}
反序列化流/对象操作输入流
反序列化流可以把Java的对象从文件中读,属于字节流的一种。

//1.创建序列化流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data\\stu.txt"));
//2.从文件中读取对象
Student stu = (Student)ois.readObject();
//3.访问对象
System.out.println(stu);
//4.关闭流
ois.close();
注意:当执行 ois.readObject() 时,ObjectInputStream 会读取流中记录的版本号,并与本地 JVM 内存中加载的类的版本号进行严格的相等性校验。一旦发现两者不匹配,为了防止由于结构不一致导致的内存越界或状态损坏(例如试图将数据反序列化到已被删除的字段中),JVM 会立即阻断并抛出 InvalidClassException。反序列化对象时需要保证当前对象的类拓扑结构没有发生变化。若要强制读取对象,则需要手动设置类的版本号serialVersionUID,示例如下:

/// Serializable接口中没有抽象方法,属于标记型接口
/// 一旦实现了这个接口,表明当前类可以被序列化
/// 可序列化是类的对象可以使用序列化流的前提
class Student implements Serializable{
@Serial
private static final long serialVersionUID = 1L;//手动设置类的版本号
private String name;
private int age;
private transient String address;//使用transient修饰的属性不会被序列化
public Student(String name,int age,String address){
this.age = age;
this.name = name;
this.address = address;
}
public int getAge() {
return age;
}
public void setAge(int age) throws IllegalArgumentException{
if (age <= 0 || age > 150) {
throw new IllegalArgumentException();
}
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) throws IllegalArgumentException{
if (name.length() < 3 || name.length() > 10) {
throw new IllegalArgumentException();
}
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return this.name + " " + Integer.toString(this.age) + " " + this.address;
}
}
建议:
Java 原生的序列化机制由于存在严重的安全漏洞(如反序列化任意代码执行攻击)且性能低下,在现代分布式系统架构中已不被推荐使用。
练习:批量化序列化和反序列化对象
package com.example.helloworld;
import java.io.*;
import java.util.*;
/// Serializable接口中没有抽象方法,属于标记型接口
/// 一旦实现了这个接口,表明当前类可以被序列化
/// 可序列化是类的对象可以使用序列化流的前提
class Student implements Serializable{
@Serial
private static final long serialVersionUID = -7885932473776034884L;
private String name;
private int age;
private String address;//使用transient修饰的属性不会被序列化
public Student(String name,int age,String address){
this.age = age;
this.name = name;
this.address = address;
}
public int getAge() {
return age;
}
public void setAge(int age) throws IllegalArgumentException{
if (age <= 0 || age > 150) {
throw new IllegalArgumentException();
}
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) throws IllegalArgumentException{
if (name.length() < 3 || name.length() > 10) {
throw new IllegalArgumentException();
}
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return this.name + " " + Integer.toString(this.age) + " " + this.address;
}
@Override
public boolean equals(Object obj) {
// 1. 检查是否是同一个对象的引用
if(obj == this) return true;
// 2. 检查是否为空以及类类型是否相同
if(obj == null || this.getClass() != obj.getClass()) return false;
// 3. 强转并比较各个属性的值
Student stu = (Student) obj;
return age == stu.age && address.equals(stu.address) && name.equals(stu.name);
}
}
class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student s1 = new Student("ljm",21,"引镇");
Student s2 = new Student("zld",23,"纺织城");
Student s3 = new Student("王二",25,"Singapore");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data\\stu.txt"));
// oos.writeObject(s1);
// oos.writeObject(s2);
// oos.writeObject(s3);
List<Student>list = new ArrayList<>();
list.add(s1);
list.add(s2);
list.add(s3);
oos.writeObject(list);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data\\stu.txt"));
List<Student>list1 = (ArrayList<Student>)ois.readObject();
ois.close();
System.out.println(list1);
System.out.println(list1.equals(list));//必须重写类的equals方法,否则一直返回false
}
}
打印流

打印流分为PrintStream(字节打印流)和PrintWriter(字符打印流)组成。
特点:
- 打印流只操作文件目的地,不操作数据源
- 其特有的写出方法可以实现,数据原样写出,例如打印97,true到文件中
- 其特有的写出方法可以实现,自动刷新,自动换行,打印一次数据 = 写出 + 换行 + 刷新
字节打印流 System.out的类


class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.创建字节打印流
PrintStream ps = new PrintStream(new FileOutputStream("data\\a.txt"));
PrintStream ps1 = System.out;//特殊的字节打印流,由虚拟机创建,指向控制台,在系统中是唯一的,无法关闭
//2.输出
ps.println(97);//数据原样写出
ps.println(true);
ps.printf("%s和%s共有%d人\n","zld","ljm",2);
//3.释放资源
ps.close();
ps1.close();
ps1.println("猜猜我能执行吗?");
}
}
字符打印流


class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
PrintWriter pw = new PrintWriter(new FileOutputStream("data\\a.txt"),true);
pw.println(108);
pw.print("你好Javaweb");
pw.printf("你好%s,%d","zld",123);
pw.close();
}
}
解压缩流/压缩流

解压缩流
/// 解压zip文件到指定目录
public static void unzip(String zipfile,String dst) throws IOException{
File f1 = new File(zipfile);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(f1)));
ZipEntry entry;
while((entry = zis.getNextEntry()) != null){
System.out.println(entry);
if(entry.isDirectory()){
File target_dir = new File(dst,entry.getName());
if(!target_dir.mkdirs()){
throw new ZipException("Failed to unzip" + zipfile);
}
}else{
File target_file = new File(dst,entry.getName());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target_file));
byte[] data = new byte[1024];
int len;
while((len = zis.read(data)) != -1){
bos.write(data,0,len);
}
bos.close();
zis.closeEntry();//表示压缩包中的一个文件处理完成
}
}
}
压缩流
/// zos:压缩包的输出流
/// src:当前要加入压缩包的文件或目录
/// src_root:要加入压缩包的源文件或目录
public static void zip(ZipOutputStream zos, String src_root,String src) throws IOException {
File src_path = new File(src);
String root_path = new File(src_root).getAbsolutePath();
if (src_path.isFile()) {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src_path));
ZipEntry entry = new ZipEntry(src_path.getName());
zos.putNextEntry(entry);
byte[] data = new byte[1024];
int len;
while ((len = bis.read(data)) != -1) {
zos.write(data, 0, len);
}
bis.close();
zos.closeEntry();
return;
}
for (File file : src_path.listFiles()) {
if (file.isFile()) {
String relativePath = file.getAbsolutePath().substring(root_path.length() + 1);
ZipEntry entry = new ZipEntry(relativePath);
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
byte[] data = new byte[1024];
int len;
while ((len = bis.read(data)) != -1) {
zos.write(data, 0, len);
}
bis.close();
zos.closeEntry();
}else{
zip(zos,root_path, file.getAbsolutePath());
}
}
}
commons-io





浙公网安备 33010602011771号