IO和NIO

本文详细介绍了Java的IO流体系,包括字节流、字符流、节点流与处理流,强调了缓冲流的作用和使用。讨论了对象序列化机制,包括Serializable接口、transient关键字以及自定义序列化方法。同时,深入探讨了Java NIO(New IO)的特点,如通道(Channel)和缓冲区(Buffer),以及字符集和文件锁的概念。最后提到了Files工具类在文件操作中的便利性。
摘要由CSDN通过智能技术生成

Java的IO是通过java.io包下的类和接口来支持。
File类:可以代表与平台无关的文件或目录。文件和目录都可以使用File来操作,File不能访问文件内容本身,访问内容本身需要使用输入/输出流。
File类使用文件路径字符串来创建File实例,可以是绝对路径也可以是相对路径,默认情况下,系统总是一句用户的工作路径来解释相对路径,该路径由系统属性“user.dir”执行,通常也就是运行Java虚拟机所在的路径。该文件类提供了非常多的方法:P680-681页
文件过滤器:File类的list方法可以接受一个FilenameFilter参数,该参数可以对文件过滤,只列出符合条件的文件。
IO流:
Input、output各种物理节点内的数据。
流的分类,按方向来分:
A、输入流:用于读取数据。主要由InputStream和Reader作为基类
B、输出流:用于写出数据。主要由OutputStream和Writer作为基类
从运行程序所在的内存来看,如果数据是从程序自身流出,用输出流,如果数据是流入程序自身,用输入留流。

按数据单元来分来分:
A、字节流:每次读、写一个字节(Byte),Byte就是计算机分配数据的最小单元,因此, 字节流可以读、写任意的文件。InputSream 和OutputStream作为基类
B、字符流:每次读、写一个字符。可以非常方便地读、写文本文件。Reader和Writer作为 基类。
以Stream结尾的都是字节流:InputStream、OutputStream、
以Reader、Write结尾的都是字符流:Reader、Write 属于抽象基类
文件读取要加File,如InputSream ——> FileInputSream Reader——>FileReader
输入流的最基本方法:
read():每次读取一个字节(字符)
Int Read(xxx[] buff):读取N个字节(字符)存储到数组中,返回实际读取的字节(字符)数,读取完毕,若已经没有数据,返回-1。
Read(xxx[]cbuf,int off,int len ):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组中时,从off位置开始,返回实际的读取数。
如果使用较小的数组去读取文件时,在输出时有可能造成乱码,原因是文件存储采用的是GBK编码方式,每个字符占2个字节,如果read方法只读取了半个字符,就会导致乱码。

