Java 黑马程序员学习笔记(进阶篇25)
一、转换流(InputStreamReader/OutputStreamWriter)
转换流是字节流与字符流的桥梁,核心作用是 “指定编码格式读写文本文件”,解决FileReader/FileWriter默认编码(UTF-8)无法修改的问题。
例如:若文本文件是 GBK 编码,用FileReader(默认 UTF-8)读取会乱码,此时必须用转换流指定 GBK 编码。
1. 输入转换流:InputStreamReader
① 作用:
将 “字节输入流” 转换为 “字符输入流”,并指定编码格式读取文本。
② 构造方法
| 构造方法 | 说明 |
|---|---|
InputStreamReader(InputStream in, String charsetName) | 将字节流in转换为字符流,按charsetName编码读取(如 "GBK"、"UTF-8") |
InputStreamReader(InputStream in) | 按默认编码(UTF-8)转换(等价于FileReader) |
③ 案例:用 GBK 编码读取 GBK 格式的文本文件
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;public class InputStreamReaderDemo {public static void main(String[] args) {// 需求:读取GBK编码的文件(避免乱码)try (// 1. 创建字节输入流(读GBK文件)FileInputStream fis = new FileInputStream("D:\\gbk_file.txt");// 2. 转换为字符流,指定GBK编码InputStreamReader isr = new InputStreamReader(fis, "GBK")) {char[] buffer = new char[1024];int readLen;while ((readLen = isr.read(buffer)) != -1) {System.out.print(new String(buffer, 0, readLen)); // 正常显示中文,无乱码}} catch (IOException e) {e.printStackTrace();}}
}
2. 输出转换流:OutputStreamWriter
① 作用:
将 “字符输出流” 转换为 “字节输出流”,并指定编码格式写入文本。
② 构造方法
| 构造方法 | 说明 |
|---|---|
OutputStreamWriter(OutputStream out, String charsetName) | 将字节流out转换为字符流,按charsetName编码写入(如 "GBK") |
OutputStreamWriter(OutputStream out) | 按默认编码(UTF-8)转换(等价于FileWriter) |
③ 案例:用 GBK 编码写入文本文件
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;public class OutputStreamWriterDemo {public static void main(String[] args) {// 需求:用GBK编码写入文件(供其他GBK程序读取)try (// 1. 创建字节输出流(写文件)FileOutputStream fos = new FileOutputStream("D:\\gbk_output.txt");// 2. 转换为字符流,指定GBK编码OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK")) {osw.write("用GBK编码写入的中文!"); // 按GBK编码写入osw.flush(); // 字符流必须flushSystem.out.println("GBK编码写入成功!");} catch (IOException e) {e.printStackTrace();}}
}
3. 转换流总结:
FileReader=InputStreamReader(new FileInputStream(file), "UTF-8")(默认编码);FileWriter=OutputStreamWriter(new FileOutputStream(file), "UTF-8")(默认编码);- 当需要指定编码(如 GBK、GB2312)时,必须用转换流,不能用
FileReader/FileWriter。
4. 综合练习
编程题目:使用缓冲字符流按行读取文本文件并打印
请编写一个 Java 程序,实现以下功能:
(1) 按 “字节输入流→转换流→缓冲字符流” 的顺序包装流对象,读取当前项目路径下 a.txt 文件的内容:
- 用
FileInputStream绑定源文件a.txt(字节流); - 用
InputStreamReader将字节流转换为字符流(使用默认编码); - 用
BufferedReader包装字符流,利用其readLine()方法支持按行读取。
(2) 通过循环调用 BufferedReader.readLine() 方法,逐行读取文件内容,直到读取到 null(表示文件末尾)。
(3) 每读取一行内容,立即在控制台打印该行内容。
(4) 操作完成后,关闭缓冲流资源(关闭缓冲流会自动关闭其包装的底层流)。
(5) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。
要求:
- 源文件为
a.txt(使用相对路径,位于项目根目录)。 - 必须使用
while循环实现逐行读取,循环终止条件为readLine()返回null。
package demo4;import java.io.*;public class test4 {public static void main(String[] args) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")));String line;while((line = br.readLine()) != null) {System.out.println(line);}br.close();}
}
二、序列化流与反序列化流(ObjectInputStream/ObjectOutputStream)
序列化流(对象流)用于 “将 Java 对象转换为字节序列”(序列化)和 “将字节序列恢复为 Java 对象”(反序列化),核心应用是对象的持久化存储(如存到文件)或网络传输。
1. 核心概念
- 序列化(Serialize):对象 → 字节序列(如
User对象→byte[]); - 反序列化(Deserialize):字节序列 → 对象(如
byte[]→User对象); - 序列化条件:对象所属的类必须实现
java.io.Serializable接口(标记接口,无任何抽象方法,仅用于标识 “该类可序列化”)。
2. 序列化流:ObjectOutputStream
① 作用
将对象序列化(写入字节序列到文件或网络)。
② 构造方法
| 构造方法 | 说明 |
|---|---|
ObjectOutputStream(OutputStream out) | 包装一个字节输出流(如new ObjectOutputStream(new FileOutputStream("D:\\obj.txt"))) |
③ 常用方法
| 方法名 | 返回值 | 功能说明 |
|---|---|---|
void writeObject(Object obj) | void | 将对象obj序列化并写入(obj所属类必须实现Serializable) |
void close() | void | 关闭流 |
2. 反序列化流:ObjectInputStream
① 作用:
将字节序列反序列化为对象(从文件或网络读取)。
② 构造方法
| 构造方法 | 说明 |
|---|---|
ObjectInputStream(InputStream in) | 包装一个字节输入流(如new ObjectInputStream(new FileInputStream("D:\\obj.txt"))) |
③ 常用方法
| 方法名 | 返回值 | 功能说明 |
|---|---|---|
Object readObject() | Object | 读取字节序列并反序列化为对象(需强制类型转换) |
void close() | void | 关闭流 |
3. 关键细节
(1)serialVersionUID:
① 序列化时,JVM 会为类生成一个默认的序列化版本号(serialVersionUID),用于反序列化时校验 “类结构是否一致”。
② 若类结构修改(如增减字段),默认版本号会变化,导致反序列化失败(InvalidClassException)。
③ 解决方案:手动定义serialVersionUID(常量),固定版本号,避免结构微调导致的反序列化失败:
private static final long serialVersionUID = 1L;
(2) 使用步骤
① 序列化:
// 1. 创建输出流(关联文件)
FileOutputStream fos = new FileOutputStream("user.obj");
// 2. 包装为ObjectOutputStream
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 3. 序列化对象(User类需实现Serializable)
oos.writeObject(new User("zhangsan", 20));
// 4. 释放资源
oos.close();
② 反序列化:
// 1. 创建输入流(关联文件)
FileInputStream fis = new FileInputStream("user.obj");
// 2. 包装为ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(fis);
// 3. 反序列化(强转类型)
User user = (User) ois.readObject();
// 4. 释放资源
ois.close();
③ 关键逻辑:为什么要强制类型转换?
强转是为了匹配目标变量的类型,在你的代码中,目标变量user的类型是User,而readObject()返回的是Object类型。
- Java 是强类型语言,要求变量的类型与赋值的对象类型必须匹配(或存在继承关系的向上转型)。
Object是User的父类,父类类型的引用(Object)可以指向子类对象(User),但反之不行 —— 不能直接将Object类型的引用赋值给User类型的变量,必须通过向下转型(强转) 明确告知编译器:“我确认这个Object对象实际是User类型,请允许赋值”。
- “小转大”(向上转型):自动转换,安全(子类→父类)。
- “大转小”(向下转型):必须强转,且仅当父类引用实际指向子类对象时才安全(否则抛
ClassCastException)。
4. 综合练习
题目:学生对象集合的序列化与反序列化
请编写 Java 程序,实现以下功能:
(1) 定义 Student 类,包含姓名(name,String 类型)、年龄(age,int 类型)、地址(address,String 类型)三个属性,提供构造方法和 toString 方法,且该类必须支持序列化。
(2) 创建测试类 test1,在 main 方法中创建 3 个 Student 对象(示例数据:"zhangsan",23,"南京";"lisi",24,"重庆";"wangwu",25,"深圳"),将这 3 个对象存入 ArrayList<Student> 集合中。
(3) 在 test1 中,使用对象输出流(ObjectOutputStream)将上述 ArrayList 集合序列化后写入到当前项目根目录下的 "b.txt" 文件中。
(4) 创建测试类 test2,在 main 方法中使用对象输入流(ObjectInputStream)从 "b.txt" 文件中读取序列化的集合数据,反序列化为 ArrayList<Student> 集合。
(5) 在 test2 中遍历反序列化得到的集合,打印每个 Student 对象的信息(通过 toString 方法)。
(6) 操作完成后,关闭所有流资源。
(7) 程序需声明抛出 IOException 和 ClassNotFoundException(无需捕获,直接通过 throws 声明)。
要求:
- Student 类必须实现 java.io.Serializable 接口,并显式定义 serialVersionUID(如 private static final long serialVersionUID = 1L)。
- 序列化和反序列化的文件 "b.txt" 位于项目根目录。
- ArrayList 集合的泛型需指定为 Student 类型。
- 流资源的关闭操作需直接在 main 方法中完成(无需使用 try-with-resources)。
package demo2;import java.io.Serial;
import java.io.Serializable;public class Student implements Serializable {@Serialprivate static final long serialVersionUID = -4447452452582978504L;private String name;private int age;private String address;public Student() {}public Student(String name, int age, String address) {this.name = name;this.age = age;this.address = address;}/*** 获取* @return name*/public String getName() {return name;}/*** 设置* @param name*/public void setName(String name) {this.name = name;}/*** 获取* @return age*/public int getAge() {return age;}/*** 设置* @param age*/public void setAge(int age) {this.age = age;}/*** 获取* @return address*/public String getAddress() {return address;}/*** 设置* @param address*/public void setAddress(String address) {this.address = address;}public String toString() {return "Student{name = " + name + ", age = " + age + ", address = " + address + "}";}
}
package demo2;import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;public class test1 {public static void main(String[] args) throws IOException {Student s1 = new Student("zhangsan",23,"南京");Student s2 = new Student("lisi",24,"重庆");Student s3 = new Student("wangwu",25,"深圳");ArrayList<Student> list = new ArrayList<Student>();list.add(s1);list.add(s2);list.add(s3);ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt"));oos.writeObject(list);oos.close();}
}
package demo2;import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;public class test2 {public static void main(String[] args) throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"));ArrayList<Student> list = (ArrayList<Student>) ois.readObject();for (Student student : list) {System.out.println(student);}ois.close();}
}
三、字节打印流(PrintStream)与字符打印流(PrintWriter)
作用:方便地打印各种数据类型(如 int、String、对象),自动转换为字符串,无需手动处理编码,常用于输出日志、控制台打印等。
1. 字节打印流(PrintStream)
① 核心构造方法(常用)
PrintStream 是字节流,构造方法主要用于关联输出目标(文件、其他字节流等),并可指定是否自动刷新:
| 构造方法签名 | 说明 |
|---|---|
PrintStream(String fileName) | 关联指定路径的文件(若文件不存在则创建) |
PrintStream(File file) | 关联指定的File对象 |
PrintStream(OutputStream out) | 包装一个字节输出流(如FileOutputStream) |
PrintStream(OutputStream out, boolean autoFlush) | 包装字节输出流,并指定是否自动刷新(autoFlush=true时,调用println()/printf()/format()会自动刷新缓冲区) |
② 特点:
- 继承
OutputStream,默认关联控制台(System.out就是PrintStream对象)。 - 可通过构造方法关联文件或其他输出流(如
new PrintStream("log.txt"))。 - 提供
print()(不换行)、println()(自动换行)方法,支持所有基本类型和对象(对象会调用toString())。 - 自动刷新:若构造方法中指定
autoFlush=true,则调用println()、printf()或format()时会自动刷新缓冲区。
③ 使用实例
场景 1:向文件写入数据,开启自动刷新
import java.io.FileNotFoundException;
import java.io.PrintStream;public class PrintStreamDemo {public static void main(String[] args) throws FileNotFoundException {// 1. 创建PrintStream,关联文件"printStream.txt",开启自动刷新// 底层通过FileOutputStream包装文件,autoFlush=truePrintStream ps = new PrintStream(new FileOutputStream("printStream.txt"), true);// 2. 写入数据(支持多种类型)ps.print("姓名:"); // 不换行ps.println("张三"); // 换行(因autoFlush=true,此处会自动刷新)ps.print("年龄:");ps.println(20); // 写入int类型,自动转为字符串ps.println("地址:北京市"); // 写入字符串并换行// 3. 关闭流ps.close();}
}
输出文件内容(printStream.txt):
姓名:张三
年龄:20
地址:北京市
2. 字符打印流(PrintWriter)
① 核心构造方法(常用)
| 构造方法签名 | 说明 |
|---|---|
PrintWriter(String fileName) | 关联指定路径的文件 |
PrintWriter(File file) | 关联指定的File对象 |
PrintWriter(OutputStream out) | 包装字节输出流(默认使用平台默认编码) |
PrintWriter(OutputStream out, boolean autoFlush) | 包装字节输出流,指定自动刷新 |
PrintWriter(OutputStream out, boolean autoFlush, String charset) | 包装字节输出流,指定自动刷新和编码(JDK 10+) |
PrintWriter(Writer writer) | 包装字符输出流(如FileWriter) |
PrintWriter(Writer writer, boolean autoFlush) | 包装字符输出流,指定自动刷新 |
② 使用实例
场景 1:写入中文内容,指定 UTF-8 编码避免乱码
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;public class PrintWriterDemo {public static void main(String[] args) throws IOException {// 1. 创建PrintWriter,关联文件"printWriter.txt",指定UTF-8编码,开启自动刷新// 底层通过FileOutputStream(字节流)包装文件,再指定编码为UTF-8PrintWriter pw = new PrintWriter(new FileOutputStream("printWriter.txt"), true, StandardCharsets.UTF_8);// 2. 写入中文数据(因指定编码,不会乱码)pw.println("学生信息:");pw.print("姓名:");pw.println("李四");pw.print("年龄:");pw.println(22);pw.println("专业:计算机科学与技术");// 3. 关闭流pw.close();}
}
输出文件内容(printWriter.txt):
学生信息:
姓名:李四
年龄:22
专业:计算机科学与技术
三、压缩流与解压缩流
作用:
对文件或数据流进行压缩(减少体积)或解压缩,常用java.util.zip包,支持 ZIP 格式。
1. 解压缩流
(1) 作用:
读取 ZIP 压缩文件,解压出其中的条目(文件 / 文件夹)。
(2) 核心方法:
getNextEntry():获取下一个压缩条目(返回ZipEntry,若为null表示结束)。closeEntry():关闭当前条目。read(byte[] b):读取当前条目的数据。
(3) 综合练习 1
题目:ZIP 文件解压工具实现
请编写 Java 程序,实现以下功能:
(1) 编写一个静态方法 unzip(File src, File dest),该方法接收两个参数:src 表示待解压的 ZIP 压缩文件(File 类型),dest 表示解压后的文件存储目标目录(File 类型)。
(2) 在 unzip 方法中,使用 ZipInputStream 读取 src 对应的压缩文件,通过循环遍历压缩文件中的每个 ZipEntry(压缩条目)。
(3) 若遍历到的 ZipEntry 是目录(通过 isDirectory() 判断),则在目标目录 dest 下创建与该条目对应的目录(需使用 mkdirs() 保证多级目录创建成功)。
(4) 若遍历到的 ZipEntry 是文件,则在目标目录 dest 下创建与该条目对应的文件,使用 FileOutputStream 将 ZipInputStream 中的文件数据写入到该文件中。
(5) 每个文件或目录处理完成后,需关闭当前的 ZipEntry(调用 closeEntry() 方法)。
(6) 在 main 方法中,创建 File 对象 src 指向路径为 "D:\\aaa.zip" 的压缩文件,创建 File 对象 dest 指向路径为 "D:\\" 的目标目录,调用 unzip 方法执行解压操作。
(7) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。
要求:
- 必须使用
ZipInputStream和ZipEntry处理压缩文件。 - 创建目录时必须使用
mkdirs()方法,确保父目录不存在时也能成功创建。 - 写入文件时使用
FileOutputStream,通过循环读取ZipInputStream中的字节并写入到目标文件。 - 所有流资源(
ZipInputStream、FileOutputStream)必须在使用完成后关闭。 - 目标文件和目录的路径由
dest目录与ZipEntry的名称拼接而成(使用new File(dest, entry.toString()))。
package demo1;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.ZipInputStream;public class test5 {public static void main(String[] args) throws IOException {File src = new File("D:\\aaa.zip");File dest = new File("D:\\");unzip(src,dest);}public static void unzip(File src, File dest) throws IOException {ZipInputStream zip = new ZipInputStream(new FileInputStream(src));ZipEntry entry;while((entry = zip.getNextEntry()) != null) {if(entry.isDirectory()) {File file = new File(dest, entry.toString()); // 不太理解file.mkdirs();} else {FileOutputStream fos = new FileOutputStream(new File(dest, entry.toString()));int b;while((b = zip.read()) != -1) {fos.write(b);}fos.close();zip.closeEntry();}}zip.close();}
}
关键逻辑:File file = new File(dest, entry.toString());
① entry.toString() (第二个参数)
entry是什么?:entry是ZipEntry类型的对象,它代表了 ZIP 压缩文件中的一个条目。这个条目可以是一个文件,也可以是一个目录。entry.toString()做什么?:这个方法会返回该条目的名称,这个名称包含了它在压缩包内的路径信息。
② new File(dest, entry.toString()) (组合起来)
File 类的这个构造方法 File(File parent, String child) 是一个非常常用的方法,它的作用是:
- 根据一个父级
File对象 (dest) 和一个子路径字符串 (entry.toString()),来构造一个新的File对象。 - 它会自动处理不同操作系统下的路径分隔符(比如 Windows 的
\和 Linux/Mac 的/)。
(4) 综合练习 2
题目:文件压缩器
具体需求如下:
(1) 功能:将一个指定的源目录(例如 D:\aaa)下的所有文件(暂时不考虑子目录)打包压缩成一个 ZIP 格式的压缩文件。
(2) 压缩文件位置与命名:
- 压缩文件(
.zip)应与源目录位于同一个父目录下。 - 压缩文件的名称应基于源目录名。例如,如果源目录是
aaa,那么压缩文件就应该命名为aaa.zip。
(3) 压缩包内结构:压缩包内的文件应直接存放在根目录下,无需保留源目录的层级结构。例如,将 D:\aaa\1.txt 和 D:\aaa\2.jpg 压缩后,aaa.zip 内部应直接包含 1.txt 和 2.jpg 两个文件。
要求:
- 使用
java.util.zip包中的相关类(如ZipOutputStream,ZipEntry)来实现压缩功能。 - 注意 IO 流资源的正确关闭。
- 程序结构清晰,可包含一个或多个辅助方法来完成具体任务(例如,一个方法用于遍历目录,另一个方法用于压缩单个文件)。
提示:
- 你可能需要使用
File类的getParentFile()和listFiles()方法来处理目录和文件。 - 压缩文件时,需要为每个被压缩的文件创建一个
ZipEntry对象,并将其添加到ZipOutputStream中。
package demo1;import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;public class test7 {public static void main(String[] args) throws IOException {File src = new File("D:\\aaa");File destParent = src.getParentFile(); // 不太理解File dest = new File(destParent, src.getName() + ".zip"); // 不太理解ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));toZip(src,zos,src.getName()); // 不太理解zos.close();}public static void toZip(File src, ZipOutputStream zos, String name) throws IOException { // 不太理解File[] files = src.listFiles();for (File file : files) {if (file.isFile()) {ZipEntry entry = new ZipEntry(name + "\\" + file.getName()); // 不太理解zos.putNextEntry(entry); // 不太理解FileInputStream fis = new FileInputStream(file);int b;while((b = fis.read()) != -1) {zos.write(b);}fis.close();zos.closeEntry();} else {toZip(file, zos, name + "\\" + file.getName());}}}
}
关键逻辑 1:File destParent = src.getParentFile();
File src = new File("D:\\aaa"); // src 指向 "D:\aaa" 这个目录
File destParent = src.getParentFile();
src是一个File对象,代表源目录D:\aaa。src.getParentFile()是File类的一个方法,它的作用是获取当前File对象所代表的文件或目录的父目录。- 在这个例子中,
src的父目录就是D:\。 - 所以,
destParent这个File对象指向的就是D:\这个路径。
关键逻辑 2:File dest = new File(destParent, src.getName() + ".zip");
File dest = new File(destParent, src.getName() + ".zip");
① 这行代码的作用是构建一个表示目标 ZIP 文件的 File 对象。
② 它使用了 File 类的一个构造函数:File(File parent, String child)。这个构造函数会创建一个新的 File 对象,表示由 parent 目录和 child 文件名组成的路径。
③ destParent:父目录,这里是 D:\。
④ src.getName():获取源目录 src 的名称,也就是 "aaa"。
(1)src.getName()File 类的 getName() 方法会提取路径中最后一个分隔符(\)后的部分;
(2) 路径 D:\aaa 中,最后一个 \ 后面的就是 aaa,因此返回字符串 "aaa"。
⑤ src.getName() + ".zip":拼接成 "aaa.zip"。
⑥ 所以,dest 这个 File 对象指向的就是 D:\aaa.zip 这个文件。
关键逻辑 3:toZip(src,zos,src.getName());
toZip(src,zos,src.getName());
① 这行代码是调用一个自定义的 toZip 方法,开始执行具体的压缩操作。
② 它传递了三个参数:
src:源目录D:\aaa。告诉toZip方法要压缩哪个目录里的内容。zos:ZipOutputStream对象。这是 Java 提供的用于写入 ZIP 格式数据的流。toZip方法需要通过它来将文件内容写入到D:\aaa.zip文件中。src.getName():源目录的名称"aaa"。这个参数非常关键,它被用作ZIP 压缩包内部的根目录名称。这样,当你解压aaa.zip时,所有文件都会被解压到一个名为aaa的文件夹里,保持了原有的目录结构。
关键逻辑 4:ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
① ZipEntry:可以理解为 ZIP 压缩包内部的一个 “文件记录” 或 “目录项”。它记录了压缩包内一个文件或文件夹的名称、大小、修改时间等信息。
② name + "\\" + file.getName():这是在构建这个 “文件记录” 的名称
name:在第一次调用时是"aaa"。file.getName():是当前正在被压缩的文件名,例如"photo.jpg"。- 拼接后就是
"aaa\photo.jpg"。
关键逻辑 5:zos.putNextEntry(entry);
zos.putNextEntry(entry);
- 这是
ZipOutputStream的一个核心方法。它的作用是告诉 ZIP 输出流:“我接下来要写入的内容,就是属于刚才创建的那个entry(文件记录)的”。 - 调用这个方法后,
zos会做好准备,接下来通过zos.write()写入的字节数据,都会被压缩并存储到这个entry所代表的文件中。
