Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作

Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作

观前提醒:

在看这篇博客之前,建议你先看完这两篇博客:

Java 文件操作 和 IO(2)-- Java文件系统操作

Java 文件操作 和 IO(1)-- 文件相关知识背景介绍


在讲Java文件内容操作之前,我们还是需要再来看看,java当中,操作文件的简单介绍,知道文件操作的区分,脑里有个大概的图,才能更好的理解这部分的知识!

1. Java中操作文件的简单介绍

使用java来操作文件,主要是通过java标准库提供的一系列的类,而这些类,又可以分为两种操作方向

  1. 文件系统操作
    这里主要是关于 文件 或 目录 的创建,文件的删除,文件的重命名,创建目录等…

  2. 文件内容操作
    这里就是对某一个具体的文件的内容进行读和写了,又由于文件有两种种类,所以,我们又区分了字节流操作和字符流操作。
    (1)字节流:读写文件,以字节为单位,是针对二进制文件使用的。
    (2)字符流:读写文件,以字符为单位,是针对文本文件使用的。

用一张图来看的话,是这样的:
在这里插入图片描述
这篇博客里面,讲的是文件内容操作中的字节流操作,下一篇博客,Java 文件操作 和 IO(4)-- Java文件内容操作(2)-- 字符流操作 讲的文件内容操作中的 字符流操作

2. Java文件内容操作–字节流

2.1 流概念的介绍 和 输入输出

流:

Java中,针对某一个文件中的内容的操作,主要是通过一组 “ 流对象 ” 来实现的。

那么,问题来了,什么叫做 ‘ 流 ’ ?
首先,‘流’这个词呢,本身就是一个很形象的比喻。
讲到流,我们第一反应,应该可以想到 ‘水流’,那么,水流有什么特点呢?延绵不绝,连续不断。

比如:我现在要接 100ml的水
我可以 1 次性的把 100ml的水接完
也可以分 2 次,一次接50ml
也可以分 10 次,一次接10ml
也可以分 100 次,一次接 1 ml
… … … …
所以,综上所述,我们可以有无数种接水的方式。

那么,计算机当中的流 和 水流中的流,是非常相似的

比如:我现在要从某一个具体的文件当中读取 100个字节 的数据(就像现在要接 100ml的水一样)
我可以 1 次性的把 100个字节的数据,全都读取出来
也可以分 2 次,一次读取 50个字节的数据
也可以分 10 次,一次读取 10个字节的数据
也可以分 100 次,一次读取 1个字节的数据
… … … …
所以,综上所述,读取数据的方式,我们也有无数种的方式。

正因为,我们从文件中读写数据的特点,和水流的特点,非常相似,所以就发明了流这个说法。

因此,计算机中,针对读写文件,也是使用了流(Stream)这样的词来表示。

这个说法,是操作系统层面相关的数据,和语言无关,比如:java当中,文件中的流,C语言中,文件中的流,C++中,文件中的流,都是一样的。
所以,各种编程语言操作文件,都叫 “流”。

那么,Java当中,提供了一组类,来表示流,有多少个呢?几十个!!!
但是,我们并不需要学习全部,在 本篇博客Java 文件操作 和 IO(4)-- Java文件内容操作(2)-- 字符流操作中,我们主要介绍其中的 8个就行。
不要因为看到要学 8个类,就慌了,因为,文件内容操作这块,比起文件系统操作这块,好学很多,因为这八个类所提供的方法都是类似的,比较有规律,你学懂了其中的一个类,其他的,都大差不差。

针对上述所说的几十个类,又分成了两个大的类别:

  1. 字节流:读写文件,以字节为单位,是针对二进制文件使用的。
    其中,关于字节流的类中,典型的代表有:InputStream类(输入) 和 OutputStream类(输出)
  2. 字符流:读写文件,以字符为单位,是针对文本文件使用的。
    其中,关于字符流的类中,典型的代表有:Reader类(输入) 和 Writer类(输出)

输入输出:

讲到这,我们得聊一聊,什么叫做输入,什么叫做输出?
首先,我们需要明确的知道一个点:输入输出,是以 CPU 作为参考点的。

输入输出,讲的就是以 CPU 为参考点,来看数据的流向:
硬盘 → CPU 的,叫做 输入
CPU → 硬盘 的,叫做 输出

在这里插入图片描述

举个这样的例子,你可能就会容易记点:把CPU想象成你的嘴巴,硬盘可以想象成食物的来源之类的,那么,你吃东西的时候,食物从你的嘴巴进入( 硬盘 → CPU),是不是就叫做 摄入食物(输入),嘴巴把 食物吐出来(CPU → 硬盘),是不是就叫做 吐出食物(输出)

输入:就是从文件(存储在硬盘当中的文件)当中 数据,到 CPU当中。
输出:就是CPU往文件(存储在硬盘当中的文件)当中数据。

一定要搞清楚,输入,输出,读,写,这四个词的意思和他们之间的区别,这对于你学习计算机是很有帮助的。

2.2 针对字节流对象的两个类的演示

字节流操作,主要是两个类:InputStream类OutputStream类
在讲这两个类之前,我们需要知道,这两个类的共同点:
两个类,全部都是抽象类!!!

在这里插入图片描述

抽象类有个非常大的特点:这个类,不能够实例化对象,也就是说,无法通过new关键字,创建一个对象。如图:
在这里插入图片描述
那么,我们该如何去实例化对象呢?
我们通过这两个类的子类:FileInputStream类FileOutputStream类,来实例化 InputStream类OutputStream类 这两个类的对象(通过实例化(new)父类的子类对象,传给父类的引用,这叫做向上转型,在多态的博客中详细介绍了)
如图:
在这里插入图片描述
详细的介绍呢,我们就在每个类的单独介绍中再讲。

学习这两个类之前,我们需要知道,InputStream类OutputStream类,都是抽象类,抽象类,不能够实例化本类的对象,必须通过new子类对象来进行使用。

2.2.1 字节流:InputStream类(输入)

InputStream类是一个抽象类,本身不能够实例化对象,需要用它的子类对象,来实例化对象。
InputStream类的子类,有很多,我们本篇博客使用的是:FileInputStream类。
FileInputStream类是针对二进制文件进行读操作的一个类。

在这里插入图片描述

使用 FileInputStream类 实例化对象的注意点

1. 实例化对象的时候,我们需要传入参数

传入的参数,可以是一个路径,绝对路径,相对路径都可以,也可以是一个File对象(如何创建一个File对象,并把File对象,作为参数,在Java 文件操作 和 IO(2)-- Java文件系统操作已经讲过了)
这一块,我的演示就以相对路径:"./test.txt",作为例子,表示当前的项目路径下的 test.txt 文件。
在这里插入图片描述

2. 使用FileInputStream类的时候,我们需要处理一个异常 FileNotFoundException

如果你当前传入的这个路径,计算机没有找到文件,就会抛出这个异常。

在这里插入图片描述
这里处理异常的方式,直接在main( )方法 的后面 加 throws FileNotFoundException,或者使用 catch捕捉异常,就可以了,抛出异常时,交给JVM处理就行(这块不懂的,可以看看我写的JAVA 异常)。

2.2.2.1 打开文件
InputStream inputStream = new FileInputStream("/test.txt");

一旦这条语句,执行成功,也就是,创建对象的操作,一旦成功,就相当于“ 打开文件 ”这个的操作。

进行文件操作,不是一上来就能进行读写操作的,而是需要先打开,然后才能进行读写操作,这个是由操作系统定义的流程,也就是说,不同的编程语言,进行文件内容操作的时候,都是需要走这个流程的。

你可以认为,这句代码所表示的打开文件操作,就是根据你传入的 文件路径File对象 所对应的路径,定位到计算机的硬盘空间中的某一个文件,然后打开

2.2.2.2 关闭文件

我们先不着急对文件内容进行读写操作,我们还需要讲一下,与打开文件相对应的
关闭文件 ” 操作。

打开 相当于从系统中申请一块资源,关闭 就相当于释放资源,这样的操作,是C++代码经常有的操作。

关闭文件,我们使用的是: close()方法。

inputStream.close();

使用 close()方法还需要注意一个点:处理IOException异常
在这里插入图片描述

这里还需要知道一点:IOException异常是 FileNotFoundException异常的父类,当我们处理IOException异常的时候,其实,等同于把FileNotFoundException异常也解决了。
在这里插入图片描述
但是,你光是写了 close()方法 还不够,我们还必须要让 close()方法 执行成功!!!
比如,当我们这个程序中,写了一个 return语句,导致了 包含的close()方法 的一系列代码,没有执行,也就是没有执行 关闭文件的操作,也是不行的!!!
在这里插入图片描述