输出流的最基本方法:
Write(int/byte):输出一个字节/字节(往创建的对象(文件等))
Void write(xxx[] buff):输出数组中所有字节/字符(往创建的对象输出(文件等))
Void wirte(xxx[] cbuf,int off,int len):输出数组的中间一段(往创建的对象输出(文件等))
字符流可以直接输出字符串,字符串底层相当于封装了一个字符数组。Writer多了如下方法:
Void write(String str):将str字符串里包含的字符输出到指定输出流中
Void writer(String str,int off,int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中,输出字符串时,windows平台使用\r\n换行。Linux使用\n换行
流的分类,按角色来分:
节点流:直接关联代表数据源的IO节点,
处理流(过滤流、包装流):建立在其他流的基础之上,用于对其他流进行包装。
使用节点流,程序时直接连接到实际的数据源的,处理流用于对一个已经存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流并不会直接连接到实际的数据源, 处理流可以采用完全相同的输入/输出代码来访问不同的数据源,底层通过及诶单流实际访问数据源,通过包装不同的节点流,可以消除不同节点流的实现差异,提供更方便的方法来完成输入/输出流。

流的概念模型:
InputStream/Reader:所有输入流的基类,处理输入流时,相当于把输入设备抽象成一个“水管”,这个水管里的每个(水滴)依次排列,可以从水管中取出水滴(读取数据),每取出一个或多个水滴后,隐式的记录指针自动向后移动,InputStream/Reader都提供了一些方法来控制记录指针的移动。

OutputStream/Reader:所有输出流的基本,处理输出流时,相当于把输入设备抽象成一跟空的“水管”,可以向水管放入“水滴”(写出数据),写入一个或多个水滴水滴后,隐式的记录指针自动向后移动。

处理流主要功能:
1、提高性能:处理流中的缓冲流增加缓冲的方式来提高输入/输出的效率
2、操作便捷:不管是什么输出设备,程序以相同的操作代码去输入/输出数据。
使用完IO流后,必须要关闭IO流,关闭流可以保证流的物理资源被回收,还可以将输出流缓冲区中的 数据flush到物理节点里(在执行close方法之前,自动执行输出流的flush()方法)
处理流和节点流的判断:若构造器参数是一个物理节点,那么这种流是节点流,如果构造器参数是已经存在的流(包括节点流和处理流),那么这种流就一定是处理流。
使用处理流后,关闭输入/输出资源时,只要关闭最上层的处理流即可,关闭最上层的处理流时,系统会自动关闭被处理流包装的节点流。

访问 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类InputStream OutputSream Reader Writer
文件 FileInputStream FileOutputSream FileReader FileWriter
数组 ByteArrayStream ByteArrayOutputStream CharArrayReader CharArrayWriter
管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
字符串 StringReader StringWriter
----------------------------------------------------以上是节点流----------------------------------------------------
抽象基类FilterInputStream FilterOutputSream FilterReader FilterWriter 过滤流
缓冲流 BufferedInputStream BufferedOutputSream BufferedReader BufferedWriter
打印流 PrintStream PrintWrite
转换流 InputStreamReader OutputStreamWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream
对象流 ObjectInputStream ObjectOutputStream

转换流:将自接力转换成字符流,使操作更加方便。
如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出,BufferedReader具有readLine()方法,很方便读取一行内容,所以通常把读取文本内容的输入流包装成BufferedReader。
缓冲流:
内存速度 > 外设(磁盘、网络)速度
传统做法:内存输出一个数据单元,外设处理一个单元,必须等外设处理好数据单元之后,内存才会继续输出下一个单元——造成程序性能浪费
现今,加一个缓冲,内存先把所有数据输出缓冲——缓冲的数据留给外设慢慢处理,接下来内存就可以继续处理下一个程序。
当使用缓存流时,务必使用flush()方法将数据“缓冲”到底层物理设备中——尤其在程序突然退出时,可能有数据还未输出到物理设备。
如果正常调用close()关闭流,程序会保证先执行flush()——有close就可以。
尤其是BufferedReader:可以每次读取一行,

打印流:用起来特别爽!
总结:一般输入流用BufferedReader比较合适,输出流就用PrintStream比较合适。

转换流:负责将字节流转换成字符流。很多情况下,程序拿到的只是字节流,但该流通数据只能是字符,此时就可以把字节流转换成字符流。传入一个字节流、返回一个字符流。
字符流与字节流区别:字节流功能更强大,可以读写所有文件,字符流读写文件更加方便。

推回输入流:PushbackInputStream PushbackReader
推回输入流都带有一个推回缓冲区,推回输入流有三个unread()方法,当程序调用该方法时,系统将会把指定数组的内容推回到该缓冲区,而推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有读取了缓冲区的内容后,若还没装满read()所需的数组时,才会从原输入流中读取。默认推回缓冲区的长度为1,若程序推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发:Pushback buffer overflow的IOException异常。
特殊流:
DataInputStream提供了系列readXxx()专门用于读取不同类型的数据。
DataOutputStream提供了系列writeXxx(Xxx 数据)专门用于写入不同类型的数据。
DataInputStream与DataOutputStream是用java的内部格式来记录数据的。
因此DataOutputStream输出的数据,应该使用DataInputStream来读取。

重定向标准输入、输出
System.out标准输出:默认输出到电脑屏幕
System.setOut方法换掉标准输出,将输出重定向到另外一个输出流
System.in标准输入:默认代表电脑的键盘
System.setIn方法换掉标准输入,将输入重定向到另外一个输入流

Java虚拟机读写其他进程的数据:
Runtime有个exec(“程序的绝对路径”)方法可用于运行平台上的程序。程序运行起来就变成了进程,因此该方法就返回运行的进程。即一个Process对象,Processor对象代表由Java程序启动的子进程。Process对象提供了如下方法:
InputStream getErrorStream():返回子进程错误输入流——程序向进程读数据
InputStream getInputStream():返回子进程的普通输入流——程序向进程读数据
OutputStream getOutputStream():返回子进程的普通输出流——程序向进程写数据

RandomAccessFile:是Java输入/输出流体系中功能最丰富的文件内容访问类。既可以读取文件内容,也可以向文件输出数据,而且RandomAccessFile还支持“随机访问”,程序可以直接跳转到文件的任意地方来读写数据。因此,如果只需要访问文件部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。
普通输出流从文件开始的地方开始输出,每次重新输出都会将文件的所有内容覆盖,若需要给文件追加内容,则应该使用RandomAccessFile。RandomAccessFile只能读写文件,不能读写其他的IO流。
RandomAccessFile对象包含了一个记录指针标识当前读写的位置,创建RandomAccessFile对象时,记录指针位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节,RandomAccessFile也提供了如下方法操作记录指针:
Long getFilePointer():返回文件记录指针的当前位置
Void seek(long pos):将文件记录指针定位到pos位置
将指针移动到末尾位置,传入RandomAccessFile对象的length()方法获取文件长度。
RandomAccessFile包含了InputStream的三个read方法和OutputStream的三个write方法,用法也一致,此外,RandomAccessFile还包含了一系列不同的readXxx()和writeXxx()方法来完成输入输出。RandomAccessFile包含如下两个构造器
RandomAccessFile​(File file, String mode)
RandomAccessFile​(String name, String mode)
两个构造器基本相同,一个通过file对象,一个通过文件字符创路径创建对象,但需要多指定一个参数mode,mode参数指定了RandomAccessFile的访问模式,有一下4个值:
“r”:以只读方式打开指定文件,不允许写入,写入将抛出IOException异常
“rw”:以读、写方式打开指定文件,如果该文件不存在,则尝试创建该文件。
“rws”:以读、写方式打开指定文件,相对于”rw”模式,还要求对文件的内容或元数据的每 个更新都同步写入到底层存储设备。
“rwd”:以读、写方式打开指定文件,相对于”rw”模式,还要求对文件内容或元每个更新都 同步写入到底层存储设备。
RandomAccessFile依然不能向文件指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有内容,如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区(即创建一个临时文件保存),等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面。
实现步骤:
1、先将记录指针移动到文件需要插入的位置
2、通过File.createTempFile创建一个临时文件,并使用临时文件对象方法deleteOnExit设置 为虚拟机退出时关闭
3、将文件记录指针位置后面的所有内容都读取出来并写入临时文件中
4、将记录指针重新移回需要插入的位置,插入需要插入文件的内容
5、插入完毕后,从临时文件读取内容,将内容追加到源文件后面。
扩展:
RandomAccessFile通常用于实现多线程、断点下载工具。

对象序列化:
内存中 → 可以通过磁盘永久保存
Java对象 → 可以通过网络传给其他程序
序列化:把内存中的java对象转换成与平台无关的二进制流(字节文件(二进制文件)),该二进制文件内容既可保存到磁盘,也可通过网络传输。以备以后重新恢复成对象,序列化机制使得对象可以脱离程序的运行而独立存在。
反序列化:读取序列化的二进制数据,将数据恢复成为原始的java对象。
广义来说,Java的序列化还包括:
XML序列化:扩平台、跨语言的。把内存中的Java对象转换为XML文件内容,XML文件内容即可保存到磁盘,也可以通过网络传输。
JSON序列化:扩平台、跨语言的。把内存中的java对象转换为JSOM文件内容,JSON内容页既可保存到磁盘,也可通过网络传输。
序列化:是现代应用(分布式、多应用整合)的最基础的核心技术。
对象序列化:
Serializable:标志接口(实现该接口无需实现任何方法)。实现该接口的对象即可被序列化。
与前面所学的类区别在于:额外实现了一个Serializable接口。
通过ObjectOutputStream将该对象写入磁盘或写入网络;
ObjectInputStream从磁盘或网络读取对象。
让对象可序列化,必须让对象实现Serializable或Externalizable两个接口之一,Serializable只是一个标志接口,实现该接口无需实现任何方法,它表明该类的实例是可序列化的。
对象可序列化:必须满足如下要求:
1、该类实现了Serializable接口。
2、该类的所有参与序列化的成员变量也必须是可序列化的(即定义成员变量的类型也要实现Serializable,如String、数据类型等),如果定义的成员变量是对象,如private Student student;则是否能序列化,需要判断Student这个类本身有没有实现Serializable接口。
即使类实现了Serializable接口,如果有定义没有实现Serializable类型的成员变量,则这个类也不能序列化。
所有网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如保存到HttpSession或ServletContext属性的Java对象,通常建议:每个JavaBean类都实现Serializable
一旦某类实现了Serializable接口,就可以通过如下步骤序列化对象到IO节点:
ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream(fileName));
Oos.writeObject(对象);
序列化到IO节点后,可以通过反序列化恢复Java对象,如下步骤:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
类名 对象名 = (类名)ois.readObject();

