Java 面向对象 · 交互课程

输入输出流与文件操作

输入输出流与文件操作(IO)

目标:掌握控制台 I/O(Scanner / PrintStream);理解 Java IO 的两大体系——字节流与字符流,会用 FileInputStream / BufferedReader / DataInputStream 等常见流读写文件;理解对象序列化(Serializable)的条件与作用;熟悉 File 类的常用方法。

一、程序数据如何「进」和「出」

程序的输入输出最基础的场景是控制台 I/O:从键盘读入数据、向屏幕打印结果。

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 的所有流都归入两大体系,二者的差别是处理的数据单位:

在两大根之下,又按「用途」派生出几类常用流:

类别基类常见子类典型用途
字节输入流InputStreamFileInputStream、BufferedInputStream、DataInputStream、ObjectInputStream读文件/网络的原始字节、基本类型、对象
字节输出流OutputStreamFileOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream写文件/网络的原始字节、基本类型、对象
字符输入流ReaderBufferedReader、InputStreamReader、FileReader按字符/行读文本
字符输出流WriterBufferedWriter、OutputStreamWriter、FileWriter按字符/行写文本

几条要点:

交互实验:Java IO 流继承树(StreamHierarchyTree)

Java IO 分两大体系:字节流(InputStream / OutputStream,处理原始 8 位字节)与字符流(Reader / Writer,处理 16 位 Unicode 字符)。点击任意流,看它的类别、继承链与关键方法。

字节流(8 位)字符流(16 位 Unicode)InputStreamFileInputStreamBufferedInputStreamDataInputStreamObjectInputStreamOutputStreamFileOutputStreamBufferedOutputStreamDataOutputStreamObjectOutputStreamReaderBufferedReaderInputStreamReaderFileReaderWriterBufferedWriterOutputStreamWriterFileWriter

实线框 = 具体可用类,虚线框 = 抽象基类(不能直接 new)。点上方类别标签可只看对应一类。

← 点击左侧任意流查看它的类别、继承链与关键方法

交互实验:流装饰器分层图(FilterStreamLayers)

过滤流是装饰器:自己不产生数据,只是把底层流「包」起来并加新能力。点「再包一层」看套娃生长; 点任意一层看它新增的方法。

对外变量类型:FileInputStream

FileInputStream——节点流:真正从文件读字节,是整条链的「芯」

+ read()

整条链可用能力(由内到外)

  • 1. read()

包裹代码

FileInputStream dis = fis;

三、对象序列化 Serializable

有时要保存的不是一个数或一行文本,而是一个对象(比如一局游戏的状态)。Java 用序列化把对象转成字节流存到文件/网络,再用反序列化还原。

// 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 字节,字节数 = 字符数。

换行符:
总字节 12总字符 102 行字节数 ≠ 字符数(UTF-8 多字节 + 换行符)
字节流 FileInputStream.read()已读 0 / 12
read() → 104 (68 “h”)
-1EOF
字符流 BufferedReader.readLine()已读 0 / 2
readLine() → “hello” (5 个字符,换行符已剥离)
(末尾)null
行字符数文本字节换行字节磁盘总字节
hello5516
world5516
读到的和存的不一样:当前换行符是 \n(1 字节)。readLine()返回的字符串不含换行符,所以「字符数」只算文本;而磁盘上每行还多占换行符的字节(见上表「换行字节」列)。 中文等 UTF-8 多字节字符则让「文本字节」大于「字符数」——这正是字节数 ≠ 字符数的两个来源。
换行符字节(\r / \n) 多字节字符的组成字节(≥0x80,显示为 ·) 普通可打印 ASCII 字节

试着把样例切到 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,递归序列化整棵对象图。

字节流大小 53 字节(教学简化模型)序列化字段 6transient 跳过 1static 跳过 1
写入字节流(对象 → 字节)1 / 10
写类描述符 Player(serialVersionUID=1) (+14 字节)
字段类型处置字节贡献
scoreint序列化4B
nameString序列化10B
passwordStringtransient 跳过—
MAX_SCOREintstatic 跳过—
boardBoard序列化—
board.rowsint序列化4B
board.colsint序列化4B
board.filledint序列化4B
谁进了字节流:只有非 transient、非 static 的实例字段会被写出;嵌套对象会沿引用递归(它自己也得可序列化)。transient字段(如密码 / 缓存)和 static 字段都不占字节。每个类描述符带 serialVersionUID,反序列化时用它做版本兼容校验。
类描述符(含 serialVersionUID) 写入 / 读回的字段 transient 跳过 static 跳过 NotSerializableException

把样例切到 Player、视角切「序列化」逐步走:能看到 password(transient)和 MAX_SCORE(static)都标成「跳过」、不占字节;而 board 会递归展开出它自己的类描述符和三个字段。再切到 Legacy——没实现 Serializable,writeObject() 直接抛 NotSerializableException,字节流为 0。

思考题

  1. 「字节流」和「字符流」的本质区别是什么?为什么读文本一般不直接用 FileInputStream?
  2. 给 FileInputStream 套一层 BufferedInputStream,改变了什么、没改变什么?
  3. 一个对象要能被 ObjectOutputStream 序列化需要满足什么条件?transient 字段会怎样?
  4. new File("a/b.txt") 执行后,文件一定存在吗?isAbsolute() 返回什么?为什么?

学习小测验

学习小测验

Q1

要从文件读取一个以二进制存储的 int 值,最合适的流是( )

Q2

一个对象要被 ObjectOutputStream.writeObject() 序列化,必须满足( )

Q3

new File("a/b.txt").isAbsolute() 的返回值是( )

Q4

要按行读取一个文本文件(readLine),应使用( )

Q5

给 FileInputStream 套一层 BufferedInputStream 的主要目的是( )

Q6

要把字节流按 UTF-8 解码成字符流,应使用( )

Q7

给 Player 的 password 字段加上 transient 后,执行 writeObject(player) 时它会( )

Q8

static 字段在对象序列化时会( )

Q9

Player 引用了一个 Board 对象(board 字段)。要把 player 完整序列化,必须满足( )