Java I/O (输入/输出)
I/O(输入输出)
使用输入机制,允许程序读取外部数据(包括来自磁盘、光盘等存储设备的数据)、用户输入的数据
使用输出机制,允许程序记录运行状态,将程序数据输出到磁盘、光盘等存储设备中
Java 的 I/O 通过 java.io 包下的类和接口支持。 java.io 包下主要包括输入、输出两种 IO 流,每种输入、输出流又分为字节流和字符流两大类:
字节流以字节为单位处理输入、输出操作
字符流以字符来处理输入、输出操作
Java IO 流采用装饰器设计模式,将 IO 流分成底层节点流和上层处理流
节点流用于和底层物理存储节点直接关联,不同的物理节点获取节点流的方式可能存在一定差异,但程序可以把不同的物理节点流包装成统一的处理流,
从而允许程序使用统一的输入、输出代码来读写不同物理存储节点上的资源
Java 7 在 java.nio 及其子包下提供了一些列全新 API , 这些 API 是对原有 NIO 的升级,因此也被称为 NIO2 ,通过这些 NIO2 ,程序可以更高效地进行输入输出操作
1. File 类
---------------------------------------------------------------------------------------
File 是 java.io 包下代表与平台无关的文件和目录。
不管是文件还是目录都是使用 File 来操作的, File 能新建、删除、重命名文件和目录
File 不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入输出流
访问文件和目录
-----------------------------------------------
File 类可以使用文件路径字符串来创建实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。
默认情况下,系统总是依据用户的工作路径来解释相对路径,这个路径由系统属性 “user.dir” 指定,通常也是运行 Java 虚拟机所在的路径。
一旦创建了 File 对象之后,就可以调用 File 对象的方法来访问, File 类提供了很多方法来操作文件和目录
文件过滤器
------------------------------------------------
File 类的
String[] list(FilenameFilter filter)
方法接受一个 FilenameFilter 参数,通过该参数可以只列出符合条件的文件
FilenameFilter 接口包含:
boolean accept(File dir, String name)
方法,该方法将依次对指定 File 的所有子目录或文件进行迭代,方法返回 true, 则 list() 方法列出该目录或文件
例子:
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 流
--------------------------------------------------------------------------------------------------------------------
Java 的 IO 流是实现输入输出的基础,它可以方便地实现数据的输入、输出操作。
在 Java 中把不同的输入、输出源(键盘、文件、网络连接等) 抽象表述为 “流” (stream), 通过流的方式允许 Java 程序使用相同的方式来访问不同的输入/输出源
stream 是从源(stream)到接收(sink)的有序数据。
流的分类
---------------------------------------------------------------------------------------------
按照不同的分类方式,可以将流分为不同的类型
1. 输入流和输出流
--------------------------------------------------
输入流:只能从中读取数据,而不能向其写入数据
输出流:只能向其写入数据,而不能从中读取数据
这里的输入、输出都是从程序运行所在内存的角度来划分的。输入、输出涉及一个方向问题,数据从内存到硬盘,通常称为输出流;相反,数据从硬盘到内存,称为输入流。
Java 的输入流主要由 InputStream 和 Reader 作为基类,而输出流则主要由 OutputStream 和 Writer 作为基类。
输入流基类: 输出流基类:
InputStream OutputStream
Reader Writer
2. 字节流和字符流
--------------------------------------------------
字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同
字节流:操作的数据单元是 8 位的字节
字符流:操作的数据单元是 16 位的字符
字节流基类: 字符流基类:
InputStream Reader
OutputStream Writer
3. 节点流和处理流
---------------------------------------------------
按照流的角色分,可以分为节点流和处理流
可以从/向一个特定的 IO 设备(如磁盘、网络)读写数据的流,称为节点流,节点流也被称为低级流(Low Level Stream)。
当使用节点流进行输入/输出时,程序直接连接到实际的数据源,和实际的输入/输出节点连接。
处理流用于对一个已存在的流进行连接或封装,通过封装后的流实现数据读/写功能。处理流也被称为高级流。
当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。
使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地放生变化。
Java 使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法完成输入/输出功能。
因此,处理流也称为包装流。
流的概念模型
-----------------------------------------------------------------------------------------------
Java 的 IO 流共涉及40多个类,都是从如下4个抽象基类派生的
InputStream/Reader : 所有输入流的基类,前者是字节输入流,后者是字符输入流
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
输入流使用隐式的记录指针来表示当前正准备从哪个单元开始读取,程序从InputStream/Reader 取出一个或多个单元后,记录指针自动向后移动。
当执行输出时,程序依次把数据单元放入到输出流的管道中,输出流同样采用隐式的记录指针来标识当前数据单元即将放入的位置,每当程序向OutputStream/Writer里输出一个或多个数据单元后,记录指针自动向后移动。
Java 的处理流模型体现了 Java 输入输出流设计的灵活性,功能主要体现在以下两个方面:
1. 性能的提高: 主要以增加缓冲的方式来提高输入/输出效率
2. 操作的便捷: 处理流可能提供了一些列便捷的方法来一次输入/输出打批量的内容。而不是输入输出一个或多个数据单元
处理流可以嫁接在任何已存在的流的基础上,这就允许 Java 应用程序采用相同的代码、透明的方式访问不同的输入输出设备的数据流。
通过使用处理流, Java 程序无须理会输入/输出节点是磁盘、网络还是其他的输入输出设备,程序只要将这些节点流包装成处理流,就可以使用相同的输入输出代码读写不同的输入输出设备的数据。
*
*
*
3. 字节流和字符流
---------------------------------------------------------------------------------------------------------------------------
InputStream 和 Reader
----------------------------------------------------------------------
InputStream 和 Reader 是所有输入流的抽象基类,本身并不能创建实例执行输入,但它们将称为所有输入流的模版,它们的方法是所有输入流都可以使用的方法
InputStream:
abstract int read() :从输入流读取单个字节,返回所读取的字节数据
int read(byte[] b) :从输入流中最多读取 b.length 个字节数据,并将其存储在字节数组 b 中,返回读实际读取的字节数
int read(byte[] b, int off, int len):从输入流中最多读取 len 个字节数据,并将其存储在数组 b 中,放入数组 b 中时,并不是从数组起点开始,而是从 off 位置开始存放,返回实际读取的字节数。
Reader:
int read() :从输入流中读取单个字符,返回所读取的字符数据
int read(char[] cbuf):从输入流中最多读取 cbuf.length 个字符数据,并将其存放在字符数组 cbuf 中,返回实际读取的字符数
abstract int read(char[] cbuf, int off, int len):从输入流中最多读取 len 个字符的数据,并将其存储在字符数组 cbuf 中,放入数组 cbuf 中时,并不是从数组的起点开始,而是从 off 位置开始,返回实际读取的字符数。
java.io.InputStream java.io.Reader
java.io.FileInputStream java.io.InputStreamReader
java.io.FileReader
FileInputStream 和 FileReader:它们都是节点流-----会和指定的文件关联
例子:
public class FileInputStreamTest
{
public static void main(String[] args) throws IOException
{
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileInputStreamTest.java");
// 创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
// 关闭文件输入流,放在finally块里更安全
fis.close();
}
}
例子:
public class FileReaderTest
{
public static void main(String[] args)
{
try(
// 创建字符输入流
FileReader fr = new FileReader("FileReaderTest.java"))
{
// 创建一个长度为32的“竹筒”
char[] cbuf = new char[32];
// 用于保存实际读取的字符数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fr.read(cbuf)) > 0 )
{
// 取出“竹筒”中水滴(字符),将字符数组转换成字符串输入!
System.out.print(new String(cbuf , 0 , hasRead));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
OutputStream和Writer
-------------------------------------------------------------------
OutputStream和Writer 也非常相似,两个流都提供了如下三个方法:
abstract void write(int b) :将字节/字符输出到输出流,c 既可以代表字节,也可以代表字符
void write(byte[]/char[] b) :将字节数组/字符数组中的数据输出到输出流中
void write(byte[]/char[] b, int off, int len) :将字节数组/字符数组中从 off 位置开始,长度为 len 的字节/字符输出到输出流中。
Writer 里还包含如下两个方法:
void write(String str) :将 str 字符串里包含的字符输出到指定输出流
void write(String str, int off, int len) :将 str 字符串里从 off 位置开始,长度为 len 的字符输出到指定的输出流
例子:
public class FileOutputStreamTest
{
public static void main(String[] args)
{
try(
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileOutputStreamTest.java");
// 创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.txt"))
{
byte[] bbuf = new byte[32];
int hasRead = 0;
// 循环从输入流中取出数据
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 每读取一次,即写入文件输出流,读了多少,就写多少。
fos.write(bbuf , 0 , hasRead);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
public class FileWriterTest
{
public static void main(String[] args)
{
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();
}
}
}
*
*
*
4. 输入/输出流体系
------------------------------------------------------------------------------------------------------------------------
处理流的用法
-------------------------------------------------------------------------------------
处理流的功能,可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法, 让程序员只需关心高级流的操作。
使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层 I/O 设备、文件交互
识别处理流:只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流;而所有节点流都是直接以物理IO节点作为构造器参数的。
java.io.OutputStream
java.io.FilterOutputStream
java.io.PrintStream
java.lang.Object
java.io.Writer
java.io.PrintWriter
例子:
public class PrintStreamTest
{
public static void main(String[] args)
{
try(
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos))
{
// 使用PrintStream执行输出
ps.println("普通字符串");
// 直接使用PrintStream输出对象
ps.println(new PrintStreamTest());
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
由于 PrintStream 类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出包装成 PrintStream 后进行输出
程序使用处理流非常简单,通常只需要在创建处理流时传入一个节点流作为构造器参数即可,这样创建的处理流就是包装了该节点流的处理流。
在使用处理流包装底层节点流之后,关闭输入输出流资源时,只要关闭最上层的处理流即可。关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流。
输入/输出流体系
------------------------------------------------------------------------------------------------------------------------------------
Java 输入/输出流体系中常用的流分类
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|
| 分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|
| 抽象基类 | InputStream | OutputStream | Reader | Writer |
| | | | | |
| 访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
| | | | | |
| 访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
| | | | | |
| 访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
| | | | | |
| 访问字符串 | | | StringReader | StringWriter |
| | | | | |
| 缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
| | | | | |
| 转换流 | | | InputStreamReader | OutputStreamWriter |
| | | | | |
| 对象流 | ObjectInputStream | ObjectOutputStream | | |
| | | | | |
| 过滤器流 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
| | | | | |
| 打印流 | | PrintStream | | PrintWriter |
| | | | | |
| 推回输入流 | PushbackInputStream | | PushbackReader | |
| | | | | |
| 特殊流 | DataInputStream | DataOutputStream | | |
| | | | | |
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|
一般规则: 如果进行输入输出的内容是文本内容,应该考虑使用字符流;如果进行输入输出的内容是二进制内容,则应该考虑使用字节流。
还有一些特殊功能的字节流位于 JDK 其他包下:
AudioInputStream:
CipherInputStream:
DeflaterInputStream
ZipInputStream:
...
以数组为物理节点的节点流,字节流以字节数组为节点,字符流以字符数组为节点,这种以数组为物理节点的节点流除了创建节点流对象时需要传入一个字节数组或者字符数组之外,用法上与文件节点流完全相似。
字符流还可以使用字符串作为物理节点,用于实现从字符串读取内容,或将内容写入字符串(用 StringBuffer 充当字符串)
例子:
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 将字节流转换成字符输出流
Java 使用 System.in 代表标准输入,即键盘输入,但这个标准输入流是 InputStream 类的实例,使用不太方便,而且键盘输入的内容都是文本内容,
所以可以使用 InputStreamReader 将其转换成字符输入流,普通的 Reader 读取输入内容时依然不太方便,可以将普通的 Reader 再次包装成 BufferReader, 利用 BufferReader 的 readLine() 方法可以一次读取一行内容
例子:
public class KeyinTest
{
public static void main(String[] args)
{
try(
// 将Sytem.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader))
{
String line = null;
// 采用循环方式来一行一行的读取
while ((line = br.readLine()) != null)
{
// 如果读取的字符串为"exit",程序退出
if (line.equals("exit"))
{
System.exit(1);
}
// 打印读取的内容
System.out.println("输入内容为:" + line);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
推回输入流: PushbackInputStream 和PushbackReader
----------------------------------------------------------------------------------------
它们都提供了如下三个方法:
void unread(byte[]/char[] cbuf) :将一个字节/字符数组内容推回到推回缓冲区,从而允许重复读取刚刚读取的内容
void unread(byte[]/char[] cbuf, int off, int len) :将一个字节/字符数组里从 off 开始,长度为 len 字节/字符的内容推回到推回缓冲区,从而允许重复读取刚刚读取的内容
void unread(int c) :将一个字节/字符推回到推回缓冲区,从而允许重复读取刚刚读取的内容。
这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的 unread() 方法时,系统将会把指定数组的内容推回到该缓冲区,
而推回输入流每次调用 read() 方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满 read() 所需的数组时才会从原输入流读取。
当程序创建一个 PushbackInputStream 和PushbackReader 时需要指定推回缓冲区的大小,默认缓冲区的长度为 1 。
如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发 Pushback buffer overfloww 的 IOException 异常。
例子:
public class PushbackTest
{
public static void main(String[] args)
{
try(
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"PushbackTest.java") , 64))
{
char[] buf = new char[32];
// 用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
// 循环读取文件内容
while ((hasRead = pr.read(buf)) > 0)
{
// 将读取的内容转换成字符串
String content = new String(buf , 0 , hasRead);
int targetIndex = 0;
// 将上次读取的字符串和本次读取的字符串拼起来,
// 查看是否包含目标字符串, 如果包含目标字符串
if ((targetIndex = (lastContent + content)
.indexOf("new PushbackReader")) > 0)
{
// 将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
// 重新定义一个长度为targetIndex的char数组
if(targetIndex > 32)
{
buf = new char[targetIndex];
}
// 再次读取指定长度的内容(就是目标字符串之前的内容)
pr.read(buf , 0 , targetIndex);
// 打印读取的内容
System.out.print(new String(buf , 0 ,targetIndex));
System.exit(0);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
*
*
*
5. 重定向标准输入/输出
-----------------------------------------------------------------------------------------------------------------------------------
System 类提供了如下三个重定向标准输入/输出方法
static void setErr(PrintStream err) : 重定向标准错误输出
static void setIn(InputStream in) : 重定向标准输入流
static void setOut(PrintStream out) : 重定向标准输出流
例子:
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();
}
}
}
例子:
public class RedirectIn
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("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();
}
}
}
*
*
*
6. Java 虚拟机读写其他进程的数据
----------------------------------------------------------------------------------------------------------------------------
使用 Runtime 对象的 exec() 方法可以运行平台上的其他程序,该方法产生一个 Process 对象, Process 对象代表由该 Java 程序启动的子进程。
Process 类提供了如下三个方法,用于让程序和其子进程进行通信。
abstract InputStream getErrorStream() :获取子进程的错误流
abstract InputStream getInputStream() :获取子进程的输入流
abstract OutputStream getOutputStream():获取子进程的输出流
此处的输入流、输出流容易混淆,如果试图让子进程读取程序的数据,那么应该用输入流还是输出流?不是输入流,而是输出流。
要站在 Java 程序的角度来看问题,子进程读取 Java 程序的数据,就是让 Java 程序把数据输出到子进程(就像把数据输出到文件中一样,只是现在由子进程节点代替了文件节点),所以应该使用输出流。
例子:
public class ReadFromProcess
{
public static void main(String[] args)
throws IOException
{
// 运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");
try(
// 以p进程的错误流创建BufferedReader对象
// 这个错误流对本程序是输入流,对p进程则是输出流
BufferedReader br = new BufferedReader(new
InputStreamReader(p.getErrorStream())))
{
String buff = null;
// 采取循环方式来读取p进程的错误输出
while((buff = br.readLine()) != null)
{
System.out.println(buff);
}
}
}
}
例子:
public class WriteToProcess
{
public static void main(String[] args)
throws IOException
{
// 运行java ReadStandard命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("java ReadStandard");
try(
// 以p进程的输出流创建PrintStream对象
// 这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream()))
{
// 向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("普通字符串");
ps.println(new WriteToProcess());
}
}
}
// 定义一个ReadStandard类,该类可以接受标准输入,
// 并将标准输入写入out.txt文件。
class ReadStandard
{
public static void main(String[] args)
{
try(
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(
new FileOutputStream("out.txt")))
{
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
ps.println("键盘输入的内容是:" + sc.next());
}
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
*
*
*
7. java.io.RandomAccessFile
-------------------------------------------------------------------------------------------------------------------------------
RandomAccessFile 是 Java 输入输出体系中功能最丰富的文件内容访问类,提供了众多的方法来访问文件内容,既可以读取文件内容,也可以向文件输出数据
RandomAccessFile 支持“随机访问” 方式,程序可以直接跳转到文件任意地方读写数据
局限性:只能读写文件,不能读写其他 IO 节点。
RandomAccessFile 包含如下两个方法操作文件记录指针:
long getFilePointer() : 返回文件记录指针的当前位置
void seek(long pos) : 将文件记录指针定位到 pos 位置
RandomAccessFile 的含义是可以自由访问文件的任意地方(与 InputStream 、 Reader 需要依次向后读取相区分)。
构造器:
RandomAccessFile(File file, String mode)
RandomAccessFile(String name, String mode)
mode 参数指定 RandomAccessFile 的访问模式,由如下4个值:
"r" :以只读方式打开指定文件,如果试图对该 RandomAccessFile 执行写入方法,将抛出 IOException 异常。
"rw" : 以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
"rws" : 以读、写方式打开指定文件。相对于 "rw" 模式,还要求对文件的内容或元数据的每个更新都同步到写入到底层存储设备。
"rwd" : 以读、写方式打开指定文件。相对于 "rw" 模式,还要求对文件的内容的每个更新都同步到写入到底层存储设备。
例子:
public class RandomAccessFileTest
{
public static void main(String[] args)
{
try(
RandomAccessFile raf = new RandomAccessFile(
"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();
}
}
}
例子:
public class AppendContent
{
public static void main(String[] args)
{
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();
}
}
}
RandomAccessFile 依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容。
如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加的文件后面。
例子:
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");
}
}
上面程序中使用 File 的 createTempFile(String prefix, String suffix) 方法创建了一个临时文件(该临时文件将在 JVM 退出时被删除),用以保存被插入文件的插入点后面的内容。
程序先将文件插入点后的内容读入临时文件中,然后重新定位到插入点,将需要插入的内容添加到文件后面,最后将临时文件的内容添加到文件后面。
*
*
*
8. 对象序列化
-------------------------------------------------------------------------------------------------------------------------------------
对象序列化的目标就是将对象保存到磁盘上,或允许在网络中直接传输对象。
对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。
其他程序一旦获得了这种二进制流,就可以将这种二进制流恢复成原来的 Java 对象。
序列化的含义和意义
-------------------------------------------------------------------------------------------
序列化机制使得对象可以脱离程序的运行而独立存在。
对象序列化(Serialize)指的是将一个 Java 对象写入 IO 流中,对象的反序列化 (Deserialize)则是指从 IO 流中恢复该 Java 对象。
如果需要让某个对象支持序列化机制,则必须让它的类是可以序列化的 ( serializable ),为了让某个类是可序列化的,该类必须实现如下两个接口之一:
Serializable : 该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的
Externalizable
所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常。比如 RMI 远程方法调用过程中的参数和返回值。
所有需要保存到磁盘的对象的类都应该时可序列化的,比如 Web 应用中需要保存到 HttpSession 或 ServletContex 属性的 Java 对象。
使用対向流实现序列化
--------------------------------------------------------------------------------------------
一旦某个类实现了 Serializable 接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化对象
1. 创建一个 java.io.ObjectOutputStream ,这个输出流是一个处理流,所以必须建立在其他节点流的基础上
ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream("object.txt"));
2. 调用 ObjectOutputStream 对象的 writeObject(Object obj) 方法,输出可序列化对象
oos.writeObject(obj);
例子:
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();
}
}
}
从二进制流中恢复 Java 对象,需要反序列化,反序列化步骤如下:
1. 创建一个 java.io.ObjectInputStream 输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。
ObjectInputStream ois = new ObjectInputStream( new FileInputStream("object.txt"));
2. 调用 ObjectInputStream 对象的 readObject() 方法读取流中的对象,该方法返回一个 Object 类型的 Java 对象,如果程序知道该 Java 对象的类型,则可以将该对象强制类型转换成真实的类型。
Person p = (Person)ois.readObject();
例子:
public class ReadObject
{
public static void main(String[] args)
{
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 异常。
还有一点指出,反序列化机制无须通过构造器来初始化 Java 对象。
如果使用序列化机制向文件中写入了多个 Java 对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取。
当一个可序列化类由多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的,否则反序列化时抛出 InvalidClassException 异常。
如果父类是不可序列化的,只是带有无参数构造器,则该父类中定义的成员变量值不会序列化到二进制流中。
对象引用的序列化
-----------------------------------------------------------------------------------
如果某个类的成员变量的类型不是基本类型或 String 类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
如 Teacher 类持有一个 Person 类的引用,只有 Person 类是可序列化的, Teacher 类才是可序列化的。如果 Person 类不可序列化,则无论 Teacher 类是否实现 Serializable 、Externalizable 接口,则 Teacher 类都是不可序列化的。
Java 序列化机制采用一种特殊的序列化算法,内容如下:
1. 所有保存到磁盘中的对象都有一个序列化编号。
2. 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过(在本次虚拟机中),系统才会将该对象转换成字节序列输出。
3. 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象
由于 Java 序列化机制使然,如果多次序列化同一个 Java 对象,只有第一次序列化时才会被该 Java 对象转换成字节序列输出,这样可能引起一个潜在问题-----当程序序列化一个可变对象时,
只有第一次使用 writeObject() 方法输出时才会将该对象转换成字节序列输出,当程序再次调用 writeObject() 方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量已被改变,改变的实例变量值也不会输出。
自定义序列化
--------------------------------------------------------------------------------------
在一些特殊场景下,如果一个类里包含的某些实例变量是敏感信息,如银行账户信息等,这时不希望系统将该实例变量值进行序列化,
或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发 java.io.NotSerializableException 异常。
通过在实例变量前使用 transient 关键字修饰,可以指定 Java 序列化时无须理会该实例变量。
transient 关键字只能用来修饰实例变量,不能用来修饰程序的其他部分。
例子:
public class Person
implements java.io.Serializable
{
private String name;
private transient 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;
}
}
使用 transient 关键字修饰实例变量虽然简单、方便,但被 transient 修饰的实例变量将被完全隔离在序列化机制之外,这样导致在饭序列化恢复 Java 对象时无法取得该实例变量的值。
Java 提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(与 transient 关键字效果相同)
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊方法用以实现自定义序列化:
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() 方法可以恢复它。通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要怎样序列化。
在默认情况下,该方法会调用 out.defaultWriteObject() 来保存 Java 对象的各实例变量,从而可以实现序列化 Java 对象状态的目的。
readObject() 方法负责从流中读取并恢复对象实例变量,通过重写该方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化。
在默认情况下,该方法会调用 in.defaultReadObject() 来恢复 Java 对象的非 transient 实例变量。通常情况下, readObject() 方法与 writeObject() 方法对应,
如果 writeObject() 方法中对 Java 对象的实例进行了一些处理,则应该在 readObject() 方法中对其实例变量进行相应的反处理,以便正确恢复该对象。
readObjectNoData(), 当序列化流不完整时, readObjectNoData() 方法可以用来正确地初始化反序列化对象。例如,接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,
或者序列化流被篡改,系统都会调用 readObjectNoData() 方法来初始化反序列化的对象
例子:
public 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;
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer)in.readObject()).reverse()
.toString();
this.age = in.readInt();
}
}
writeObject() 方法存储实例变量的顺序应该和 readObject() 方法中恢复实例变量的顺序一致,否则不能正常恢复该 Java 对象
还有一种更测底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象,如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下特殊方法
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplace() 方法将由序列化机制调用,只要该方法存在。
因为该方法可以拥有 private, protected, package-private 等访问权限,所以其子类有可能获得该方法。
例子:
public 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;
}
// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace()throws ObjectStreamException
{
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
public class ReplaceTest
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("replace.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("replace.txt")))
{
Person per = new Person("孙悟空", 500);
// 系统将per对象转换字节序列并输出
oos.writeObject(per);
// 反序列化读取得到的是ArrayList
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
Java 的序列化机制保证在序列化某个对象之前,先调用该对象的 writeReplace() 方法,如果该方法返回一个 Java 对象,则系统转为序列化这个返回的对象。
系统在序列化某个对象之前,会调用该对象的 writeReplace() 和 writeObject() 两个方法,系统总是先调用 writeReplace() 方法,如果该方法返回另一个对象,
系统将再次调用返回对象的 writeReplace()方法... 直到该方法不再返回另一个对象为之。程序最后将调用该对象的 writeObject() 方法来保存对象的状态。
与 writeReplace() 方法相对的是, 序列化机制里还有一个特殊的方法,它可以实现保护性复制整个对象,这个方法就是:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
这个方法会紧接着 readObject() 之后被调用,该方法的返回值将会代替原来的反序列化对象,而原来 readObject() 反序列化的对象将会被立即丢弃。
例子:
public class Orientation
implements java.io.Serializable
{
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
private Orientation(int value)
{
this.value = value;
}
// 为枚举类增加readResolve()方法
private Object readResolve()throws ObjectStreamException
{
if (value == 1)
{
return HORIZONTAL;
}
if (value == 2)
{
return VERTICAL;
}
return null;
}
}
另一种自定义序列化机制
-------------------------------------------------------------------------------------------------
Java 提供了另一种序列化机制,这序列化方式完全由程序员决定存储和恢复对象数据。
要实现该目标, Java 类必须实现 Externalizable 接口,该接口定义如下两个方法:
void readExternal(ObjectInput in) : 需要序列化的类实现反序列化,该方法调用 DataInput ( ObjectInput 的父接口 ) 的方法恢复基本类型实例变量值, 调用 ObjectInput 的 readObject() 方法恢复引用类型的实例变量值
void writeExternal(ObjectOutput out) : 需要序列化的类实现保存对象的状态,该方法调用 DataOutput ( ObjectOutput 的父接口 ) 方法保存基本类型的实例变量值,调用 ObjectOutput 的 writeObject() 方法保存引用类型的实例变量值。
例子:
public class Person
implements java.io.Externalizable
{
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 void writeExternal(java.io.ObjectOutput out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
虽然实现 Externalizable 接口能带来一定的性能提升,但由于实现 Externalizable 接口导致编程复杂度的增加,所以大部分时候都采用实现 Serializable 接口的方式实现序列化。
关于对象序列化,还有如下几点需要注意:
1. 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量、 transient (也被称为瞬态实例变量)实例变量都不会被序列化
2. 实现 Serializable 接口的类如果需要让某个实例变量不被序列化,则可在该实例变量前加 transient 修饰符。
3. 保证序列化对象的实例变量类型也是可序列化的,否则需要使用 transient 关键字来修饰该实例变量,要不然,该类是不可实例化的。
4. 防序列化对象时必须由序列化对象的 class 文件
5. 当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取
版本
---------------------------------------------------------------------------------------------------------------------------------
随着项目的升级,系统的 class 文件也会升级, Java 如何保证两个 class 文件的兼容性?
Java 序列化机制允许为序列化类提供一个 private static final long serialVersionUID 值,该变量的值用于标识 Java 类的序列化版本。
如果一个类升级后,只要它的 serialVersionUID 类变量值保持不变,序列化机制也会把它们当成同一个序列化版本。
public class Test
{
private static final long serialVersionUID = 512L;
}
为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的类中加入 serialVersionUID 类变量, 具体数值自己定义,
这样,即使在某个对象被序列化之后,它所对应的类被修改了,该对象也依然可以被正确地反序列化。
如果不显示定义 serialVersionUID 类变量的值,该值将由 JVM 根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,
从而造成对象的反序列化因为版本不兼容而失败。
serialver.exe 工具可获得该类的 serialVersionUID 类变量的值:
例如:
serialver Person
不显示指定 serialVersionUID 值的另一个坏处是,不利于程序在不同 JVM 之间移植。因为不同的编译器对该类变量的计算策略可能不同,从而造成虽然类完全没有改变,
但因为 JVM 不同,也会出现序列化版本不兼容而无法正确反序列化现象。
如果类的修改确实会导致该类的反序列化失败,则应该为该类的 serialVersionUID 类变量重新分配值。
对类的哪些修改可能导致该类实例的反序列化失败:
--------------------------------------------------------
1. 如果修改类时仅仅修改了方法,则反序列化不受任何影响,类定义无须修改 serialVersionUID 类变量的值
2. 如果修改类时仅仅修改了静态变量或瞬态实例变量,则饭序列化不受任何影响,类定义无须修改 serialVersionUID 值
3. 如果修改类时修改了非瞬态的实例变量的值,则可能导致序列化版本不兼容。
如果対向流中的对象和新类中包含同名的实例变量,而实例变量类型不同,则反序列化失败,类定义应该更新 serialVersionUID 值;
如果対向流中的对象比新类中包含更多的实例变量,则多出的实例变量值被忽略,序列化版本可以兼容,类定义可以不更新 serialVersionUID 值;
如果新类比対向流中的对象包含更多的实例变量,则序列化版本也可以兼容,类定义可以不更新 serialVersionUID 值,但反序列化得到的新对象中多出的实例变量值都是 null (引用类型实例变量)或 0 (基本类型实例变量)。
*
*
*
9. NIO
-----------------------------------------------------------------------------------------------------------------------------
前面介绍的输入流、输出流都是阻塞式的输入、输出,不仅如此,传统的输入流、输出流都是通过字节的移动处理的(即使不直接去处理字节流,底层的实现还以依赖于字节处理),
也就是说,面向流的输入/输出系统一次只能处理一个字节,因此,面向流的输入/输出系统通常效率都不高。
JDK 1.4 开始, Java 提供了一些列改进的输入/输出处理的新功能,这些功能被统称为新IO ( New IO 简称 NIO ),新增了许多用于处理输入输出的类,
这些类在 java.nio 包及子包下,并且对原 java.io 包中的很多类都以 NIO 为基础进行了改写,新增了满足 NIO 的功能
Java 新 IO 概述
------------------------------------------------------------------------------------
新 IO 采用内存映射文件的方式来处理输入/输出, 新 IO 将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了,通过这种方式来进行输入/输出币传统的输入输出要快得多。
Java 中 新 IO 相关的包:
ava.nio : 主要包含各种与 Buffer 相关的类
java.nio.channels :主要包含与 Channel 和 Selector 相关的类
java.nio.charset :主要包含与字符集相关的类
java.nio.channels.spi :主要包含与 Channel 相关的服务提供者编程接口
java.nio.charset.spi :主要包含与字符集相关的服务提供者编程接口
Channel(通道)和 Buffer(缓冲)是NIO中的两个核心对象。
Channel 是对传统的输入/输出系统的模拟,在NIO系统中,所有的数据都需要通过通道传输; Channel 与传统的 InputStream/OutputStream 最大的区别在于它提供了一个 map() 方法,
通过 map() 方法可以直接将“一块数据”映射到内存中。 如果说传统的输入输出系统是面向流的处理,则 新IO 是面向块的处理。
Buffer 可以被理解成一个容器,它的本质是一个数组,发送到 Channel 中的所有对象都必须首先放到 Buffer 中,而从 Channel 中读取的数据也必须先放到 Buffer 中。
除了 Channel 和 Buffer 之外,新IO 还提供了用于将 Unicode 字符串映射成字节序列以及逆映射操作的 Charset 类,也提供了用于支持非阻塞式输入输出的 Selector 类
使用 Buffer
--------------------------------------------------------------------------------------
NIO 中一个主要的特性是 java.nio.Buffer ,缓冲区( Buffer)提供了一个比流抽象的、更高效和可预测的 I/O 。 Buffer 代表了一个有限容量的容器 —— 其本质是一个数组,通道 Channel 使用 Buffer 实例来传递数据。
从编程角度看,流和通道的差别在于流是基于字节的,而通道是基于块的(也就是 Buffer)。
流设计为一个字节接着一个字节、按顺序提供数据,虽然基于性能考虑,流也可以传输字节数组,但基本的流概念要求一次传送一个字节。
通道不同,通道使用的是缓冲区中的数据块,即 Buffer , 在读写通道数据时,这些数据必须存储在缓冲区中,一次读写一个缓冲区的数据。
从内部结构看, Buffer 就像一个数组,它可以保存多个类型相同的数据。
Buffer 是一个抽象类,其最常用的子类是 ByteBuffer, 它可以在底层字节数组上进行 get/set 操作。
除了 ByteBuffer 之外,对应于其他基本数据类型( boolean 除外)都由相应的Buffer 类:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
除了 ByteBuffer 之外,它们都采用相同或相似的方法来管理数据,只是各自管理的数据类型不同而已,
这些 Buffer 类都没有提供构造器,通过使用如下静态方法来得到一个 Buffer 对象:
static XxxBuffer allocate(int capacity) : 创建一个容量为 capacity 的 XxxBuffer 对象
Buffer 中的三个重要概念:
容量(capacity):缓冲区的容量表示该 Buffer 的最大数据容量,即最多可以存储多少数据。缓冲区的容量不能为负值,创建后不能改变
界限(limit): 第一个不应该被读出或者被写入的缓冲区位置索引。也就是说,位于 limit 后的数据即不可读也不可写
位置(position): 用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)。
除此之外, Buffer 还支持一个可选的标记 mark , 类似于传统IO流中的mark, Buffer 允许直接将 position 定位到 mark 处。
这些值满足如下关系:
0 <= mark <= position <= limit <= capacity
Buffer 的主要作用就是装入数据,然后输出数据。开始时Buffer的 position 为0, limit 为 capacity, 程序可以通过 put() 方法向Buffer中放入一些数据(或者从 Channel 中获取一些数据),
每放入一些数据, Buffer 的 position 相应地向后移动一些位置。
当Buffer装入数据结束后,调用Buffer 的 flip() 方法,该方法将 limit 设置为 position 所在的位置,并将 position 设为0,这就使得Buffer的读写指针又移到了开始位置。
也就是说,Buffer 调用 flip() 方法之后, Buffer 为输出数据做好了准备;
当Buffer 输出数据结束之后, Buffer 调用 clear() 方法,clear() 方法不是清空Buffer 的数据,它仅仅将 position 置为0,将 limit 置为 capacity, 这样为再次向Buffer 中装入数据做好准备
Buffer flip(): 为从Buffer中取出数据做好准备
Buffer clear(): 为再次向Buffer中装入数据做好准备
Buffer 中的一些常用方法
--------------------------
int capacity():返回Buffer 的 capacity 大小
boolean hasRemaining() :判断当前位置( position)和界限( limit)之间是否还有元素可供处理
int limit():返回 Buffer 的界限( limit)的位置
Buffer limit(int newLimit): 重新设置界限( limit) 的值,返回 Buffer 对象
int position():返回 Buffer 中 position 值
Buffer position(int newPosition):设置 Buffer 的 position, 并返回 position 被修改后的 Buffer 对象
int remaining():返回当前位置和界限( limit)之间的元素个数
Buffer reset():将位置( position)转到 mark 所在的位置
Buffer rewind():将位置设置为0,取消mark设置
Buffer 的所有子类提供了两个重要方法:用于向Buffer中放入数据和从Buffer中取出数据
put():
get():
当使用 put()和get()方法放入、取出数据时,Buffer即支持对单个数据的访问,也支持对批量数据的访问(以数组为参数)
当使用 put()和get()方法访问Buffer中的数据时,分为相对和绝对两种
相对( Relative): 从Buffer的当前position处开始读取或写入数据,然后将位置( position)的值按处理元素个数增加
绝对( Absolute): 直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时,并不会影响位置( position)的值
例子:
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());
// 调用flip()方法
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());
}
}
使用 Channel
-----------------------------------------------------------------------------------------------------------------------------
Channel 类似于传统的流对象,但与传统的流对象有两个主要的区别:
1. Channel 可以直接将指定文件的部分或全部直接映射成 Buffer
2. 程序不能直接访问Channel中的数据,包括读、写都不行,Channel 只能与 Buffer 进行交互。
如果要从Channel中取得一些数据,必须先用 Buffer 从 Channel 中取出一些数据,然后让程序从 Buffer 中取出数据。
如果要将程序中的数据写入Channel,先让程序将数据放入 Buffer 中,程序在将 Buffer 中的数据写入Channel
Java 为Channel接口提供了如下实现类:
DatagramChannel :支持UDP网络通信 Channel
FileChannel :文件操作 Channel
Pipe.SinkChannel :进程间管道通信 Channel
Pipe.SourceChannel :进程间管道通信 Channel
SelectableChannel :A channel that can be multiplexed via a Selector
ServerSocketChannel :支持 TCP 网络通信的 Channel
SocketChannel :支持 TCP 网络通信的 Channel
...
所有的 Channel都不应该通过构造器来直接创建,而是通过传统节点 InputStream 、 OutputStream 的 getChannel() 方法返回对应的 Channel, 不同的节点流获得的 Channel 不一样。
FileInputStream , FileOutputStream 的 getChannel() 返回 FileChannel
PipedInputStream , PipedOutputStream 的 getChannel() 返回 Pipe.SinkChannel, Pipe.SourceChannel
Channel 中最常用的三类方法:
abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) : 用于将 Channel 对应的部分或全部数据映射成 ByteBuffer
read() : 一系列重载方法,读取数据
write():一系列重载方法,写入数据
例子:
public class FileChannelTest
{
public static void main(String[] args)
{
File f = new File("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()); // ①
// 使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
// 直接将buffer里的数据全部输出
outChannel.write(buffer); // ②
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
RandomAccessFile 中也包含 getChannel() 方法,返回的 FileChannel 是只读还是读写的,取决于 RandomAccessFile 打开文件的方式
例子:
public class RandomFileChannelTest
{
public static void main(String[] args)
throws IOException
{
File f = new File("a.txt");
try(
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel())
{
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel
.MapMode.READ_ONLY, 0 , f.length());
// 把Channel的记录指针移动到最后
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
}
如果习惯于传统IO的多次读取过程,或者担心 Channel 对应的文件过大,使用 map() 方法一次将所有文件的内容映射到内存引起性能下降,也可以使用 Channel 和 Buffer 传统的多次读取过程
例子:
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
// 创建文件输入流
FileInputStream fis = new FileInputStream("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("GBK");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}
字符集和 Charset
---------------------------------------------------------------------------------------------------------------------------------
Encode : 把明文的字符序列转换成计算机理解的二进制序列称为编码
Decode : 把二进制序列转换成明文字符序列称为解码
当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列
当需要读取文本文件时,程序必须把二进制序列转换为一个个的字符
Java 默认使用 Unicode 字符集,但很多操作系统并不使用 Unicode 字符集,那么当从系统中读取数据到 Java 程序时,就可能出现乱码问题
JDK 1.4 提供了 Charset 来处理字节序列和字符序列(字符串)之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取 Charset 所支持字符集方法
Instances of this class are immutable.
Charset 类提供了如下静态方法,获取当前 JDK 所支持的所有字符集
static SortedMap<String,Charset> availableCharsets()
例子:
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));
}
System.out.println("当前系统使用的字符集:" + System.getProperty("file.encoding"));
}
}
知道了字符集别名之后,程序可以调用
static Charset forName(String charsetName)
静态方法创建对应的 Charset 对象, charsetName 参数就是相应字符集的别名
Charset cs = Charset.forName("GBK");
获得 Charset 之后,就可以通过该对象的 newDecoder(), newEncoder() 这两个方法分别返回 CharsetDecoder 和 CharsetEncoder 对象,
代表该 Charset 的解码器和编码器。
调用 CharsetDecoder 的 decode() 方法就可以将 ByteBuffer (字节序列)转换成 CharBuffer (字符序列)
调用 CharsetEncoder 的 encode() 方法就可以将 CharBuffer 或 String (字符序列)转换成 ByteBuffer (字节序列)
Java 7 新增了一个 StandardCharsets 类,该类定义了一些常用的字符集常量
static Charset ISO_8859_1
static Charset US_ASCII
static Charset UTF_16
static Charset UTF_16BE
static Charset UTF_16LE
static Charset UTF_8
例子:
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));
}
}
Charset 类也提供了如下三个便捷方法:
CharBuffer decode(ByteBuffer bb) :将 ByteBuffer 中的字节序列转换成字符序列的便捷方法
ByteBuffer encode(CharBuffer cb) :将 CharBuffer 中的字符序列转换成字节序列的便捷方法
ByteBuffer encode(String str) :将 String 中的字符序列转换成字节序列的便捷方法
String 类也提供了如下两个重载方法,用系统默认或指定的字符集将字符串转换成字节序列
byte[] getBytes() :Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array.
byte[] getBytes(Charset charset) :Encodes this String into a sequence of bytes using the given charset, storing the result into a new byte array.
文件锁
--------------------------------------------------------------------------
如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效阻止多个进程并发修改同一个文件
在 NIO 中,Java提供了 FileLock 支持文件锁定功能,在 FileChannel 中提供了 lock()/tryLock() 方法可以获得文件锁 FileLock 对象, 从而锁定文件。
lock() 和 tryLock() 方法存在区别:
当 lock() 试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;
tryLock() 是尝试锁定文件,它将直接返回而不是阻塞,如果获得了该文件锁,该方法返回该文件锁,否则返回 null
如果 FileChannel 只想锁定文件的部分内容,而不是锁定全部,则可以使用如下重载方法:
FileLock lock(long position, long size, boolean shared)
FileLock tryLock(long position, long size, boolean shared)
当 shared 为 true 时,表明是一个共享锁,它将允许多个进程来读取该文件,但阻止其他进程获得对该文件的排它锁
当 shared 为 false时,表明该锁是一个排它锁,它将锁住对该文件的读写。
程序可以通过调用 FileLock 的 isShared() 来判断它获得的锁是否为共享锁
直接使用 lock() , tryLock() 方法获得的文件锁是排它锁
处理完文件后通过调用 FileLock 的 release() 方法释放文件锁。
例子:
public class FileLockTest
{
public static void main(String[] args)
throws Exception
{
try(
// 使用FileOutputStream获取FileChannel
FileChannel channel = new FileOutputStream("a.txt")
.getChannel())
{
// 使用非阻塞式方式对指定文件加锁
FileLock lock = channel.tryLock();
// 程序暂停10s
Thread.sleep(10000);
// 释放锁
lock.release();
}
}
}
文件锁虽然可以用于控制并发访问,但对于高并发访问的情形,还是推荐使用数据库来保存程序信息,而不是文件
关于文件锁还需要指出如下几点:
1. 在某些平台上,文件锁仅仅是建议性的,并不是强制的。这意味着即使一个程序不能获得文件锁,也可以对该文件进行读写
2. 在某些平台上,不能同步地锁定一个文件并把它映射到内存中
3. 文件锁是由 Java 虚拟机所持有的,如果两个 Java 程序使用同一个 Java 虚拟机运行,则它们不能对同一个文件进行加锁。
4. 在某些平台上关闭 FileChannel 时,会释放 Java 虚拟机在该文件上的所有锁,因此应该避免对同一个被锁定的文件打开多个 FileChannel.
*
*
*
10. Java 7 的 NIO.2
---------------------------------------------------------------------------------------------------------------------------
Java 7 对原有的 NIO 进行了重大改进,改进主要包括如下两个方面内容
1. 提供了全面的文件 IO 和文件系统访问支持 :表现为 Java 7 新增的 java.nio.file 包及各个子包
2. 基于异步 Channel 的 IO : 表现为 Java 7 在 java.nio.channels 包下增加了多个以 Asynchronous 开头的 Channel 接口和类
Java 7 把这种改进称为 NIO.2
Path, Paths 和 Files 核心 API
--------------------------------------------------------------------------
NIO.2 为了弥补 File 类的不足,引入了 Path 接口,Path 接口代表一个平台无关的平台路径
NIO.2 还提供了 Files , Paths 两个工具类,其中 Files 包含了大量的静态工具方法来操作文件, Paths 则包含了两个返回 Path 的静态工厂方法。
例子:
public class PathTest
{
public static void main(String[] args)
throws Exception
{
// 以当前路径来创建Path对象
Path path = Paths.get(".");
System.out.println("path里包含的路径数量:"
+ path.getNameCount());
System.out.println("path的根路径:" + path.getRoot());
// 获取path对应的绝对路径。
Path absolutePath = path.toAbsolutePath();
System.out.println(absolutePath);
// 获取绝对路径的根路径
System.out.println("absolutePath的根路径:"
+ absolutePath.getRoot());
// 获取绝对路径所包含的路径数量
System.out.println("absolutePath里包含的路径数量:"
+ absolutePath.getNameCount());
System.out.println(absolutePath.getName(3));
// 以多个String来构建Path对象
Path path2 = Paths.get("g:" , "publish" , "codes");
System.out.println(path2);
}
}
Files 是一个操作文件的工具类,提供了大量便捷的工具方法
例子:
public class FilesTest
{
public static void main(String[] args)
throws Exception
{
// 复制文件
Files.copy(Paths.get("FilesTest.java")
, new FileOutputStream("a.txt"));
// 判断FilesTest.java文件是否为隐藏文件
System.out.println("FilesTest.java是否为隐藏文件:"
+ Files.isHidden(Paths.get("FilesTest.java")));
// 一次性读取FilesTest.java文件的所有行
List<String> lines = Files.readAllLines(Paths
.get("FilesTest.java"), Charset.forName("gbk"));
System.out.println(lines);
// 判断指定文件的大小
System.out.println("FilesTest.java的大小为:"
+ Files.size(Paths.get("FilesTest.java")));
List<String> poem = new ArrayList<>();
poem.add("水晶潭底银鱼跃");
poem.add("清徐风中碧竿横");
// 直接将多个字符串内容写入指定文件中
Files.write(Paths.get("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());
}
}
使用 FileVisitor 遍历文件和目录
--------------------------------------------------------------------------------------
Files 类提供了如下两个方法遍历文件和目录:
static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) : 遍历 start 路径下的所有文件和目录
static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) :该方法最多遍历 maxDepth 深度的文件
FileVisitor 代表一个文件访问器,walkFileTree() 方法会自动遍历 start 路径下的所有文件和目录,遍历文件和目录都会触发 FileVisitor 中相应的方法
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) : 访问文件失败时触发
返回值 FileVisitResult 对象是一个枚举类,代表访问之后的后续行为:
CONTINUE :代表继续访问的后续行为
SKIP_SIBLINGS :代表继续访问的后续行为,但不访问该文件或目录的兄弟文件或目录
SKIP_SUBTREE : 代表继续访问的后续行为, 但不访问该文件或目录的子目录树
TERMINATE :代表终止访问的后续行为
实际编程时没有必要为 FileVisitor 的4个方法都提供实现,可以继承 SimpleFileVisitor<T> 来实现自己的文件访问器,这样根据需要,有选择地重写指定的方法就可以了
例子:
public class FileVisitorTest
{
public static void main(String[] args)
throws Exception
{
// 遍历g:\publish\codes\15目录下的所有文件和子目录
Files.walkFileTree(Paths.get("d:", "java", "Java资料" , "pdf注解", "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;
}
});
}
}
使用 WatchService 监控文件变化
-----------------------------------------------------------------------------
NIO2 的 Path 接口提供了如下重载方法监听文件系统的变化
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)
WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
用 watcher 监听该 Path 代表的目录下文件变化, events 参数指定要监听哪些类型事件
WatchService 代表一个文件系统监听服务,它负责监听 path 代表的目录下的文件变化,
一旦使用 register() 方法完成注册,接下来就可以调用 WatchService 的方法类获取被监听目录的文件变化事件
WatchKey poll():获取下一个 WatchKey , 如果没有 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;
}
}
}
}
访问文件属性
--------------------------------------------------------------------------------
Java 7 NIO2 的 java.nio.file.attribute 包下提供了大量的工具类,通过这些工具类,开发者可以非常简单地读取、修改文件属性
XxxAttributeView : 代表某种文件属性的视图
XxxAttributes : 代表某种文件属性的集合,程序一般通过XxxAttributeView 对象获取 XxxAttributes
FileAttribute<T> 是其他 XxxAttributeView 的父接口。
AclFileAttributeView :通过 AclFileAttributeView,开发者可以为特定的文件设置 ACL (Access Control List) 及文件所有者属性。
List<AclEntry> getAcl() :返回值代表该文件的权限集
void setAcl(List<AclEntry> acl) :设置修改该文件的 ACL
DosFileAttributeView : 主要用于获取或修改文件的 DOS 相关属性,比如文件是否只读、隐藏、是否为系统文件、是否是存档文件等
DosFileAttributes readAttributes() :返回 DosFileAttributes 对象, 由 DosFileAttributes 对象获取相关属性值
void setArchive(boolean value):
void setHidden(boolean value):
void setReadOnly(boolean value):
void setSystem(boolean value):
FileOwnerAttributeView : 主要用于获取或修改文件所有者
UserPrincipal getOwner():
void setOwner(UserPrincipal owner) :
PosixFileAttributeView : 主要用于获取或设置 POSIX ( Portable Operating System Interface of UNIX) 属性
PosixFileAttributes readAttributes():
void setPermissions(Set<PosixFilePermission> perms):
UserDefinedFileAttributeView : 可以让开发者自定义一些属性
例子:
public class AttributeViewTest
{
public static void main(String[] args)
throws Exception
{
// 获取将要操作的文件
Path testPath = Paths.get("AttributeViewTest.java");
// 获取访问基本属性的BasicFileAttributeView
BasicFileAttributeView basicView = Files.getFileAttributeView(
testPath , BasicFileAttributeView.class);
// 获取访问基本属性的BasicFileAttributes
BasicFileAttributes basicAttribs = basicView.readAttributes();
// 访问文件的基本属性
System.out.println("创建时间:" + new Date(basicAttribs
.creationTime().toMillis()));
System.out.println("最后访问时间:" + new Date(basicAttribs
.lastAccessTime().toMillis()));
System.out.println("最后修改时间:" + new Date(basicAttribs
.lastModifiedTime().toMillis()));
System.out.println("文件大小:" + basicAttribs.size());
// 获取访问文件属主信息的FileOwnerAttributeView
FileOwnerAttributeView ownerView = Files.getFileAttributeView(
testPath, FileOwnerAttributeView.class);
// 获取该文件所属的用户
System.out.println(ownerView.getOwner());
// 获取系统中guest对应的用户
UserPrincipal user = FileSystems.getDefault()
.getUserPrincipalLookupService()
.lookupPrincipalByName("guest");
// 修改用户
ownerView.setOwner(user);
// 获取访问自定义属性的FileOwnerAttributeView
UserDefinedFileAttributeView userView = Files.getFileAttributeView(
testPath, UserDefinedFileAttributeView.class);
List<String> attrNames = userView.list();
// 遍历所有的自定义属性
for (String name : attrNames)
{
ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
userView.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();
System.out.println(name + "--->" + value) ;
}
// 添加一个自定义属性
userView.write("发行者", Charset.defaultCharset()
.encode("疯狂Java联盟"));
// 获取访问DOS属性的DosFileAttributeView
DosFileAttributeView dosView = Files.getFileAttributeView(testPath
, DosFileAttributeView.class);
// 将文件设置隐藏、只读
dosView.setHidden(true);
dosView.setReadOnly(true);
}
}