如果使用序列化机制向文件中写入多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取,当一个可序列化类有多个父类时(包括直接和间接父类),这些父类要么有无参的构造器,要么也是可序列化的——否则反序列化时抛出InvalidClassExcption异常。如果父类是不可序列化的,只带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。

对象引用的序列化:
当程序序列化一个对象时,如果该对象持有一个引用类型成员变量,为了在反序列化可以正常恢复该对象,程序会顺带将引用类型变量对象也进行序列化,所以序列化对象中的引用类型成员变量对应的类也必须是可序列化的。
当程序多次writeObject同一个对象时,实际上并未输出多个对象,程序多次调用readObject时恢复的全部是同一个对象。
对象引用的序列化原理:当程序第一次序列化某个对象时(包括用writeObject输出、或关联序列化时),系统才会真正将该对象的数据输出到磁盘或者网络,并为该序列化对象分配一个版本化编号,以后如果再次序列化该对象,并不会真正输出该对象数据,而是输出版本化编号。

序列化流程:
当程序视图序列化一个对象时,程序先检查该对象是否已经被序列化过,若该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转成字节序列并输出,并生成一个序列化版本号,如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
注意:当使用Java序列化机制序列化可变对象时,同样只有第一次调用writerObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream中,后面即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的对象也不会被输出,依然是输出一个序列化版本号(即修改后的实例变量不会序列化进去)