那么,如何确保close()方法,一定会执行呢?
使用 finally 关键字

finally关键字,在JAVA 异常这篇博客有详细的介绍到,大家可以跳转过去看看。

但是,有 finally 就一定可以了吗?
在这里插入图片描述
注意:try代码块里面的代码,和finally代码块里面的代码,并不是共用的

此时需要把变量的声明,放到 try 代码块的上面(外边),定义成 null。
在这里插入图片描述

现在的这段代码,就可以完完全全的确保,close()方法(文件关闭操作),一定会被执行到了

但是,写到这,这个代码虽然能解决问题,但是,它又引入了一个新的问题:代码不美观!代码太丑,也不行!

这时,就需要引入一个语法,叫做:try with resource 语法

2.2.2.3 try with resource 语法的简单介绍

try with resource 的语法是怎么样的呢?

我们先看我们写过的代码:

public class demo1 {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = null;
        try {
             inputStream = new FileInputStream("/test.txt");
        }finally {
            inputStream.close();
        }
    }
}

然后,我把一部分代码注释掉,用 try with resource 语法,来等效的替换它。

public class demo1 {
    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")) {
            
        }
    }
}

在这里插入图片描述
这就是 try with resource 的语法演示。

具体语法如下:

try (ResourceType resource = new ResourceType()) {
// 使用资源
} catch (ExceptionType e) {
// 处理异常
}

try with resource 语法,就是把需要进行关闭的资源(需要执行关闭操作 close方法 的类),放到 try 后面的括号里面,只要出了 try 的代码块,就会自动调用该资源的 close方法。
这个语法,不仅可以达到关闭文件资源的操作,而且,也简化了代码,使代码看起来更加美观!

但是,也不是所有的类,都可以使用 try with resource 语法(需要执行关闭操作的类放到 try 后面的括号里面去的()),这里有一个要求:要求这个类,需要实现 Closeable接口!!!

比如:我们这里示范的这个 InputStream类,它是实现了 Closeable接口,才可以使用 try with resource 语法的
在这里插入图片描述

为什么必须要求这个类实现 Closeable接口 才能使用 try with resource 语法呢?
我们 Ctrl + 鼠标 点击 Closeable接口,可以看到,这个Closeable接口里面,就只有一个方法:close()方法

所以说,使用 try with resource 语法 的前提这个类 实现了 Closeable接口约定好这个类,一定有 close()方法,从而在出了 try 代码块之后,可以让 JVM 自动调用 close()方法!!!

这个 try with resource 语法,try括号后面,还可以写多个对象,不同对象之间,使用 ;进行分开。如:
在这里插入图片描述

总结: try with resource 语法,十分建议大家重点掌握,如果你未来是从事开发岗位或者其他岗位,这种简洁的代码语法,是十分常用的。

在下面的代码演示中,我会以 try with resource 语法 的形式,进行文件的打开和关闭操作。

2.2.2.4 不关闭文件会造成的后果

Java 当中,我们对于关闭操作,可能见的就比较少了,但对于 C++ 来说,确实用的非常多

C++当中,申请内存,释放内存,这是 C++程序员日常写程序需要思考的问题。这就相当于 你开的车是手动挡,需要不停的踩离合,挂挡。
Java当中,只需要申请内存就可以了,释放的话,交给 GC(‌GC(Garbage Collection)在Java中指的是垃圾收集或垃圾回收机制‌) 来处理就可以了。这就相当于 你开的车是自动挡,只需要踩油门,踩刹车就可以了。

但是 文件资源是文件资源,内存资源是内存资源,也就是说:文件资源 不等同于 内存资源
虽然 GC 能够自动管理内存资源,但是,不能够自动管理文件资源文件资源,需要我们手动释放!!!
如果不手动释放,就会引起“ 文件资源泄露 ”,类似于“ 内存泄漏 ”。有个特别形象的比喻叫做:占着茅坑不上厕所。

那么文件资源到底占用的是计算机中的什么资源呢?(这块的内容,看看就好了,知道有这么个东西就行)
在进程中,有一个描述进程的结构,叫 PCB,PCB中,有一个重要的属性,叫做文件描述符表(可以看作是一个固定长度的顺序表)。
每次程序打开一个文件,就会在文件描述符表中,申请一个表项(占个坑),如果光是打开,不关闭,就会使这里的文件描述符表中的表项,很快就会耗尽。耗尽了之后,如果你再次打开文件,就会打开失败!当前程序之后的很多逻辑,就会出现 bug。

