Java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入、输出两种IO流,每种输入、输出流又可分为字节流和字符流两大类,其中字节流以字节为单位来处理输入、输出操作,而字符流以字符来处理输入、输出操作。
1 File类
File类是java.io包下代表与平台无关的文件和目录,File可以新建、删除、重命名文件和目录,File不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。
1.1 访问文件和目录
- File类可以使用文件路径字符串来创建File实例,该文件路径字符串可以是绝对路径,也可以是相对路径,相对路径就是运行JVM时所在的路径
- File类访问文件名相关的方法
- String getName():返回此File对象所表示的文件名或路径名
- String getPath():返回此File对象所对应的路径名
- File getAbsoluteFile():返回此File对象的绝对路径
- String getAbsolutePath():返回此File对象所对应的绝对路径名
- String getParent():返回此File对象所对应目录的父目录名
- boolean renameTo(File newName):重命名此File对象所对应的文件或目录
- File类文件检测相关方法
- boolean exits():判断File对象所对应的文件或目录是否存在
- boolean canWrite():判断File对象所对应的文件或目录是否可写
- boolean canRead():判断File对象所对应的文件或目录是否可读
- boolean isFile():判断File对象所对应的是否是文件,而不是目录
- boolean isDirectory:判断File对象所对应的是否是目录,而不是文件
- boolean isAbsolute():判断File对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径
- File类获取常规文件信息:
- long lastModified():返回文件的最后修改时间
- long length():返回文件内容的长度
- File类文件操作相关方法:
- boolean createNewFile():当次File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件
- boolean delete():删除File对象所对应的文件或目录
- static File createTempFile(String prefix, String suffix):在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名
- static File createTempFile(String prefix, String suffix, File directory):在directory所指定的文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名
- void deleteOnExit():注册一个删除钩子,指定当java虚拟机退出时,删除File对象所对应的文件和目录
- File类目录操作相关方法:
- boolean mkdir():试图创建一个File对象所对应的目录
- String[] list():列出File对象的所有子文件名和路径名
- File[] listFiles():列出File对象的所有子文件和路径
- static File[] listRoots():列出系统所有的根路径
1.2 文件过滤器
- 在File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件
- FilenameFilter是一个函数式接口,里面只有一个抽象方法accept(File dir, String name),该方法将依次对指定File的所有子目录或者文件进行迭代
import java.io.File;
public class FilenameFilterTest {
public static void main(String[] args) {
File file = new File(".");
// 使用Lambda表达式(目标类型为FilenameFilter)实现文件过滤器。
// 如果文件名以.java结尾,或者文件对应一个路径,返回true
String[] nameList = file.list((dir, name) -> name.endsWith(".java")
|| new File(name).isDirectory());
for (String name : nameList) {
System.out.println(name);
}
}
}
2 Java IO流
流——不同的输入输出源,用来和JVM进行交互,是一组有序数据
2.1 流的分类
- 输入流和输出流——按照流的流向来分
- 输入流:只能从中读取数据,而不能向其写入数据,主要由InputStream和Reader作为基类实现
- 输出流:只能向其写入数据,而不能从中读取数据,主要由OutputStream和Writer作为基类实现
- 字节流和字符流——按照操作单元的不同来分
- 字节流:操作的数据单元是8位的字节,主要由InputStream和OutputStream作为基类实现
- 字符流:操作的数据单元是16位的字符,主要由Reader和Writer作为基类实现
- 节点流和处理流——按照流的角色来分
- 节点流:从一个特定的IO设备读写数据的流,此时程序直接连接到实际的数据源,和实际的输入输出节点连接
- 处理流:对一个已存在的流进行连接或封装,通过封装后的流来实现数据读写功能,此时程序不是直接连接到实际的数据源,没有和实际的输入输出节点连接,使用处理流,可以实现很好的封装
2.2 字节流和字符流
2.2.1 输入流:InputStream和Reader
- InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但他们将成为所有输入流的模板,所以他们的方法是所有输入流都可以使用的方法
- InputStream提供的三个方法:
- int read():从输入流中读取单个字节,返回所读取的字节数据
- int read(byte[] b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数
- int read(byte[] b, int off, int len):从输入流中最多读取len个字节的数据,并将其存储在字节数组b中,放入数组b时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数
- Read提供的三个方法:
- int read():从输入流中读取单个字符,返回所读取的字符数据
- int read(char[] cbuf):从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数
- int read(char[] cbuf, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数
- InputStream提供的三个方法:
- FileInputStream和FileReader,用于读取文件的输入流,它们都是节点流,会直接和指定文件关联。
- 注意:程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源,Java 7改写了所有的IO资源类,为他们实现了AutoClosed接口,因此都可通过自动关闭资源的try语句来关闭这些IO流
public class IStreamTest {
public static void main(String[] args) throws IOException {
// 创建字节输入流
FileInputStream fis = new FileInputStream(".//src//IStreamTest.java");
byte[] bbuf = new byte[1024];
int hasRead;
while ((hasRead = fis.read(bbuf)) > 0) {
System.out.print(new String(bbuf, 0, hasRead));
}
fis.close();
// 创建字符输入流
try (
FileReader fr = new FileReader(".//src//IStreamTest.java")) {
char[] cbuf = new char[32];
while ((hasRead = fr.read(cbuf)) > 0) {
System.out.print(new String(cbuf, 0, hasRead));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
2.2.2 输出流:OutputStream和Writer
- OutputStream和Writer是所有输出流的抽象基类,本身并不能创建实例来执行输出,但他们将成为所有输出流的模板,所以他们的方法是所有输出流都可以使用的方法,两个流都提供了如下三个方法:
- void write(int c):将指定的字节/字符输出到输出流中,其中c既可以代表字节,也可以代表字符
- void write(byte[]/char[] buf):将字节数组/字符数组中的数据输出到指定输出流中
- void write(byte[]/char[] buf, int off, int len):将字节数组/字符数组中从off位置开始,长度为len的数据输出到输出流中
- Writer流的写入String数据的方法:
- void write(String str):将str字符串里包含的字符输出到指定输出流中
- void write(String str, ing off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中
- 注意:使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点里
public class OStreamTest {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream(".//src//OStreamTest.java");
// 创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.txt")) {
byte[] bbuf = new byte[32];
int hasRead;
while ((hasRead = fis.read(bbuf)) > 0) {
fos.write(bbuf, 0, hasRead);
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
try (
FileWriter fw = new FileWriter("poem.txt")) {
fw.write("锦瑟 - 李商隐\r\n");
fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");
fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");
fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");
fw.write("此情可待成追忆,只是当时已惘然。\r\n");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
2.3 处理流
- 处理流:可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法,让程序员只需关心高级流的操作
- 处理流的思路:使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备、文件交互
- 区别处理流和节点流:处理流的构造器参数是一个已经存在的流,节点流的构造器是物理IO节点
- 处理流的优势:
- 操作简单
- 执行效率高
public class PrintStreamTest {
public static void main(String[] args) {
try (
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos)) {
// 使用PrintStream执行输出
ps.println("普通字符串");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
- 在使用处理流包装了底层节点流之后,关闭输入输出流资源时,只要关闭最上层的处理流即可,关闭上层的处理流时,系统会自动关闭被该处理流包装的节点流
- 由于PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。
2.4 输入\输出流体系
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayInputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedInputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedInputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | InputStreamWriter | ||
对象流 | ObjectInputStream | ObjectInputStream | ||
抽象基类 | FilterInputStream | FilterInputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输出流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
注:粗体表示节点流,必须与物理节点关联,斜体表示抽象基类,无法直接创建实例
- 常用规则:如果进行输入输出的内容是文本内容,则应该考虑使用字符流;如果进行输入输出的是二进制内容,则应该考虑使用字节流
public class StringNodeTest {
public static void main(String[] args) {
String src = "从明天起,做一个幸福的人\n"
+ "喂马,劈柴,周游世界\n"
+ "从明天起,关心粮食和蔬菜\n"
+ "我有一所房子,面朝大海,春暖花开\n"
+ "从明天起,和每一个亲人通信\n"
+ "告诉他们我的幸福\n";
char[] buffer = new char[32];
int hasRead = 0;
try (
StringReader sr = new StringReader(src)) {
// 采用循环读取的访问读取字符串
while ((hasRead = sr.read(buffer)) > 0) {
System.out.print(new String(buffer, 0, hasRead));
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
try (
// 创建StringWriter时,实际上以一个StringBuffer作为输出节点
// 下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter()) {
// 调用StringWriter的方法执行输出
sw.write("有一个美丽的新世界,\n");
sw.write("她在远方等我,\n");
sw.write("哪里有天真的孩子,\n");
sw.write("还有姑娘的酒窝\n");
System.out.println("----下面是sw的字符串节点里的内容----");
// 使用toString()方法返回StringWriter的字符串节点的内容
System.out.println(sw.toString());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
- 转换流用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流
- 缓冲流增加了缓冲功能,可以提高输出输出效率,增加缓冲功能后需要使用flush()才可以将缓冲区的内容写入到实际的物理节点
2.5 重定向标准输入输出
- 标准输入——System.in,代表键盘
- 标准输出——System.out,代表屏幕
- System类提供了如下三个重定向标准输入/输出方法:
- static void setErr(PrintStream err):重定向“标准”错误输出流
- static void setIn(InputStream in):重定向“标准”输入流
- static void setOut(PrintStream out):重定向“标准”输出流
public class RedirectIn {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream(".//src//RedirectIn.java")) {
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while (sc.hasNext()) {
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class RedirectOut {
public static void main(String[] args) {
try (
// 一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))) {
// 将标准输出重定向到ps输出流
System.setOut(ps);
// 向标准输出输出一个字符串
System.out.println("普通字符串");
// 向标准输出输出一个对象
System.out.println(new RedirectOut());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
2.6 RandomAccessFile
RandomAccessFile是Java输入输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。与普通的输入输出流不同的是,RandomAccessFile支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。
RandomAccessFile适用于:
- 访问文件部分内容,无须将文件从头读取
- 向已存在的文件后追加内容
- 只能读写文件,不能读写其他IO节点
RandomAccessFile对象也包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件记录指针位于文件头,RandomAccessFile可以自由移动该记录指针,提供了如下方法来操作文件记录指针:
- long getFilePointer():返回文件记录指针的当前位置
- void seek(long pos):将文件记录指针定位到pos位置
RandomAccessFile有两个构造器,一个参数是String文件名,另一个是File参数指定文件本身,还需要指定一个mode参数:
- “r”:以只读方式打开指定文件
- “rw”:以读、写方式打开指定文件,如果文件不存在,尝试创建该文件
- “rws”:以读、写方式打开指定文件,相对于“rw”方式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
- “rwd”:以读写方式打开指定文件,相对于“rw”方式,还要求对文件内容的每个更新都同步写入到底层存储设备
public class RandomAccessFileTest {
public static void main(String[] args) {
//读取文件并移动文件记录指针
try (
RandomAccessFile raf = new RandomAccessFile(
".//src//RandomAccessFileTest.java", "r")) {
// 获取RandomAccessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:"
+ raf.getFilePointer());
// 移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = raf.read(bbuf)) > 0) {
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf, 0, hasRead));
}
} catch (IOException ex) {
ex.printStackTrace();
}
//文件后添加内容
try (
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt", "rw")) {
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
- RandomAssessFile不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某处位置开始输出,则新输出的内容会覆盖文件中原有的内容,如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入到文件后,再将缓冲区的内容追加到文件后面
public class InsertContent {
public static void insert(String fileName, long pos, String insertContent) throws IOException {
File tmp = File.createTempFile("tmp", null);
tmp.deleteOnExit();
try (
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
// 使用临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp)) {
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存------
byte[] bbuf = new byte[64];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0) {
// 将读取的数据写入临时文件
tmpOut.write(bbuf, 0, hasRead);
}
// ----------下面代码插入内容----------
// 把文件记录指针重新定位到pos位置
raf.seek(pos);
// 追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0) {
raf.write(bbuf, 0, hasRead);
}
}
}
public static void main(String[] args) throws IOException {
insert("InsertContent.java", 45, "插入的内容\r\n");
}
}
3 对象序列化
对象序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
3.1 序列化的含义与意义
- 对象的序列化是指(Serialize)讲一个Java对象写入到IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象,如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(serializable),为了让某个类是可序列化的,该类必须实现如下两个接口之一:
- Serializable
- Externalizable
- Java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。
3.2 序列化的实现
- 使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法
序列化对象的步骤:
- 创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础上
- 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
反序列化的步骤:
- 创建一个ObjectInputStream,这个输入流是一个处理流,所以必须建立在其他节点流的基础上
- 调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型
- 序列化之后的结果是一行乱码(非自然语言,可以理解为某种加密解密过程,加密的时某个类的实例解密的也是某个类的实例,我们通常编码存放的是类,存放不了实例)
import java.io.*;
class Person implements java.io.Serializable {
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name, int age) {
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
// name的setter和getter方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
// age的setter和getter方法
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
}
public class WriteObject {
public static void main(String[] args) {
try (
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
Person per = new Person("孙悟空", 500);
// 将per对象写入输出流
oos.writeObject(per);
} catch (IOException ex) {
ex.printStackTrace();
}
try (
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
// 从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person p = (Person) ois.readObject();
System.out.println("名字为:" + p.getName() + "\n年龄为:" + p.getAge());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
- 反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属的class文件,否则将会引发ClassNotFoundException
- 反序列化机制无须通过构造器来初始化对象
4 NIO和NIO.2
4.1 NIO
4.1.1 NIO概述
- NIO采用内存映射文件的方式来处理输入输出。NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,通过这种方式操作速度要比传统的输入输出方式快的多
- NIO相关的包:
- java.nio包:主要包含各种与Buffer相关的类
- java.nio.channels包:主要包含与Channel和Selector相关的类
- java.nio.charset包:主要包含与字符集相关的类
- java.nio.channels.spi包:主要包含与Channel相关的服务提供者编程接口
- java.nio.charset.spi包:主要包含与字符集相关的服务提供者编程接口
- NIO的两大核心对象:Channel(通道)、Buffer(缓冲)
- Channel是对传统的输入/输出系统的模拟,在NIO系统中所有的数据都需要通过通道传输,Channel方法与传统的InputStream、OutputStream的最大区别是在于它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中;
- Buffer的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先方到Buffer中
4.1.2 Buffer
- Buffer就像是一个数组,它可以保存多个类型相同的数据,Buffer是一个抽象类,有如下子类ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer(没有boolean的Buffer子类),常用ByteBuffer和CharBuffer,其中ByteBuffer有一个子类,MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回
- static xxxBuffer allocate(int capacity):创建一个容量为capacity的xxxBuffer对象
- Buffer三个重要的概念:
- 容量(capacity):缓冲区的容量,创建后不可更改
- 界限(limit):第一个不该被读取或写入的缓冲区位置索引,位于limit之后的数据不可读不可写
- 位置(position):用于指明下一个可以被读取或写入的缓冲区位置索引
- Buffer的主要作用就是装入数据,然后输出数据,开始时buffer的position为0,limit为capacity,程序可以通过put()方法向Buffer中放入一些数据,同时随着数据的进入,position的位置也要相应的向后移动,当状如数据结束之后,调用flip()方法,该方法将limit设置为position所在位置,并将position设为0,使Buffer的读写指针移动到开始位置,为输出数据做好准备,当输出数据结束后,调用clear()方法,将position设置为0,limit设置为capacity,为再次装入数据做好准备
- Buffer的一些常用方法:
- int capacity():返回Buffer的capacity大小
- boolean hasRemaining():判断当前位置和界限之间是否还有元素可供处理
- int limit():返回Buffer的界限位置
- Buffer limit(int newLt):重新设置界限的值,并返回一个具有新的limit的缓冲区对象
- int position():返回Buffer中的position值
- Buffer position(int newPs):设置Buffer的position,并返回position被修改后的Buffer对象
- int remaining():返回当前位置和界限之间的元素个数
- put():放入数据
- get():取出数据
- 注意:为了灵活的实现定位,Buffer提供了一个可选标记mark,Buffer允许将position定位到该mark处,这些值满足:
0≤mark≤position≤limit≤capacity
0
≤
m
a
r
k
≤
p
o
s
i
t
i
o
n
≤
l
i
m
i
t
≤
c
a
p
a
c
i
t
y
- Buffer mark():设置Buffer的mark值
- Buffer reset():将位置转到mark值所在的位置
- Buffer rewind():将位置设置为0,取消设置的mark
- 当使用put()和get()方法放入、取出数据时,Buffer支持对单个数据的访问,也支持对批量数据的访问(以数组作为参数),操作数据的方式分为相对和绝对两种:
- 相对(Relative):从Buffer的当前position处开始读取或写入数据
- 绝对(Absolute):直接根据索引向Buffer中读取或写入数据
public class BufferTest {
public static void main(String[] args) {
// 创建Buffer
CharBuffer buff = CharBuffer.allocate(8);
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c');
System.out.println("加入三个元素后,position = " + buff.position());
System.out.println(buff.get());
// 调用flip()方法,准备从Buffer输出数据
buff.flip();
System.out.println("执行flip()后,limit = " + buff.limit());
System.out.println("position = " + buff.position());
// 取出第一个元素
System.out.println("第一个元素(position=0):" + buff.get());
System.out.println("取出一个元素后,position = " + buff.position());
// 调用clear方法,准备再次写入数据
buff.clear();
System.out.println("执行clear()后,limit = " + buff.limit());
System.out.println("执行clear()后,position = " + buff.position());
System.out.println("执行clear()后,缓冲区内容并没有被清除:" + "第三个元素为:" + buff.get(2));
System.out.println("执行绝对读取后,position = " + buff.position());
}
}
- 通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer,但是创建成本高于普通Buffer,同时读取效率高于普通Buffer,适用于长期存在的Buffer,短期的浪费资源
4.1.3 Channel
- Channel类似传统的流对象,但是与传统的流对象有两个主要区别:
- Channel可以直接将指定文件的部分或全部直接映射成Buffer
- 程序不能直接访问Channel中的数据,Channel只能与Buffer进行交互
- Channel的创建方法:通过传统的节点InputStream和OutputStream的getChannel()方法来返回对应的Channel
- Channel常用的三类方法:
- map()用于将Channel对应的部分或全部数据映射成ByteBuffer
- map()方法的方法签名为:MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);第一个参数执行映射时的模式,分别有制度、读写等模式;而第二个、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer。
- read()用户从Buffer中读取数据
- write()用于向Buffer中写入数据
- map()用于将Channel对应的部分或全部数据映射成ByteBuffer
public class FileChannelTest {
public static void main(String[] args) {
File f = new File(".//src//FileChannelTest.java");
try (
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt")
.getChannel()) {
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
// 直接将buffer里的数据全部输出
outChannel.write(buffer);
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
public class ReadFile {
public static void main(String[] args) throws IOException {
try (
// 创建文件输入流
FileInputStream fis = new FileInputStream(".//src//ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel()) {
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(256);
// 将FileChannel中数据放入ByteBuffer中
while (fcin.read(bbuff) != -1) {
// 锁定Buffer的空白区
bbuff.flip();
// 创建Charset对象
Charset charset = Charset.forName("UTF-8");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}
4.1.4 字符集和Charset
- Charset用来处理字节序列和字符序列之间的转换关系,该类包括解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset是不可变类。
public class CharsetTest {
public static void main(String[] args) {
// 获取Java支持的全部字符集
SortedMap<String, Charset> map = Charset.availableCharsets();
for (String alias : map.keySet()) {
// 输出字符集的别名和对应的Charset对象
System.out.println(alias + "----->" + map.get(alias));
}
}
}
- 一旦知道了字符集的别名之后,程序就可以调用Charset的forName()方法来创建对应的Charset对象,forName()方法的参数就是对应字符集的别名。获得了Charset对象之后,就可以通过该对象的newDecoder()和newEncoder()方法分别返回CharsetDecoder和CharsetEncoder对象,代表该Charset的解码器和编码器,使用以下方实现编码解码:
- CharBuffer decode(ByteBuffer bb):将ByteBuffer中的字节序列转换成字符序列的便捷方法
- CharBuffer encode(CharBuffer cb):将CharBuffer中的字符序列转换成字节序列的便捷方法
- ByteBuffer encode(String str):将String中的字符序列转换成字节序列的便捷方法
public class CharsetTransform {
public static void main(String[] args) throws Exception {
// 创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
// 获取cn对象对应的编码器和解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
// 创建一个CharBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put('孙');
cbuff.put('悟');
cbuff.put('空');
cbuff.flip();
// 将CharBuffer中的字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
// 循环访问ByteBuffer中的每个字节
for (int i = 0; i < bbuff.capacity(); i++) {
System.out.print(bbuff.get(i) + " ");
}
// 将ByteBuffer的数据解码成字符序列
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}
4.1.5 文件锁
- 使用文件锁可以有效阻止多个进程并发修改同一个文件,可以控制文件的全部或部分字节的访问
- NIO中使用FileLock来实现文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件,两个方法的区别是:当lock()方法试图锁定某个文件时,如果无法获得文件锁,进程一直阻塞,而tryLock()则是尝试锁定文件,不会阻塞进程
- 锁定部分内容的方法:
- lock(long position, long size, boolean shared):对文件从position开始,长度为size的内容加锁,该方法是阻塞式的
- tryLock(long position, long size, boolean shared):对文件从position开始,长度为size的内容加锁,该方法是非阻塞式的
- 注意:当shared是true时,表明该锁是共享锁,可以多个进程来读取文件,但是其他进程无法获得排它锁,当shared为false时,表明该锁是一个排它锁,他将锁住对该文件的读写,可以通过FileLock的isShared来判断, 直接使用lock()和tryLock()得到的是排他锁
- 处理完文件后通过FileLock的release()方法释放文件锁
4.2 NIO.2
- NIO和NIO.2的区别
- NIO.2提供了全面的文件IO和文件系统访问支持
- NIO.2提供了基于异步Channel的IO
4.2.1 Path、Paths和Files
- 由于File类的功能有限,不能利用特定文件系统的特征,且性能不高,因此NIO.2提供了如下接口和工具类:
- Path接口,代表一个平台无关的平台路径
- Files工具类包含了大量静态的工具方法来操作文件
public class FilesTest {
public static void main(String[] args) throws Exception {
// 复制文件
Files.copy(Paths.get(".//src//FilesTest.java"), new FileOutputStream("a.txt"));
// 判断FilesTest.java文件是否为隐藏文件
System.out.println("FilesTest.java是否为隐藏文件:" + Files.isHidden(Paths.get(".//src//FilesTest.java")));
// 一次性读取FilesTest.java文件的所有行
List<String> lines = Files.readAllLines(Paths.get(".//src//FilesTest.java"), Charset.forName("gbk"));
System.out.println(lines);
// 判断指定文件的大小
System.out.println("FilesTest.java的大小为:" + Files.size(Paths.get(".//src//FilesTest.java")));
List<String> poem = new ArrayList<>();
poem.add("水晶潭底银鱼跃");
poem.add("清徐风中碧竿横");
// 直接将多个字符串内容写入指定文件中
Files.write(Paths.get(".//src//pome.txt"), poem, Charset.forName("gbk"));
// 使用Java 8新增的Stream API列出当前目录下所有文件和子目录
Files.list(Paths.get(".")).forEach(path -> System.out.println(path));
// 使用Java 8新增的Stream API读取文件内容
Files.lines(Paths.get("FilesTest.java"), Charset.forName("gbk")).forEach(line -> System.out.println(line));
FileStore cStore = Files.getFileStore(Paths.get("C:"));
// 判断C盘的总空间,可用空间
System.out.println("C:共有空间:" + cStore.getTotalSpace());
System.out.println("C:可用空间:" + cStore.getUsableSpace());
}
}
4.2.2 FileVisitor
- Files类提供了如下两种方法来遍历文件和子目录:
- walkFileTree(Path start, FileVisitor<? Super Path> visitor):遍历start路径下的所有文件和子目录
- walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? Super Path> visitor):功能同上,最多遍历maxDepth深度的文件
- FileVisitor代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会出发FileVisitor中相应的方法,FileVisitor中定义了如下4个方法:
- FileVisitResult postVisitDirectory(T dir, IOException exc):访问子目录之后触发该方法
- FileVisitResult preVisitDirectory(T dir,BasicFileAttributes attrs):访问子目录之前触发该方法
- FileVisitResult visitFile(T file,BasicFileAttributes attrs):访问file文件时触发该方法
- FileVisitResult visitFileFailed(T file, IOException exc):访问file文件失败是触发该方法
- 以上四个方法都返回一个FileVisitResult对象,他是一个枚举类,代表了访问之后的后续行为,后续行为有如下几种:
- CONTINUE:继续访问
- SKIP_SIBLINGS:继续访问,但不访问该文件或目录的兄弟文件或目录
- SKIP_SUBTREE:继续访问,但不访问该文件或目录的子目录树
- TERMINATE:终止访问
public class FileVisitorTest {
public static void main(String[] args)
throws Exception {
// 遍历g:\publish\codes\15目录下的所有文件和子目录
Files.walkFileTree(Paths.get("g:", "publish", "codes", "15")
, new SimpleFileVisitor<Path>() {
// 访问文件时候触发该方法
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问" + file + "文件");
// 找到了FileInputStreamTest.java文件
if (file.endsWith("FileInputStreamTest.java")) {
System.out.println("--已经找到目标文件--");
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
// 开始访问目录时触发该方法
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问:" + dir + " 路径");
return FileVisitResult.CONTINUE;
}
});
}
}
4.2.3 WatchService
- NIO.2的Path类提供了如下一个方法来监听文件系统的变化:
- register(WatchService watcher, WatchEvent.Kind<?>… events):用watcher监听该Path代表的目录下的文件变化。events参数指定要监听哪些类型的事件
- WatchService代表一个文件系统监听服务,负责监听path代表的目录下的文件变化,一旦使用register()方法完成注册之后,接下来就可调用WatchService的如下三个方法来获取被监听目录的文件变化事件:
- WatchKey poll():获取下一个WatchKey,没有返回null,适合监控指定时间
- WatchKey poll(long timeout, TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey
- WatchKey take():获取下一个WatchKey,没有就一直等待,适合长期一直监控
public class WatchServiceTest {
public static void main(String[] args) throws Exception {
// 获取文件系统的WatchService对象
WatchService watchService = FileSystems.getDefault().newWatchService();
// 为C:盘根路径注册监听
Paths.get("C:/").register(watchService
, StandardWatchEventKinds.ENTRY_CREATE
, StandardWatchEventKinds.ENTRY_MODIFY
, StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
// 获取下一个文件改动事件
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.context() + " 文件发生了 " + event.kind() + "事件!");
}
// 重设WatchKey
boolean valid = key.reset();
// 如果重设失败,退出监听
if (!valid) {
break;
}
}
}
}
4.2.4 访问文件属性
- NIO.2在java.nio.file.attribute包下提供了大量的工具类,用于获取文件属性,这些工具类主要分为如下两类:
- XxxAttributeView:代表某种文件属性的“视图”
- XxxAttributes:代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取
- 文件视图:
- AclFileAttributeView:为特定文件设置ACL及文件所有者属性,getAcl获取文件的权限集,setAcl(List)修改文件的ACL
- BasicAttributeView:获取或修改文件的基本属性
- DosFileAttributeView:获取或修改文件DOS相关属性
- FileOwnerAttributeView:获取或修改文件的所有者
- PosixFileAttributeView:获取或修改POSIX属性
- UserDefinedAttributeView:自定义属性