注意:当序列化对象时,只有第一次序列化时才会真正把该对象的数据输出到磁盘或者网络。此后,即使再次修改了该对象的状态时,再次序列化该对象时,你的修改对再次序列化没有任何影响。

递归序列化:当对某个对象序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化,如果被引用的u相等实例变量也引用了其他对象,则被引用的对象也会被序列化,这就是递归序列化。

transient关键字:
有些时候、程序希望将某些field排除在序列化机制之外,该消息是可能泄露的或是无法序列化的。
可用transient修饰它。Transient只能用于修饰实例变量,不可修饰Java程序中的其他部分。
可见:如用户的账号、密码等敏感信息,不要序列化,否则会有安全隐患。
Java序列化时只序列化该对象的状态信息,不会序列化以下成员变量和方法:
1、transient修饰的实例变量
2、static类变量。
3、方法不会序列化
所以不要用transient修饰static变量。

自定义序列化:
对于一些有点敏感,但又不是极度敏感的信息,希望可以保存在序列化机制之内,但又不是直接保存,——可以在序列化之后对某些field进行加密。
通过实现如下方法(从Serializable接口的API文档copy),可完成自定义序列化:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
Private void readObjectNoData() throws ObjectStreamException
writeObject方法负责写入特定类的实例状态,以便相应的readObject()方法可以恢复它,通过重写该方法,完全可以获得对序列化机制的控制,自主决定进行实例变量需要序列化。相反的readObject()负责从流中读取并恢复对象实例,重写该方法,获得对反序列化的控制,自主决定如何反序列化实例变量,通常情况下,两个方法是对应的,writeObject对实例变量进行了处理,则readObject方法对实例变量进行相应的反处理,以便正确恢复。默认的情况下,这两个方法时直接使用out.writeObject方法和in.readObject方法操作实例变量。
当序列化流不完整时,readObjectNoData方法可以用来正确地初始化反序列化的对象。如:接收方使用的反序列化类的版本不用于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统调用readObjectNoData来初始化反序列化对象
实现类似逻辑:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
//将name实例变量值反转后写入二进制流
Out.writeObject(new StringBuffer(name).reverse());
Out.writeObject(age)
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
this.name = ((StringBuffer)in.readObject()).reverse().toString();
This.age = age
}
以上代码先将名字转换成可变字符串进行倒转后再写入,取出时也响应的倒转再转换成不可变的字符串。