相信大家或多或少的听过,Java当中,有自动扩容,那为什么不能给文件描述符表进行自动扩容呢?但是,站在内核的开发角度来说,自动扩容需谨慎!(这块的内容,也是看看就好了,知道有这么个东西就行)
内核里的每个操作,都是要给所有的进程提供服务的,如果你自动扩容的话,指不定你那次插入操作,刚刚好触发了自动扩容,导致这个的插入操作,耗费的时间很长,造成程序卡顿,对于用户来说,就会感觉到明显的卡顿。所以,进行自动扩容,这样就会进一步的加大内核操作中的不可控因素。
至于更详细的东西,这里就不展开讲了,有兴趣的可以自己去百度查一查。

但是,就算能够为文件描述符表自动扩容,也解决不了文件资源泄露的问题,因为文件资源泄露,是一个持续性的过程,而计算机的内存,也是有限的,为文件描述符表自动扩容一次,就会消耗一次内存资源,迟早有一天,内存资源也会耗光。

总结来说:关闭文件,非常非常重要!!

2.2.2.5 读取文件的操作(三种 read方法)

由于是第一次讲解对文件内容进行操作,会讲的比较细致一点,导致,这篇文章的字数太多,所以,我将 三种 read方法 的介绍,单独分离出来一篇博客:

字节流操作:InputStream类 读取文件的操作(三种 read 方法)

请你先看点击这个链接,跳转到另一篇博客中,看完那篇博客,再回来看下面的内容。

2.2.2 字节流:OutputStream类(输出)

OutputStream类是一个抽象类,本身不能够实例化对象,需要用它的子类对象,来实例化对象。
OutputStream类子类,有很多,我们本篇博客使用的是:FileOutputStream类。
FileOutputStream类是针对二进制文件进行写操作的一个类。

在这里插入图片描述
当然,针对 FileOutputStream类 的使用,同样需要注意两点:
1. FileOutputStream类的括号里面需要传入参数

传入的参数,可以是一个路径,绝对路径,相对路径都可以,也可以是一个File对象(如何创建一个File对象,并把File对象,作为参数,在Java 文件操作 和 IO(2)-- Java文件系统操作已经讲过了)

这里我就以 “ output.txt ” 为例子(就算你项目路径下没有这个文件,你使用OutputStream类的时候,也会自动创建,这个后面讲)
在这里插入图片描述
2. 需要处理异常

处理异常这里,就不细说了,和 InputStream类 开头介绍注意事项的处理异常那块所讲的大差不差。

写文件的操作(三种 write方法):

大前提:此处写文件的操作,写入的都是字节数据,写入到 output.txt 这个文本文件中,会自动解码,转换成有意义的文字信息。

1. 带 int 参数的 write()方法

在这里插入图片描述

这个方法表示的就是说:一次写入一个字节
没有返回值

应该有人会疑惑,不是写入一个字节吗?怎么括号里面的参数,怎么是 int类型的,不应该是 byte类型吗?

这是因为,在 java 开发过程中,在此处希望的参数的取值范围是 0~255,主要目的是和 InputStream类 中提供的 read()方法 相匹配。

如果使用的是 byte类型,作为参数的类型,那么这个参数的取值范围是:-128~127(至于为什么是这么个范围,这里就不展开讲了,自行百度即可),那么这个范围,和它对应的 read()方法 的范围:0~255,是不匹配的。
主要也是因为,java当中,没有 unsigned类型(有符号和无符号,C语言中的)。

演示操作:
我们就以 把 “ abc ” 三个字母output.txt 文件当中,并且,我当前的项目路径,是没有这个文件的
在这里插入图片描述

abc,这三个字母所对应的是 ASCII码中的 97,98,99。

演示代码:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class demo4 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("./output.txt")){
//            操作文件
//            向output.txt文件当中,写入 a,b,c 三个字母
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);


        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:
在这里插入图片描述
用红线划出的代码句,会有两种情况:
第一种:如果项目路径中,存在目标文件,则正常写入数据
第二种:如果项目路径当中,没有目标文件,则会自动创建出该文件,然后再写入数据。

在这里插入图片描述
自动创建出来的 output.txt 文件中,成功写入了 a,b,c 这三个英文字母。

2. 带 byte[ ] 参数的 write( )方法

在这里插入图片描述

括号里面的是 byte[ ] 数组的引用,可以是 b 也可以是 bytes ,是你创建 byte[ ] 数组时的引用。

此处的 byte[ ] 数组 ,存放的是要写入目标文件的数据

这里的演示,目的是把 c,b,a 这三个字母,按顺序的写入到 output.txt文件 当中。

演示代码:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class demo5 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("./output.txt")) {
//            操作文件
//            向output.txt文件当中,写入 a,b,c 三个字母

            byte[] bytes = new byte[]{99,98,97};
            outputStream.write(bytes);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:
在这里插入图片描述
成功地把 c,b,a 这三个字母,按顺序的写入到output.txt文件当中。

3. 带 三个参数的 write( )方法

在这里插入图片描述
这里的三个参数,和 InputStream类 当中的带三个参数的 read( )方法,是相似的。

byte[ ] 数组 :是你准备要写入目标文件的数据

int off :是数组下标,是你要从 byte[ ] 数组 的哪个数组下标开始进行写操作,可以对数组内的数据,选择性的写入文件。

int len:表示的是,你准备从 byte[ ]数组中,取多少(len)个字节的数据往目标文件中写入。

演示操作:

我这里有一个 字节数组 bytes,存放着 a, b, c, d, e 五个英文字母所对应的 ASCII值:97,98,99,100,101。
想在,我想从该数组的 1 下标开始进行写操作,只写 3 个字节大小的数据(一个英文字母,占一个字节的大小),也就是按顺序写入三个英文字母

最终的结果应该是:output.txt文件中,显示的是 b, c, d 这三个英文。

演示代码:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class demo6 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("./output.txt")) {
            //操作文件
            //创建字节数组,往里面存放数据 a, b, c, d, e
            byte[] bytes = new byte[]{97,98,99,100,101};

            //从 1 下标开始进行写写操作,只写 3 个字节大小的数据
            outputStream.write(bytes,1,3);
            
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:
在这里插入图片描述
数据写入完成。

遗留的问题

看到这,你应该是没有什么问题,也基本可以理解。

那不知道你有没有发现:每当我进行写操作的时候,上一次的数据,消失不见了呢?
在这里插入图片描述

文件写入覆盖 和 文件追加写入

对于文件写入操作来说,每一次程序所执行的写入操作,都是会清除上次程序执行写入操作所写入的文件内容的,也就是说,本次运行程序,写入的文件内容,再次执行程序后,会清除上次写入的文件内容,当打开文件(程序执行)的一瞬间,上次文件里面的内容,就会被清空了!!!

注意:这里的文件覆盖,指的是相同的写文件程序两次运行的结果!

这种现象,不仅仅是Java的有,C语言的文件操作中,按照写方式(写入操作)来打开文件,也会有同样的现象产生。

这种现象,是操作系统的行为你使用什么语言来进行文件操作是没有关系的。

那么,如何让文件内容不清空,能够让我们继续写呢?
我们可以采取追加写的模式,避免文件内容被清空

追加写模式:在new OutputStream对象的时候,往括号里面写上一个 true,就表示采用追加写模式了,平常不写true的时候,默认是false

在这里插入图片描述
此时,我们再一次执行代码:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class demo6 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("./output.txt",true)) {
            //操作文件
            //创建字节数组,往里面存放数据 a, b, c, d, e
            byte[] bytes = new byte[]{97,98,99,100,101};

            //从 1 下标开始进行写写操作,只写 3 个字节大小的数据
            outputStream.write(bytes,1,3);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:
在这里插入图片描述
注意:追加写模式,说的是两次程序运行的结果。
也就是说,追加写模式,是让上次程序执行写文件操作所写入的文件内容,再次执行程序时,不会清除该文件已经写入的内容。
说的直白点:只要你采用追加写,无论执行多少次同一个写文件程序,文件中的内容都不会被清除

到这里,就讲完了 OutputStream类 的简单基本用法了。

3. 总结

这一篇博客,就先讲这么多,因为我写的实在是有点太多了,字数已经超过 16900+ 了,再写下去,恐怕你们都不一定能有多少耐心看下去。

关于 文件内容操作中字符流操作的部分,我就在Java 文件操作 和 IO(4)-- Java文件内容操作(2)-- 字符流操作中,再进行讲解吧。

本篇博客,主要是在 字节流的 InputStream类 中,讲了很多,希望看到这里的读者,能慢慢地消化这篇博客的知识,把代码都自己写一写。

最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值