别大意,你可能还没掌握好Java IO

2021年11月19日 阅读数:6
这篇文章主要向大家介绍别大意,你可能还没掌握好Java IO,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

你们好,我是小菜,一个渴望在互联网行业作到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!
死鬼~看完记得给我来个三连哦!java

本文主要介绍 Java中的I/O系统

若有须要,能够参考程序员

若有帮助,不忘 点赞数组

微信公众号已开启,小菜良记,没关注的同窗们记得关注哦!微信

前言:app

对程序语言的设计者来讲,建立一个好的输入/输出 (I/O) 系统是一项艰难的任务

Java IO:即 Java 输入/输出系统。大部分程序都须要处理一些输入,并由输入产生一些输出,所以Java为咱们提供了 java.iodom

做为一个合格的程序开发者,说到 IO 咱们并不会陌生,JAVA IO 系统的知识体系以下:函数

看完以上的图,才会恍然,原来 Java.io 包中为咱们提供了这么多支持。而咱们恍然的同时也没必要感到惊慌,俗话说万变不离其宗,咱们只须要根据源头进行扩展,相信就能够很好的掌握IO知识体系。工具

File类

读写操做少不了与文件(File)打交道,所以咱们想要掌握好 IO 流,不妨先从文件入手。学习

文件(File)这个词即非单数也非复数,它既能表明一个特殊的文件,又能表示一个目录下的文件集。测试

列表

File 若是表示的是一个目录下的文件集的时候,咱们想要获得一个目录能够怎么作?

File已经为咱们准备好了 API,根据返回值类型,咱们不难猜到每一个 API 方法的用处。

已知咱们 D 盘目录下有个 TestFile 文件夹,该文件夹下有如下文件:

名称列表

若是咱们想要获取指定目录下的名称列表,咱们可使用这两个API:

  • list()
  • list(FilenameFilter filter)

不带参数的 list() 方法默认是列出指定目录下的全部文件名称。若是咱们想要指定名称的目录名称列表咱们即可以使用另外一个方法:

咱们指望获取带有test关键字的文件名称,而结果也如咱们所愿。

文件列表

有时候咱们的不少操做不仅仅针对于某个文件,而是在整个文件集上作操做。要产生这个文件集,那咱们就须要借助 File 的另外API方法了:

  • listFiles()
  • listFiles(FilenameFilter filter)
  • listFiles(FileFilter filter)

有了以上经验,咱们不难猜到 listFiles() 的做用即是列出全部的文件列表:

图中咱们已经获取到了文件集,该方法会返回的一样是一个数组,不过是一个 File类型的数组。

聪明的你确定也已经知道了若是获取带指定关键字的文件集

与上述列出文件名称一模一样,真是个小机灵鬼~

可是listFiles(FileFilter filter) 这个方法传递的参数与上有何异?咱们不妨一试:

一样是一个接口,一样须要重写 accept() 方法,可是这个方法只有一个 File 的参数。所以这两个参数都是用于文件过滤的,功能大同小异~

目录工具

建立目录

File 类的好用之处不只能让你对于已有目录文件的操做,还能让你无中生有!

文件的特性无外乎:名称,大小,最后修改日期,可读/写,类型等

那么咱们经过 API 也理应可以得到:

以上什么类型都获取到了,惟独少了个类型,虽说 File 没有提供直接获取类型的方法,可是咱们能够经过获取文件的全名,而后经过裁剪获取到文件的后缀,即可获取到文件的类型:

转手一操做,自给自足也能获取文件类型,真是个小机灵鬼~

以上咱们都是基于文件目录存在的状况下操做的,那么若是咱们想要操做的文件目录不存在。或者因为咱们的粗心将文件目录名称输入错了,那么将会发生什么状况,操做进程是否可以正常进行?

结果即是抛出异常了,的确抛出异常才是正常的现象,针对一个不存在的文件目录进行操做岂不是瞎胡闹

所以在咱们不肯定文件目录是否存在的状况下咱们能够这样操做:

在图中咱们能够看到两个咱们没见过的API方法,分别是 exists()mkdirs().

  • exists(): 用于验证文件目录是否存在
  • mkdirs(): 用于建立目录

