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

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

前言:


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


Java IO:即 Java 输入/输出系统。大部分程序都需要处理一些输入,并由输入产生一些输出,因此Java为我们提供了 java.io


作为一个合格的程序开发者,说到 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 文件中


目录
相关文章
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
1月前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
67 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
2月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
1月前
|
Java 数据处理 开发者
揭秘Java IO流:字节流与字符流的神秘面纱!
揭秘Java IO流:字节流与字符流的神秘面纱!
35 1
|
1月前
|
自然语言处理 Java 数据处理
Java IO流全解析:字节流和字符流的区别与联系!
Java IO流全解析:字节流和字符流的区别与联系!
81 1
|
1月前
|
Java
Java 中 IO 流的分类详解
【10月更文挑战第10天】不同类型的 IO 流具有不同的特点和适用场景,我们可以根据具体的需求选择合适的流来进行数据的输入和输出操作。在实际应用中,还可以通过组合使用多种流来实现更复杂的功能。
48 0
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
1月前
|
存储 Java 程序员
【Java】文件IO
【Java】文件IO
37 0
|
2月前
|
数据采集 Java 数据挖掘
Java IO异常处理:在Web爬虫开发中的实践
Java IO异常处理:在Web爬虫开发中的实践
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
46 2