除此之外,还有一种更彻底的自定义机制,甚至可以在序列化时将该对象替换成其他对象,如下特殊方法:
ANY-ACCESSS-MODIFIER Object writerReplace() throws ObjectStreamException;
序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回一个对象,则系统转为序列化另一个对象,调用另一个对象的writeReplace方法,一次类推,直到该方法不再返回一个对象为止,随后再调用writeObject方法保存该返回对象的状态。

安全性角度:transient更安全,Transient修饰的field被彻底排除在序列化之外。
方便角度:自定义序列化更方便,反序列化时还能读取到状态。

另一种自定义序列化机制:
Java类实现Externalizable接口,并实现如下两个方法
Void readExternal(ObjectInput in):需要序列化的类实现该方法来实现反序列化。
Void writerExternal(ObjectOutput out):需要序列化的类实现该方法来实现序列化。
两个方法的实现方式前面实现Serializable接口时自定义序列化重写的readObject和writeObject两个方法的实现基本一致。
不同的是,使用Externalizable机制反序列化对象时,程序会先使用public的无参构造器创建实例,然后才执行readExternal方法进行反序列化,因此,实现Externalizable接口的序列化类必须提供public的无参构造器。
两种序列化机制的对比:
实现Serializable接口 实现Externalizable接口
系统自动存储必要信息(也可以通过重写readObject和writeObject两个方法完成自定义序列化) 程序员自行决定存储哪些信息
Java内建支持,只需要实现该接口即可,无需任何代码支持 仅提供了两个孔方法,实现该接口必须实现该接口的两个方法。
性能略差 性能略好

序列化版本:反序列对象时必须提供该对象的class文件,Java序列化机制允许为序列化类提供一个private static final的serialVersionUID值,该类变量的值用于表示该Java类的序列化版本,即使序列化的类升级了,只要serialVersionUID类变量的值保持不变,序列化机制也会把它们当成同一个序列化版本。
当程序要反序列化时,需要2个东西才能反序列化成功:
A、序列化的数据
B、类文件(class文件)

Java序列化机制识别一个类是否发生了改变,依赖的是Java类的序列化版本号。
一个可序列化的Java类,总有一个序列化版本号。
A、如果没有显示指定序列化版本号,JVM会根据类的相关信息计算,分配一个序列化版本号
B、显式指定一个序列化版本号
private static final long serialVersionUID = 512L;
为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的类中加入这个类变量,具体数值自己定义,这样,即使在某个对象被反序列化之后,它所对应的类被修改了,该对象也依然可以被正确地反序列化。
【自动分配序列化版本号】的后果:JVM会根据类的相关信息计算序列化版本号,修改后的类的计算结果与修改前的类的计算结果往往不同,因此只要Java类源程序发生了修改,每次编译时都会重新生成一个新的序列化版本号。通过命令:serialver 类名 获取版本号
如果java对对象执行了序列化之后,如果对java源程序修改,再次编译,此时序列化版本号发生了改变,即源程序的对象的序列化版本号和对象执行序列化时的版本号不一致,此时如果反序列化恢复数据,则系统认为不是同一个序列化版本号的对象,恢复失败,会报错(InvalidClassException)。
此外,不显示指定serialVersionUID 类变量的值不利于程序在不同的JVM之间移植,不同的编译器对该类变量的计算策略可能不同,从而造成类没有改变的情况下,在不同JVM下,也会出现序列化版本不兼容而无法正确反序列化的现象。
【所以推荐】:开发者自己为类指定版本号,只有当你增加、或删除实例变量时才需要增加版本号。
指定序列化版本号,在类体第一行加入代码:
private static final long serialVersionUID = -482114349955616921L;//数字可以随意一个
当指定了序列化版本号后,java源程序中加入方法、类变量等不影响序列化的成员时,序列化版本号不会改变,因此可以反序列化恢复数据。