经过以上先验证后操做,咱们成功避免了异常。这里须要了解的是,除了 mkdirs() 能够建立目录以外,还有一个 mkdir() 也是能够建立目录的,这两个方法除了少了一个 s 以外,还有其余区别呢?

  • mkdir(): 只能建立一层目录
  • mkdirs(): 能够建立多层目录

咱们目前的场景是 Test 目录不存在,dir01 这个目录天然也不存在,那么这个时候就得建立两层目录。可是咱们使用 mkdir() 这个方法是行不通的,它没法建立。所以遇到这种状况咱们应当使用 mkdirs()这个方法。

File类型

File 能够是一个文件也能够是一个文件集,文件集中可包含一个文件或者是一个文件夹,若是咱们想要针对一个文件作肚读写操做,却无心对一个文件夹进行了操做,那就尴尬了,所以咱们能够借助 isDirectory 来判断是不是文件夹:

输入与输出

上面咱们谈到 File 类的基本操做,接下来咱们便进入了I/O模块。

输入和输出咱们常用 这个概念,如输入流和输出流。这是个抽象的概念,表明任何与能力产出数据的数据源对象或是有能力接受数据的接收端对象。 屏蔽了实际 I/O 设备找那个处理数据的细节!

I/O 能够分为 输入输出 两部分。

输入流中又分为 字节输入流(InputStream)字符输入流(Reader),任何由 InputStreamReader 派生而来的类都实现了 read() 这个方法,用来读取单个字节或字节数组。

输出流中又分为 字节输出流(OutputStream)字符输出流(Writer),任何由 OutputStreamWriter 派生而来的类都实现了 write() 这个方法,用来写入单个字节或字节数组。

所以咱们能够看出 Java 中的规定:与输入有关的全部类都应该从 InputStream 继承,与输出有关的全部类都应该从 OutputStream 继承

InputStream

用来表示那些从不一样数据源产生输入的类

那些不一样数据源具体又是哪些?常见的有:1. 字节数组 2. String 对象 3. 文件 4. “管道”(一端输入,一端输出)

其中每一种数据源都有对应的 InputStream 子类能够操做:

功能
ByteArrayInputStream 容许将内存的缓冲区看成 InputStream 使用
StringBufferInputStream 已废弃,将String转换成 InputStream
FileInputStream 用于从文件中读取信息
PipedInputStream 产生用于写入相关 PipedOutPutStream的数据,实现 管道化 的概念
SequenceInputStream 将两个或多个 InputStream 对象转换成一个 InputStream
FilterInputStream 抽象类,做为装饰器的接口,为其余InputStream 提供有用的功能

OutPutStream

该类别的类决定了输出所要去往的目标:1. 字节数组 2. 文件 3. 管道

常见的 OutPutStream 子类有:

功能
ByteArrayOutputStream 在内存中建立缓冲区,全部送往 “流” 的数据都要放置在此缓冲区
FileOutputStream 用于将信息写入文件
PipedOutputStream 任何写入其中的信息都会自动做为相关 PipedInputStream 的输出,实现 管道化 的概念
FilterOutputStream 抽象类,做为装饰器 的接口,为其余 OutputStream 提供有用的功能

装饰器

咱们经过以上的认识,都看到了无论是输入流仍是输出流,其中都有一个抽象类FilterInputStreamFilterOutputStream,这些类至关因而一个装饰器。在Java 中I/O 操做须要多种不一样的功能组合,而这个即是使用装饰器模式的理由所在。

何为装饰器?装饰器必须具备和它所装饰对象的相同接口,但它也能够扩展接口,它能够给咱们提供了至关多的灵活性,但它也会增长代码的复杂性。

FilterInputStreamFilterOutputStream 是用来提供装饰器类接口以控制特定输入流(InputStream)和输出流(OutputStream)的两个类。

FilterInputStream

InputStream 做为字节输入流,那么读取的数据理应用字节数组接收,以下:

咱们得借助一个 byte 数组来接收读取到值,而后转为字符串类型。

既然咱们有了装饰器FilterInputStream ,那是否能够借助装饰器的子类来帮咱们实现读操做呢?咱们先来看下经常使用的FilterInputStream子类有哪些:

功能
DataInputStream 与 DataOutputStream 搭配使用,咱们能够按照可移植方式从流读取基本数据类型(int,char,long)
BufferedInputStream 使用它能够防止每次读取时都得进行实际写操做。表明"缓冲区"

其中DataInputStream容许咱们读取不一样的基本数据类型数据以及String对象,搭配相应的DataOutputStream,咱们就能够经过数据"" 将基本类型的数据从一个地方迁移到另外一个地方。

而后说到BufferedInputStream 以前咱们先看一组测试代码:

现有三个文本文件,其中test01.txt 大小约为 610Mtest02/test03均为空文本文件

那咱们如今分别用普通的 InputStream + OutputStream 和装饰后的BufferedInputStream + BufferedOutputStream 写入文本

普通组合:

缓冲区组合:

能够看出两种方式的分别耗时,4864 ms1275 ms。 使用普通组合至关因而缓冲区的 4 倍之久,若是文件更大的话,这个差别但是惊人的!惊讶的同时确定也有所诧异,这是为何呢?

若是用read()方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的。即使使用read(byte b[])方法一次读取多个字节,当读取的文件较大时,也会频繁的对磁盘操做。

BufferedInputStream的API文档解释为:在建立BufferedInputStream时,会建立一个内部缓冲区数组。在读取流中的字节时,可根据须要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。也就是说,Buffered类初始化时会建立一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。所以这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高不少。

BufferedInputStream/BufferedOutputStream不直接操做数据源,而是对其余字节流进行包装,它们是 处理流

程序把数据保存到 BufferedOutputStream 缓冲区中,并无当即保存到文件里,缓冲区中的数组在如下状况会保存到文件中:

  • 缓冲区已满
  • flush() 清空缓冲区
  • close() 关闭流
FilterOutputStream

OutputStream 的基本操做以下:

经过调用write() 方法即可将值写入文件中,这里有两点须要注意:

  • 写入文档默认是覆盖的方式

按咱们理解调用两次该方法,文本文件中的内容应该是两行 公众号:小菜良记,可是实际上只用一行,这是由于后面写入的内容会覆盖前面已经存在的内容,解决方法即是在构造函数的时候加上append = true

  • 写入与读取的区别在于,读取的时候若是文件不存在会报错,可是写入的时候若是文件不存在,会默认帮你建立文件

OutputStream中一样存在装饰器类FilterOutputStream,如下即是装饰器类的经常使用子类:

功能
DataOutputStream 与DATAInputStream搭配使用,能够按照可移植方式向流中写入基本类型数据(int,char,long等)
BufferedOutputStream 使用它避免每次发送数据时都要进行实际的写操做,表明 使用缓冲区,能够调用flush清空缓冲区

DataOutputStreamBufferedOutputStream 在上面已经讲到,这里就再也不赘述。

Reader 与 Writer

Java 1.1 的时候,对基本的I/O流类库进行了重大的修改,增添了 ReaderWriter 两个类。在我以前局限的认知中,会误觉得这两个类的出现是为了替代 InputStreamOutputStream ,但事实也并不是与我局限认知所似。

InputStreamOutputStream 是以面向字节的形式为 I/O 提供功能,而 ReaderWriter是提供兼容 Unicode于面向字符的形式为 I/O 提供功能

这二者共存,并提供了适配器 - InputStreamReaderOutputStreamWriter

  • InputStreamReader 能够把 InputStream 转换为 Reader
  • OutputStreamWriter 能够把 OutputStream 转换为 Writer

这二者虽然不能说彻底相同,但也是极为类似,对照以下:

字节流 字符流
InputStream Reader
OutputStream Writer
FileInputStream FileReader
FileOutputStream FileWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter

甚至装饰者类都几乎类似:

字节流 字符流
FilterInputStream FilterReader
FilterOutputStream FilterWriter
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
PrintStream PrintWriter

使用ReaderWriter 的方式也十分简单:

咱们顺便看下装饰器的使用BufferedReaderBufferedWriter

RandomAccessFile

RandomAccessFile 适用于由大小已知的记录组成的文件,因此咱们可使用 seek() 将记录从一处转移到另外一处,而后读取或者修改记录。文件中记录的大小不必定都相同,只要咱们可以肯定哪些记录有多大以及它们在文件中的位置便可。

咱们从图中能够看到 RandomAccessFile 并不是继承于 InputStreamOutputStream 两个接口,而是继承于有些陌生的DateInputDataOutput

真是个有点特立独行的类~咱们继续来看下它的构造函数:

咱们这边只截取了构造函数的一部分,毕竟只截重点就行~

观察构造器能够发现,这里定义了四种模式:

r 以只读的方式打开文本,也就意味着不能用write来操做文件
rw 读操做和写操做都是容许的
rws 每当进行写操做,同步的刷新到磁盘,刷新内容和元数据
rwd 每当进行写操做,同步的刷新到磁盘,刷新内容

这有什么用呢?说白了就是 RandomAccessFile 这个类什么都要。既能读,又能写

从本质上来讲,RandomAccessFile 的工做方式相似于把 DataInputStreamDataOutputStream 组合起来使用,还添加了一些方法,其中方法getFilePointer() 用于查找当前所处的文件位置,seek() 用于在文件内移至新的位置,length() 用于判断文件的最大尺寸。第二个参数用于代表咱们是 "随机读(r)" 仍是 "既读又写(rw)",但它不支持单独 写文件。咱们实际来操做一下:

获取只读RandomAccessFile

获取可读可写RandomAccessFile

咱们首先从向文件中写入了test 四个单词,而后将头指针移动3位后继续写入File四个单词,结果就变成了testFile,这是由于移动指针后是以第四个位置开始写入。

ZIP

看到zip这个词,咱们理所应当的就会想到压缩文件,没错压缩文件在 Java I/O中也是极其重要的存在。也许更应该说对文件的压缩在咱们的开发中也是极其重要的存在。

Java 内置类中提供了须要关于ZIP 压缩的类,可使用 java.util.zip 包中的ZipOutuputStreamZipInputStream 来实现文件的 压缩解压缩。咱们先来看下如何对文件进行压缩~

ZipOutputStream

ZipOutputStream 的构造方法以下:

public ZipOutputStream(OutputStream out) {/* doSomething */}

咱们须要传入一个 OutputStream 对象。所以咱们也大体能够认为 压缩文件 至关因而向一个 压缩文件中写入数据,听起来可能会有点绕。咱们先看下ZipOutputStream中有哪些API:

方法 返回值 说明
putNextEntry(ZipEntry e) void 开始写一个新的 ZipEntry,并将流内的位置移至此 entry 所值数据的开头
write(byte[] b, int off, int len) void 将字节数组写入当前 ZIP 条目数据
setComment(String command) void 设置此 ZIP 文件的注释文字
finish() void 完成写入ZIP 输出流的内容,无须关闭它所配合的 OutputStream

咱们来演示一下如何压缩文件:

场景:咱们须要将D盘目录下的 TestFile文件夹压缩到 D盘下的 test.zip

具体的操做逻辑以下:

经过以上步骤咱们即可以很顺利的将一个文件压缩

ZipInputStream

说完如何将文件压缩,那天然要会如何将文件解压缩!

public ZipInputStream(InputStream in) {/* doSomethings */}

ZipInputStream 与压缩流相似,构造函数一样须要传入一个 InputStream 对象,毋庸置疑,API确定也是一一对应的:

方法 返回值 说明
read(byte[] b, int off, int len) int 读取目标 b 数组内 off 偏移量的位置,长度是 len 字节
avaiable() int 判断是否已读完目前 entry 所指定的数据,已读完返回 0,不然返回 1
closeEntry() void 关闭当前 ZIP 条目并定位流以读取下一个条目
skip(long n) long 跳过当前 ZIP 条目中指定的字节数
getNextEntry() ZipEntry 读取下一个ZipEntry,并将流内的位置移至该 entry 所指数据的开头
createZipEntry(String name) ZipEntry 以指定的name参数新建一个ZipEntry对象

那下面咱们便动手操做一下如何解压一个文件:

没必要被代码长度吓到,认真阅读便会发现解压文件也很简单:

咱们经过 getNextEntry() 方法来获取到一个ZipEntry,这里取到文件方式相似于深度遍历,每次返回的目录大体以下:

每次都会遍历完一个目录下的全部文件,例如 dir01 文件夹下的全部文件,才会继续遍历 dir02 文件夹,因此咱们没必要使用递归的方式去获取全部文件。取到每个文件后,经过 ZipFile获取输出流,而后写入到解压后的文件中。大体流程以下:

新 I/O

JDK1.4的java.nio.* 包中引入了新的 JavaI/O 类库,其目的也简单,就是提升速度。实际上,旧的I/O包已经使用 nio 从新实现过,以便充分利用这种速度提升。

只要使用的结构更接近于操做系统执行I/O的方式,那么速度天然也会提升,所以就产生了两个概念:通道缓冲器

咱们该怎么理解 通道缓冲器 两个概念呢。咱们能够认为缓冲器至关因而一辆煤矿中的小火车,通道至关于火车的轨道,小火车载着满满的煤矿从矿源运往它处。所以咱们并无直接和通道交互,而是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器得到数据,要么向缓冲器发送数据。

ByteBuffer是惟一直接与通道直接交互的缓冲器,能够存储未加工字节的缓冲器。

ByteBuffer buffer = ByteBuffer.allocate(1024);

ByteBuffer 的建立方式一般能够经过allocate()方法来指定大小建立。同时ByteBuffer中支持 4 中建立 ByteBuffer

为了更好支持 新I/O旧 I/O 类库中有三个类被修改了,用以产生FileChannel。这个被修改的类分别的:FileInputStreamFileOutputStream以及用于读写兼备的 RandomAccessFile。这里值得注意的是这些都是字节操做流,由于字符流不能用于产生通道,可是 Channels 中提供了实用的方法,用于在通道中产生 ReaderWriter

获取通道

咱们在上面已经了解到了有三个类支持产生通道,具体产生通道的方法以下:

以上即是建立通道的三种方式,而且进行了读写操做的测试。咱们看一下图中的测试代码,而后总结一下:

  • getChannel()方法将会产生一个 FileChannel。咱们能够向它传送可用于读写的ByteBuffer。咱们将字节存放于 ByteBuffer 的方法之一是:使用 put()方法直接对它们进行填充,填入一个或多个字节,或基本数据类型的值。不过,也可使用 wray()方法将已存在的字节数组 "包装" 到 ByteBuffer 中。这样子就能够不用在复制底层的数组,而是把它做为所产生的 ByteBuffer 的存储器,能够称之为 数组支持的ByteBuffer
  • 咱们还能够看到 FileChannel 使用到的 position() 方法,这个方法能够在文件内随处移动FileChannel,在这里,咱们把它移动到最后,而后进行其余的读写操做。
  • 对于只读访问,咱们必须显式地使用静态的allocate() 方法来分配 ByteBuffer。若是咱们想要获取更好的速度咱们也可使用 allocateDirect() ,以产生一个与操做系统有更高耦合性的 "直接" 缓冲器。可是这种分配的开支会更大,而且具体实现也随操做系统的不一样而不一样。
  • 若是咱们想要的调用 read() 来向ByteBuffer 存储字节,就必须调用缓冲器上的flip() 方法,这是用来告知 FileChannel ,让它准备好让别人读取字节的准备,固然,这也是为了获取最大速度。这里咱们用 ByteBuffer 来接收字节后就没有继续使用缓冲器来进一步操做,若是须要继续read() 的话,咱们就必须得调用 clear() 方法来为每一个 read() 方法作准备。

通道相连

程序员每每都是比较懒惰的,上面那种读取后再通知 FileChannel 的方式彷佛有些麻烦。那么有没有更加简单的方法?确定是有的,否则我也不会问是吧~ 那就是让一个通道与另一个通道直接相链接,这就得借助特殊的方法transferTo()transferFrom() 。具体使用以下:

借助方法1方法2 均可以成功将文件写入到 test03.txt 文件中

END

I/O 操做是咱们平常开发中或不可缺的知识,因此这块咱们也得好好掌握!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一块儿学习的男人。 💋

微信公众号已开启,小菜良记,没关注的同窗们记得关注哦!