Java EE:4.文件操作和IO(第二弹):数据流(上篇)
书接上文:Java EE:4.文件操作和IO(第一弹):认识文件+Java中操作文件~~
3.文件内容的读写——数据流
Java中针对文件内容的操作,主要是通过 一组“流对象”来实现的
计算机中的“流”和“水流”非常相似:
要接 100ml 的水~~
1次把 100ml 都接完
分2次,一次接 50ml
分10次,一次接 10ml
分100次,一次接 1ml
……
无数种接水方法~~
从文件读取 100字节 数据
1次把 100字节 都读完
分2次,一次读 50字节
分10次,一次读 10字节
分100次,一次读 1字节
……
无数种读数据的方法~~
因此,计算机中针对读写文件,也是使用 流(Stream)这个词(不是 Steam 蒸汽)
流 是操作系统层面的术语,和语言无关
各种编程语言操作文件,都叫 流~~
Java中提供了一组类,表示 流~~
闲聊:
这里的一组是几十个~~非常非常多的!!咱们课堂上不讲那么多,就讲其中8个就好了~~
掌握了一个,其他的就都会了~~
Q1:StringTokenizer,这个快读过几天就忘😭
这个纯属出题人无聊,用 IO卡用例~~这个确实没办法,只能 背~~
工作中都用不上这样的代码~~
C++中也有一个”快读的操作“,但这个更简单,设置一个属性就可以了~~(std:cin、std::count)
针对上述几十个流,分成两个大的类别:字节流和字符流
注意:字节 != 字符,一个字符可能对应多个字节,取决于编码方式(字符集)
其中 InputStream、OutputStream、Reader、Weiter 就是流对象体系中最顶层的父类,Java中其他的流对象都是直接/间接继承自这几个类~~
1.字节流
读写文件,以字节为单位,是针对二进制文件使用的
InputStream:输入,从文件读数据
OutputStream:输出,往文件写数据
2.字符流
读写文件,以字符为单位,是针对文本文件使用的
Reader:输入,从文件读数据
Weiter:输出,往文件写数据
啥叫输入?啥叫输出?
一屁股坐在 CPU 上,迎面而来的叫输入,离你而去的叫输出
数据的流向:
从硬盘=>CPU:输入
从CPU=>硬盘:输出
3.1InputStream概述
方法
| 修饰符及返回值类型 | 方法签名 | 说明 |
| int | read() | 读取一个字节的数据,返回-1代表已经完全读完了 |
| int | read(byte[] b) | 最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;-1代表已经读完了 |
| int | read(byte[] b,int off,int len) | 最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1代表已经读完了 |
| void | close() | 关闭字节流 |
代码演示
当我们尝试去 new 一个 InputStream 时,发现会报错,查看发现 InputStream 是一个抽象类
闲聊:
啥是抽象类??抽象类和接口有啥区别??还记得不??
抽象类不能进行实例化,是一个抽象的概念,不能具体的创建一个实例出来
其余和普通类没啥区别,也可以有属性,也可以有方法,也可以有成员~~
还有一个点是普通类没有的,那就是还可以有抽象方法
抽象方法:没有定义,只有声明
比如 Interface 里面的方法,本身就是抽象方法~~
而接口只是包含抽象方法,不能有属性,也不能有普通方法,当然,后来的话,放开了限制,允许接口里面可以有一些静态的 static 类型的成员,这也是后话了
还有一个核心的区别,抽象类在继承的时候是单继承的模式,而接口的话,一个类可以实现多个接口
这些内容,都基础到面试都不会有人考~~
放到十年前,秋招的时候,确实是面试的重点~~
但现在都卷成一匹马了~~
既然不能实例化,那么我们可以 new 它的子类,它的子类有很多,我们当前主要使用 FileInputStream(),而这个主要就针对一个文件进行输入的操作(InputStream 这个流对象体系,不仅仅可以给文件提供操作,我们这里只是拿文件来讲解,后续网络编程,也离不开 流~~)
InputStream inputStream=new FileInputStream();但是这里我们需要注意,使用 FileInputStream() 的时候,需要在构造方法中填写一个参数
而这个参数,可以填写文件的路径(绝对路径/相对路径),也可以填写一个 File 对象
但是我们填写路径之后,发现还是存在报错,原因是需要处理异常👇
换而言之,我们当前写的这个路径,不一定是个存在的文件
没有这个文件,当然没办法去进行创建因此我们会向上抛异常👇
package file; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; /** * Created with IntelliJ IDEA. * Description: * User: CoderYanger * Date: 2026-06-05 * Time: 19:39 */ public class Demo7 { public static void main(String[] args) throws FileNotFoundException { InputStream inputStream=new FileInputStream("./test.txt"); } }这里的创建对象操作,一旦成功,就相当于“打开文件”,类似C语言中的 fopen,要先打开,然后才能读写~~这个属于操作系统定义的流程~~
我们可以认为,打开操作,就是根据 文件路径,定位到对应的硬盘空间
与打开相对应的“关闭”👇
文件资源泄露问题
有打开,就有关闭
打开相当于向系统申请资源
关闭相当于向系统释放资源
咱们同学们对于关闭资源就没有一个直观的认识,不像C++的同学,隔壁C++的同学通常会考虑申请内存就要去释放内存,这是C++代码中最常见的问题(相当于手动挡,要不停的踩离合挂挡)
但是对于咱们来说,内存只要申请就行了,释放交给 GC 自动完成(相当于自动挡,只需要踩油门踩刹车就行了)
虽然给我们带来了很大的便利,但是问题来了,JVM 也不是无所不能的,在内存管理这块确实做的很好,但是文件资源≠内存资源,虽然 GC 能够自动管理内存,但是不能自动管理文件,这就需要咱们手动释放~~
不手动释放,就会引起“文件资源泄露”,就类似于C++的“内存泄漏”(占着茅坑不拉屎,话糙理不糙~~)
占用的是什么资源呢??其实我们一开始给大家讲进程的时候就谈到了:
描述进程有个重要的结构:PCB,PCB中有个重要的属性,叫做文件描述符表
每次程序打开一个文件,就会在文件描述符表中(可以理解为固定长度的顺序表)申请一个表项(占个坑)
如果光打开,不关闭,就会使这里的文件描述符表里的表项耗尽了,后续再次打开,就会打开失败,后续的很多逻辑就Bug了~~这就是文件资源泄露造成的后果
Q1:为啥不能自动扩容呢??
内核里的每个操作,都是要给所有的进程提供服务的~~
如果搞了个自动扩容,就会导致后续进行插入操作时非常慢,而内核又给进程频繁的提供这样的操作,指不定这一下就会给用户造成明显卡顿的情况,就会进一步加大内存操作中的不可控因素~~(内核的内存更宝贵~~)
而且就算能自动扩容,也解决不了文件资源泄露的问题(持续的),总有一刻还是会把计算机内存消耗殆尽的~~所以该关闭还是要关闭~~
闲聊:
在搜狗的时候~有一次改个功能,上线
对面的兄弟,他有个功能,让我帮他带上去~~
带:其实是[违规操作],因为上线需要仪式感嘛,但是上有政策下有对策,就同意!互相帮助~~
先上线“预发布环境”一台机器,观察几个小时,这台机器没啥Bug,再全量上线,发布到所有的机器上(几十台机器,一台是预上线)
下午快下班了~~咱们推全量~~
突然,预上线机器就挂了~~报警短信暴风雨一样~~
就发现报了个错,文件打开失败~~
当时代码是5分钟创建若干个日志文件,突然就创建不出来了~~
后来经过排查,发现是对面的兄弟打开文件,没有关闭~~
其实是写关闭了,但是触发了一定条件,没执行到~~
服务器经过了几个小时的时间,才终于把文件描述符表泄露殆尽~~
在这几个小时的时间,其他代码都能正常运行,就一时没发现
幸好只是在一台预上线机器上,要是全部服务器都推送上线,并且是在半夜突然爆发消耗殆尽的问题……那么所有服务器就都挂了~~
差点年终奖就没了~~
Q1:他写的Bug,为啥影响到我的年终奖??
因为上线是我发起的,所以我是第一责任人~~我有责任保证这个版本是稳定可靠的!!
对面兄弟,也要背锅(我俩55开吧~~)
Q2:组长:来来来,你告诉我这是怎么上线的,咋测试的?嗯?
测试不粘锅~~
通常,开发都是第一责任人,运维有些时候背锅
测试绝大部分情况都不背锅
因为“测试不可能测出所有的Bug”
Q3:那测试啥时候背锅呢??
测试组有明确的流程,但是你没有遵守流程,导致问题
其实遵守流程也不难,就听话 就得了呗~~
Q4:有人说裁员就裁测试
要裁肯定一起裁~~
Q5:那太喜欢按部就班了
别说的太早~~
java111有个同学,在小红书实习,转正30w的水平的公司
刚进公司,必然是先从简单的干,简单的干好了,才能给你安排复杂的有挑战的活~~
Q6:那就硬磕一家公司混到管理
倒不一定,但是3个月太短了~~
Q7:两三年还劝你走,怎么办??
此处不留爷,自有留爷处
求神问卜不如本事傍身~~
本身技术过硬,还怕找不到好工作嘛~~
我们再接着谈论 close()的问题👇
通过上述图片内容可知,IOException 是 FileNotFountException 的父类,所以在我们 Alt+Enter 选择添加异常到方法签名后,会发现,异常 IOException 直接替换掉了之前的 FileNotFountException (因为继承的语义:is - a,好比你遇到一只狸花猫,你可以叫它狸花猫,也可以叫它小猫~~)
package file; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; /** * Created with IntelliJ IDEA. * Description: * User: CoderYanger * Date: 2026-06-05 * Time: 19:39 */ public class Demo7 { public static void main(String[] args) throws IOException { InputStream inputStream=new FileInputStream("./test.txt"); inputStream.close();//关闭文件 } }通过上面的闲聊部分,我们知道,我们写了这个关闭部分的代码,但是中间可能经过 return、抛异常等操作导致执行不到,咋办??用 finally 操作👇
package file; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class Demo7 { public static void main(String[] args) throws IOException { InputStream inputStream=null; try{ inputStream=new FileInputStream("./test.txt"); }finally{ inputStream.close();//关闭文件 } } }此时无论中间出异常还是 return 都可以确保能够 close() 关闭
但是此时又引入了一个新的问题:“丑”~~
丑 一定是问题!!因为我们这是一个 看脸的世界!!!
怎么解决呢??使用 try with resources 👇
try(InputStream inputStream=new FileInputStream("./test.txt")){ }//只要出了括号,就会自动调用close()方法这种写法的效果与原来的效果一模一样,而且更简洁,推荐使用这种写法~~
答疑:那我们之前学的 ReentrantLock 能不能也用类似的方式来确保 unlock 被执行呢??
不行的,换句话说,什么样的才能放到 try 括号里面??
要求这个类需要实现 Closeable 接口,而我们这里的 InputStream 实现的接口正是 Closeable 👇
而 Closeable 接口里提供的方法,正是 close() 方法,当我们实现了这个接口之后,JVM就会在出了代码块之后自动调用 close()
所以我们约定好这个类一定有 close() 方法,就可以让 JVM 自动调用~~
另一方面,其实这里的语法,不光可以允许我们在括号里写一个对象,也可以写多个👇
try(InputStream inputStream=new FileInputStream("./test.txt"); InputStream inputStream2=new FileInputStream("./test.txt")){ }//只要出了括号,就会自动调用close()方法写的这多个对象都可以被 try 自动关闭的,还是非常方便~~
读文件操作
1.一次读一个字节
为了把字节全部读取出来,我们加一个 while 循环,在 while 循环里面进行循环读取,这样我们就完成了一个最简单的循环读取文件的操作👇
package file; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class Demo7 { public static void main(String[] args) throws IOException { // InputStream inputStream=null; // try{ // inputStream=new FileInputStream("./test.txt"); // }finally{ // inputStream.close();//关闭文件 // } try(InputStream inputStream=new FileInputStream("./test.txt")){ //读文件操作 while(true){ //一次读一个字节 int data=inputStream.read(); if(data==-1){ //文件读完 break; } System.out.println(data); } }//只要出了括号,就会自动调用close()方法 } }此时没有 test 文件,如果直接运行,就会出现异常👇
于是我们在当前目录下创建出文件,并且在里面写上 hello 👇
再运行👇
发现,这怎么是一串数字啊??咋不是 hello 呢??
是因为这是按照字节读取的,一个一个字节取出来,分别打印的~~
每个字节的范围是 0~255,hello 是纯英文,对应编码方式是 ASCII,我们对比 ASCII 表就能对比出来,发现正好是 hello👇
我们再把英文 hello 改成 汉字“你好”试试看👇
运行结果👇
我们发现“你好”对应了6个字节,说明编码方式是 UTF-8,我们来查一下 UTF-8 码表👇
因此我们需要使用16进制来对,因为一个16进制数字就是4 bit,2个16进制数字就是1字节~~
Java中咋打印16进制??直接用 printf 打印就行👇
System.out.printf("0x%x\n",data);//16进制方式打印打印后对比发现,确实能够对上👇
通过以上操作,需要我们明白:
①流对象的基本操作
②熟悉码表的基本概念
2.一次读多个字节
此时用到的构造方法👇
read(byte[] b):一次读若干个字节,读取到的数据放到参数 b 中
这个代码有点奇怪~~
因为这里是用参数来作为方法的返回值,这种写法在 C++ 比较常见,但是在 Java 中比较少见,这种写法也称之为“输出型参数”
啥是输出型参数??
一般来说,把 函数 / 方法 想象成工厂,参数就是原材料,返回值就是产品
但是有的时候,也会使用参数来接受返回值
因为在 Java 中,如果参数是引用类型,方法内部修改对象的内容,能够影响到方法外部~~
这个是 Java SE 阶段在方法变量章节重点讲过的,包括在 C语言 的时候也讲过,只不过在C语言中叫指针,交换两个变量,直接拿值类型交换没用,因为形参是实参的拷贝,你只是在函数内部生效,函数外部不会受到影响,于是在C语言中,需要使用指针,借助指针来引用函数外面的变量,但是在 Java 中没有指针了,叫引用了,所以如果是引用类型的话,方法内部的修改,能够影响到方法外部引用指向的对象,正是这样的语法特点,使得引用类型参数是可以作为输出型参数的
闲聊:对于理解不了的同学~~
其实Java把这里设计的太复杂了,如果Java不考虑和C++的兼容,完全删掉内置类型,全都是引用类型的话,这里的规则就简单了~~
但是虽然有点困惑,也比隔壁C++的情况简单多了~~
隔壁C++传参的方式:值、指针、引用、const 引用、右值引用……在这些的基础上又引出了一个很复杂的话题:如何实现完美转发~~老麻烦了~~
这就好比你在食堂打饭~~
拿个空餐盘(空数组),来到窗口,餐盘递给打饭阿姨,阿姨咔咔给你一顿打饭~~
然后把餐盘交给你,这个时候的 data数组(餐盘)就有一些有意义的数据了,而 n 就代表实际上打到了多少饭~~
输出型参数,本质上还是语法上线质量我们的发挥
Java 和 C++中,要求一个方法只能有一个返回值
如果希望返回多个数据(上述 read 就是希望同时返回 长度 和 内容数据)
就只能通过参数来凑了
同样的问题,在 Python 和 Go 是不存在的,因为都能支持,一个函数同时返回多个值
//比如Python上就可以这么写 def func(): return a,b x,y=func() //此时x取到a,y取到bC++里都是返回一个长度,通过参数返回一个数组的,导致Java也是这样,这属于时代的局限性~~
新版C++就因此引入了右值引用+std::tuple 解决上述问题,现在的C++可以近似认为支持同时返回多个值了~~但是相比于 Python 和 Go 还是没有那么完美~~
package file; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class Demo7 { public static void main(String[] args) throws IOException { // InputStream inputStream=null; // try{ // inputStream=new FileInputStream("./test.txt"); // }finally{ // inputStream.close();//关闭文件 // } try(InputStream inputStream=new FileInputStream("./test.txt")){ //读文件操作 while(true){ //一次读一个字节 // int data=inputStream.read(); // if(data==-1){ // //文件读完 // break; // } // System.out.printf("0x%x\n",data);//16进制方式打印 //一次读多个字节,数组的长度,自定定义 byte[] data=new byte[1024]; //读操作,就会尽可能把字节数组给填满 //填不满的话,能填几个就是几个 //此处的 n 就表示时机读了几个字节 int n=inputStream.read(data); if(n==-1){ //文件读完 break; } for(int i=0;i<n;i++){//理想是1024,但实际长度只有n System.out.printf("0x%x\n",data[i]); } } }//只要出了括号,就会自动调用close()方法 } }
此处的1024肯定填不满,我们可以把 1024 改成 3 试试看,加上分割线再来看效果👇
运行结果👇
相当于这两次 while 循环中的 n 就都是 3 了
3.每次读操作把数据放到数组某个部分
read(byte[] b,int off,int len):把读到的数据放到 b 数组的 offset 下标位置处,放置 len 个元素
这里的 off 是 offset 偏移量(数组下标)
这个版本适合:有非常大的数组,每次读操作都把数据放到数组的某个部分~~
说明(课件内容)
InputStream 只是一个抽象类,要使用还需要具体的实现类,关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream
阶段性小结
文件 IO
1.文件系统操作 File
2.文件内容操作 流对象
字节流(以字节为基本单位)
InputStream→搭配子类 FileInputStream
OutputStream→搭配子类 FileOutputStream
字符流(以字符为基本单位)
Reader→搭配子类 FileReader
Writer→搭配子类 FileWriter
FileInputStream的核心使用
1)打开
2)读:read
①无参数版本:一次读一个字节,读到 -1 说明读完了
②把读取内容放到一个字节数组里,尽可能把数组填满,返回值就是实际的长度,返回 -1 也说明读取完毕
③也是往数组里读,只不过是往数组中指定的 offset 位置,放 len 个元素
3)关闭
防止文件资源泄露问题
使用 try with resourse 这样的语法完成对关闭的处理,由于流对象实现了 Closeable 接口,所以放到括号里,除了代码块就能自动进行关闭
3.2FileInputStream概述(课件内容,此处知识融合至上述内容,仅作补充)
构造方法
| 签名 | 说明 |
| FileInputStream(File file) | 利用 File 构造文件输入流 |
| FileInputStream(String name) | 利用文件路径构造文件输入流 |
代码演示
示例1
将文件完全读完的两种方式。相比较而言,后一种的 IO 次数更少,性能更好
import java.io.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "Hello" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { while (true) { int b = is.read(); if (b == -1) { // 代表文件已经全部读完 break; } System.out.printf("%c", b); } } } }import java.io.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "Hello" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len == -1) { // 代表文件已经全部读完 break; } for (int i = 0; i < len; i++) { System.out.printf("%c", buf[i]); } } } } }示例2
这里我们把文件内容中填充中文看看,注意,写中文的时候使用 UTF-8编码。hello.txt 中填写“你好中国”
注意:这里我利用了这几个中文的 UTF-8 编码后长度刚好是 3 个字节和长度不超过 1024 字节的现状,但这种方式并不是通用的
import java.io.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len == -1) { // 代表文件已经全部读完 break; } // 每次使用 3 字节进行 utf-8 解码,得到中文字符 // 利用 String 中的构造方法完成 // 这个方法了解即可,不是通用的解决办法 for (int i = 0; i < len; i += 3) { String s = new String(buf, i, 3, "UTF-8"); System.out.printf("%s", s); } } } } }
3.3利用Scanner进行字符读取(课件补充内容)
上述例子中,我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类
构造方法 说明 Scanner(InputStream is,String charset) 使用 charset 字符集进行 is 的扫描读取 示例1
import java.io.*; import java.util.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { try (Scanner scanner = new Scanner(is, "UTF-8")) { while (scanner.hasNext()) { String s = scanner.next(); System.out.print(s); } } } } }
答疑
Q1:为什么Java对数组求最大值或者筛选数组必须转化为流,而不能直接操作??
可以直接操作呀,也提供流式操作的风格~~
Q1追问:Arrays.stream() ??
这是 Java 仿照函数式编程,引入的一套 API
和文件 IO 流 没啥直接关系的~~
Q2:read()里的 byte[] 是一个缓冲区吗??
可以这么理解,缓冲区本身就是内存空间~~
更多推荐






















所有评论(0)