输入输出流与文件操作
输入输出流与文件操作(IO)
目标:掌握控制台 I/O(Scanner / PrintStream);理解 Java IO 的两大体系——字节流与字符流,会用
FileInputStream/BufferedReader/DataInputStream等常见流读写文件;理解对象序列化(Serializable)的条件与作用;熟悉File类的常用方法。
一、程序数据如何「进」和「出」
程序的输入输出最基础的场景是控制台 I/O:从键盘读入数据、向屏幕打印结果。
- 读入用
java.util.Scanner:nextInt()/nextDouble()读数值,next()读一个不含空格的词,nextLine()读一整行; - 输出用
System.out(一个PrintStream):println()换行打印,printf()按格式打印。
import java.util.Scanner;
public class ConsoleIO {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 读整数
double avg = sc.nextDouble(); // 读浮点
String name = sc.next(); // 读一个词
System.out.println("n=" + n + ", avg=" + avg + ", name=" + name);
System.out.printf("保留两位: %.2f%n", avg);
}
}
控制台只是「流」的一种。当数据要持久化到文件或在网络上传输时,就需要 Java 的 IO 流体系——下面看它的层次结构。
二、流的层次结构:字节流 vs 字符流
Java IO 的所有流都归入两大体系,二者的差别是处理的数据单位:
- 字节流(
InputStream/OutputStream):一次读写一个字节(8 位),适合图片、音频、二进制文件等原始数据; - 字符流(
Reader/Writer):一次读写一个字符(16 位 Unicode),适合文本,天然处理换行与编码。
在两大根之下,又按「用途」派生出几类常用流:
| 类别 | 基类 | 常见子类 | 典型用途 |
|---|---|---|---|
| 字节输入流 | InputStream | FileInputStream、BufferedInputStream、DataInputStream、ObjectInputStream | 读文件/网络的原始字节、基本类型、对象 |
| 字节输出流 | OutputStream | FileOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream | 写文件/网络的原始字节、基本类型、对象 |
| 字符输入流 | Reader | BufferedReader、InputStreamReader、FileReader | 按字符/行读文本 |
| 字符输出流 | Writer | BufferedWriter、OutputStreamWriter、FileWriter | 按字符/行写文本 |
几条要点:
- 缓冲流(
Buffered*):给底层流套一层内存缓冲区,攒够一批再读写磁盘,减少频繁的系统调用——性能优化,不改变数据内容; - 数据流(
Data*):在字节流之上直接读写 Java 基本类型与String(readInt()/writeUTF()等),是「二进制存数值」的标准工具; - 对象流(
Object*):把整个对象序列化成字节、再还原,见下一节; - 桥梁流(
InputStreamReader/OutputStreamWriter):在字节流与字符流之间转换,负责编码/解码(如 UTF-8)。
交互实验:Java IO 流继承树(StreamHierarchyTree)
Java IO 分两大体系:字节流(InputStream / OutputStream,处理原始 8 位字节)与字符流(Reader / Writer,处理 16 位 Unicode 字符)。点击任意流,看它的类别、继承链与关键方法。
实线框 = 具体可用类,虚线框 = 抽象基类(不能直接 new)。点上方类别标签可只看对应一类。
← 点击左侧任意流查看它的类别、继承链与关键方法
交互实验:流装饰器分层图(FilterStreamLayers)
过滤流是装饰器:自己不产生数据,只是把底层流「包」起来并加新能力。点「再包一层」看套娃生长; 点任意一层看它新增的方法。
FileInputStreamFileInputStream——节点流:真正从文件读字节,是整条链的「芯」
整条链可用能力(由内到外)
- 1. read()
包裹代码
FileInputStream dis = fis;
三、对象序列化 Serializable
有时要保存的不是一个数或一行文本,而是一个对象(比如一局游戏的状态)。Java 用序列化把对象转成字节流存到文件/网络,再用反序列化还原。
- 条件:对象所属类必须
implements java.io.Serializable(这是一个标记接口,没有方法要实现);不实现则抛NotSerializableException; - 写:
ObjectOutputStream.writeObject(obj)——对象 → 字节流; - 读:
ObjectInputStream.readObject()——字节流 → 对象; - 例外:字段加
transient修饰则不参与序列化(如密码、临时缓存);静态字段也不序列化。
// Player implements Serializable
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.bin"));
out.writeObject(player); // 对象 -> 字节流
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.bin"));
Player p2 = (Player) in.readObject(); // 字节流 -> 对象
in.close();
序列化是「网络版 ERASE」把玩家/棋盘状态在客户端与服务端之间传输的基础——模块 7、10 会再次用到它。交互式动画(对象 → 字节流 → 还原)见下文「六、对象序列化流程」。
四、File 类常用方法
java.io.File 表示文件或目录的抽象路径名,本身不读内容,只负责「定位与属性查询」。
File f = new File("data/a.txt"); // 构造时不检查文件是否存在
f.exists(); // 是否真实存在
f.isDirectory();// 是目录吗
f.length(); // 字节数
| 方法 | 作用 |
|---|---|
getName() | 返回文件/目录名(不含路径) |
getParent() | 返回父目录路径,无父目录返回 null |
isAbsolute() / isRelative() | 是否为绝对路径(以盘符或根开头)/ 相对路径 |
isDirectory() / isFile() | 是目录 / 是普通文件 |
exists() | 路径对应的文件或目录是否真实存在 |
length() | 文件字节数(不是目录时) |
canRead() / canWrite() | 当前进程是否可读 / 可写 |
mkdir() / mkdirs() | 建单层目录 / 递归建多层目录(成功返回 true) |
delete() | 删除文件;删目录须先清空其内容 |
list() / listFiles() | 列出目录下的文件名(数组)/ File 对象 |
文本 vs 二进制读写小结:读文本首选 BufferedReader(readLine() 按行,注意指定 UTF-8 编码);写文本用 BufferedWriter(newLine() 写平台换行)。读二进制数值用 DataInputStream、存对象用 ObjectInputStream——选流的关键是数据是什么单位以及要不要处理编码。
五、文件读取:字节视图 vs 行视图
同一个文件,用字节流和字符流读出来「看到的东西」完全不同。下面这个交互实验让你逐步走一遍两种读法,直观看到三件事:① UTF-8 多字节让「磁盘字节数 ≠ 字符数」;② 换行符因平台而异(Windows \r\n 2 字节 / Unix \n 1 字节);③ readLine() 返回的字符串不含换行符。
// 字节流:逐字节,连 \r \n 都看得见,末尾 read() 返回 -1
FileInputStream in = new FileInputStream("a.txt");
int b;
while ((b = in.read()) != -1) { /* b 是 0–255 的原始字节 */ }
// 字符流:按行,换行符被剥离,末尾 readLine() 返回 null
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"), "UTF-8"));
String line;
while ((line = br.readLine()) != null) { /* line 不含换行符 */ }
交互实验:文件读取演示(FileReadDemo)
同一个文件,用字节流FileInputStream.read()和字符流BufferedReader.readLine()读出来完全不同:前者逐字节、连换行符都看得见,后者按行、把换行符悄悄剥掉。下面逐步走一遍,看「字节数 ≠ 字符数」和「换行占几字节」。
两行英文,每个字符恰好 1 字节,字节数 = 字符数。
| 行 | 字符数 | 文本字节 | 换行字节 | 磁盘总字节 |
|---|---|---|---|---|
| hello | 5 | 5 | 1 | 6 |
| world | 5 | 5 | 1 | 6 |
readLine()返回的字符串不含换行符,所以「字符数」只算文本;而磁盘上每行还多占换行符的字节(见上表「换行字节」列)。 中文等 UTF-8 多字节字符则让「文本字节」大于「字符数」——这正是字节数 ≠ 字符数的两个来源。试着把样例切到
mix.txt(含中文)、换行切到 Windows\r\n:汇总条里「总字节」会明显大于「总字符」,明细表「换行字节」列每行变成 2——这两处正是「字节数 ≠ 字符数」的来源。
六、对象序列化流程:对象 → 字节流 → 还原
第三节讲了序列化的「条件与 API」,这一节把过程摊开看:一个对象(连同它引用的其他对象)到底是怎么被写进字节流、又怎么被读回来的。下面这个类同时踩到三个关键点——transient 字段、static 字段、嵌套对象 board:
class Board implements Serializable {
int rows, cols, filled; // 都是实例字段 → 都会被序列化
}
class Player implements Serializable {
static final int MAX_SCORE = 9999; // static:属于类,不随实例写
transient String password; // transient:被排除出字节流
int score;
String name;
Board board; // 嵌套对象 → 递归序列化(Board 也得可序列化)
}
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.bin"));
out.writeObject(player); // 整棵对象图 Player → Board 一起写进字节流
out.close();
交互实验:对象序列化流程(SerializationFlow)
保存一个对象(而不只是一个数或一行文本)时,Java 用 ObjectOutputStream.writeObject() 把整棵对象图转成字节流,再用 ObjectInputStream.readObject() 还原。下面逐步走一遍,看三件事:① transient / static 字段不进字节流;② 嵌套对象会递归序列化;③ 没实现 Serializable 直接抛异常。
ERASE 玩家状态:int/String/transient/static + 一个嵌套对象 board,递归序列化整棵对象图。
| 字段 | 类型 | 处置 | 字节贡献 |
|---|---|---|---|
| score | int | 序列化 | 4B |
| name | String | 序列化 | 10B |
| password | String | transient 跳过 | — |
| MAX_SCORE | int | static 跳过 | — |
| board | Board | 序列化 | — |
| board.rows | int | 序列化 | 4B |
| board.cols | int | 序列化 | 4B |
| board.filled | int | 序列化 | 4B |
transient字段(如密码 / 缓存)和 static 字段都不占字节。每个类描述符带 serialVersionUID,反序列化时用它做版本兼容校验。把样例切到
Player、视角切「序列化」逐步走:能看到password(transient)和MAX_SCORE(static)都标成「跳过」、不占字节;而board会递归展开出它自己的类描述符和三个字段。再切到Legacy——没实现Serializable,writeObject()直接抛NotSerializableException,字节流为 0。
思考题
- 「字节流」和「字符流」的本质区别是什么?为什么读文本一般不直接用
FileInputStream? - 给
FileInputStream套一层BufferedInputStream,改变了什么、没改变什么? - 一个对象要能被
ObjectOutputStream序列化需要满足什么条件?transient字段会怎样? new File("a/b.txt")执行后,文件一定存在吗?isAbsolute()返回什么?为什么?
学习小测验
学习小测验
要从文件读取一个以二进制存储的 int 值,最合适的流是( )
一个对象要被 ObjectOutputStream.writeObject() 序列化,必须满足( )
new File("a/b.txt").isAbsolute() 的返回值是( )
要按行读取一个文本文件(readLine),应使用( )
给 FileInputStream 套一层 BufferedInputStream 的主要目的是( )
要把字节流按 UTF-8 解码成字符流,应使用( )
给 Player 的 password 字段加上 transient 后,执行 writeObject(player) 时它会( )
static 字段在对象序列化时会( )
Player 引用了一个 Board 对象(board 字段)。要把 player 完整序列化,必须满足( )