书接上文: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取到b

C++里都是返回一个长度,通过参数返回一个数组的,导致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[] 是一个缓冲区吗??

可以这么理解,缓冲区本身就是内存空间~~

更多推荐