缓冲流
1 缓冲流概述
-
缓冲流自带长度为8192的缓冲区。字节缓冲流是8k,字符缓冲流是16k。
-
缓冲流可以显著提高字节流的读写性能,对于字符流的提升不明显,而是带来了两个特有的方法。
体系结构
2 字节缓冲流
构造方法
方法 | 说明 |
---|---|
public BufferedInputStream(InputStream in) | 把基本字节输入流包装成缓冲流 |
public BufferedOutputStream(OutputStream out) | 把基本字节输出流包装成缓冲流 |
注意
- 缓冲流对象的默认缓冲区大小8192,可手动指定。
- 真正“干活”的还是基本流,缓冲流仅提供一个缓冲区
- 关闭流的时候只需要关闭缓冲流,缓冲流底层会关闭基本流。
public static void main(String[] args) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\test\\aaa.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\test\\bbb.txt"));
byte[] bytes = new byte[10];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
bis.close();
}
3 字符缓冲流
构造方法
方法 | 说明 |
---|---|
public BufferedReader(Reader r) | 把基本字符输入流包装成缓冲流 |
public BufferedWriter(Writer w) | 把基本字符输出流包装成缓冲流 |
特有方法
方法 | 说明 | 来自类 |
---|---|---|
String readLine() | 一次读取一行数据,没有数据返回null | BufferedReader |
void newLine() | 各操作系统通用换行 | BufferedWriter |
注意
readLine
方法
- 读到换行符会停止,所以一次读一行数据。
- 但该方法不会将换行符读取到内存中
public static void main(String[] args) throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\test\\ccc.txt"));
bw.write("第一行"); //第一行
bw.newLine(); //换行
bw.write("第二行"); //第二行
bw.close();
BufferedReader br = new BufferedReader(new FileReader("D:\\test\\ccc.txt"));
String readLine = br.readLine();
System.out.println(readLine);
br.close(); //第一行
}
转换流
1 转换流简述
可以让字节流附加上字符流的特性。只有字符流有转换流。
作用
- (已淘汰)使用指定字符集读写。可以根据字符集一次读取多个字符,读取数据不会乱码。
- 让字节流使用字符流中的方法。
2 InputStreamReader
把字节流转换为字符流读入。
2.1 构造方法
方法 | 说明 |
---|---|
public InputStreamReader(InputStream in) | 把基本字符输入流包装成转换流 |
public InputStreamReader(InputStream in, String charsetName) | 把基本字符输入流包装成指定字符集的转换流 |
2.2 使用指定字符集读取数据
读取使用GBK编码的文件
private static void read1() throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream("src\\do30ConvertStream\\text.txt"));//使用默认字符集UTF-8
int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b);
//乱码 �������
}
isr.close();
}
可以创建指定字符集的转换流
private static void read2() throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream("src\\do30ConvertStream\\text.txt"), "GBK");//指定GBK字符集
int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b); //你好世界
}
isr.close();
}
上述方法已在JDK11被淘汰,JDK11以后可使用FileReader中的构造方法:FileReader(File file, Charset charset)
//创建指定字符集的字符输入流对象
private static void read3() throws IOException {
FileReader fr = new FileReader("src\\do30ConvertStream\\text.txt", Charset.forName("GBK"));//指定GBK字符集
int b;
while ((b = fr.read()) != -1) {
System.out.print((char) b); //你好世界
}
fr.close();
}
2.3 使用字节流读取一行中文
字节流读取中文会出现乱码,且字节流中没有读取一整行数据的方法。所以可以使用转换流,让字节流拥有字符流的特性。
关键代码:BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("路径")));
private static void test() throws IOException {
//普通的字节流
FileInputStream fis = new FileInputStream("src\\do30ConvertStream\\newText.txt");
//转换流:将字节流转换为字符流,使其可以读取中文不乱码
InputStreamReader isr = new InputStreamReader(fis);
//缓冲流:可以一次读取一行数据
BufferedReader br = new BufferedReader(isr);
String s = br.readLine();
System.out.println(s);
br.close();
}
3 OutputStreamWriter
把字符流转换为字节流写出。
3.1 构造方法
方法 | 说明 |
---|---|
OutputStreamWriter(OutputStream out) | 把基本字符输出流包装成转换流 |
OutputStreamWriter(OutputStream out, String charsetName) | 把基本字符输出流包装成指定字符集的转换流 |
3.2 使用指定字符集写出数据
使用GBK编码写文件
private static void writer1() throws IOException {
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("src\\do30ConvertStream\\text.txt"), "GBK");
osw.write("你好世界");
osw.close();
}
上述方法已在JDK11被淘汰,JDK11以后可使用FileWriter中的构造方法:FileWriter(File file, Charset charset)
//创建指定字符集的字符输出流对象(JDK11)
private static void writer2() throws IOException {
FileWriter fw = new FileWriter("src\\do30ConvertStream\\text.txt", Charset.forName("GBK"));
fw.write("你好世界");
fw.close();
}
序列化流
1 序列化流
序列化流也叫对象操作输出流,可以将Java中的对象写到本地文件中。只有字节流有序列化流。
作用
将对象信息以“看不懂”的形式存到本地文件,当我们不希望一些数据能被修改时,可以使用。
2 序列化流方法
构造方法 | 说明 |
---|---|
ObjectOutputStream(OutputStream out) | 将字节输出流包装成序列化流 |
特有方法 | 说明 |
---|---|
final void writeObject(Object obj) | 将对象序列化到文件中 |
3 序列化操作
想让对象序列化,就必须先让该对象JavaBean类实现
Serializable
接口。
如果没有实现 Serializable
接口,会抛出异常 NotSerializableException
。
Serializable
接口时一个标记型接口 ,里面没有抽象方法,只是用来标识该JavaBean类可以被序列化。
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
//省略标准JavaBean
}
private static void test() throws IOException {
//1 创建对象
Student s = new Student("张三", 20);
//2 创建序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src\\do31ObjectStream\\text.txt"));
//3 序列化
oos.writeObject(s);
//4 关流
oos.close();
}
运行结果
�� sr ObjectStream.Studentz���It�� I ageL namet Ljava/lang/String;xp t 张三
4 反序列化流
反序列化流也叫对象操作输入流。与序列化流对应,反序列化流可以将序列化到本地文件中的对象读取到程序中。
5 反序列化流方法
构造方法 | 说明 |
---|---|
ObjectInputStream(InputStream in) | 将字节输入流包装成反序列化流 |
特有方法 | 说明 |
---|---|
final void readObject(Object obj) | 将文件中的对象反序列化到程序中 |
6 反序列化操作
反序列化后的对象是Object类型,需要进一步强转成我们需要的类型。
private static void test() throws IOException, ClassNotFoundException {
//1 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\do31ObjectStream\\text.txt"));
//2 反序列化,并强转
Student s = (Student) ois.readObject();
//3 使用对象
System.out.println(s);
//4 关流
ois.close();
}
运行结果
Student{name = 张三, age = 20}
7 常见问题及操作
7.1 序列号前后不一致
问题描述
当序列化流将对象写到文件后,修改了对象的JavaBean类。再使用反序列化流读取数据就会有异常InvalidClassException
。
原因
序列化流写入对象的同时,也会将该对象在这一刻的所有信息封装成的序列号一同写入文件。而修改JavaBean类,会使该对象的序列号发生改变。当反序列化流读取数据时发现两次序列号不一致 ,就会抛出异常。
处理思路
- 固定序列号 :在JavaBean类中手动定义序列号。
注意!一定要在JavaBean类中的所有属性、方法等全部做完之后 ,再生成序列号!
序列号固定格式:
private static final long serialVersionUID = ?
操作方法 1: 在实现了InvalidClassException
的JavaBean类中手动书写序列号
import java.io.Serializable;
public class Student implements Serializable {
// 手动书写序列号
private static final long serialVersionUID = 1L;
private String name;
private int age;
//省略标准JavaBean
}
操作方法 2: IDEA自动生成
首先需要设置IDEA,让其可以自动生成:
-
打开IDEA的设置
-
在设置中搜索
Serializable
-
勾选如下两个选项,应用即可
设置完成后,在JavaBean类中可以看到类名被“标注”了。
将鼠标悬停在类名上,会有弱警告:“这个类没有定义序列号”。
单击类名,按下 alt + enter
,选择如下解决方案,IDEA就自动生成了一个序列号
7.2 序列化跳过某属性
需求描述
不想将JavaBean类中的某些属性序列化到文件。用户的一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作中被传输。
实现方法
给不想序列号的属性添加 transient
关键字。
关键字
transient
瞬态的
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
- transient关键字只能修饰变量,而不能修饰方法和类。
- 如果是自定义类变量,则该类需要实现Serializable接口。
- 被transient关键字修饰的变量不再能被序列化,而静态变量不论是否被transient修饰,均不能被序列化。
public class Student implements Serializable {
@Serial
private static final long serialVersionUID = 8841963093773026223L;
private String name;
//transient修饰的age属性
private transient int age;
//省略标准JavaBean
}
重新运行上述的序列号和反序列化代码。
运行结果
Student{name = 张三, age = 0}
age的值为默认初始值0,可见age属性没有参与序列化操作。
7.3 反序列化多个对象
问题描述
当序列化多个对象后,想将对象再反序列化出来,可能会有“不知道具体序列化了多少个对象”的问题。而readObject(Object obj)
方法读取不到数据后会抛出异常EOFException
,所有不能使用循环读取。
解决方法
ArrayList集合实现了Serializable
接口。将要序列化的对象储存到ArrayList集合中,再序列化ArrayList集合,反序列化时只需要读取ArrayList集合即可。
import java.io.*;
import java.util.ArrayList;
public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
oos();
ois();
}
private static void oos() throws IOException {
Student s1 = new Student("张三", 20);
Student s2 = new Student("李四", 21);
Student s3 = new Student("王五", 22);
//1 将要序列化的对象储存到ArrayList集合中
ArrayList<Student> list = new ArrayList<>();
list.add(s1);
list.add(s2);
list.add(s3);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src\\do31ObjectStream\\text.txt"));
// 2 序列化ArrayList集合
oos.writeObject(list);
oos.close();
}
private static void ois() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\do31ObjectStream\\text.txt"));
//3 反序列化ArrayList集合
ArrayList<Student> studentArrayList = (ArrayList<Student>) ois.readObject();
for (Student s : studentArrayList) {
System.out.println(s);
}
ois.close();
}
}
class Student implements Serializable {
@Serial
private static final long serialVersionUID = 8841963093773026223L;
private String name;
private int age;
//省略标准JavaBean
}
运行结果
Student{name = 张三, age = 20}
Student{name = 李四, age = 21}
Student{name = 王五, age = 22}
打印流
1 打印流概述
-
打印流只能操作文件目的地,不能操作数据源。即打印流只能写,不能读。
-
打印流可以将数据原样写出。比如写’97’,在文件中写入的就是’97’而不是’a’
-
可以自动刷新,自动换行。打印流打印数据 = 写出 + 换行 + 刷新
2 字节打印流
构造方法
方法 | 说明 |
---|---|
PrintStream(OutputStream out / File f / String Pathname) | 关联字节输出流/文件对象/文件路径 |
PrintStream(OutputStream out, boolean autoFlush) | 自动刷新与否 |
PrintStream(String fileName, Charset c) | 指定字符编码 |
PrintStream(OutputStream out, boolean autoFlush, String encoding / Charset c) | 自动刷新与否,并指定字符编码 |
- 即使字节打印流关联的不是字节输出流,构造方法底层也会先跟据给定的文件对象或路径创建字节输出流对象,再关联字节输出流。
- 字节流底层没有缓冲区,所以自动刷新与否都一样,都是直接写入文件。
特有方法
方法 | 说明 |
---|---|
void println(Xxx x) | 原样写出数据。自动刷新、自动换行 |
void print(Xxx x) | 原样写出数据。不换行 |
void printf(String format, Object... args) | 原样写出数据。可以使用占位符、不换行 |
占位符: 如 %d(整形)、 %s(字符串占位符)、 %f(浮点数占位符)
private static void test() throws FileNotFoundException {
//创建字节打印流对象
PrintStream ps = new PrintStream(new FileOutputStream("src\\do32printStream\\text.txt"), true, Charset.forName("UTF-8"));
//原样写出数据
ps.println("99");
ps.print("a");
ps.printf("%s World", "Hello");
ps.close();
}
运行结果
//text.txt中的内容
99
aHello World
3 字符打印流
构造方法
方法 | 说明 |
---|---|
PrintWriter(Writer out / File f / String Pathname) | 关联字符输出流/文件对象/文件路径 |
PrintWriter(Writer out, boolean autoFlush) | 关联字符输出流,自动刷新与否 |
PrintWriter(OutputStream out, boolean autoFlush) | 关联字节输出流,自动刷新与否 |
PrintWriter(String fileName, Charset c) | 指定字符编码 |
PrintWriter(OutputStream out, boolean autoFlush, Charset c) | 关联字节输出流,自动刷新与否,并指定字符编码 |
字符流底层有缓冲区,想要自动刷新需要手动开启,且只有关联字符输出流或字节输出流时才能开启自动刷新。
特有方法
方法 | 说明 |
---|---|
void println(Xxx x) | 原样写出数据。自动刷新、自动换行 |
void print(Xxx x) | 原样写出数据。不换行 |
void printf(String format, Object... args) | 原样写出数据。可以使用占位符、不换行 |
private static void test() throws IOException {
//创建字符打印流对象
PrintWriter pw = new PrintWriter(new FileWriter("src\\do32printStream\\text.txt"));
//原样写出数据
pw.println("99");
pw.print("a");
pw.printf("%s World", "Hello");
pw.close();
}
运行结果
//text.txt中的内容
99
aHello World
4 输出语句原理
输出语句其实就是一个特殊的字节打印流。
该打印流在虚拟机启动时自动创建。其默认写出的位置不是某个文件,而是控制台。
public final class System {
public static final PrintStream out = null;
}
System.out
就是创建一个PrintStream对象,而println()
是PrintStream类中的方法。
private static void test() {
PrintStream ps = System.out;
ps.println("HelloWorld");
}
如果我们提前关闭该流,控制台将不会再打印数据。
5 输出语句重定向
可以改变虚拟机的输出语句流的位置,让输出语句不再输出到控制台,而是写入文件。
private static void test() throws FileNotFoundException {
System.out.println("第一条输出语句");
PrintStream ps = new PrintStream("D:\\text.txt");
//重定向
System.setOut(ps);
System.out.println("第二条输出语句");
}
在控制台中只输出了第一条语句
在”D:\text.txt“中,有第二条输出语句
压缩流
1 压缩流概述
压缩流用于将文件/文件夹压缩为.zip 文件。解压缩流用于将.zip 文件解压成普通文件。
压缩流继承于字节流。
继承体系
2 压缩流
压缩流只能将文件/文件夹压缩为 .zip 类型。
压缩的本质就是把每一个文件/文件夹封装成ZipEntry对象放到 .zip 文件夹中。
2.1 构造方法
方法 | 说明 |
---|---|
ZipOutputStream(OutputStream out) | 根据字节输出流创建压缩流对象 |
ZipOutputStream(OutputStream out, Charset charset) | 根据字节输出流创建压缩流对象,可指定字符集 |
2.2 特有方法
方法 | 说明 |
---|---|
void putNextEntry(ZipEntry e) | 编写新的ZIP文件条目并将流定位到条目数据的开头。 如果仍然有效,则关闭当前条目。 如果没有为条目指定压缩方法,将使用默认压缩方法,如果条目没有设置修改时间,则使用当前时间。 |
void closeEntry() | 关闭当前 ZIP 条目并定位流以读取下一个条目。 |
2.3 压缩
ZipEntry的构造方法
ZipEntry的构造方法 | 说明 |
---|---|
ZipEntry(String name) | 根据路径创建ZipEntry对象,如果路径中某个文件夹不存在,会创建一个该文件夹 |
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class zipOutputStreamDemo {
public static void main(String[] args) throws IOException {
//需要压缩的文件夹
File src = new File("D:\\test");
//压缩后的文件夹的父级路径
File destParentFile = src.getParentFile(); // test
//压缩后的文件夹的完整路径
File dest = new File(destParentFile, src.getName() + ".zip"); // D:\\test.zip
toZip(src, dest);
}
private static void toZip(File src, File dest) throws IOException {
//1.创建压缩流
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
//2.获取需要压缩的文件夹里的每一个文件,将其变成ZipEntry对象,放到压缩包中
String name = src.getName();
toZipEntry(src, zos, name);
//3.释放资源
zos.close();
}
/**
* 获取需要压缩的文件夹里的每一个文件,将其变成ZipEntry对象,放到压缩包中
* @param src 数据源
* @param zos 压缩流
* @param name 压缩包内部的路径
*/
private static void toZipEntry(File src, ZipOutputStream zos, String name) throws IOException {
//1.进入src文件夹并遍历
File[] files = src.listFiles();
for (File file : files) {
if (file.isFile()) {
//2.如果是文件,将其变成ZipEntry对象,放到压缩包中
//创建ZipEntry对象,用来表示需要压缩的每一个文件/文件夹
ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
//把ZipEntry对象放到压缩包中
zos.putNextEntry(entry);
//将文件中的数据写到压缩包对应的文件中
FileInputStream fis = new FileInputStream(file);
int b;
while ((b = fis.read()) != -1) {
zos.write(b);
}
fis.close();
zos.closeEntry();
} else {
//3.如果是文件夹,递归
toZipEntry(file, zos, name + "\\" + file.getName());
}
}
}
}
3 解压缩流
解压缩流只能解压 .zip
的压缩文件/文件夹。
压缩包里的每一个文件都是ZipEntry对象,
解压的本质就是把每一个ZipEntry对象按照层级拷贝到本地的另一个文件夹中。
3.1 构造方法
方法 | 说明 |
---|---|
ZipInputStream(InputStream in) | 根据字节输入流创建解压缩流对象 |
ZipInputStream(InputStream in, Charset charset) | 根据字节输入流创建解压缩流对象,可指定字符集 |
3.2 特有方法
方法 | 说明 |
---|---|
ZipEntry getNextEntry() | 获取下一个ZipEntry对象,如果没有返回null |
void closeEntry() | 关闭当前 ZIP 条目并定位流以读取下一个条目。 |
3.3 解压
/**
* 解压缩
* @param src 数据源
* @param dest 目的地
*/
private static void unzip(File src, File dest) throws IOException {
//1.创建解压缩流对象
ZipInputStream zis = new ZipInputStream(new FileInputStream(src));
//2.
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
System.out.println(entry);
if (entry.isDirectory()) {
//文件夹:在目的地创建一个同样的文件夹
File file = new File(dest, entry.getName());
file.mkdirs();
} else {
//文件:拷贝到目的地的相同层级下
FileOutputStream fos = new FileOutputStream(new File(dest, entry.getName()));
int b;
while ((b = zis.read()) != -1) {
fos.write(b);
}
fos.close();
//关闭当前 ZIP 条目并定位流以读取下一个条目
zis.closeEntry();
}
}
zis.close();
}