如果类的修改确定会导致该类反序列化失败,则应该为该类的serialVersionUID 类变量重新分配值,有一下几种情况:
1、如果修改类时仅仅修改了方法、静态变量或瞬态实例变量,则反序列化不受影响,类定义无需修改serialVersionUID 类变量的值
2、如果修改类时修改了非瞬态的实例变量,可能导致序列化版本不兼容,如果对象流中的对象和新类中包含同名的实例变量,而实例变量不同,则反序列化失败,类定义应该更新
serialVersionUID类变量的值。
3、如果对象流中的对象比心累中包含更多的实例变量,则多出的实例变量值被忽略,序列化版本依然可以兼容。如果心累比对象流中的对象包含了更多的实例变量,则序列化版本也可以建瓯让,类定义可以不更改新serialVersionUID类变量的值,但反序列化时,得到的新对象中多出的实例变量的值都是null(引用类型)或0(基本数据类型)

NIO(New IO):书本P715—730
传统的IO读取数据时,如果没有读到校友的数据,程序将会在此处组设该线程的执行,不仅如此,传统的输入流、输出流都是通过字节的移动来处理的(即使不直接去处理字节流,底层依然是依赖字节处理),因此,面向流的输入/输出系统一次只能处理一个字节,效率比较低。
新增的叫NIO,在java.nio包以及子包下,NIO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(模拟了操作系统上的虚拟内存的概念),通过这种方式进行输入/输出效率要块得多。NIO是一种面向块的处理。
Channer(通道)和Buffer(缓冲)是新IO的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输,Channel与传统的InputStream和Output最大的区别是它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。
Buffer可以理解成一个容器,本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中,此处的Buffer和io的竹筒(数组)类似,可以到Channel取数据,也可以使用Channel直接将文件的某块数据映射成Buffer。
新IO海提供了将Unicode字符串映射成字节序列以及逆映射操作的Charset类,还提供了支持非阻塞式输入/输出的Selector类。
Buffer:是一个抽象类,最常用的子类ByteBuffer,可以在底层数组上进行get/set操作。此外还有对应基本数据类型的Buffer子类:CharBuffer、IntBuffer…除ByteBuffer外,其余都采用相同的方法来管理数据,只是各自管理的数据不同而已,这些Buffer类都没有提供任何构造器,使用如下类方法创建对象:
Static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象。
实际应用主要使用ByteBuffer和CharBuffer,ByteBuffer类还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,MappedByteBuffer对象由Channel的map()方法返回。
Buffer中三个重要的概念:
1、容量(capacity):缓冲区的容量表示Buffer最多能装多少数据,不能为负,不能修改。
2、界限(limit):第一个不应该被读出或者写入的缓冲区位置索引,即位于limit后的数据既不能被读也不能被写。
3、位置(position):用于指明下一个可以被读出或者写入的缓冲区位置索引(类似于IO流中的记录指针)
4、mark:可选标记,类似于传统IO的mark,Buffer允许直接将position定位到该mark处。
以上四个满足关系: 0 <= mark <= position <= limit <= capacity

Buffer的主要作用就是装入数据、然后输出数据(作用和传统IO的竹筒(数组)类似)
操作流程:
1、开始(刚创建)时,Buffer的position为0,limit为capacity,程序可以通过put()方法向Buffer中放入数据,每放入一个数据,Buffer的position相应地向后移动一个位置,
2、当Buffer装入数据结束后,调用Buffer的flip()方法将limit设置为position所在的位置,并将position设为0,使得Buffer的读写指针又移动到了开始位置,即Buffer调用flip()方法之后,Buffer为输出数据做好了准备
3、当Buffer输出数据结束后,Buffer调用clear()方法。Clear()方法不会清空Buffer的数据,该方法仅仅将position位置移动为0,将limit设置为capacity,为再次向Buffer装入数据做好准备。

总之:flip()方法为从Buffer中取出数据做好准备,而clear()为再次向Buffer装入数据做好准备。
Buffer还包含如下方法:
Capacity():返回Buffer的容量大小
Boolean hasRemaining():判断当前位置(position)和界限(limit)之间是否还有元素可供处理
Int limit():返回Buffer的界限的位置
Mark():设置Buffer的mark位置,只能在0和位置(position)之间做mark
Int position():返回Buffer的position值
Buffer position(int newPs):设置Buffer的position,并返回修改后的Buffer对象。
Int remaining():返回当前位置和界限之间的元素个数。
Buffer reset()将位置转到mark所在的位置
Buffer rewind():将位置(position)设置为0,取消设置的mark
Buffer使用put()和get()方法时, 分为相对和绝对两种
相对:从Buffer的当前position处开始读取或写入数据,position随处理数据移动
绝对:直接根据索引读写Buffer中的数据,使用绝对方式不会影响位置(position)的值

ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer,直接Buffer的创建成本较高,需要先有ByteBuffer才能创建,但直接Buffer的读取效率更好,因此,直接Buffer适用于长生存期的Buffer.

Channel接口:类似于传统IO的流对象,但有以下两点区别:
1、Channel可以直接将指定文件的部分或全部直接映射成Buffer
2、程序不能直接访问Channel中的数据,Channel只能与Buffer进行交互,程序读写数据只能通过Buffer。

Channel接口实现类:DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel等。
Channel需要通过传统的IO节点InputStream、OutStream的getChannel()方法来返回对应的Channel,Channel主要方法:
MapperByteBuffer map(FileChannel.MapMode mode,long position,long size):用于将Channel对应的部分或全部数据映射成Buffer,返回MapperByteBuffer 对象,MapperByteBuffer 为ByteBuffer的子类,参数1指定执行映射时的模式,有只读和读写等模式,参数二和参数三用于控制Channel的哪些数据映射成ByteBuffer。
read()和write():都有一系列重载形式,用于从Buffer中读取数据或向Buffer中写入数据。

字符集和Charset:JDK提供Charset处理字节序列和字符序列之间的转换关系。
Charset.availableCharsets():返回JDK支持的所有编码格式的Map集合
创建Charset对象:Charset charset = Charset.forName(“UTF-8”);
创建编码器对象:CharsetDecoder charsetDecoder = charset .getEncoder()
创建解码器对象:CharseEncoder charsetEncoder = charset .getDecoder()
有了这些对象就可以调用CharsetDecoder 对象的decode方法将ByteBuffer(字节序列)转换成CharBuffer(字符序列),调用CharseEncoder 对象的encode方法将CharBuffer(字符序列)转换成ByteBuffer(字节序列)。

文件锁:如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制进行通信,使用文件锁可以有效组织多个进行并发修改同一个文件,大部分操作系统都提供了该功能
使用FileConnel中的lock()和trylock()方法获取文件锁FileLock对象。lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞,而trylock()是尝试锁定文件,不会阻塞,如果得到了文件锁,就返回文件锁,否则返回null。
Lock(long position,long size,boolean shared):对文件从position开始,长度为size的内容枷锁,该方法是阻塞式的。
trylock(long position,long size,boolean shared):对文件从position开始,长度为size的内容枷锁,该方法是非阻塞式的。
当参数shared为true时,表名该锁是一个共享锁,允许多个进程来读取文件,但组织其他进程获得对该文件的排他锁,当shared是false时,表名该锁是一个排他锁,锁住对该文件的读写,程序可以通过调用文件锁FileLock的isShared判断它是否为共享锁。

Path:代表一条路径(即可以是文件的路径,也可以是目录的路径)。
Paths:工具类。负责把String包装为Path
Path代表一条路径,Paths是Path的工具类。

Files工具类
Files:做文件IO的工具类。基本上文件的复制、移动、删除、读取、写入、重命名、隐藏、创建、创建快捷方式、等文件操作都只要一个方法即可。
Files还可以用于遍历文件树——File通过递归也可以实现该功能。
调用该类的walkFileTree(Path start,FileVisitor<? super Path> visitor)方法即可遍历文件树,第二个参数是一个FileVisitor对象,该对象需要重写如下4个方法:
FileVisitResult postVisitDirectory(T dir, IOException exc):访问某个目录结束后,激发该方法
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs):访问某个目录之前,激发该方法

FileVisitResult visitFile(T file, BasicFileAttributes attrs) :访问文件时激发该方法
FileVisitResult visitFileFailed(T file, IOException exc):访问文件失败时激发该方法
四个方法的返回值都是:FileVisitResult,用于控制是否继续遍历,该返回值有如下枚举值:
CONTINUE:继续访问 SKIP_SIBLING:忽略兄弟节点 SKIP_SUBTREE:忽略子树
TERMINATE:终止访问

Java IONIO 都可以用于文件读写操作,但是它们的实现方式不同,因此在性能上也略有差异。 针对文件读写操作,我们可以通过编写测试程序来对比 Java IONIO 的性能。下面是一个简单的测试程序: ```java import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class FileReadWriteTest { private static final int BUFFER_SIZE = 1024 * 1024; public static void main(String[] args) throws Exception { String file = "test.txt"; int size = 1024 * 1024 * 100; // 测试 Java IO 的文件写入性能 long start = System.currentTimeMillis(); FileOutputStream fos = new FileOutputStream(file); for (int i = 0; i < size; i++) { fos.write('a'); } fos.close(); long end = System.currentTimeMillis(); System.out.println("Java IO 文件写入耗时:" + (end - start) + "ms"); // 测试 Java IO 的文件读取性能 start = System.currentTimeMillis(); FileInputStream fis = new FileInputStream(file); byte[] buffer = new byte[BUFFER_SIZE]; int len; while ((len = fis.read(buffer)) != -1) { // do nothing } fis.close(); end = System.currentTimeMillis(); System.out.println("Java IO 文件读取耗时:" + (end - start) + "ms"); // 测试 NIO 的文件写入性能 start = System.currentTimeMillis(); FileChannel fc = new FileOutputStream(file).getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE); for (int i = 0; i < size / BUFFER_SIZE; i++) { fc.write(byteBuffer); byteBuffer.clear(); } fc.close(); end = System.currentTimeMillis(); System.out.println("NIO 文件写入耗时:" + (end - start) + "ms"); // 测试 NIO 的文件读取性能 start = System.currentTimeMillis(); fc = new FileInputStream(file).getChannel(); byteBuffer = ByteBuffer.allocate(BUFFER_SIZE); while (fc.read(byteBuffer) != -1) { byteBuffer.flip(); byteBuffer.clear(); } fc.close(); end = System.currentTimeMillis(); System.out.println("NIO 文件读取耗时:" + (end - start) + "ms"); } } ``` 该测试程序分别测试了 Java IONIO 的文件写入和文件读取性能。其中,文件大小为 100MB,缓冲区大小为 1MB。 运行该测试程序,可以得到如下结果: ``` Java IO 文件写入耗时:220ms Java IO 文件读取耗时:219ms NIO 文件写入耗时:248ms NIO 文件读取耗时:177ms ``` 可以看出,在该测试条件下,Java IONIO 的文件读取性能差异不大,但是 NIO 的文件写入性能略逊于 Java IO。不过需要注意的是,这只是一个简单的测试,实际情况下可能会因为多种因素而产生差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值