Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作
文章目录
- Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作
- 观前提醒:
- 1. Java中操作文件的简单介绍
- 2. Java文件内容操作--字节流
- 2.1 流概念的介绍 和 输入输出
- 流:
- 输入输出:
- 2.2 针对字节流对象的两个类的演示
- 2.2.1 字节流:InputStream类(输入)
- 2.2.2.1 打开文件
- 2.2.2.2 关闭文件
- 2.2.2.3 try with resource 语法的简单介绍
- 2.2.2.4 不关闭文件会造成的后果
- 2.2.2.5 读取文件的操作(三种 read方法)
- 2.2.2 字节流:OutputStream类(输出)
- 写文件的操作(三种 write方法):
- 1. 带 int 参数的 write()方法
- 2. 带 byte[ ] 参数的 write( )方法
- 3. 带 三个参数的 write( )方法
- 遗留的问题
- 文件写入覆盖 和 文件追加写入
- 3. 总结
观前提醒:
在看这篇博客之前,建议你先看完这两篇博客:
Java 文件操作 和 IO(2)-- Java文件系统操作
Java 文件操作 和 IO(1)-- 文件相关知识背景介绍
在讲Java文件内容操作之前,我们还是需要再来看看,java当中,操作文件的简单介绍,知道文件操作的区分,脑里有个大概的图,才能更好的理解这部分的知识!
1. Java中操作文件的简单介绍
使用java来操作文件,主要是通过java标准库提供的一系列的类,而这些类,又可以分为两种操作方向:
-
文件系统操作:
这里主要是关于 文件 或 目录 的创建,文件的删除,文件的重命名,创建目录等… -
文件内容操作:
这里就是对某一个具体的文件的内容进行读和写了,又由于文件有两种种类,所以,我们又区分了字节流操作和字符流操作。
(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个类,就慌了,因为,文件内容操作这块,比起文件系统操作这块,好学很多,因为这八个类所提供的方法都是类似的,比较有规律,你学懂了其中的一个类,其他的,都大差不差。
针对上述所说的几十个类,又分成了两个大的类别:
- 字节流:读写文件,以字节为单位,是针对二进制文件使用的。
其中,关于字节流的类中,典型的代表有:InputStream类(输入) 和 OutputStream类(输出) - 字符流:读写文件,以字符为单位,是针对文本文件使用的。
其中,关于字符流的类中,典型的代表有: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, ebyte[] 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, ebyte[] 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类 中,讲了很多,希望看到这里的读者,能慢慢地消化这篇博客的知识,把代码都自己写一写。
最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!