Work Better Than Yesterday!
这个是学习Java编程思想上面的Java nio笔记,这个应该有点像异步IO吧,以后再学习。这里总结了一下学习的笔记。
学过操作系统以后,知道文件系统的文件,其实就是字节集合。IO系统就是input,output系统,对文件系统进行输入和输出的操作。当然,泛化以后也是指网络流的。对IO的操作,可以进行很多很多的封装,例如:按字符,按行,随机存储,二进制,缓冲等等,于是jdk的类库就给我们提供了许许多多的便利。关于java io系统的整体关系图,google一下,有很多了,这里就不粘贴了。
这个类应该是我们最常用到的了,即可以代替一个目录,也可代替一个文件;所以既可以判断一个文件是否存在,文件长度,又可以获取目录的子文件。
最原始的操作方式,也是最灵活的了。顾名思义,就是对字节进行操作。两个顶级接口分别是InputStream
和OutputStream
。直接在IDE是看这两个接口有哪些子类和实现类即可。比较常用的就是FileInputStream
,GZipInputStream
,ByteArrayInputStream
和ObjectInputStream
了,同样的Output
接口分别对应于outputstream
的。
这是以字符的形式进行操作的,很方便的对文本进行操作,不过要注意编码方式。两个顶级的接口是Reader
和Writer
。同样查看IDE就可以查看实现类和子类。比较常用的是BufferedReader
, InputStreamReader
, FileReader
,同样的Writer
接口对应于writer。
这是独立的一个类,它适用于大小已知的文件,因为它可以随意seek到一个位置,然后读取和写入文件,不过在初始化的时候就要指定wr权限。这点和C一样的。
上面的笔记都是很简单的,也是因为比较简单,重点是学习下面的nio。代码在Bitbucket上JavaTextBox项目的io包下。
nio就是new io
的意思,是java1.4引入的。nio的引入是为了提高速度,实际上,旧的IO包已经使用nio重新实现过了,因此,即使我们不显式地用nio编写代码,也能从中受益。所谓新的IO系统,其实就是采用了更接近于操作系统执行IO的方式:通道(Channel)和缓冲器(Buffer)。可以这样子理解,文件就是一个矿藏,我们要挖采它就需要开取一条通道,就像火车道一样,最后用一个卡车来运输,卡车就是缓冲器。通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。我们只需要对缓冲器进行操作即可,就像在使用c来操作文件一样。
首先,必须打开文件流,才能够获取到通道。并没有使用新的接口来产生通道,而是使用了已有的字节操作流来获取通道,分别是FileInputStream、FileOuputStream和RandomAccessFile,这三个类是操作字节的,性质上是一样的,所以并没有矛盾。
虽然FileChannel由这三个字节操作流获取,并不代表通道是由这三个字节操作流实现,它们是分别独立的实现。注意到,不管是否用RandomAccessFile获取通道,都是可以调用fc.position()来移动位置的,但是RandomAccessFile拿到的通道是既能读又能写。
对于nio,我们最主要的操作是在缓冲器这里,所以肯定不止一种缓存器,但是最基本的就是ByteBuffer了。nio的目标是快速移动大量数据,所以缓冲器的大小设置很重要,这个需要实际运行的系统决定。我们可以通过下面三种方法获得一个缓冲器:
allocateDirect比allocate的速度更快,因为与操作系统高度耦合了,但是开支也会很大,而且是根据不同的操作系统而不同。wrap是直接把一个数组包装成为一个缓冲器,然后这个数组其实就是存储器了。
缓冲器就是一个容器,所以对它的操作无非就是get和put,而这个是最基层的缓冲器,即是字节为单位的,所以,put和get的都是字节或者字节数组:
实际上,这两个方法是有很多重载的,可以操作各种类型的字节和buffer。
卡车装满了以后,就要送进通道,系统就会去写入文件;或者,准备好了卡车,送进通道,系统就会从文件中把内容写入缓冲器。调用的接口是FileChannel的read和write,如下:
注意到的是,如果read返回的是-1便结束了,这无疑是unix和c的分界符,如果我们在unix下用c写过代码的话。
如果要把一个文件直接写入到另外一个文件,可以直接使用transfer的接口:
上面虽然知道了基本的使用,但是,实际上缓冲器的细节还是有点复杂的,不了解的话,是不能好好的使用的。实际上,缓冲器就是一个数组,但是它有四个关键索引:position
位置,limit
界限,capacity
容量,mark
标记。
position
是当前移动的指针的位置,每次调用get,或者put方法的时候,指针都会往后移动,至于移动多少个字节要根据get和put操作的字节数。
capacity
是这个缓冲器的容量,即大小,能存多少数据。
limit
是对缓冲器进行操作的时候的限制。如果对缓冲器进行写的时候,我们先调用clear()来清空数据,这时候limit被设置为capacity。如果对缓冲器进行读的时候,我们先调用flip()来准备读取,这时候limit被设置为position,即限制指向了写入时的最后一个数据位置,然后再把position设置为0。
mark
是一个辅助的功能,用于标记位置。调用mark()的时候会把mark设置为当前的position,当我们调用reset()的时候就会把position设置为mark。
详细的api如下:
学过操作系统和网络,都知道字节存放顺序的,一般就是大端和小端。大端(big end)就是高位优先,将数的高位存放在低位地址的存储器上;小端(little end)是地位优先,将地位数存放低位地址的存储器上。注意的是,左边的是低位地址的存储器,右边是高位地址的存储器,刚好和我们想的相反了。例如:
00000000 01100001
,这是两个字节的数,从左往右数,分别是低位-高位存储器。如果用大端方式读取,就是97
;如果用小端方式读取,就是24832
了,即实际的二进制是01100001 00000000
。
对ByteBuffer设置字节顺序的接口是:
注意到的是一个存储器一般都是存储一个字节的,所以,这个字节顺序基本没有影响,但是如果是需要多个字节存储的数字的时候就有影响了。
上面用到的都是最基本的字节缓冲器,是比较底层的一些操作,而实际上,java在上层封装了很多基本数据类型的视窗来查看ByteBuffer的。对int,float,long,double,char,short
分别有对应的缓冲器。只要在获取得到ByteBuffer以后调用asXXX的方法就能获取得到相应的视图缓冲器了,或者调用视图缓冲器的wrap方法,如下图所示:
需要注意两个问题,一是ShortBuffer调用put方法的时候,可能需要考虑到转型。二是编码的问题,如果是用ByteBuffer写的数据,然后用CharBuffer读取,则会出现乱码,需要用Charset
来decode,或者直接用CharBuffer来写再读就不会乱码,如下例子所示:
以上的nio基本够用,性能也有所提高,但是,映射文件文件访问是会更加显著的提高速度。内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。MappedByteBuffer是继承自ByteBuffer,获取的方法是调用channel的map(),如下:
MappedByteBuffer几乎具有所有视图缓冲器的方法,所以操作极其方便,最大的区别是,对MappedByteBuffer操作完了以后,是不需要通过filechannel来read和write的,因为是filechannel映射过来的。但是,映射文件中的所有输出必须使用RandomAccessFile。
学过操作系统,线程互斥的知识,就知道多线程的时候锁问题了。在这里,实际上是调用FileChannel来对文件加锁的。SocketChannel
,DatagramChannel
, ServerSocketChannel
不需要加锁,因为他们是从单进程实体继承而来的,我们通常不在两个进程之间共享网络socket。如果,竞争同一文件的线程不在同一个java虚拟机上,或者,一个是java的线程,一个是操作系统的本地线程,这种问题,能否锁住?肯定是可以的,这里的文件锁和java线程那边的锁不一样,这个文件锁对其他的系统进程是可见的,java的文件加锁是直接映射到操作系统的加锁工具的。
tryLock()方法是非阻塞的,所以有可能获取不到锁,因为被别的线程锁了;而lock()方法是阻塞的,直接等待获取到锁为止。退出临界区要调用release()方法来释放锁。锁可以是独占的,也可以是共享的,这个要根据操作系统是否支持,可以调用FileLock.isShared()进行查询。
也可以对文件进行部分的锁,调用的方法如下: