`
shangjava
  • 浏览: 1188168 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

18.IO、文件、NIO【草案四】

阅读更多

(这一个章节将讲到Java里面比较重要的一个章节,这里说一句抱歉,因为最近换工作的原因,一直没有时间继续书写教程,不过接下来我会一直坚持写下去的哈,希望大家能够支持。这个章节主要涉及到常用的文件读写,包括高级的文件IO内容——java.nio,因为这些内容在如今的一些项目里面也属于相当常见的一部分,如果有什么遗漏或者笔误的话,希望读者来Email告知:silentbalanceyh@126.com,谢谢!这一部分篇幅可能比前边章节长很多,也是为了保证能够将Java里面IO和文件操作部分内能写的都写入,如果有遗漏希望读者来Email,概念上有混淆的地方请告知,里面有些内容参考了一些原文数据进行了翻译以及思考注解。)
本章目录:
1.IO类相关内容
2.文件和目录
3.文件高级操作
  关于初探部分就先讲这一点点,接下来一起深入了解一下Java NIO的核心知识。

  iii.NIO详解【1】——缓冲区(Buffer)【深入理解,总结自《Java-NIO》】:
  【*:下边的Buffer又指代抽象的缓冲区结构模型,同样代表Java语言里面的Buffer类的实例,这里不区分二者的概念了。】
  Buffer类基本概念:
  一般而言,Buffer的数据结构是一个保存了原始数据的数组,在Java语言里面封装成为一个带引用的对象。Buffer一般称为缓冲区,该缓冲区的优点在于它虽然是一个简单数组,但是它封装了很多数据常量以及单个对象的相关属性。针对Buffer而言主要有四个主要的属性:
  • 容量(Capacity):容量描述了这个缓冲区最多能够存放多少,也是Buffer的最大存储元素量,这个值是在创建Buffer的时候指定的,而且不可以更改
  • 限制(Limit):不能够进行读写的缓冲区的第一个元素,换句话说就是这个Buffer里面的活动元素数量
  • 位置(Position):下一个需要进行读写的元素的索引,当Buffer缓冲区调用相对get()和set()方法的时候会自动更新Position的值
  • 标记(Mark):一个可记忆的Position位置的值,当调用mark()方法的时候会执行mark = position,一旦调用reset()的时候就执行position = mark,和Position有点不一样,除非进行设置,否则Mark值是不存在的。
  按照上边的对应关系可以知道:
0 <= mark <= position <= limit <= capacity
  这几个关系可以用下图进行描述:
  [1]Buffer的基本操作:
  Buffer管理(Accessing):
  一般情况下Buffer可以管理很多元素,但是在程序开发过程中我们只需要关注里面的活跃元素,如上图小于limit位置的这些元素,因为这些元素是真正在IO读写过程需要的。当Buffer类调用了put()方法的时候,就在原来的Buffer中插入了某个元素,而调用了get()方法过后就调用该位置的活跃元素,取出来进行读取,而Buffer的get和put方法一直很神秘,因为它存在一个相对和绝对的概念:
  在相对版本put和get中,Buffer本身不使用index作为参数,当相对方法调用的时候,直接使用position作为基点,然后运算调用结果返回,在相对操作的时候,如果position的值过大就有可能抛出异常信息;同样的相对版本的put方法调用的时候当调用元素超越了limit的限制的时候也会抛出BufferOverflowException的异常,一般情况为:position > limit
  在绝对版本put和get中,Buffer的position却不会收到影响,直接使用index进行调用,如果index越界的时候直接抛出Java里面常见的越界异常:java.lang.IndexOutOfBoundException
  针对get和put方法的两种版本的理解可以查阅API看看方法get的定义【这里查看的是Buffer类的子类ByteBuffer的API】
public abstract byteget()throwsBufferUnderflowException
publicByteBuffer get(byte[] dst)throwsBufferUnderflowException
publicByteBuffer get(byte[] dst,intoffset,intlength)throwsBufferUnderflowException,IndexOutOfBoundException
public abstract byteget(int index)throwsIndexOutOfBoundException
  【*:从上边的API详解里面可以知道,Buffer本身支持的两种方式的访问是有原因的,因为Buffer本身的设计目的是为了使得数据能够更加高效地传输,同样能够在某一个时刻移动某些数据。当使用一个数组作为参数的时候,整个Buffer里面的 position位置放置了一个记录用的游标,该游标不断地在上一次操作完结的基础上进行移动来完成Buffer本身的数据的读取,这种情况下一般需要提供一个length参数,使用该参数的目的就是为了防止越界操作的发生。如果请求的数据没有办法进行传输,当读取的时候没有任何数据能够读取的时候,这个缓冲区状态就不能更改了,同时这个时候就会抛出BufferUnderflowException的异常,所以在向缓冲区请求的时候使用数组结构存储时,如果没有指定length参数,系统会默认为填充整个数组的长度,这种情况和上边IO部分的缓冲区的设置方法类似。也就是说当编程过程需要将一个Buffer数据拷贝到某个数组的时候(这里可以指代字节数组),需要显示指定拷贝的长度,否则该数组会填充到满,而且一旦当满足异常条件:即limit和position不匹配的时候,就会抛异常。】
  [2]Buffer填充(Filling):
  先看一段填充ByteBuffer的代码:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
  当这些字符传入的时候都是以ASCII值存储的,上述操作的存储步骤图如下:
  这里需要留意一点就是填充的时候需要进行强制转换,因为Java里面所有的字符格式都是16bit的Unicode格式,而上边代码里面填充的时候使用的参数是字符的,如果不进行强制转换会出现数据丢失的情况,ASCII码表示字符的时候使用的是8位数据,而Unicode方式保存字符本身和ASCII保存有很大的区别,为了不出现乱码保证存储到缓冲区字符的正确性,一定记住需要进行强制换转,转换成为对应的字节方式保存。再继续针对该Buffer进行填充操作:
buffer.put(0,(byte)'M').put((byte)'w');
  第一个方法使用了绝对的方式使用index参数替换了缓冲区中的第一个字节,而第二个使用了相对版本的put方法,所以最终形成的Buffer存储结构图为:
  从上边两个操作可以知道,Buffer类在填充该Buffer的时候使用相对方法和绝对方法有很大的区别,上图可以看到原来存入的“Hello”现在变成了“Mellow”,这就是Buffer填充操作的一个缩略图。
  [3]Buffer的反转(Flipping)
  当我们在编程过程中填充了一个Buffer过后,就会对该Buffer进行消耗(Draining)操作,一般是将该Buffer传入一个通道(Channel)内然后输出。但是在Buffer传入到通道中过后,通道会调用get()方法来获取Buffer里面数据,但是Buffer传入的时候是按照顺序传入到通道里面的,如上边的结构可以知道,本身存储的数据可能为“Mellow”,但是当通道读取Buffer里面的内容的时候,有可能取到不正确的数据,原因在于通道读取Buffer里面的数据的时候标记是从右边开始的,这样读取的数据如果从position开始就会有问题【*:当然不排除有这样一种情况缓冲区提供了读取策略是双向的,那么这样读取出来的所有的字符就有可能是反向的】。其实这点概念很容易理解,因为Buffer读取过后会按照顺序读入到通道(Channel)中,而通道获取数据的时候会从最右边的position位置开始,所以针对这样的情况如果需要正确读取里面的内容就需要对Buffer进行反转操作,该操作的手动代码如下:
buffer.limit(buffer.position()).position(0);
  但是因为Java中Buffer类的API提供了类似的操作,只需要下边的方法就可以了:
buffer.flip();
  经过flip操作过后,其存储结构就会发生相对应的变化:
  【*:这个地方仔细想想,经过了Flip操作过后,从逻辑意义上讲,确实Buffer被反转了,因为这个时候通道类(Channel)读取Buffer的时候会从position地方继续读取,不会出现读取异常的情况。与其说是“反转”,不如说是“重置”,只是这里的“重置”不会清空缓冲区里面的数据,仅仅是将缓冲区的limit属性和position属性进行重设,和真正调用reset方法的时候还是存在一定区别的,至于这里Flip翻译称为“反转”我不做说明,只要读者能够理解上边的步骤而且知道这是一次Flip操作就可以了,这里正确理解的是“重置position”我们在编程中也经常看见rewind()方法,该方法和flip()类似,但是该方法不会影响到limit的变化,它仅仅会将position设置为0,所以可以直接使用rewind方法进行“重新读取”还需要说明一点是如果进行了两次flip()操作的话,第二次操作会同时将position和limit设置为0,这样的话如果再进行基于缓冲区的相对读取过程就会BufferOverflowException
  [4]Buffer的“消费”(Draining):
  当一个Buffer缓冲区填满数据过后,应用程序就会将该缓冲区送入一个通道内进行“消费”,这种“消费”操作实际上使用通道来读取Buffer里面存储的数据,如果需要读取任何一个位置上的元素,则需要先flip操作才能够顺利接受到该Buffer里面的元素数据,也就是说在Channel通道调用get()方法之前先调用flip()方法【*:这里这种方式的调用是相对调用过程,从参数可以知道,这里的get()是相对方法的常用调用方式】在通道“消费”Buffer的过程中,有可能会使得position达到limit,不过Buffer类有一个判断方法hasRemaining(),该方法会告诉程序position是否达到了limit,因为position一旦超越了limit过后会抛出BufferOverflowException异常,所以最好在迭代读取Buffer里面的内容的时候进行判断,同时Buffer类还提供了一个remaining()方法返回目前的limit的值。
  *:Buffer并不是线程安全的,如果需要多线程操作一个Buffer,需要自己定义同步来操作Buffer,提供一个相关例子:
  ——[$]Fill和Drain两个方法代码例子——
packageorg.susan.java.io;

importjava.nio.CharBuffer;

public classBufferFillDrain {
private static intindex = 0;
private staticString[] strings = {
"A random string value",
"The product of an infinite number of monkeys",
"Hey hey we're the Monkees",
"Opening act for the Monkees: Jimi Hendrix",
"'Scuse me while I kiss this fly'",
"Help Me! Help Me!"
};
private static voiddrainBuffer(CharBuffer buffer){
while(buffer.hasRemaining()){
System.out.print(buffer.get());
}
System.out.println();
}
private static booleanfillBuffer(CharBuffer buffer){
if( index >= strings.length)return false;
Stringstring = strings[index++];
for(inti = 0; i < string.length(); i++ )
buffer.put(string.charAt(i));
return true;
}
public static voidmain(String args[])throwsException{
CharBuffer buffer = CharBuffer.allocate(100);
while(fillBuffer(buffer)){
buffer.flip();
drainBuffer(buffer);
buffer.clear();
}
}
}
  该方法的输出如下:
A random string value
The product of an infinite number of monkeys
Hey hey we're the Monkees
Opening act for the Monkees: Jimi Hendrix
'Scuse me while I kiss this fly'
Help Me! Help Me!
  【*:这段输出其实看不出来什么问题,但是NIO的效率明显胜过IO,这个是可以通过一些测试的例子来证明的。】
  当一个Buffer进行了Fill和Drain操作过后,如果需要重新使用该Buffer,就可以使用reset()方法,这里reset()就是清空数据并且重置该Buffer,对比上边的Flip()操作的“重置position”就很容易理解Buffer的使用过程了。这里列举了很多方法,防止混淆摘录Java API里面的Buffer抽象类的所有方法列表,这是抽象类Buffer里面的所有方法列表,上边介绍的方法若没有在此出现则就应该在它的子类中:
public abstractObject array():返回底层缓冲区的实现数组
public abstract intarrayOffset():返回该缓冲区底层实现数组的偏移量
public intcapacity():返回该缓冲区的容量
publicBuffer clear():清除该缓冲区
publicBuffer flip():反转此缓冲区
public abstract booleanhasArray():判断该缓冲区是否有可访问的底层实现数组
public abstract booleanhasRemaining():判断该缓冲区在当前位置和限制之间是否有元素
public abstract booleanisDirect():判断该缓冲区是否为直接缓冲区
public abstract booleanisReadOnly():判断该缓冲区是否为只读缓冲区
public intlimit():返回该缓冲区的限制
publicBuffer limit(intnewLimit):设置此缓冲区的限制
publicBuffer mark():在此缓冲区的位置设置标记
public intposition():返回此缓冲区的位置
publicBuffer position(intnewPosition):设置此缓冲区的位置
public intremaining():返回当前位置与限制之间的元素数
publicBuffer reset():将此缓冲区的位置重置为以前标记的位置
publicBuffer rewind():重绕此缓冲区
  [5]Buffer的压缩(Compacting):
  很多时候,应用程序有可能只需要从缓冲区中读取某一部分内容而不需要读取所有,而有时候又需要从原来的位置重新填充,为了能够进行这样的操作,那些不需要读取的数据要从缓冲区中清除掉,这样才能使得第一个读取到的元素的索引变成0,这种情况下就需要使用compact()操作来完成,这个方法从上边Buffer类的列表中可以知道并不包含该方法,但是每个Buffer子类实现里面都包含了这个方法,使用该方法进行所需要的读取比使用get()更加高效,但是这种情况只在于读取部分缓冲区内的内容。这里分析一个简单的例子:
  当上边这样的情况使用了buffer.compact()操作后,情况会演变成下边这种样子:
  【*:仔细分析上边的内容,究竟发生了什么事情呢?上边这一段将可以读取到的“llow”拷贝到了索引0-3的位置,而4和5成为了不可读的部分,但是继续移动position仍然可以读取到但是它们这些元素已经“死亡”了,当调用put()方法的时候,它们就会被覆盖掉,而且limit设置到了容量位置,则该Buffer就可以重新被完全填充。当Buffer调用了compact方法过后将会放弃已经消费过的元素,而且使得该Buffer可以重新填充,这种方式类似一个先进先出的队列(FIFO),可以这样理解,compact()方法将position和limit之间的数据复制到开始位置,从而为后续的put()/read()让出空间,position的值设置为要赋值的数组的长度,limit值为容量,这里摘录一段网上讲解的compact方法的使用场景:如果有一个缓冲区需要写数据,write()方法的非阻塞调用只会写出其能够发送的数据而不会阻塞等待所有的数据都发送完成,因此write()方法不一定会将缓冲区中所有的元素都发出去,又假设现在需要调用read()方法,在缓冲区中没有发送的数据后面读入新数据,处理方法就是设置position = limit 和 limit = capacity,当然在读入新数据后,再次调用write()方法前还需要将这些值还原,这样做就会使得缓冲区最终耗尽,这就是该方法需要解决的主要问题。
  [6]Buffer的标记(Marking):
  标记方法mark()使得该Buffer能够记住某个位置并且让position在返回的时候不用返回初始索引0而直接返回标记处进行操作,若mark没有定义,调用reset()方法将会抛出InvalidMarkException的异常,需要注意的是不要混淆reset()方法和clear()方法,clear()方法单纯清空该Buffer里面的元素,而reset()方法在清空基础上还会重新设置一个Buffer的四个对应的属性,其实Marking很好理解,提供一段代码和对应的图示:
buffer.position(2).mark().position(4);
当上边的buffer调用了方法reset过后:
  如上边所讲,position最终回到了mark处而不是索引为0的位置
  [7]Buffer的比较(Comparing):
  在很多场景,有必要针对两个缓冲区进行比较操作,所有的Buffer都提供了equals()方法用来比较两个Buffer,而且提供了compareTo()方法进行比较。既然是两个Buffer进行比较,它们的比较条件为:
  • 两个对象应该是同类型的,Buffer包含了不同的数据类型就绝对不可能相等
  • 两个Buffer对象position到limit之间的元素数量(remaining返回值)相同,两个Buffer的容量可以不一样,而且两个Buffer的索引位置也可以不一样,但是Buffer的remaining(从position到limit)方法返回值必须是相同的
  • 从remaining段的出示位置到结束位置里面的每一个元素都必须相同
  两个相同Buffer图示为(equals()返回true):
  两个不相同的Buffer图示为(equals()返回为false):
  最后针对Buffer简单总结一下,ByteBuffer里面的Big-Endian和Little-Endian已经在《Java内存模型》章节介绍了这里不重复。
  iv.NIO详解【2】——通道(Channels):
  Java NIO的API引入了一种称为通道的新型原始输入/输出提取方法,通道表示到实体(如硬件设备、文件、网络套接字或者可以执行一个或多个独特的诸如读或写之类输入/输出操作的程序组件)的开放连接。正如在java.nio.channels.Channel接口中指定的,通道可以处于打开或关闭状态,并且它们既是可异步关闭的,又是可中断的。操作的一个线程可以被阻止,而另一个线程能够关闭通道,这是通道的一个突出特点,当通道关闭时,被阻止的线程用一个异常激活,表明通道被关闭。有几个接口对通道接口进行扩张,每个接口指定一个新的输入/输出操作,一下是这些接口相关信息:
  • ReadableByteChannel接口指定一个将字节从通道读入缓冲区的read()方法
  • WritableByteChannel接口指定一个将字节从通道写入缓冲区的write()方法
  • ScatteringByteChannelGatheringByteChannel接口分别扩展ReadableByteChannelWritableByteChannel接口,采用缓冲区序列而非一个单独缓冲区增加read()和write()方法
  FileChannel类支持从连接到文件的通道读取字节或向其写入字节,以及查询和修改当前的文件位置及将文件调整为指定大小等常见操作。它定义了在整个文件或具体文件区域上获取锁定的方法;这些方法返回FileLock类的实例。最后,它定义了对要写入到存储设备的文件进行强行更新的方法、在文件和其他通道之间高效传输字节的方法,以及将文件区域直接映射到内存中的方法。你可以配置通道进行阻塞非阻塞操作。在阻塞方式下调用读、写或其他操作时,直到操作完成才能返回。例如,在缓慢的套接字上进行大型写操作可能要用很长时间。在非阻塞模式下,在缓慢的套接字上写入大型缓冲器只是排列数据(可能在一个操作系统缓冲器,或在网卡上的缓冲器中)并立即返回。线程可能转向其他任务,而由操作系统的输入/输出管理器完成工作。从文件通道中移出,可将我们带到读出及写入套接字的连接通道。你还能够以阻塞或非阻塞方式应用这些通道。在阻塞方式下,它们视客户或服务器而替代连接或接受调用。在非阻塞方式下,就没有对应项。
  通道和操作系统的底层I/O Service直接连通,而通过Channel可以连接到操作系统里面的实体,比如下图中的文件系统和网络设备:
  通道是java.nio的核心组件,这里先整体透视一下它的所有Channel相关接口的结构:
[I]java.nio.channels.Channel(1.4)
|—[I]java.nio.channels.InterruptibleChannel(1.4)
|—[I]java.nio.channels.ReadableByteChannel(1.4)
  |—[I]java.nio.channels.ByteChannel(1.4)
  |—[I]java.nio.channels.ScatteringByteChannel(1.4)
|—[I]java.nio.channels.WritableByteChannel(1.4)
  |—[I]java.nio.channels.GatheringByteChannel(1.4)
  |—[I]java.nio.channels.ByteChannel(1.4) 
  【*:ByteChannel是多继承通道接口,它同时继承了两个接口】
  Channel接口:
  该通道为所有通道接口的父接口,用于IO操作的连接用,通道表示到实体、如硬件设备、文件、网络套接字或可以执行一个或多个不同IO操作的程序组件开放的连接。通道可处于打开或关闭状态,创建通道时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个通道,视图调用IO操作的时候就会导致ClosedChannelException抛出;
  InterruptibleChannel接口:
  可被异步关闭和中断的通道,实现此接口的通道是可异步关闭的;如果某个线程阻塞于可中断通道上的IO操作,则另一个线程可调用该通道的close()方法,这将导致已阻塞线程接收到AsynchronousCloseException;实现此接口的通道也是可中断的:如果某个线程阻塞于可中断通道上的IO操作中,则另一个线程可调用该阻塞线程的interrupt方法,这将导致该通道被关闭,已阻塞线程接收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的IO操作,则该通道将关闭并且该线程立即接收到ClosedByInterruptException,并仍然设置其中断状态;当且仅当某个通道实现此接口时,该通道才支持异步关闭和中断。如有必要,可在运行时通过instanceof操作符进行测试。
  ReadableByteChannel接口:
  在任意给定时刻,一个可读取通道上只能进行一个读取操作。如果某个线程在通道上发起读取操作,那么在第一个操作完成之前,将阻塞其他所有试图发起另一个读取操作的线程。其他种类的 I/O 操作是否继续与读取操作并发执行取决于该通道的类型。
  WritableByteChannel接口:
  在任意给定时刻,一个可写入通道上只能进行一个写入操作。如果某个线程在通道上发起写入操作,那么在第一个操作完成之前,将阻塞其他所有试图发起另一个写入操作的线程。其他种类的 I/O 操作是否继续与写入操作并发执行则取决于该通道的类型。
  ScatteringByteChannel接口:
  分散读取操作可在单个调用中将一个字节序列读入一个或多个给定的缓冲区序列。分散读取通常在实现网络协议或文件格式时很有用,例如将数据分组放入段中(这些段由一个或多个长度固定的头,后跟长度可变的正文组成)。在 GatheringByteChannel 接口中定义了类似的集中写入操作。
  GatheringByteChannel接口:
  集中写入操作可在单个调用中写入来自一个或多个给定缓冲区序列的字节序列。集中写入通常在实现网络协议或文件格式时很有用,例如将数据分组放入段中(这些段由一个或多个长度固定的头,后跟长度可变的正文组成)。在 ScatteringByteChannel 接口中定义了类似的分散读取操作。
  ByteChannel接口:
  可读取和写入字节的信道。此接口只是统一了 ReadableByteChannel 和 WritableByteChannel;它没有指定任何新操作
  [1]Scatter/Gather分散/集中通道
  Java NIO读写通道系统里面提供了一个比较高效的文件读写模型,这种模型称为:Apple-s
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics