一、字节流读写
单位:字节 (byte),即 8 位二进制数。
读取方式:它不管你读的是什么文件(是图片、视频还是文本),它都把内容当成一串原始的二进制数据来搬运。
你的代码:inputStream.read(bytes)读取的就是纯粹的字节。如果你用System.out.printf("0x%02X", bytes[i])打印,看到的是内存中的真实二进制数据。
适用:所有类型的文件(万能)。
使用 Java 字节流读取任何二进制数据
法一:
public class Demo7_1 { public static void main(String[] args) throws IOException { //选择合适的 InputStream 子类(如 FileInputStream),并指定数据源。 InputStream inputStream = new FileInputStream("./1.txt"); //循环读取数据 while(true) { int data = inputStream.read(); if(data == -1) { break; } //以 16 进制格式化打印读取数据 System.out.printf("0x%X\n",data); } 关闭流(释放资源) inputStream.close(); } }以上代码就实现了从 1.txt 文件中读取字节数据的功能,我们来刨析一下:
1、为什么要 使用 InputStream inputStream = new FileInputStream("./1.txt");
“左边写父类(或接口),右边写具体的实现类” —— 这是写出高质量、可扩展 Java 代码的基本功。
(1)、语法层面:向上转型(Upcasting)
选择合适的 InputStream 子类(如 FileInputStream),并指定数据源。 InputStream inputStream = new FileInputStream("./1.txt"); 是一种典型的 “父类引用指向子类对象” 的写法,体现了 Java 中 多态(Polymorphism) 和 面向抽象编程 的核心思想FileInputStream是InputStream的子类。将子类对象赋值给父类类型的变量,称为 向上转型。这在 Java 中是自动且安全的,不需要强制类型转换。
// 向上转型(隐式) InputStream inputStream = new FileInputStream("..."); // ✅ 合法 // 向下转型(需显式强转,且有风险) FileInputStream fis = (FileInputStream) inputStream; // ⚠️ 需确保类型匹配(2)、设计层面:面向接口/抽象类编程
虽然InputStream是一个 抽象类(不是接口),但它扮演了“通用输入流”的角色。这种写法的好处是:
- 代码更通用、灵活
你后续调用的方法(如read()、close())都是通过InputStream定义的通用接口进行的。
这意味着:将来可以轻松替换数据源,而无需修改使用逻辑。
// 今天读文件 InputStream in = new FileInputStream("data.txt"); // 明天读网络 InputStream in = new SocketInputStream(socket); // 后天读内存 InputStream in = new ByteArrayInputStream(bytes);→ 只要右边换一个InputStream的实现类,左边和后续代码完全不用动
- 解耦与可维护性高
你的业务逻辑只依赖于“能读字节”这个能力(InputStream抽象),而不关心底层是文件、网络还是内存。这符合 “依赖倒置原则”(DIP)。
- 便于组合与装饰(Decorator 模式)
Java I/O 流大量使用装饰器模式,这种写法天然支持:
InputStream in = new BufferedInputStream( new FileInputStream("large.txt") );这里BufferedInputStream包装了FileInputStream,但对外仍表现为InputStream,使用方式不变
2、int data = inputStream.read();
是 Java 字节流(InputStream)中最基础、最核心的单字节读取操作。它的作用是从输入流中读取下一个字节的数据,并以int类型返回。
(1)、返回值含义
⚠️ 注意:虽然读的是byte(有符号,-128~127),但返回int是为了能用-1表示结束,同时避免负数字节(如0xFF= -1)与结束标志混淆。
(2)为什么返回int而不是byte?
如果返回byte,那么当读到字节值为-1(即0xFF)时,程序无法区分这是“有效数据”还是“流结束”。使用int后:有效字节被提升为0~255的正整数;-1专用于表示“结束”,无歧义。
3、关闭字节流
inputStream.close();是 Java I/O 操作中释放系统资源的关键一步。它的作用是关闭输入流,并释放与该流关联的所有底层系统资源(如文件句柄、网络连接、内存缓冲区等)。
为什么必须调用close()?
防止资源泄漏(Resource Leak)
每打开一个文件,操作系统会分配一个 文件描述符(File Descriptor)。
如果不关闭流,这些描述符会一直被占用,直到程序退出。
操作系统对单个进程能打开的文件数有限制(如 Linux 默认 1024),资源耗尽会导致程序崩溃(抛出
IOException: Too many open files)。
确保数据完整性(对输出流更重要)
虽然
InputStream主要是读取,但某些流(如带缓冲的BufferedInputStream)可能在内部维护状态,及时关闭有助于清理。
符合“谁打开,谁关闭”原则
良好的编程习惯:打开资源后,必须显式或隐式地关闭它。
法二
public class Demo7_2 { public static void main(String[] args) throws IOException { InputStream inputStream = new FileInputStream("./1.txt"); //通过 read 读取数据,一次就读一个字节数组 while(true) { //长度随便定,无所谓 byte[] bytes = new byte[1024]; //read 方法会尽可能的把参数的数组填满 //返回值表示实际读取到的字节数 int n = inputStream.read(bytes); if(n == -1) { break; } for (int i = 0; i < n; i++) { System.out.printf("0x%X\n",bytes[i]); } } //若上面代码出现异常,close() 可能执行不到 inputStream.close(); } }法二与法一的区别是,这使用字节流逐块读取文件并以十六进制形式打印每个字节
4、int n = inputStream.read(bytes);
是 Java 字节流(InputStream)中“批量读取”数据的标准写法,属于 I/O 编程中最核心、最常用的模式之一。
- 参数:
byte[] b—— 一个字节数组,作为缓冲区(buffer),用于接收读取到的数据。 - 返回值:
int—— 表示实际读取到的字节数(不是数组长度!)。
✅ 所以
int n = inputStream.read(bytes);的意思是:
“尝试从输入流中读取最多bytes.length个字节,存入bytes数组,并把实际读到的字节数赋给变量n。”
✅ 具体过程(以文件大小 > 1024 字节为例)
假设文件总长度为2500 字节,你使用byte[1024]缓冲区:
| 循环次数 | 调用read(bytes)后 | 实际返回值n | 读取的字节范围 | 流内部指针位置 |
|---|---|---|---|---|
| 第 1 次 | 读取前 1024 字节 | n = 1024 | 字节 0 ~ 1023 | 指向第1024字节(即下一次从 1024 开始) |
| 第 2 次 | 读取接下来的 1024 字节 | n = 1024 | 字节 1024 ~ 2047 | 指向第2048字节 |
| 第 3 次 | 读取剩余 452 字节 | n = 452 | 字节 2048 ~ 2499 | 指向文件末尾(EOF) |
| 第 4 次 | 尝试再读 | n = -1 | 无数据 | 已到末尾,返回 -1 |
法三:
public class Demo7_3 { public static void main(String[] args) throws IOException { InputStream inputStream = null; try { inputStream = new FileInputStream("./1.txt"); //通过 read 读取数据,一次就读一个字节数组 while(true) { //长度随便定,无所谓 byte[] bytes = new byte[1024]; //read 方法会尽可能的把参数的数组填满 //返回值表示实际读取到的字节数 int n = inputStream.read(bytes); if(n == -1) { break; } for (int i = 0; i < n; i++) { System.out.printf("0x%X\n",bytes[i]); } } } finally { //关闭文件 inputStream.close(); } } }法三保证了inputStream.close()被执行
5、保证inputStream.close()被执行
在法二中,虽然方法声明了throws IOException,但read()或printf()在循环中仍可能抛出异常。一旦异常抛出,程序会直接跳转到调用栈上层,跳过close()语句。结果:文件句柄未释放 → 资源泄漏(Resource Leak)。
法四:
public class Demo8 { // try with resource public static void main(String[] args) { try(InputStream inputStream = new FileInputStream("./1.txt")) { while(true) { byte[] bytes = new byte[1024]; int n = inputStream.read(bytes); if(n == -1) { break; } for (int i = 0; i < n; i++) { System.out.printf("0x%X\n",bytes[i]); } } // close 不必写了 //会在 try 结束的时候,自动调用 close() }catch (IOException e) { e.printStackTrace(); } } }6、使用try-with-resources确保资源释放
try-with-resources是 Java 7 引入的一种自动资源管理(Automatic Resource Management, ARM) 语法,用于确保每个打开的资源在使用完毕后都能被正确关闭,即使发生异常也不会泄漏资源。
“任何实现了Closeable/AutoCloseable的资源,都必须用try-with-resources管理!
✅ 基本语法
try (ResourceType resource = new ResourceType(...)) { // 使用 resource 的代码 } catch (ExceptionType e) { // 处理异常 } // resource 在此处自动关闭(无论是否抛出异常)前提:资源类必须实现
java.lang.AutoCloseable接口(Closeable是其子接口)。
Java I/O 中的所有流(如FileInputStream、FileReader)都实现了该接口。
7、3 种read方法
1、int read()
读取单个字节(8 位)。
返回该字节的 无符号整数值(0 ~ 255)。
如果已到达流末尾(EOF),返回
-1。
2、int read(byte[] b)
尝试读取 最多
b.length个字节 到数组b中。返回 实际读取的字节数(
0 ≤ n ≤ b.length)。如果已到流末尾,返回
-1。
3.int read(byte[] b, int off, int len)
从输入流中读取 最多
len个字节,存入数组b的 从索引off开始的位置。返回 实际读取的字节数。
流结束时返回
-1。
使用 Java 字节流写任何二进制数据
法一:
public class Demo9_1 { public static void main(String[] args) { //如果没有 true 会把原来的内容清空再写 try(OutputStream outputStream = new FileOutputStream("./1.txt")) { // 97 十进制,表示 a outputStream.write(97); outputStream.write(98); outputStream.write(99); } catch (IOException e) { e.printStackTrace(); } } }法二:
public class Demo9_2 { public static void main(String[] args) { //加了 true, 实现了追加写的作业,原来的内容不消失 try(OutputStream outputStream = new FileOutputStream("./1.txt",true)) { // 97 十进制,表示 a outputStream.write(97); outputStream.write(98); outputStream.write(99); } catch (IOException e) { e.printStackTrace(); } } }法三:
public class Demo9_3 { public static void main(String[] args) { try(OutputStream outputStream = new FileOutputStream("./1.txt",true)) { byte[] bytes = {97,98,99}; outputStream.write(bytes); }catch (IOException e) { e.printStackTrace(); } } }法四:
ublic class Demo9_4 { public static void main(String[] args) { try (OutputStream outputStream = new FileOutputStream("./1.txt",true)) { byte[] bytes = {97,98,99,100,101,102,103,104,105,106}; outputStream.write(bytes,2,3); } catch (IOException e) { e.printStackTrace(); } } }二、字符流读写
单位:字符 (char),即 16 位 Unicode 字符。
读取方式:它专门用于读取文本。它在底层其实也是读取字节,但它会根据指定的编码规则(如 UTF-8、GBK),自动将“字节”翻译成“人类可读的字符”。
适用:纯文本文件(.txt, .java, .html 等)。
使用字符流读取数据
public class Demo10 { public static void main(String[] args) { try(Reader reader = new FileReader("./1.txt")) { //使用 read 方法读取数据 // while(true) { // int data = reader.read(); // if(data == -1) { // break; // } // char c = (char)data; // System.out.println(c); // } while(true) { char[] chars = new char[1024]; int n = reader.read(chars); if(n == -1) { break; } for (int i = 0; i < n; i++) { System.out.println(chars[i]); } } } catch(IOException e) { e.printStackTrace(); } } }1.int read()
这是最基本的方法,用于逐个字符读取。
2.int read(char[] cbuf)
此方法通过字符数组进行批量读取,能显著提升读取效率。
3.int read(char[] cbuf, int off, int len)
这是最灵活的重载方法,允许将字符读入数组的指定部分。
使用字符流写数据
public class Demo11 { public static void main(String[] args) { try(Writer writer = new FileWriter("./1.txt",true)) { writer.write('我'); writer.write('真'); writer.write('美'); char[] chars = {'我','真','美'}; writer.write(chars); String s = "你也挺美的"; writer.write(s); }catch (IOException e) { e.printStackTrace(); } } }1、void write(int c)
功能:写入单个字符。参数
c是一个表示字符的整数(0-65535,对应Unicode字符)。示例:
writer.write(65); // 写入字符 'A' writer.write('H'); // 直接写入字符 'H'
2、void write(char[] cbuf)
- 功能:写入整个字符数组
cbuf中的所有字符。 - 示例:
char[] chars = {'H', 'e', 'l', 'l', 'o'}; writer.write(chars);
3、void write(char[] cbuf, int off, int len)
- 功能:写入字符数组
cbuf中从索引off开始的len个字符。这是对字符数组进行部分写入的精确控制方法。 - 示例:
char[] chars = {'H', 'e', 'l', 'l', 'o'}; writer.write(chars, 0, 3); // 只写入 "Hel"
4、void write(String str)
- 功能:写入整个字符串
str。 - 示例:
writer.write("Hello, World!");
5、void write(String str, int off, int len)
- 功能:写入字符串
str中从索引off开始的len个字符。这是对字符串进行部分写入的精确控制方法。 - 参数:
str:要写入的字符串。startingIndex(off):获取字符部分的起始索引。lengthOfstring(len):要写入的字符串长度。
- 示例:
String str = "GeeksForGeeks"; writer.write(str, 0, 5); // 写入 "Geeks"[11](@ref)
三、字节流与字符流的区别
假设文件里存的是中文字符“中”:
在磁盘上(UTF-8编码):它实际上占用了 3 个字节,数据可能是
0xE4 0xB8 0xAD。字节流读取:
它会读出 3 个独立的字节:
E4、B8、AD。如果你直接把这些字节转成字符串打印,可能会显示乱码(比如
涓),因为它不知道这 3 个字节合起来才表示一个“中”字。
字符流读取:
它会根据编码规则(UTF-8),把这 3 个字节自动拼装、翻译,最终给你返回一个字符
'中'。