Java:IO流——增强篇
目录
前言
一、缓冲流——让数据传输飞起来 🚀
1、缓冲思想
2、缓冲字节流
3、缓冲字符流
二、标准流——程序三大通道🚦
1、标准输入流(System.in)
2、标准输出流(System.out)
3、标准错误流(System.err)
4、PrintStream:输出的艺术
三、转换流——编码翻译官🌍
四、对象流——对象的休眠运输仓
1、序列化机制
2、对象流介绍
3、序列化接口
4、集合序列化
5、transient 关键字
6、序列化版本号
五、数据流——基本数据类型的专用通道
六、随机访问流——文件的任意门
七、Properties类——配置文件的管家
前言
上一篇我们介绍了 File类 和 IO流的节点流
Java:IO流——基础篇-CSDN博客https://blog.csdn.net/qq_73698057/article/details/150849027?spm=1001.2014.3001.5502接下来我们看一下IO流的增强流
一、缓冲流——让数据传输飞起来 🚀
缓冲流(Buffered Streams)也叫高效流,是一种非常有用的增强流
提供了缓冲功能,可以提高 IO 操作的效率
1、缓冲思想
通过在内存中引入一个缓冲区
将数据暂存到缓冲区中
然后按一定的块大小进行读取或写入操作
这样就可以减少频繁的 IO操作,提高效率
简单来说就是:
如果你需要去超市买 30个鸡蛋
🐌节点流 每次上下楼只能买一个鸡蛋
🚀增强流 通过 缓冲区(小篮子)
假设这个小篮子一次可以装20个鸡蛋
那么只需要两次就可以完成
📚常见缓冲流:
流类型 | 功能 | 特点 |
---|---|---|
BufferedInputStream | 缓冲字节输入流 | 提升读取效率 |
BufferedOutputStream | 缓冲字节输出流 | 提升写入效率 |
BufferedReader | 缓冲字符输入流 | 支持逐行读取 |
BufferedWriter | 缓冲字符输出流 | 支持批量写入 |
2、缓冲字节流
既然说增强流很快,那么我们来验证一下,真的操作次数少就会效率高吗?
示例:
拷贝一张图片
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class TestBuffered {public static void main(String[] args) throws IOException {//普通流(3100ms左右)FileInputStream fis = new FileInputStream("day29/testBuffered/img.jpg");FileOutputStream fos = new FileOutputStream("day29/testBuffered/img_copy.jpg");long start = System.currentTimeMillis();int data;while((data = fis.read()) != -1){fos.write(data);}long end = System.currentTimeMillis();System.out.println("耗时:" + (end - start) + "ms");fos.flush();fos.close();fis.close();//增强流(高效流)(0-1ms)BufferedInputStream bis = new BufferedInputStream(fis);BufferedOutputStream bos = new BufferedOutputStream(fos);long start1 = System.currentTimeMillis();byte[] bytes = new byte[1024];int pos;while((pos = bis.read(bytes)) != -1){bos.write(bytes,0,pos);}long end1 = System.currentTimeMillis();System.out.println("耗时:" + (end1 - start1));bos.flush();bos.close();bis.close();}
}
注意:
缓冲流是增强流
底层借助其他流实现功能
所以构造器要求一定要传一个字节流对象
3、缓冲字符流
专为文本处理而生
提供了很多便利功能:
- readLine():一次性读取整行数据(不包含换行符)
- newLine():智能换行
- ready():检查是否还有数据可读
示例:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;public class TestBufferedChar {public static void main(String[] args) throws Exception{BufferedReader br = new BufferedReader(new FileReader("day29/testBufferedChar/test"));BufferedWriter bw = new BufferedWriter(new FileWriter("day29/testBufferedChar/test1"));String line;while((line = br.readLine()) != null){bw.write(line);//ready()返回false,可以理解为读到文件最后一行了if(br.ready()){bw.newLine();} }bw.flush();bw.close();br.close();}
}
二、标准流——程序三大通道🚦
Java为每个程序预设了三个流对象
用于处理标准输入、标准输出、标准错误
这些标准流在Java中自动创建,无需显式打开或关闭
流名称 | 对象 | 类型 | 用途 |
---|---|---|---|
标准输入流 | System.in | InputStream | 接收用户输入(键盘) |
标准输出流 | System.out | PrintStream | 正常信息输出(控制台) |
标准错误流 | System.err | PrintStream | 错误信息输出(控制台) |
1、标准输入流(System.in)
它是一个字节流(InputStream)
可以使用 Scanner 或 BufferedReader 等类来读取标准输入流的数据
比如我们之前提到的 Scanner sc = new Scanner(System.in);
大学生入门:Random类及其易踩坑的点-CSDN博客https://blog.csdn.net/qq_73698057/article/details/149612440?spm=1001.2014.3001.5502
2、标准输出流(System.out)
它也是一个字节流(PrintStream)
可以使用 System.out.println(); 或 System.out.print(); 等方法来输出数据
3、标准错误流(System.err)
它还是一个字节流(PrintStream)
比如我们异常处理时用到的 printStackTrace()方法:
public void printStackTrace(){printStackTrace(System.err);
}
4、PrintStream:输出的艺术
它可以改变输出的方向:
System.out.println("默认输出到控制台");// 重定向输出到文件
PrintStream ps = new PrintStream("src/dir/c.txt");
System.setOut(ps);System.out.println("现在输出到文件了!"); // 这行会写入文件
所以拷贝文件还可以这样写,更加优雅:
public class TestPrintStream{public static void main(String[] args) throws Exception{BufferedReader br = new BufferedReader(new FileReader("day29/testBufferedChar/test"));PrintStream ps = new PrintStream("day29/testBufferedChar/test1");String line;while((line = br.readLine()) != null){if(br.ready()){ps.println(line); }else{ps.print(line);}}ps.close();br.close();}
}
这里大家可能会有疑问
既然 PrintStream 是增强流
为什么它实例化对象的时候,不需要通过字节流对象呢?
而是直接写入文件路径
PrintStream ps = new PrintStream("day29/testBufferedChar/test1");
原因可以通过源码解答:
public PrintStream(String fileName) throws FileNotFoundException{this(false, new FileOutputStream(fileName)); }
三、转换流——编码翻译官🌍
当遇到不同编码格式的文件时
转换流就像一位精通多国语言的翻译官
java.io.OutputStreamWriter:将字节输出流转换为字符输出流,并指定编码
java.io.InputStreamReader:将字节输入流转换为字符输入流,并指定编码
话不多说,直接上代码:
// 读取GBK编码文件,写入UTF-8编码文件
InputStreamReader isr = new InputStreamReader(new FileInputStream("GBK.txt"), "GBK");
BufferedReader br = new BufferedReader(isr);OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("UTF8.txt"), "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);String line;
while((line = br.readLine()) != null) {bw.write(line);if(br.ready()) bw.newLine();
}
四、对象流——对象的休眠运输仓
为什么要叫它休眠运输仓呢?
这主要是源于它的一个机制:
1、序列化机制
用于将对象和字节序列之间进行转换
- 序列化:将活生生的对象“冷冻”成字节序列
- 反序列化:将冷冻的对象“解冻”复活
2、对象流介绍
- java.io.ObjectOutputStream:将Java对象转换为字节序列,并输出到内存、文件、网络等地方
- java.io.ObjectInputStream:从某个地方读取出对象的字节序列,并生成对应的对象
示例:
准备一个Student类,使用对象流将学生对象保存在文件中,并读取出来
public class Student {private String name;private int age;private double score;public Student(){}public Student(String name,int age){this.name = name;this.age = age;}public Student(String name,int age,double score){this.name = name;this.age = age;this.score = score;}public String getName() {return name;}public int getAge() {return age;}public double getScore() {return score;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + ", score=" + score + "]";}
}import java.io.ObjectInputStream;public class TestObject {public static void main(String[] args) throws Exception{Student s1 = new Student("张三", 18);ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day29/testObject/object.txt"));oos.writeObject(s1);oos.close();}}
看上去好像没什么问题
但是一运行发现事情不对
报错了!
抛出异常:java.io.NotSerializableException
异常信息显示 Student 的类型无法进行序列化
那我们还需要做哪些准备工作呢?
3、序列化接口
Java中,只有实现了 Serializable接口 的对象才可以进行序列化和反序列化
源码:
package java.iopublic interface Serializable{
}
我们发现这只是一个“标识”接口,并没有抽象方法需要实现
这个时候我们就可以解决上面的问题了:
public class Student implements Serializable{private String name;private int age;private double score;public Student(){}public Student(String name,int age){this.name = name;this.age = age;}public Student(String name,int age,double score){this.name = name;this.age = age;this.score = score;}public String getName() {return name;}public int getAge() {return age;}public double getScore() {return score;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + ", score=" + score + "]";}
}//写入
import java.io.ObjectInputStream;public class TestObject {public static void main(String[] args) throws Exception{Student s1 = new Student("张三", 18);ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day29/testObject/object.txt"));oos.writeObject(s1);oos.close();}
}//读取
import java.io.FileInputStream;public class TestObject {public static void main(String[] args) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day29/testObject/object.txt"));Student s = (Student)ois.readObject();System.out.println(s);ois.close();}
}
不要掉以轻心,还有隐藏的问题:
写入对象次数要和读取次数一致
否则会抛出 EOFException 异常
4、集合序列化
现在我们实现了写入读取单个对象了
那么要是一次性写很多呢?手写不得累死
所以我们可以用到之前学习的集合来进行操作
Java集合 之 单列集合-CSDN博客
java集合 之 多列集合-CSDN博客
java 集合 之 集合工具类Collections-CSDN博客
操作思路:
- 这时我们可以先将多个对象添加到一个集合中
- 然后序列化集合对象
- 反序列化就先读取集合对象
- 然后从集合中获取所有对象
写入:
public class Test_WriteList {public static void main(String[] args) throws Exception {//1.实例化流对象ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/com/test/list.txt"));//2.准备多个Student对象并加入List集合Student s1 = new Student("tom",20);Student s2 = new Student("zs",21);Student s3 = new Student("jack",19);List<Student> list = new ArrayList<>();list.add(s1);list.add(s2);list.add(s3);//3.将集合写入文件oos.writeObject(list);System.out.println("write list success!");//4.操作完成后,关闭流oos.close();} }//运行结果: write list success!
读取:
public class Test_ReadList {public static void main(String[] args) throws IOException, ClassNotFoundException {ObjectInputStream oos = new ObjectInputStream(new FileInputStream("src/com/test/list.txt"));//2.读取集合对象List<Student> list = (List<Student>) oos.readObject();if(list == null) {System.out.println("read null");return;}System.out.println("read list size: " + list.size());//3.遍历输出for (Student stu : list) {System.out.println(stu);}//4.关闭资源oos.close();} }//运行结果: read list size: 3 Student{name='tom', age=20} Student{name='zs', age=21} Student{name='jack', age=19}
5、transient 关键字
用于修饰类中的属性
可以使对象在序列化时忽略掉这个属性
比如一些敏感属性password
我们不想它被序列化
就可以用 transient 修饰
拿上述案例为例的话,如果把 age 忽略掉
那么反序列化之后遍历,会发现年龄都是 0
6、序列化版本号
继续拿刚才的案例来说
假如我们突然反悔了
想继续序列化age了
然后把transient删了
结果编译运行报错了!
抛出异常:java.io.InvalidClassExceotion (无效的类)
异常具体信息中提到了:serialVersionUID,这个就是序列化版本号
序列化版本号:
Java中,serialVersionUID 是一个用于序列化的静态变量
用于表示序列化类的版本号
它是一个长整型数字
用于确保序列化和反序列化前后类的版本一致性
当一个类实现 Serializable接口 并进行序列化时,如果没有显式定义 serialVersionUID,那么系统就会根据类的结构和内容自动生成一个默认序列化版本号。
但是我们一旦修改了类的结构(新增或删除类的成员变量,修改类的继承关系等等),那么这个默认版本号就变了!所以我们刚刚会报错
我们只需要给之前的 Student类 中加上这个就OK了:
private static final long serialVersionUID = 1L;
IO流的主要流讨论完毕
接下来我们看一些特殊流
五、数据流——基本数据类型的专用通道
二进制数据有字节流
文本数据有字符流
那么基本数据类型和字符串也不能受冷落
- DataInputStream:把读取到的字节,转化为指定类型的数据
- DataOutputStream:把指定类型的数据,转化为字节写出去
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
dos.writeLong(1000L);
dos.writeInt(5);
dos.writeUTF("Hello");DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
long l = dis.readLong(); // 1000
int i = dis.readInt(); // 5
String s = dis.readUTF(); // "Hello"out.close();
in.close();
六、随机访问流——文件的任意门
java.io.RandomAccessFile
诶!这玩意长得和之前的不一样!
它并没有继承之前提到的四个抽象父类
它既可读又可写
源码:
package java.io;public class RandomAccessFile implements DataOutput,DataInput,Closeable{//省略...//传入文件及读写方式:public RandomAccess(File file, String mode) throws FileNotFoundException; //跳过pos个字节:public void seek(long pos) throws IOException; }
String mode:操作的模式
毕竟它又可读又可写,所以需要确定一下具体要读还是写
- “r”模式:以只读方式来打开指定文件夹,如果试图写入,会抛出:IOException异常
- “rw”模式:以读写方式打开,如果文件不存在,则试图创建该文件
- “rws”模式:以读写方式打开,还要求对文件内容或元数据的每个更新都同步写入底层设备
- “rwd”模式:以读写方式打开,还要求文件内容每个更新都同步写入到底层设备
元数据:描述数据的数据,比如文件大小、文件名称、图片大小等等
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
raf.seek(6); // 跳到第6个字节位置
raf.write("briup".getBytes()); // 在指定位置写入内容
七、Properties类——配置文件的管家
它是专门用来处理配置文件的工具类,继承自 Hashtable
配置文件通常以 .properties 为扩展名
Properties props = new Properties();
props.load(new FileInputStream("config.properties")); // 加载配置
String value = props.getProperty("key"); // 读取配置
props.setProperty("key", "new_value"); // 修改配置
props.store(new FileOutputStream("config.properties"), "更新配置"); // 保存配置
示例:
将配置文件信息读取出来,然后修改配置文件
原配置文件信息:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.Properties;public class Test {public static void main(String[] args) {String path = "day29/homework/T3/db.properties";Properties p = new Properties();try {p.load(new FileInputStream(path));System.out.println("原始配置:");String driver = p.getProperty("driver");String url = p.getProperty("url");String username = p.getProperty("username");String password = p.getProperty("password");System.out.println("driver:" + driver);System.out.println("url:" + url);System.out.println("username:" + username);System.out.println("password:" + password);p.setProperty("driver", "com.day29.homework.T3.MyDriver");p.setProperty("url", "127.0.0.1:6666");p.setProperty("username", "root");p.setProperty("password", "123456");p.store(new FileOutputStream(path), "更新配置");} catch (Exception e) {}} }
修改之后: