文章目录
- I/O基础
- 一丶I/O流
- 二丶文件IO(NOI.2特性)
- `2.1 路径(Path)`
- `2.2 文件操作(File Operations)`
- `2.2.1 预备知识`
- `2.2.2 检查文件/目录(Checking a File or Directory)`
- `2.2.3 删除文件/目录`
- `2.2.4 拷贝文件/目录(Copying a File or Directory)`
- `2.2.5 移动文件/目录`
- `2.2.6 管理元数据{ Managing Metadata (File and File Store Attributes) }`
- `2.2.7 读/写/创建/打开文件`
- `2.2.8 随机文件访问(Random Access Files)`
- `2.2.9 创建/读取目录`
- `2.2.10 链接,符号,其它(Links, Symbolic or Otherwise)`
- `2.2.11 遍历文件树(Walking the File Tree)`
- `2.2.12 监视目录的改变`
- `2.3 其它有用的方法`
- `2.4 老版本的文件I/O代码(Legacy File I/O Code)`
- `2.5 遍历文件树的例子代码`
I/O基础
- I/O Streams。java.io;I/O流,极大的简化了I/O操作。
- serialization。序列化,能够读写被对象。
- File I/O,File system operations。java.nio.file;文件IO,文件系统操作。
一丶I/O流
流支持许多不同的数据,例如简单的byte数据,基本数据类型,区域化字符,对象类型。(从基本的数据类型,到高级的对象)
不管怎样的数据类型,所有的流对于程序来说都呈现出一致的简单模型:输入流读数据,输出流写数据。
数据源,接收数据的目的地址,能够是任何存储,生成,消费数据的地方。例如:磁盘文件,程序,外部设备,网络socket,数组。
我们使用的程序实例都以下面的文件 “xanadu.txt” 作为例子:
In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.
1. 字节流(byte stream)
byte stream。输入输出8位(8-bit)的字节(byte)。所有比特流的类都继承自InputStream and OutputStream。有许多的byte stream类,我们先以FileInputStream and FileOutputStream为例子, 其它类型byte stream类的用法与这两个很相似,不同之处在于被构造的方式不一样。
public abstract class InputStream // InputStream是抽象类
extends Object
implements Closeable
public abstract class OutputStream // OutputStream同样是抽象类
extends Object
implements Closeable, Flushable
举个例子:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyBytes {
public static void main(String[] args) throws IOException {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("xanadu.txt");
out = new FileOutputStream("outagain.txt");
int b;
while( (b = in.read()) != -1 ){
out.write(b);
}
} finally {
if(in != null){
in.close();
}
if(out != null){
out.close();
}
}
}
}
字节流是一种低级的I/O,我们应该避免使用,它只被用在最原始的I/O。由于上面的文本xanadu.txt是字符数据,所以我们最好使用下面我们要介绍的字符流(character streams)
1.1 Java中的大小端问题
参考博客链接:关于字节流(byte流)读取大小端的问题
2. 字符流(Character Streams)
Java使用Unicode存储字符,Character stream I/O会将这种内部形式与地方性的字符集互相转换。例如,在西方的语言环境中,地方字符集通常是ASCII码的8-bit superset。
如果I18n不是首要关心条件的话,我们可以不必关心字符集的问题,如果字符集问题挺重要的话,我们可以参考文档 Internationalization。
2.1 字符流的基本使用
所有的字符流都继承自 Reader 和Writer抽象类。与字节流一样,file I/O在字符流中也有相应的类:
public abstract class Reader // Reader:读字符的抽象类
extends Object
implements Readable, Closeable
public abstract class Writer // Writer:写字符的抽象类
extends Object
implements Appendable, Closeable, Flushable
public class CopyCharacters {
public static void main(String[] args) throws IOException {
FileReader in = null;
FileWriter out = null;
try {
in = new FileReader("xanadu.txt");
out = new FileWriter("characteroutput.txt");
int c;
while ((c = in.read()) != -1) {
//char ch = (char)c;
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
注意到byte stream和characters Stream都使用int来接收读取的返回值,然后CopyCharacters程序中,返回值只占int类型的后16位,而CopyBytes则是后8位。
2.2 使用字节流的字符流(Character Streams that Use Byte Streams)
字符流通常是字节流的包装。在物理上,字符流仍然是使用字节流来执行I/O操作,字符流则处理字符和字节之间的转化。例如:FileReader使用FileInputStream,而FileWriter`使用FileOutputStream
有两种byte-to-character "bridge" streams
:InputStreamReader
and OutputStreamWriter
。
如果没有相关的字符包装类符合你的预期,那么可以考虑使用这两个"桥接"流,从字节流中创建字符流。
2.3 面向行的I/O(Line-Oriented I/O)
字符流的I/O操作通常发生在一个更大的数据单元上,而不仅仅是单个字符。常用的一个数据单元就是行:一行代表着一串以行终结符结尾的字符。行终结符可能为"\r\n"
,"\r"
,"\n"
。
现在我们修改上面的CopyCharacters
代码,使其能够支持面向行的I/O。为了支持行I/O,我们需要使用新的类:BufferedReader 和PrintWriter,关于缓冲流,我们之后会讲。
// 核心的操作就是:
// BufferedReader.readLine // 读入一行
// PrintWriter.println // 写入一行
public class CopyLines {
public static void main(String[] args) throws IOException {
BufferedReader inputStream = null;
PrintWriter outputStream = null;
try {
inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new PrintWriter(new FileWriter("characteroutput.txt"));
String l;
while ((l = inputStream.readLine()) != null) {
outputStream.println(l);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
3. 缓冲流(Buffered Streams)
3.1 核心概念/思想
我们之前看到的例子大多都是非缓冲流。
非缓冲,意味着每次读/写请求直接被底层操作系统来处理。
这可能会导致程序的效率不高,因为它的每次都会触发底层操作系统的响应,如:硬盘访问,网络活动,或者其它开销昂贵的操作。
为了减少此类开销,Java平台实现了缓冲流。缓冲输入流从某块内存读数据,我们称此区域为缓冲区(buffer),只有当buffer为空的时候,底层的输入API(the native input API)才会被调用。类似的,缓冲输出流也会写数据到一块缓冲区,只有当缓冲区满的时候,底层的输出API(the native output API)才会被调用。
程序能够通过包裹的方式来将一个非缓冲流转化为一个缓冲流——非缓冲流对象作为缓冲流类的构造方法参数。如:
in = new BufferedReader(new FileReader("xanadu.txt"));
out = new BufferedWriter(new FileWriter("characteroutput.txt"));
Java中有4个缓冲流用来包裹非缓冲流:
- BufferedInputStream 和BufferedOutputStream 用来包裹字节流,形成缓冲字节流。
- BufferedReader 和BufferedWriter 用来包裹字符流,形成缓冲字符流。注意,前面提到的PrintWriter虽然是面向行的流,但它并非缓冲流,它用来实现格式化输出的,后面的4.Scanner and formatting会讲。
3.2 刷新缓冲流(Flushing Buffered Streams)
在特定的零界点将缓冲区中的数据写出去,而非等待它填满;这种操作我们通常叫做刷新缓冲区/刷洗缓冲区(flushing the buffer)。
一些缓冲输出流支持自动刷新(autoflush)——通过构造器的一个可选参数。当自动刷新开启时,特定的事件能够导致缓冲区被刷新。例如,PrintWriter
对象在每次调用println
或者format
的时候,会自动刷新缓冲区。
当然,我们也能够手动调用flush
方法来刷新缓冲流。flush
方法在任意输出流上都是一个合理的操作,但是它只对缓冲流产生实质性的效果。
4. Scanner 与 formatting
4.1 Scanner
// java.util.Scanner
public final class Scanner implements Iterator<String>, Closeable {/**/}
Scanner对象将格式化的输入分离成一个个的tokens,然后根据数据类型转化单独的tokens。
默认情况下,scanner使用空白符来分离tokens,也就是Character.isWhitespace所说的空白符。下面给出一个使用Scanner的例子:
public class ScanXan {
public static void main(String[] args) throws IOException {
Scanner s = null;
try {
s = new Scanner(new BufferedReader(new FileReader("xanadu.txt")));
while (s.hasNext()) {
System.out.println(s.next());
}
} finally {
if (s != null) {
s.close();
}
}
}
}
注意到,即使Scanner并不是一个流,你仍然需要关闭它表示你已经处理完它的底层流。
为了使用自定义的token分隔符,我们可以调用方法useDelimiter(RegExp)
,如:
s.useDelimiter(",\\s*");//指定一个正则表达式
4.1.2 转化成单独的tokens
scanner支持分离出String类型,7中基本数据类型(8种中除了char),BigInteger,BigDecimal,还有支持区域化分隔符的的数字。如US中的"32,767"表示"32767"。我们使用nextXX()
来分离出不同的数据类型,使用useLocale
函数来指定区域。
// 下面打印输出为:
public class ScanSum {
public static void main(String[] args) throws IOException {
Scanner s = null;
double sum = 0;
try {
s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt")));
s.useLocale(Locale.US);
while (s.hasNext()) {
if (s.hasNextDouble()) {
sum += s.nextDouble();
} else {
s.next();
}
}
} finally {
s.close();
}
System.out.println(sum);
}
}
/* 其中usnumbers.txt文本内容为:
8.5
32,767
3.14159
1,000,000.1 */
4.2 格式化(formatting)
实现格式化的流对象要么是PrintWriter的实例,要么是 PrintStream的实例。
其中PrintWriter是字符流类,而 PrintStream是字节流类。
我们日常使用时,唯一可能使用的PrintStream类就是 System.out and System.err。一般,我们需要创建格式化输出流的时候,我们需要实例化PrintWriter
,而非PrintStream。
print
andprintln
。使用恰当的toString()方法,转化传入的参数值,然后打印出来。format
。参考formatting。
/*
% 1$ +0 20 .10 f
格式开始 参数索引 flags 宽度 精度 conversion
d decimal十进制
f floating point
n 平台无关性的行终结符 \n只会产生`\u000A` linefeed
x 16进制
s string
tB 区域月
*/
5. 命令行下的I/O(I/O from the Command Line)
在命令行下与用户进行交互时,Java平台提供两种方式来支持:标准流(Standard Streams),控制台(Console)。
java平台提供三种标准流:Standard Input, Standard Output,Standard Error。
- Standard Input。 通过 InputStream System.in, 用于输入。
- Standard Output。 通过 PrintStream System.out, 用于输出。
- Standard Error。 通过 PrintStream System.err , 用于输出。
由于历史原因,标准流都是字节流,虽然技术两个标准输出流都是字节流,但是PrintStream
利用了内部的字符流对象来模拟许多字符流的特性。
我们知道System.in是InputStream字节抽象流,为了让其有字符流的特性,我们使用 InputStreamReader
来包裹System.in
。
InputStreamReader cin = new InputStreamReader(System.in);
// 其中InputStreamReader类的声明如下
// java.io.InputStreamReader
public class InputStreamReader extends Reader {/*...*/}
再来看看控制台Console:
public class Password {
public static void main (String args[]) throws IOException {
Console c = System.console();//获取console对象
if (c == null) {//控制台操作不允许,或者OS不支持(程序以非交互式环境启动)
System.err.println("No console.");
System.exit(1);
}
String login = c.readLine("Enter your login: ");
char [] oldPassword = c.readPassword("Enter your old password: ");//安全密码输入
if (verify(login, oldPassword)) {
boolean noMatch;
do {
char [] newPassword1 = c.readPassword("Enter your new password: ");
char [] newPassword2 = c.readPassword("Enter new password again: ");
noMatch = ! Arrays.equals(newPassword1, newPassword2);
if (noMatch) {
c.format("Passwords don't match. Try again.%n");
} else {
change(login, newPassword1);
c.format("Password for %s changed.%n", login);
}
Arrays.fill(newPassword1, ' ');
Arrays.fill(newPassword2, ' ');
} while (noMatch);
}
Arrays.fill(oldPassword, ' ');
}
// Dummy change method.
static boolean verify(String login, char[] password) {
// This method always returns
// true in this example.
// Modify this method to verify
// password according to your rules.
return true;
}
// Dummy change method.
static void change(String login, char[] password) {
// Modify this method to change
// password according to your rules.
}
}
6. 数据流(Data Stream)
Data Stream,支持8种基本数据类型 ,String类型的二进制I/O操作。
所有的数据流,都实现DataInput接口,或者 DataOutput 接口。最常用的接口实现是DataInputStream and DataOutputStream。
public class DataInputStream
extends FilterInputStream
implements DataInput
public class DataOutputStream
extends FilterOutputStream
implements DataOutput
// 其中
public class FilterOutputStream extends OutputStream
下面我们查看一个程序示例:
public class DataStreams {
static final String dataFile = "invoicedata";
static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
static final int[] units = { 12, 8, 13, 29, 50 };
static final String[] descs = { "Java T-shirt",
"Java Mug",
"Duke Juggling Dolls",
"Java Pin",
"Java Key Chain" };
public static void main(String[] args) throws IOException {
DataOutputStream out = null;
try {
out = new DataOutputStream(new
BufferedOutputStream(new FileOutputStream(dataFile)));
for (int i = 0; i < prices.length; i ++) {
out.writeDouble(prices[i]);
out.writeInt(units[i]);
out.writeUTF(descs[i]);
}
} finally {
out.close();
}
DataInputStream in = null;
double total = 0.0;
try {
in = new DataInputStream(new
BufferedInputStream(new FileInputStream(dataFile)));
double price;
int unit;
String desc;
try {
while (true) {
price = in.readDouble();
unit = in.readInt();
desc = in.readUTF();
System.out.format("You ordered %d units of %s at $%.2f%n",
unit, desc, price);
total += unit * price;
}
} catch (EOFException e) { }//这里通过异常来结束读取
System.out.format("For a TOTAL of: $%.2f%n", total);
}
finally {
in.close();
}
}
}
DataOutputStream
只能作为一个已经存在的字节流对象被创建。上面所示的Data(Out/In)putStream为一个 buffered file output byte stream。
[read/write]UTF() 表示以UTF-8的格式读/写String类型的值。
EOFException。DataInputStream会检测程序是否读到文件尾部,而不是我们前面一贯的用读取的返回值来判断。所有DataInput的读取方法都使用EOFException
而非返回值。
7. 对象流(Object Streams)
7.1 简单对象的I/O
上面我们提到数据流(Data Streams),它支持8种基本数据类型和String类型的I/O操作。
现在我们要讲对象流(Object Streams),它支持对象的I/O操作;但是支持是有条件的——它们必须实现标记接口(marker interface) Serializable.
// java.io.Serializable
public interface Serializable {
}
// 此接口没有任何class body,内部没有任何声明。
我们使用的对象流为: ObjectInputStream 和ObjectOutputStream类。它们都实现了 ObjectInput 和ObjectOutput接口,而这两个接口是上一节两个谈到的两个接口的子接口——DataInput
和DataOutput
两个接口。也就是说Data Stream实现的基本数据类型的I/O方法,在Object Streams种仍然成立;所以对象流能混合基本数据类型和对象。
下面给出一个对象流的例子:
public class ObjectStreams {
static final String dataFile = "invoicedata";
static final BigDecimal[] prices = { //Object
new BigDecimal("19.99"),
new BigDecimal("9.99"),
new BigDecimal("15.99"),
new BigDecimal("3.99"),
new BigDecimal("4.99") };
static final int[] units = { 12, 8, 13, 29, 50 };//primitive type
static final String[] descs = { "Java T-shirt", //"primitive" type String
"Java Mug",
"Duke Juggling Dolls",
"Java Pin",
"Java Key Chain" };
public static void main(String[] args)
throws IOException, ClassNotFoundException {
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new
BufferedOutputStream(new FileOutputStream(dataFile)));
out.writeObject(Calendar.getInstance());
for (int i = 0; i < prices.length; i ++) {
out.writeObject(prices[i]);
out.writeInt(units[i]);
out.writeUTF(descs[i]);
}
} finally {
out.close();
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new
BufferedInputStream(new FileInputStream(dataFile)));
Calendar date = null;
BigDecimal price;
int unit;
String desc;
BigDecimal total = new BigDecimal(0);
date = (Calendar) in.readObject();// read a Object
System.out.format ("On %tA, %<tB %<te, %<tY:%n", date);
try {
while (true) {
price = (BigDecimal) in.readObject();
unit = in.readInt();
desc = in.readUTF();
System.out.format("You ordered %d units of %s at $%.2f%n",
unit, desc, price);
total = total.add(price.multiply(new BigDecimal(unit)));
}
} catch (EOFException e) {}
System.out.format("For a TOTAL of: $%.2f%n", total);
} finally {
in.close();
}
}
}
代码与Data Streams的代码做对比,非常简单;有一点注意事项:
readObject()返回一个Object类型的对象,我们需要强制类型转换,可能会抛出 ClassNotFoundException
异常。
7.2 复杂对象的I/O
7.2.1 场景1
我们考虑这种情况:
// 假设对象a引用对象b,c;而对象b引用对象d,e。
class A{
B b;
C c;
}
class B{
D d;
E e;
}
假设现在有一个类型为A的对象a,当我们调用writeobject(a)时,会发生什么?
对象流不仅仅写对象a,而是将所有能够重建a对象的相关对象写入。 如下图所示:
/*
S t r e a m
_____________________________________
writeobject(a) ----→ ... | c | e | d | b | a | ... ----→ readObject()
_____________________________________
a a
↙ ↘ ↙ ↘
b c b c
↙ ↘ ↙ ↘
d e d e
I/O of multiple referred-to objects
*/
// 阅读下面的源代码,它输出的结果为:
//1
//a: 1, b: 2, c: 3, d: 4, e: 5
interface Interfaces extends Serializable{//必须要让所有类都实现Serializable
int getValue();
}
class A implements Interfaces{
int value = 1;
B b;
C c;
A(){
this.b = new B();
this.c = new C();
}
@Override
public int getValue() {
return this.value;
}
}
class B implements Interfaces{
int value = 2;
D d;
E e;
B(){
this.d = new D();
this.e = new E();
}
@Override
public int getValue() {
return this.value;
}
}
class C implements Interfaces{
int value = 3;
@Override
public int getValue() {
return this.value;
}
}
class D implements Interfaces{
int value = 4;
@Override
public int getValue() {
return this.value;
}
}
class E implements Interfaces{
int value = 5;
@Override
public int getValue() {
return this.value;
}
}
public class ComplexObjectStream {
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a = new A();
ObjectOutputStream objos = new ObjectOutputStream(new BufferedOutputStream(
new FileOutputStream("ComplexObjectStream.out")));
objos.writeObject(a); // writeObject...
if(objos != null) {
objos.close();
}
ObjectInputStream objis = new ObjectInputStream(new BufferedInputStream(
new FileInputStream("ComplexObjectStream.out")));
Interfaces obj = null;
try{
while(true){
obj = (Interfaces)objis.readObject(); // readObject()...
System.out.println(obj.getValue());
}
} catch (EOFException e){ } // EOFException:读完了
finally {
if(objis != null){
objis.close();
}
}
A aRead = (A)obj;//将读到的全部A状态打印出来
System.out.println(
"a: " + aRead.getValue() + ", b: " +
aRead.b.getValue() + ", c: " +
aRead.c.getValue() + ", d: " +
aRead.b.d.getValue() + ", e: " +
aRead.b.e.getValue());
}
}
7.2.1 场景2
同一个输出对象流上的两个对象包含同一个对象的引用,当我们再次读出来的时候,它们的引用关系是怎样的?
Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);
// ....
// 判断重新读出来的 obj1,obj2是否是同一引用?
Object ob1 = in.readObject();
Object ob2 = in.readObject();
boolean flag = obj1 == obj2;
// 输出:
//obj1 == obj2 ? true
// 由于Object没有实现Serializable,我们需要自定义类来实现。
class Obj extends Object implements Serializable{}
public class ComplexObjectStream2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Obj obj = new Obj();
ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
new FileOutputStream("ComplexObjectStream2.out")));
out.writeObject(obj);
out.writeObject(obj);
if(out != null){
out.close();
}
ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(
new FileInputStream("ComplexObjectStream2.out")));
Obj obj1 = (Obj)in.readObject();
Obj obj2 = (Obj)in.readObject();
System.out.println("obj1 == obj2 ? " + (obj1 == obj2));
if(in != null){
in.close();
}
}
}
我们通过上面的例子知道,单个对象的引用被写到同一个对象流中,它们读出来时,仍然是同一个对象的引用。
然而,当我们把当个对象的引用写入到不同的对象流中,它们读出来后将是不同的对象。
二丶文件IO(NOI.2特性)
这里介绍JDK 7中引入的文件I/O机制,对于Java SE 6中的文件I/O介绍比较简略。
java.nio.file包还有与之相关的包java.nio.file.attribute,对文件I/O,访问默认的文件系统提供了很好的支持。虽然API有很多的类,但是你只需要在关心一些切入点后,会发现API是非常直观且易用的。
导读:
- java.nio.file.Path接口。啥是路径?路径操作。
- java.nio.file.Files类。文件操作:检查/删除/拷贝/移动
- 文件I/O,目录I/O。
- Random access files。随机访问文件。
- 遍历文件树,搜索文件,观察目录改变。
- 在文件I/O的代码层面,将Java SE 7之前的代码转变为Java SE 7的代码。
2.1 路径(Path)
目录系统一般为树结构。
路径:
-
/home/sally/statusReport。Posix系统
-
C:\home\sally\statusReport。Windows系统
绝对路径与相对路径。
软链接,链接对应用程序是透明的,也就是说在链接上的操作会自动转向对目标文件的操作。
JDK 7引入的Path类,是java.nio.file包的主要切入点。
Path类有许多方法,能被用来获取路径的信息,访问路径的元素,转换路径变成其它的格式,或者取出路径的一部分。这里介绍路径的一些方法,或者叫做语义操作,因为这些操作并不会访问路径实际的文件系统。
/* 查看Path类的声明,发现
java.nio.file.Path 是一个接口类
implements Comparable接口,可以比较两个接口
implements Iterable接口,可以使用for来遍历
*/
package java.nio.file.Path;
public interface Path
extends Comparable<Path>, Iterable<Path>, Watchable//Watchable见2.2.12.3
{
// ...
}
2.1.1 创建路径
我们能够通过使用Paths.get(...)
的方式来创建一个Path
对象。
Path p1 = Paths.get("/tmp/foo");
Path p2 = Paths.get(args[0]);
Path p3 = Paths.get(URI.create("file:///Users/joe/FileTest.java"));
// Paths.get 是下列代码的简写
Path p4 = FileSystems.getDefault().getPath("/users/sally");
// /u/joe/logs/foo.log or C:\joe\logs\foo.log
Path p5 = Paths.get(System.getProperty("user.home"),"logs", "foo.log");
// java.nio.file.Paths类的完整代码:
public final class Paths {
private Paths() { }//private防止实例化
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more);
}
public static Path get(URI uri) {
String scheme = uri.getScheme();
if (scheme == null)
throw new IllegalArgumentException("Missing scheme");
// check for default provider to avoid loading of installed providers
if (scheme.equalsIgnoreCase("file"))
return FileSystems.getDefault().provider().getPath(uri);
// try to find provider
for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
if (provider.getScheme().equalsIgnoreCase(scheme)) {
return provider.getPath(uri);
}
}
throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not installed");
}
}
2.1.2 获取Path的信息
// None of these methods requires that the file corresponding
// to the Path exists.
// Microsoft Windows syntax
Path path = Paths.get("C:\\home\\joe\\foo");
// Solaris syntax
Path path = Paths.get("/home/joe/foo");
System.out.format("toString: %s%n", path.toString());
System.out.format("getFileName: %s%n", path.getFileName());
System.out.format("getName(0): %s%n", path.getName(0));
System.out.format("getNameCount: %d%n", path.getNameCount());
System.out.format("subpath(0,2): %s%n", path.subpath(0,2));
System.out.format("getParent: %s%n", path.getParent());
System.out.format("getRoot: %s%n", path.getRoot());
Method Invoked | Returns in the Solaris OS | Returns in Microsoft Windows |
---|---|---|
toString | /home/joe/foo | C:\home\joe\foo |
getFileName | foo | foo |
getName(0) | home | home |
getNameCount | 3 | 3 |
subpath(0,2) | home/joe | home\joe |
getParent | /home/joe | \home\joe |
getRoot | / | C:\ |
上面的是绝对路径,我们现在看看相对路径:
// Solaris syntax
Path path = Paths.get("sally/bar");
or
// Microsoft Windows syntax
Path path = Paths.get("sally\\bar");
Method Invoked | Returns in the Solaris OS | Returns in Microsoft Windows |
---|---|---|
toString | sally/bar | sally\bar |
getFileName | bar | bar |
getName(0) | sally | sally |
getNameCount | 2 | 2 |
subpath(0,1) | sally | sally |
getParent | sally | sally |
getRoot | null | null |
2.1.3 移除冗余
/home/./joe/foo -> /home/joe/foo
/home/sally/../joe/foo -> /home/joe/foo
上面的路径都含有冗余部分,我们可以使用path.normalize()
方法来移除冗余元素。
2.1.4 转换路径
有三种方法来转换路径:toUri,toAbsolutePath, toRealPath
Path p1 = Paths.get("/home/logfile");
// Result is file:///home/logfile
System.out.format("%s%n", p1.toUri());
/*toAbsolutePath不会消除冗余
java basisLang.fileio.ConvertPath ./
G:\00Document\01Languages\Java\untitled\src\java\.
java basisLang.fileio.ConvertPath ../../
G:\00Document\01Languages\Java\untitled\src\java\..\..
*/
public class ConvertPath {
public static void main(String[] args) {
if (args.length < 1) {
System.out.println("usage: FileTest file");
System.exit(-1);
}
// Converts the input string to a Path object.
Path inputPath = Paths.get(args[0]);
Path fullPath = inputPath.toAbsolutePath();
System.out.println(fullPath);
}
}
/*toRealPath()
如果传入true参数,则表示支持symbolic links
如果路径为相对路径,则返回绝对路径
如果路径含冗余元素,则返回没有冗余元素的路径
*/
try {
Path fp = path.toRealPath();
} catch (NoSuchFileException x) {
System.err.format("%s: no such" + " file or directory%n", path);
// Logic for case when file doesn't exist.
} catch (IOException x) {
System.err.format("%s%n", x);
// Logic for other sort of file error.
}
2.1.5 合并两个路径
path的resolve (...)
方法
// 1
Path p1 = Paths.get("/home/joe/foo");
System.out.format("%s%n", p1.resolve("bar"));//___ \home\joe\foo\bar
Path p2 = Paths.get("C:\\home\\joe\\foo");
System.out.format("%s%n", p2.resolve("bar"));// ___ C:\home\joe\foo\bar
// 2
Path p3 = Paths.get("/joe/foo");
System.out.format("%s%n", p3.resolve("bar"));// ___ \joe\foo\bar
Path p4 = Paths.get("\\joe\\foo");
System.out.format("%s%n", p4.resolve("home"));// ___ \joe\foo\home
// 3
Path p5 = Paths.get("/joe/foo");
System.out.format("%s%n", p5.resolve("/home/bar"));// ___ \home\bar
Path p6 = Paths.get("\\joe\\foo");
System.out.format("%s%n", p6.resolve("C:\\bar"));// ___ C:\bar
2.1.6 从一个路径到另一个路径
文件系统中,一个位置到另一个位置的路径,使用relativize
来完成。
/*
p1_to_p2的意思是,从p1路径到p2怎么走?
*/
Path p1 = Paths.get("joe");// or ./joe
Path p2 = Paths.get("sally");// or ./sally
Path p1_to_p2 = p1.relativize(p2);// ___ ../sally
Path p2_to_p1 = p2.relativize(p1);// ___ ../joe
Path p3 = Paths.get("home");
Path p4 = Paths.get("home/sally/bar");
Path p3_to_p4 = p1.relativize(p3);// ___ sally/bar
Path p4_to_p3 = p3.relativize(p1);// ___ ../..
System.out.format("%s%n%s%n%s%n", p1_to_p2, p2_to_p1, p3_to_p4, p4_to_p3);
2.1.7 比较两个路径
三个比较相关equals
,compareTo
,startsWith
,endsWith
。
isSameFile
与equals
的区别:
isSameFile
会去检查文件是否存在,而equals不要求文件是否存在。
Path path = ...;
Path otherPath = ...;
Path beginning = Paths.get("/home");
Path ending = Paths.get("foo");
if (path.equals(otherPath)) {
// equality logic here
} else if (path.startsWith(beginning)) {
// path begins with "/home"
} else if (path.endsWith(ending)) {
// path ends with "foo"
}
//
Path path = ...;
for (Path name: path) {
System.out.println(name);
}
String s1 = "C:\\a\\b\\c\\..\\c";
String s2 = "C:\\a\\b\\c\\";
Path path1 = Paths.get(s1);
Path path2 = Paths.get(s2);
System.out.println(path1.equals(path2));// false
// or
String s1 = "C:\\a\\b\\c";
String s2 = "C:\\a\\b\\c\\";
Path path1 = Paths.get(s1);
Path path2 = Paths.get(s2);
System.out.println(path1.equals(path2));// true
String s1 = "C:\\a\\b\\c\\e\\f";
String s2 = "C:\\a\\b\\c\\";
Path path1 = Paths.get(s1);
Path path2 = Paths.get(s2);
System.out.println(path1.startsWith(path2));// true
String s1 = "C:\\a\\b\\cd\\";
String s2 = "C:\\a\\b\\c\\";
Path path1 = Paths.get(s1);
Path path2 = Paths.get(s2);
System.out.println(path1.startsWith(path2));// false
String s1 = "C:\\a\\b\\..\\b\\c\\";
String s2 = "C:\\a\\b\\c\\";
Path path1 = Paths.get(s1);
Path path2 = Paths.get(s2);
System.out.println(path1.startsWith(path2)); // false
2.2 文件操作(File Operations)
Files 类是java.nio.file
包的另一个主要的切入点。此类提供了丰富的静态函数来读写/操作文件/目录。Files
的方法工作上Path
类的实例之上。
在继续学习文件操作之前,我们需要熟悉下面的基本概念。
2.2.1 预备知识
2.2.1.1 系统资源的释放
许多文件操作API中,例如streams ,channels继承或者实现java.io.Closeable 接口。一个Closeable类型的类/资源要求其资源不再读写的时候,close方法必须调用以释放资源。不关心资源的释放很可能会对应用的性能产生消极影响。
2.2.1.2 捕获异常
文件I/O,一般的异常有:文件不存在,无权访问,系统并不支持特定的功能,等等。所有访问文件系统的方法都能够抛出IOException
。
try-with-resources
的使用是一个很好的实践。
// a `try-with-resources` statement
Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
writer.write(s, 0, s.length());
} catch (IOException x) {// IOException
System.err.format("IOException: %s%n", x);
}
// 当然了,我们也能够使用`try-catch-finally`方法来来进行一般的I/O操作
Charset charset = Charset.forName("US-ASCII");
String s = ...;
BufferedWriter writer = null;
try {
writer = Files.newBufferedWriter(file, charset);
writer.write(s, 0, s.length());
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
} finally {
if (writer != null) writer.close();
}
除了一般的IOException,许多异常都继承自FileSystemException
,此类有一些有用的方法:
public String getFile() // 返回被调用的文件
public String getMessage()
public String getReason()
try (/*...*/) {
// ...
} catch (NoSuchFileException x) {
System.err.format("%s does not exist\n", x.getFile());
}
2.2.1.3 可变长参数
下面就是一个可变长参数的例子,已经在Java语言基础的2.1.6讲过。
Path Files.move(Path, Path, CopyOption...)
2.2.1.4 原子操作(Atomic Operations)
Files
的很多方法,都是原子操作,例如上面提到的move方法。原子操作并不能被中断,或者"部分执行",要么整个操作被执行,要么操作执行失败。当有多个处理器处理文件系统的一块区域的时候,你需要保证每个处理器处理完整的一个文件。
2.2.1.5 链式方法调用(Method Chaining)
许多I/O方法支持链式方法:一个方法调用返回一个对象,又在此对象上调用另一个方法,此方法又能够返回一个对象,等等;一直调用下去。
好处:代码紧凑,避免声明没必要的临时对象。
String value = Charset.defaultCharset().decode(buf).toString();
UserPrincipal group =
file.getFileSystem().getUserPrincipalLookupService().
lookupPrincipalByName("me");
2.2.1.6 Glob模式匹配(Glob)
Glob模式源于Unix操作系统。在Unix中有“global”命令,可缩写为glob。Glob模式与正则表达式类似;但是Glob模式的功能不及正则表达式。
Files中有两个方法能够接收Glob参数。
Glob用一个String来表示,并匹配特定的其它String,如目录/文件名。下面是一些Glob的简单的规则:
-
* -----> 匹配字符长度>=0的任意字符
-
** ------> 类似*,crosses directory boundaries,意思是匹配当前目录及其所有子目录。
-
? -------> 匹配仅仅一个字符。
-
花括号指定字符串的匹配集合,其中能够内嵌Glob
- {sun,moon,stars} 匹配其中任意一个
- {temp* , tmp* } 匹配以tmp或者temp开头的。
-
方括号指定单个字符的匹配集合
- [aeiou] 匹配其中任意一个字母(小写字母字母)
- [0-9] 匹配任意数字
- [A-Z] 匹配任意大写
- [a-z,A-Z] 匹配所有大小写字母
- 在
[
和]
里面的*
,?
, and\
仅仅匹配它们自身
-
所有其它的字符串匹配它们自身
-
对于
*
,?
,或者其它特殊字符,我们可以使用\
来转义,如\\
匹配\
,\?
匹配?
下面给出一些例子
- *.html 匹配所有
.html
结尾的串 - ??? 匹配所有只有三个字符的串
- *[0-9]* 匹配所有含有数字的字符串
- *.{htm,html,pdf} 匹配**.html**,.htm,.pdf结尾的串
- a?*.java 匹配:a开头后面跟一个或者多个相同字符,最后以 .java 结尾
- {foo*,*[0-9]*} 匹配:以foo开头,或者含有数字的串。
Glob语法很强大,简单易用。然而它并不总是符合你的需求,此时你可以参考正则表达式。更多有关Glob的语法,可以参考getPathMatcher。
2.2.1.7 Link Awareness
Files
类是"link aware"。也就是说,每个Files的方法当遇到symbolic link的时候,要么它会自动处理,要么提供可选项让用户指定特定的行为。
2.2.2 检查文件/目录(Checking a File or Directory)
之前你已经学会了Path类了,它代表着文件系统中的一个文件或者一个目录,那么,我们现在想知道此路径代表的的文件或者目录是否存在?是否可读?是否可写?是否可执行?这是本小结要解决的任务。
2.2.2.1 验证文件/目录的存在性
// exists 方法
// 具体的实现,我们暂时不关心,
// 但是我们通过下面的代码知道,它是通过异常来实现逻辑功能的。
public static boolean exists(Path path, LinkOption... options) {
try {
if (followLinks(options)) {
provider(path).checkAccess(path);
} else {
// attempt to read attributes without following links
readAttributes(path, BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
}
return true; // file exists
} catch (IOException x) {
return false; // does not exist or unable to determine if file exists
}
}
public static boolean notExists(Path path, LinkOption... options) {
try {
if (followLinks(options)) {
provider(path).checkAccess(path);
} else {
// attempt to read attributes without following links
readAttributes(path, BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
}
return false; // file exists
} catch (NoSuchFileException x) {
return true; // file confirmed not to exist
} catch (IOException x) {
return false;
}
}
// 其中 LinkOption 定义如下
public enum LinkOption implements OpenOption, CopyOption {
NOFOLLOW_LINKS;
}
public interface OpenOption {
}
public interface CopyOption {
}
注意: !Files.exists(path)
与Files.notExists(path)
并非等效。
通过查看上面提供的源码,我们知道这两个函数的代码几乎一样,但是仍然有一个地方不一样,这导致了它们的非等效性。notExists
函数多出来NoSuchFileException
异常捕获。
IOException <- FileSystemException <- NoSuchFileException
当我们测试一个文件的存在性时,它可能会有如下结果:
- 文件不存在
Files.notExists(path) == true 唯一确定
- 文件存在
Files.exists(path) == true 唯一确定
- 文件存在性未知: 当程序无权访问文件的时候会出现这种情况
/*
当我们判断两个条件是否同时为false的时候,可以使用一个或||操作
下面的例子就是
Files.exists(path) || Files.notExists(path);
*/
String pathStr = "C:\\Users\\canliture\\Desktop\\test";
Path path = Paths.get(pathStr);
boolean isExist = Files.exists(path);
boolean bothFlag = isExist || Files.notExists(path);
if(bothFlag == false){ // or if(!bothFlag) { ...
System.out.format("The existence of the path %s is unknown...", pathStr);
} else {
System.out.format("The existence of the path %s is %b ...", pathStr, isExist);
}
我们现在以一个桌面的test目录
为例子来实验此程序。
上面的程序要根据自己桌面的目录进行改动,改动的地方一般为pathStr中的用户名:
// 这里要改为你自己桌面的目录
// 一般情况下是将系列字符串中的canliture改为你自己电脑用户名
String pathStr = "C:\\Users\\canliture\\Desktop\\test";
下面我们可以开始实验,这里的实验环境为Win10,Windows其它版本操作大同小异,类Unix操作系统原理是一样的,可以自己研究。
-
首先保证桌面上没有test目录,运行程序,显然输出为
The existence of the path C:\Users\canliture\Desktop\test is false ...
-
现在我们在桌面创建一个名为
test
的目录。运行程序,一般情况下会得到:The existence of the path C:\Users\canliture\Desktop\test is true ...
-
现在我们
鼠标右键test目录
->安全(Security)
->根据当前用户名选择用户->编辑(edit)->将此用户的全部权限都禁用(deny)->确定 -
再次运行程序,得到结果:
The existence of the path C:\Users\canliture\Desktop\test is unknown...
我们看到,最后得到的结果是
unkown
,证实了当无权限访问时,两个函数都会返回false。
2.2.2.2 查看文件的可访问性
可读性,可写性,可执行性。分别对应下面三个函数
Path file = ...;
boolean isRegularExecutableFile = Files.isRegularFile(file) &
Files.isReadable(file) & Files.isExecutable(file);
许多应用程序都是先检查然后再访问,这可能会有安全问题,具体可以搜索TOCTTOU
作为一个了解。
2.2.2.3 检查是否两个路径所指代的是同一文件/目录
此时我们使用Files.isSameFile(p1, p2)
。
Path p1 = ...;
Path p2 = ...;
if (Files.isSameFile(p1, p2)) {
// Logic when the paths locate the same file
}
如果文件系统使用了symbolic links,那么不同Path可能会指向同一文件。
注意与Path的equals
的比较。
2.2.3 删除文件/目录
Files.delete(path)
或者 deleteIfExists(Path)
删除文件,目录,或者链接。
-
对于symbolic links,删除的是链接,而非链接的目的文件/目录
-
对于目录,目录必须为空,否则会删除失败
Files.delete(path)
删除成功或失败抛出如NoSuchFileException
,DirectoryNotEmptyException
异常。
try {
Files.delete(path);
} catch (NoSuchFileException x) {
System.err.format("%s: no such" + " file or directory%n", path);
} catch (DirectoryNotEmptyException x) {
System.err.format("%s not empty%n", path);
} catch (IOException x) {
System.err.println(x); // File permission problems are caught here.
}
Files.deleteIfExists(path)
在文件不存在的情况下不会抛出NoSuchFileException
异常,只会直接结束函数。这样的特点对于多线程很有好处,也就是在多个线程试图删除同一文件/目录的时候。
2.2.4 拷贝文件/目录(Copying a File or Directory)
可使用下列copy方法来拷贝文件/目录。除非使用REPLACE_EXISTING
选项,否则当目标文件不存在的时候,copy会失败。
// 1. Path -> Path
public static Path copy(Path source, Path target, CopyOption... options)
// 2. 字节流 -> Path
public static long copy(InputStream in, Path target, CopyOption... options)
// 3. Paht -> 字节流
public static long copy(Path source, OutputStream out)
/*
private static long copy(InputStream source, OutputStream sink) // private的
public enum LinkOption implements OpenOption, CopyOption {
NOFOLLOW_LINKS;
}
public enum StandardCopyOption implements CopyOption {
REPLACE_EXISTING, // Replace an existing file if it exists.
COPY_ATTRIBUTES, // Copy attributes to the new file.
ATOMIC_MOVE; // Move the file as an atomic file system operation.
}
public interface CopyOption { }
public interface OpenOption { }
目录能够被拷贝,但是目录内的文件不会被拷贝,所以拷贝之后新的文件是空的。
当拷贝一个symbolic link,那么链接的目标文件会被拷贝。如果偏要拷贝链接本身的话,可以考虑NOFOLLOW_LINKS
或REPLACE_EXISTING
这两个选项。
查看更多:Copying a File or Directory
。
-
REPLACE_EXISTING
。即使目标文件已经存在,仍然拷贝。如果目标是个非空目录,将会抛出FileAlreadyExistsException
异常。 -
COPY_ATTRIBUTES
。拷贝文件相关的属性给目标文件。
2.2.5 移动文件/目录
使用move
方法来移动文件/目录。
public static Path move(Path source, Path target, CopyOption... options)
如果目标文件存在,那么move失败。我们可以指定REPLACE_EXISTING
选项。
空目录能够被move;如果目录不为空,那么要move的话,只能移动目录,而不会移动其元素。
-
REPLACE_EXISTING
。目标已经存在仍然强制move。如果目标是symbolic link,那么symbolic link被替换,但是其指向的文件/目标并不会受影响。 -
ATOMIC_MOVE
。原子操作。如果文件系统不支持原子move,则抛出异常。
2.2.6 管理元数据{ Managing Metadata (File and File Store Attributes) }
元数据,简单讲,就是文件/目录的信息,属性。
-
它是一个一般的文件,还是一个目录,亦或者是一个链接?
-
它的size
-
创建时间,上次修改的时间
-
文件所有者,所在组
-
访问权限
Files类
包含一些方法能够获取文件的单个属性
,或者属性集
。
如果程序需要一次性同时读取多个文件属性,那么使用上面的方法一次取一个元素是非常低效的——重复地访问文件系统且一次只取一个属性影响程序性能。基于此,Files类提供了两个方法来批量获取文件属性。
Method | Comment |
---|---|
Map<String,Object> readAttributes(Path, String, LinkOption...) | |
<A extends BasicFileAttributes> A readAttributes(Path, Class<A>, LinkOption...) | . |
package java.nio.file.attribute;
public interface BasicFileAttributes {
FileTime lastModifiedTime();
FileTime lastAccessTime();
FileTime creationTime();
boolean isRegularFile();
boolean isDirectory();
boolean isSymbolicLink();
boolean isOther();
long size();
Object fileKey();
}
由于不同地文件系统有不同地属性,所以我们将相关的文件属性组合在一起形成"视图"。每种"视图"表示一种特定的文件系统实现比如Posix,DOS,或者表示通用的文件系统功能,例如文件所有者。
下面列出Java支持的"视图":
-
BasicFileAttributeView
。 所有文件系统都支持的基本属性 -
UserDefinedFileAttributeView
。上面的一些不多说,等用到了再说。
2.2.6.1 读取基本的属性
Path file = Paths.get("C:\\Users\\canliture\\Desktop\\test\\innerfile0.txt");
BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class);
System.out.println("creationTime: " + attr.creationTime());
System.out.println("lastAccessTime: " + attr.lastAccessTime());
System.out.println("lastModifiedTime: " + attr.lastModifiedTime());
System.out.println("isDirectory: " + attr.isDirectory());
System.out.println("isOther: " + attr.isOther());
System.out.println("isRegularFile: " + attr.isRegularFile());
System.out.println("isSymbolicLink: " + attr.isSymbolicLink());
System.out.println("size: " + attr.size());
2.2.6.2 设置时间戳
String str = "C:\\Users\\canliture\\Desktop\\test";
Path file = Paths.get(str);
BasicFileAttributes attr =
Files.readAttributes(file, BasicFileAttributes.class);
long currentTime = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTime);
Files.setLastModifiedTime(file, ft);
2.2.6.3 DOS File Attributes
String str = "C:\\Users\\canliture\\Desktop\\test";
Path file = Paths.get(str);
try {
DosFileAttributes attr =
Files.readAttributes(file, DosFileAttributes.class);
System.out.println("isReadOnly is " + attr.isReadOnly());
System.out.println("isHidden is " + attr.isHidden());
System.out.println("isArchive is " + attr.isArchive());
System.out.println("isSystem is " + attr.isSystem());
} catch (UnsupportedOperationException x) {
System.err.println("DOS file" +
" attributes not supported:" + x);
}
// 当然,你也能这样设置
Path file = ...;
Files.setAttribute(file, "dos:hidden", true);
不多说,等用到了再说。
Managing Metadata (File and File Store Attributes)
2.2.7 读/写/创建/打开文件
下面图,按照复杂度,列出了文件I/O的一些方法。
/*
readAllBytes newBufferedReader newInputStream
readAllLines newBufferedWriter newOutputStream newByteChannel FileChannel
<<------------------------------------------------------------------------------>>
Commonly used, Text files streams,unbuffered, channels and advanced
small files use with existing APIs ByteBuffers features,
file-locking,
memory-mapped I/O
File I/O Methods Arranged from Less Complex to More Complex */
2.2.7.1 OpenOptions
OpenOption
接口为空,StandardOpenOption
是一个具体的接口实现类。
public enum StandardOpenOption implements OpenOption {
READ,
WRITE,
APPEND,// 追加数据,被用在WRITE或者CREATE选项
TRUNCATE_EXISTING,// 将文件缩为0 bytes,被用在WRITE选项中
CREATE, // 如果文件存在,则打开,否则创建
CREATE_NEW, // 创建新文件,如果已存在,则抛出异常
DELETE_ON_CLOSE,// 当流被关闭时,删除文件,此选项适用于临时文件
SPARSE,
SYNC,// 文件(内容与元数据)与底层存储设备同步
DSYNC;// 文件(内容)与底层设备同步
}
2.2.7.2 适用于小文件的一般方法
下面的这些方法有什么好处?它会帮你打开/关闭流,你不需要关心。
适用于小文件,不适用大文件。
// 1. 从一个文件中读取所有的字节
// readAllBytes(Path) 或者 readAllLines(Path, Charset)
Path file = /*...*/;
byte[] fileArray;
fileArray = Files.readAllBytes(file);
// 2. 写所有的字节到一个文件中
// write(Path, byte[], OpenOption...)
// write(Path, Iterable< extends CharSequence>, Charset, OpenOption...)
Path file = ...;
byte[] buf = ...;
Files.write(file, buf);
2.2.7.3 文本文件的Buffered I/O方法
java.nio.file
包支持信道I/O, channel I/O会将数据移入缓冲区,能绕过一些阻塞流I/O的一些层次。
// 1. 使用Buffered Stream I/O来读文件
// newBufferedReader(Path, Charset)
Charset charset = Charset.forName("US-ASCII");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
}
// 2. 使用Buffered Stream I/O来写文件
// newBufferedWriter(Path, Charset, OpenOption...)
Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
writer.write(s, 0, s.length());
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
}
2.2.7.4 非缓冲流的方法,及其与
java.io的协作
// 1. 使用unBuffered Stream I/O 来读取一个文件
// newInputStream(Path, OpenOption...)
Path file = ...;
try (InputStream in = Files.newInputStream(file);
BufferedReader reader =
new BufferedReader(new InputStreamReader(in))) {
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException x) {
System.err.println(x);
}
// 2. 使用unBuffered Stream I/O 来 创建/写 一个文件
// newOutputStream(Path, OpenOption...)
// 此方法打开或者创建一个用来写的文件,并返回一个unbuffered output stream
// 如果没有OpenOption被指定,等效为CREATE ,TRUNCATE_EXISTING 选项
// 如果文件不存在,则创建新的
// 如果文件存在,那么truncated,也就是先置为空文件0 Byte
public class LogFileTest {
public static void main(String[] args) {
// Convert the string to a
// byte array.
String s = "Hello World! ";
byte data[] = s.getBytes();
Path p = Paths.get("./logfile.txt");
try (OutputStream out = new BufferedOutputStream(
Files.newOutputStream(p, CREATE, APPEND))) {
out.write(data, 0, data.length);
} catch (IOException x) {
System.err.println(x);
}
}
}
2.2.7.5 信道与字节缓冲相关方法(Methods for Channels and ByteBuffers)
stream I/O
一次读一个字符,channel I/O
一次读一个缓冲区。ByteChannel接口提供了基本的读写功能。
public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel{
}
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel extends Channel {
public int write(ByteBuffer src) throws IOException;
}
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
SeekableByteChannel
是一个能够维护信道上的位置,并能够改变此位置的ByteChannel
public interface SeekableByteChannel extends ByteChannel {
@Override
int read(ByteBuffer dst) throws IOException;// 从信道读字节到缓冲区ByteBuffer
@Override
int write(ByteBuffer src) throws IOException;// 写缓冲区到信道
long position() throws IOException;// 信道的当前位置
SeekableByteChannel position(long newPosition) throws IOException;// 设置信道位置
long size() throws IOException;
SeekableByteChannel truncate(long size) throws IOException;//缩短信道关联的文件(entity)
}
newByteChannel
方法返回SeekableByteChannel
类型实例对象,默认文件系统中,能够将此seekable byte channel强制类型转换为FileChannel
,它提供了更多的高级访问特性,如映射文件区域到内存中以提高访问速度,为文件区域加锁以至其它进程不能够访问它,或者在不影响信道当前位置的情况下从任意位置读写字节。
这种在文件中移动到任意位置开始读/写的功能,我们称之为随机文件访问(Random Access Files),具体内容我们将在2.2.8部分讲解。
因为SeekableByteChannel
是可读,可写的。OpenOption
如果不指定,那么默认只能读。如果要写操作的话,需要指定WRITE
,或者APPEND
。
下面演示读操作:
String pathStr = "./loginfile.log";
Path path = Paths.get(pathStr);
// Defaults to READ
try (SeekableByteChannel sbc = Files.newByteChannel(file)) {
ByteBuffer buf = ByteBuffer.allocate(10);// 前面我们说过,channel I/O一次读一个缓冲区
// Read the bytes with the proper encoding for this platform. If
// you skip this step, you might see something that looks like
// Chinese characters when you expect Latin-style characters.
String encoding = System.getProperty("file.encoding");
while (sbc.read(buf) > 0) {
buf.rewind();// position = 0;
System.out.print(Charset.forName(encoding).decode(buf));// 根据limit打印
buf.flip();// limit = position; and position = 0; 相当于buf重置为最初状态了
}
} catch (IOException x) {
System.out.println("caught exception: " + x);
}
下面简略介绍一下ByteBuffer
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer> {
// ...
/*
Rewinds this buffer. The position is set to zero and the mark is discarded.
Invoke this method before a sequence of channel-write or get operations, assuming that the limit has already been set appropriately. For example:
out.write(buf); // Write remaining data
buf.rewind(); // Rewind buffer
buf.get(array); // Copy data into array */
public final Buffer rewind() {// position置为0
position = 0;
mark = -1;
return this;
}
/*
Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.
After a sequence of channel-read or put operations, invoke this method to prepare for a sequence of channel-write or relative get operations. For example:
buf.put(magic); // Prepend header
in.read(buf); // Read data into rest of buffer
buf.flip(); // Flip buffer
out.write(buf); // Write header + data to channel
This method is often used in conjunction with the compact method when transferring data from one place to another. */
public final Buffer flip() {//limit置为当前位置,position置为0
limit = position;//
position = 0; //
mark = -1;
return this;
}
// ...
}
public abstract class Buffer {
// ...
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;//
private int limit;
private int capacity;
public final boolean hasRemaining() {
return position < limit;
}
// ...
}
下面演示写操作:
public class LogFilePermissionsTest {
public static void main(String[] args) {
// Create the set of options for appending to the file.
Set<OpenOption> options = new HashSet<>();
options.add(APPEND);
options.add(CREATE);
// Create the custom permissions attribute.
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rw-r-----");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
// Convert the string to a ByteBuffer.
String s = "Hello World! ";
byte data[] = s.getBytes();
ByteBuffer bb = ByteBuffer.wrap(data);// 写操作的ByteBuffer声明不一样
// 把要写的内容先包裹到ByteBuffer里
Path file = Paths.get("./permissions.log");
try (SeekableByteChannel sbc =
Files.newByteChannel(file, options, attr)) {
sbc.write(bb);
} catch (IOException x) {
System.out.println("Exception thrown: " + x);
}
}
}
2.2.7.6 创建常规/临时文件的方法
// 1. 创建常规文件
// createFile(Path, FileAttribute<?>...)
// 如果不指定FileAttribute,则默认属性
// 如果要创建的文件已经存在,则抛出异常
Path file = ...;
try {
// Create the empty file with default permissions, etc.
Files.createFile(file);
} catch (FileAlreadyExistsException x) {
System.err.format("file named %s" +
" already exists%n", file);
} catch (IOException x) {
// Some other sort of failure, such as permissions.
System.err.format("createFile error: %s%n", x);
}
当然了,我们也能使用前面提到的neewOutputStream
来创建文件——打开新的输出流,并立即关闭它,那么一个新的空文件就被创建了。
// 2. 创建临时文件
// createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs)
// createTempFile(String prefix, String suffix, FileAttribute<?>... attr)
// 两个方法都返回Path类型
// 上面的方法能够指定临时文件的前缀,后缀
// 第一个方法能够自定义临时文件的目录,第二个则使用系统默认的临时文件目录
try {
Path tempFile = Files.createTempFile(null, ".myapp");
System.out.format("The temporary file" +
" has been created: %s%n", tempFile)
;
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
}
2.2.8 随机文件访问(Random Access Files)
随机文件访问:非顺序地,随机地,访问文件地内容。
为了随机访问一个文件,我们需要打开文件,寻找特定地位置,然后读写此文件。
SeekableByteChannel
接口,前面我们已经提到了,它支持此功能。可以查看2.2.7.5小结。
下面给出一个使用实例:
/*
hello world1
hello world2
hello world3
*/
/*
I was here!
hello world2
hello world3hello world1I was here!
*/
Path file = Paths.get("./loginfile.log");
String s = "I was here!\n";
byte data[] = s.getBytes();
ByteBuffer out = ByteBuffer.wrap(data);
ByteBuffer copy = ByteBuffer.allocate(12);
try (FileChannel fc = (FileChannel.open(file, READ, WRITE))) {
// Read the first 12 bytes of the file to copy
int nread;
do {
nread = fc.read(copy);
} while (nread != -1 && copy.hasRemaining());
// Write "I was here!" at the beginning of the file.
fc.position(0);
while (out.hasRemaining())
fc.write(out);
out.rewind();
// Move to the end of the file. Copy the first 12 bytes to
// the end of the file. Then write "I was here!" again.
long length = fc.size();
fc.position(length-1);
copy.flip();
while (copy.hasRemaining())
fc.write(copy);
while (out.hasRemaining())
fc.write(out);
} catch (IOException e) {
System.out.println("I/O Exception: " + e);
}
2.2.9 创建/读取目录
2.2.9.1 列出文件系统的根目录
Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name: dirs) {
System.err.println(name);
}
/*我的Win10电脑输出的结果如下所示:
C:\
E:\
F:\
G:\
H:\
*/
2.2.9.2 创建一般的目录
// 1. 创建单个目录
public static Path createDirectory(Path dir,
FileAttribute<?>... attrs)
// 简单用法
Path dir = ...;
Files.createDirectory(path)
// Posix下,添加文件属性的用法
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Files.createDirectory(file, attr);
// 2. 创建具有层级深度的目录
public static Path createDirectories(Path dir,
FileAttribute<?>... attrs)
// 每个目录层级,都是在必要的情况才创建,也就是说,不存在的时候才创建
2.2.9.3 创建临时目录
// 指定临时目录所在的目录
public static Path createTempDirectory(Path dir,
String prefix,
FileAttribute<?>... attrs)
// 使用系统默认的临时目录
public static Path createTempDirectory(String prefix,
FileAttribute<?>... attrs)
2.2.9.4 列出目录内容
这里的列出目录内容,会列出文件,链接,一级子目录,隐藏的目录/文件
public static DirectoryStream<Path> newDirectoryStream(Path dir)
// 使用glob规则匹配目录
public static DirectoryStream<Path> newDirectoryStream(Path dir,
String glob)
// 使用接口过滤
public static DirectoryStream<Path> newDirectoryStream(Path dir,
DirectoryStream.Filter<? super Path> filter)
newDirectoryStream
放回一个DirectoryStream
接口的实现类对象
注意:DirectoryStream
是一个Stream,也就是说,如果我们不使用try-with-resources语句的话,必须在finally块中close它。
package java.nio.file;
public interface DirectoryStream<T> extends Closeable, Iterable<T> {
@FunctionalInterface
public static interface Filter<T> {
boolean accept(T entry) throws IOException;
}
@Override
Iterator<T> iterator();
}
1. 下面给出一个常规的代码示例:
Path dir = ...;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path file: stream) {
System.out.println(file.getFileName());
}
} catch (IOException | DirectoryIteratorException x) {
// IOException can never be thrown by the iteration.
// In this snippet, it can only be thrown by newDirectoryStream.
System.err.println(x);
}
2. 下面给出一个glob模式匹配的代码示例:
Path dir = ...;
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(dir, "*.{java,class,jar}")) {
for (Path entry: stream) {
System.out.println(entry.getFileName());
}
} catch (IOException x) {
// IOException can never be thrown by the iteration.
// In this snippet, it can // only be thrown by newDirectoryStream.
System.err.println(x);
}
3. 下面给出使用自定义接口实现来匹配的代码实例:
// 正如上面的传参类型所示:DirectoryStream.Filter<? super Path> filter
// 我们自己实现一个Filter,因为它是一个函数式接口,就一个bool accept(T)方法,很简单
// 1.
DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
public boolean accept(Path file) {
return Files.isDirectory(file);
}
};
// 2. 当然,我们可以使用正则表达式来完成定义
DirectoryStream.Filter<Path> filter = (path)-> Files.isDirectory(path);
// 下面就是实际使用了
Path dir = Paths.get("G:\\05Images");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
for (Path entry: stream) {
System.out.println(entry.getFileName());
}
} catch (IOException x) {
System.err.println(x);
}
// 3. 当然,我们的接口变量可能只用一次,为一个临时变量,我们直接省略
Path dir = Paths.get("G:\\05Images");
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(dir, (path)->Files.isDirectory(path) )) {
for (Path entry: stream) {
System.out.println(entry.getFileName());
}
} catch (IOException x) {
System.err.println(x);
}
2.2.10 链接,符号,其它(Links, Symbolic or Otherwise)
正如我们前面提到的,java.nio.file包,还有Path类是link aware
的。也就是说,当遇到一个 symbolic link 的时候,它会默认做什么,或者提供选项给用户来配置其做什么。
前面已经初步讨论过symbolic or soft links,现在我们需要了解 Hard links。
小节会给出Hard links与Soft links的简单区别。同时,教你如何:
- 创建软链接,硬链接
- 删除软链接
- 找到链接的目标文件
更多参考Links, Symbolic or Otherwise。这里不做详细介绍。
2.2.11 遍历文件树(Walking the File Tree)
也许你需要递归地遍历(recursively visit)文件树上地所有文件?亦或者你需要删除(delete every)文件树中所有.class文件 ? 或者你想**找到(find every)**过去一年内仍然没有被访问过的文件?
你需要了解 FileVisitor
接口!
2.2.11.1 FileVisitor接口
要遍历文件树,你需要实现FileVisitor
接口.此接口指明了在遍历过程中关键时刻的行为.如:
- 当一个文件被遍历时
- 当一个目录被遍历之前
- 当一个目录被遍历之后
- 当一个目录遍历失败
public interface FileVisitor<T> {
// 目录的entries被访问之前
// 目录被访问之前: preVisitDirectory
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;
// 文件被访问时: visitFile
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;
// 文件并不能被访问
// 访问文件失败: visitFileFailed
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;
// 目录 "所有的entries以及entries的所有entries" 被访问之后
// 目录被访问之后: postVisitDirectory
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
如果你并不想实现FileVisitor
的所有四个方法,你能够继承自 ***SimpleFileVisitor
*** 类,并且只重写你想重写的方法.
public class SimpleFileVisitor<T> implements FileVisitor<T> {
/**
* Initializes a new instance of this class.
*/
protected SimpleFileVisitor() {
}
/**
* Invoked for a directory before entries in the directory are visited.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE}.
*/
@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(dir);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
/**
* Invoked for a file in a directory.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE}.
*/
@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}
/**
* Invoked for a file that could not be visited.
*
* <p> Unless overridden, this method re-throws the I/O exception that prevented
* the file from being visited.
*/
@Override
public FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException
{
Objects.requireNonNull(file);
throw exc;
}
/**
* Invoked for a directory after entries in the directory, and all of their
* descendants, have been visited.
*
* <p> Unless overridden, this method returns {@link FileVisitResult#CONTINUE
* CONTINUE} if the directory iteration completes without an I/O exception;
* otherwise this method re-throws the I/O exception that caused the iteration
* of the directory to terminate prematurely.
*/
@Override
public FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException
{
Objects.requireNonNull(dir);
if (exc != null)
throw exc;
return FileVisitResult.CONTINUE;
}
}
// 上面的FileVisitResult类是个枚举类:
public enum FileVisitResult {
/**
* Continue. When returned from a {@link FileVisitor#preVisitDirectory
* preVisitDirectory} method then the entries in the directory should also
* be visited.
*/
CONTINUE,
/**
* Terminate.
*/
TERMINATE,
/**
* Continue without visiting the entries in this directory. This result
* is only meaningful when returned from the {@link
* FileVisitor#preVisitDirectory preVisitDirectory} method; otherwise
* this result type is the same as returning {@link #CONTINUE}.
*/
SKIP_SUBTREE,
/**
* Continue without visiting the <em>siblings</em> of this file or directory.
* If returned from the {@link FileVisitor#preVisitDirectory
* preVisitDirectory} method then the entries in the directory are also
* skipped and the {@link FileVisitor#postVisitDirectory postVisitDirectory}
* method is not invoked.
*/
SKIP_SIBLINGS;
}
现在给出Oracle 教程给出的一个FileVisitor实现:
public class PrintFiles
extends SimpleFileVisitor<Path> {
// Print information about
// each type of file.
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attr) {
if (attr.isSymbolicLink()) {
System.out.format("Symbolic link: %s ", file);
} else if (attr.isRegularFile()) {
System.out.format("Regular file: %s ", file);
} else {
System.out.format("Other: %s ", file);
}
System.out.println("(" + attr.size() + "bytes)");
return CONTINUE;
}
// Print each directory visited.
@Override
public FileVisitResult postVisitDirectory(Path dir,
IOException exc) {
System.out.format("Directory: %s%n", dir);
return CONTINUE;
}
// If there is some error accessing
// the file, let the user know.
// If you don't override this method
// and an error occurs, an IOException
// is thrown.
@Override
public FileVisitResult visitFileFailed(Path file,
IOException exc) {
System.err.println(exc);
return CONTINUE;
}
}
2.2.11.2 开始遍历(Kickstarting the Process)
在实现FilesVisitor
之后,怎么开始遍历呢?
Files类
有两个walkFileTree
方法来完成遍历操作:
public static Path walkFileTree(Path start,
FileVisitor<? super Path> visitor)
throws IOException
public static Path walkFileTree(Path start,
Set<FileVisitOption> options,
int maxDepth,// 最大深度,我们一般指定Integer.MAX_VALUE
FileVisitor<? super Path> visitor)
throws IOException
public enum FileVisitOption {
FOLLOW_LINKS;// Follow symbolic links.
}
简单代码示例:
// 第1个简单方法示例
Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);
// 第2个方法示例
import static java.nio.file.FileVisitResult.*;
Path startingDir = ...;
EnumSet<FileVisitOption> opts = EnumSet.of(FOLLOW_LINKS);
Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);
2.2.11.3 创建FileVisitor时的顾虑
根据上面的例子,我们运行一下程序,不难看出,文件树的遍历是深度优先遍历.但是我们不能够知道子目录的迭代顺序.
如果你的程序会对文件系统产生任何的变化(删除,拷贝,移动等),那么你就需要仔细考虑FileVisitor
的实现是否合理.
如果你要做一个递归删除时,你应该先删除该目录中的文件,然后再删除此目录本身.所以我们要考虑在postVisitDirectory
方法中删除目录自身.
如果你要做一个递归拷贝时,你需要先在preVisitDirectory
方法中创建新的目录,然后在visitFiles
时将文件拷贝到创建的目录中.如果要保留源目录的属性,你需要在所有子目录/文件全部拷贝完之后(postVisitDirectory
),再做这件事.
如果你要搜索一个文件,你要在visitFile
方法中执行比较函数( perform the comparison),这种方法能够找到所有符合你要求的文件,但是并不能找到目录.如果你想既匹配文件,又匹配目录,那么你也需要在preVisitDirectory
或者postVisitDirectory
方法中执行比较函数.
同时,你要考虑到你是否follow symbolic links
.比如,当你删除文件时,follow 并不建议.当在拷贝时,你也许期望follow.默认情况下,walkFileTree
并不follow symbolic links.
FOLLOW_LINKS
导致的循环引用会在visitFileFailed
中以FileSystemLoopException
异常的形式通知.下面时Copy例程中检测循环引用的代码段:
@Override
public FileVisitResult
visitFileFailed(Path file,
IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("cycle detected: " + file);
} else {
System.err.format("Unable to copy:" + " %s: %s%n", file, exc);
}
return CONTINUE;
}
2.2.11.4 控制遍历流(Controlling the Flow)
也许在遍历文件树寻找特定的目录时,当找到时,你想终止遍历.亦或者你想跳过特定的目录,不遍历此目录.
这就是FileVisitResult
的作用,用来控制遍历的流向.前面我们列出了FileVisitResult
的枚举类的源代码,现在我们来介绍其枚举字段的作用.
-
CONTINUE
.表示继续遍历.如果preVisitDirectory
方法返回CONTINUE
,那么此目录会被遍历. -
TERMINATE
.表示立即中断文件遍历.在此值被返回后,不会再调用遍历方法. -
SKIP_SUBTREE
.当preVisitDirectory
返回此值时,那么此目录及其子目录被跳过,树的此分支结束. -
SKIP_SIBLINGS
-
当
preVisitDirectory
返回此值时,那么此目录不会被访问,postVisitDirectory
也不会被调用,并且其未被访问的同级文件也不会被访问. -
当
postVisitDirectory
返回此值时,同级目录不再会被访问.
-
// 任何名为SCCS的目录都会被跳过
public FileVisitResult preVisitDirectory(Path dir,BasicFileAttributes attrs) {
(if (dir.getFileName().toString().equals("SCCS")) {
return SKIP_SUBTREE;
}
return CONTINUE;
}
// 只要找到特定的文件,直接终止遍历文件树
// The file we are looking for.
Path lookingFor = ...;
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
if (file.getFileName().equals(lookingFor)) {
System.out.println("Located file: " + file);
return TERMINATE;
}
return CONTINUE;
}
2.2.11.5 搜索文件
public abstract PathMatcher getPathMatcher(String syntaxAndPattern)
String pattern = ...;
PathMatcher matcher =
FileSystems.getDefault().getPathMatcher("glob:" + pattern);
// syntax:pattern
使用matches
方法来匹配.
PathMatcher matcher =
FileSystems.getDefault().getPathMatcher("glob:*.{java,class}");
Path filename = ...;
if (matcher.matches(filename)) {
System.out.println(filename);
}
了解如何递归的模式匹配?查看2.5.1 Find
源代码
2.2.12 监视目录的改变
java.nio.file
包提供了文件改变提醒API,叫做 Watch Service API.此API能让你给目录注册监视服务.注册时,你会告诉此服务你对哪些类型的服务感兴趣-----文件创建,文件删除,文件修改.当服务检测到你感兴趣的事件发生时,它会被转发给注册进程.注册进程有一个线程(或者线程池)来监测它所注册的事件.当事件传入时,它会根据需要进行处理.
2.2.12.1 Watch Service概述
WatchService API相对低级,允许你自定义它.你能直接只用它或者在此机制上创建一个更高层的API,以至于它适合你的需求.
这是一些基本步骤,来实现你的WatchService:
- 从File System创建一个
WatchService
“watcher” - 对每个你想监听的目录,向watcher注册它.当注册一个目录时,你需要指定你想要监听的事件类型.对每个你注册的目录,都会返回一个
WatchKey
实例.(WatchKey可以认为是被注册这个目录的监听对象) - 用一个无线循环来等来事件的到来,当一个事件发生的时候,key有信号并放入watcher的队列.
- 从watcher的队列中取出key,你能从key上获取文件名.
- 取出key的每个事件,然后按需处理.
- 重置key并继续等待事件
- 关闭服务:线程结束或者服务关闭时, watch service结束;
WatchKeys
是线程安全的,能够被用在java.nio.concurrent
包下.
2.2.12.2 体验一下Watch Service
尝试编译2.5.4 WatchDir
源码.使用下列指令运行程序(其中test为我们需要检测的目录,所以在体验之前此目录最好被创建).
java WatchDir test &
现在尝试着在test目录下创建/删除/编辑文件,当这些事件发生时,看控制台会有什么反应?当你玩够了,删除test目录,那么程序又会发生什么呢?
当然,你能使用-r
可选参数来监听整个test
文件树,为每个目录都注册watch service
2.2.12.3 创建Watch Service并注册事件
public interface WatchService extends Closeable {
@Override
void close() throws IOException;
WatchKey poll();
WatchKey poll(long timeout, TimeUnit unit)
throws InterruptedException;
WatchKey take() throws InterruptedException;
}
public interface WatchKey {
boolean isValid();
List<WatchEvent<?>> pollEvents();
boolean reset();
void cancel();
Watchable watchable();
}
// Watchale能够注册事件,Path类实现了此接口
public interface Watchable {
WatchKey register(WatchService watcher,
WatchEvent.Kind<?>[] events,
WatchEvent.Modifier... modifiers)
throws IOException;
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)
throws IOException;
}
// 从File System 创建一个`WatchService` "watcher"
WatchService watcher = FileSystems.getDefault().newWatchService();
接下来,在watch service上注册一个或者多个对象,任何实现Watchable
接口的对象都能够被注册.而Path类
实现了Watchable
接口,所以每个目录都能够作为一个Path对象来被注册监听.
当在Watch Service上注册一个对象时,你需要指定你想要监听的事件类型,其中 StandardWatchEventKinds
枚举类型支持的事件类型又如下几种:
ENTRY_CREATE
.目录创建时触发事件ENTRY_DELETE
.目录被删除时触发事件ENTRY_MODIFY
.目录被修改时触发事件OVERFLOW
.表示事件可能会被丢弃,你没必要注册OVERFLOW
事件.
下面是Path类的一个代码示例:
javaPath dir = ...;
try {
WatchKey key = dir.register(watcher,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY);
} catch (IOException x) {
System.err.println(x);
}
2.2.12.4 处理事件
事件处理循环中事件的顺序:
- 获取Watch Key.有三种方法可用
-
WatchKey poll();// 返回排队中可用的key,如果不可用返回null
-
WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException; // 返回排队中可用的key,如果不可用再等待timeout时间,unit为时间单位
-
WatchKey take() throws InterruptedException; // 返回排队中可用的key,如果不可用则方法wait
- 处理key的待处理事件.你能够通过key的如下方法来获取其触发的事件列表:
List<WatchEvent<?>> pollEvents();
// 下面是WatchEvent接口的详细情况:
public interface WatchEvent<T> {
public static interface Kind<T> {//An event kind, for the purposes of identification.
String name();
Class<T> type();//the type of the {@link WatchEvent#context context} value.
}
/**
* An event modifier that qualifies how a {@link Watchable} is registered
* with a {@link WatchService}.
*
* <p> This release does not define any <em>standard</em> modifiers.
*
* @since 1.7
* @see Watchable#register
*/
public static interface Modifier {
String name();// Returns the name of the modifier.
}
Kind<T> kind();// the event kind.
/**
* Returns the event count. If the event count is greater than {@code 1}
* then this is a repeated event.
*/
int count();
/**
* Returns the context for the event.
*
* <p> In the case of {@link StandardWatchEventKinds#ENTRY_CREATE ENTRY_CREATE},
* {@link StandardWatchEventKinds#ENTRY_DELETE ENTRY_DELETE}, and {@link
* StandardWatchEventKinds#ENTRY_MODIFY ENTRY_MODIFY} events the context is
* a {@code Path} that is the {@link Path#relativize relative} path between
* the directory registered with the watch service, and the entry that is
* created, deleted, or modified.
*
* @return the event context; may be {@code null}
*/
T context();
}
- 通过kind方法来得到event的类型.无论key注册的什么类型,都有可能得到一个
OVERFLOW
事件.你应该检测OVERFLOW
事件,可以选择性地处理. - 取出与事件相关联的事件,文件名被作为事件的上下文而存储. 使用
context
方法来获取. - 在key的事件被处理之后,你需要调用
reset
将key置回ready
状态.如果此方法返回false,那么说明key无效了,循环可以退出了,这一步非常重要,因为当你reset
失败,此key将不能再取出任何事件了.
watch key
有一个状态.在任何事件,其状态都可能为如下中的一种:
Ready
.key准备接收事件,最开始被创建时,就是这个状态.Signaled
.一个或者多个事件正在排队,一旦key处于有信号的状态,他就不再处于Ready
状态,知道reset
方法被调用.Invalid
.key不再活跃.为什么会变成这种状态?- key使用了
cancel
方法显式地取消了key - 目录变得不可访问
- watch service服务被关闭
- key使用了
下面是一个例子:
for (;;) {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// This key is registered only
// for ENTRY_CREATE events,
// but an OVERFLOW event can
// occur regardless if events
// are lost or discarded.
if (kind == OVERFLOW) {
continue;
}
// The filename is the
// context of the event.
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();
// Verify that the new
// file is a text file.
try {
// Resolve the filename against the directory.
// If the filename is "test" and the directory is "foo",
// the resolved name is "test/foo".
Path child = dir.resolve(filename);
if (!Files.probeContentType(child).equals("text/plain")) {
System.err.format("New file '%s'" +
" is not a plain text file.%n", filename);
continue;
}
} catch (IOException x) {
System.err.println(x);
continue;
}
// Email the file to the
// specified email alias.
System.out.format("Emailing file %s%n", filename);
//Details left to reader....
}
// Reset the key -- this step is critical if you want to
// receive further watch events. If the key is no longer valid,
// the directory is inaccessible so exit the loop.
boolean valid = key.reset();
if (!valid) {
break;
}
}
2.2.12.5 什么时候使用/不使用Watch Service
Watch Service API
是被设计用来检测文件改变的.它适用于任何像IDE,编辑器这样的应用,它们都潜在地打开许多地文件并需要确保这些文件与文件系统要同步.也适用于应用服务器来监听目录,为了部署jsp,jar,需要监听这些文件地删除.
Watch Service API
不适用于索引硬盘.大多数文件系统对文件改变提醒都有原生的支持,Watch Service API
要在合适的地方利用这种支持.然而如果文件系统不支持此机制的话,Watch Service API
就会检测文件系统,等待事件的到来.
2.3 其它有用的方法
2.3.1 判断MIME类型
MIME(Multipurpose Internet Mail Extensions), 多用途互联网邮件扩展类型;MIME是描述消息内容类型的因特网标准.MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据.
使用的方法为:
public static String probeContentType(Path path)
throws IOException
用法如下,返回null
说明并不能确定此类型.此方法的实现高度平台相关,且不是绝对正确的.文件类型通过平台默认的 文件类型检测器(file type detector) 来决定.例如.如果得到一个.class文件的类型为application/x-java,那么很让人困惑.如果默认的不符合你的需求,你能够提供一个自定义的 FileTypeDetector
.
try {
String type = Files.probeContentType(filename);
if (type == null) {
System.err.format("'%s' has an" + " unknown filetype.%n", filename);
} else if (!type.equals("text/plain") {
System.err.format("'%s' is not" + " a plain text file.%n", filename);
continue;
}
} catch (IOException x) {
System.err.println(x);
}
2.3.2 默认文件系统(Default File System)
为了得到默认的文件系统,可以使用getDefault
方法
// 注意FileSystems的复数
// java.nio.file.FileSystems
public static FileSystem getDefault()
// 一个例子
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.*");
2.3.3 路径分隔符(Path String Separator)
POSIX 文件系统,路径分隔符为/
,Windows中为\
.有两种方法取得路径分隔符:
String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();
2.3.4 文件系统的file store
// java.nio.file.FileSystems
public abstract Iterable<FileStore> getFileStores()// 文件系统中有哪些file store
// java.nio.file.Files
public static FileStore getFileStore(Path path)// 一个文件属于哪个FileStore
下面给出了两个代码例子
for (FileStore store: FileSystems.getDefault().getFileStores()) {//枚举系统所有的file store
//...
}
Path file = ...;
FileStore store = Files.getFileStore(file);// file位于哪个file store
public class DiskUsage {
static final long K = 1024;
static void printFileStore(FileStore store) throws IOException {
long total = store.getTotalSpace() / K;
long used = (store.getTotalSpace() - store.getUnallocatedSpace()) / K;
long avail = store.getUsableSpace() / K;
String s = store.toString();
if (s.length() > 20) {
System.out.println(s);
s = "";
}
System.out.format("%-20s %12d %12d %12d\n", s, total, used, avail);
}
public static void main(String[] args) throws IOException {
System.out.format("%-20s %12s %12s %12s\n", "Filesystem", "kbytes", "used", "avail");
if (args.length == 0) {
FileSystem fs = FileSystems.getDefault();
for (FileStore store: fs.getFileStores()) {
printFileStore(store);
}
} else {
for (String file: args) {
FileStore store = Files.getFileStore(Paths.get(file));
printFileStore(store);
}
}
}
}
2.4 老版本的文件I/O代码(Legacy File I/O Code)
在Java SE 7之前java.io.file
包用来操作文件I/O,但是它有很多的缺点
- 许多的方法当它们执行失败时并不会抛出异常,所以不可能获取有用的错误信息.举个例子,当一个文件删除失败的时候,程序会得到一个"删除失败"的信息,但是并不知道它是因为文件不存在,还是因为用户没有权限,或者其它问题导致的"删除失败".
rename
方法并不总是跨平台执行正常.- 没有对symbolic links的真正支持.
- 希望有更多的文件信息(metadata )的操作,如文件权限,文件所有者,或者其它的安全属性.
- 访问文件信息(metadata )很低效
- 许多
File
的方法没有伸缩性(didn’t scale).在服务器上请求一个大文件目录并列出来,很可能会导致宕机,大目录可能会导致内存资源问题,导致拒绝服务(a denial of service.). - 不可能写可靠的代码来递归地遍历文件树,且并不能很好地响应symbolic links循环引用地问题.
也许你有遗留下来的代码仍然使用的是java.io.File包下的类,你是否想要利用java.nio.file.Path的功能但是对你的代码的影响最小化.
java.io.File
类提供了toPath
方法来转换成Path
类.
Path input = file.toPath();
file.delete();
// 可以转化为:
Path fp = file.toPath();
Files.delete(fp);
当然,你也可以将java.nio.file.Path
转化为java.io.File
// java.nio.file.Path -> java.io.File
File toFile()
2.4.1 java.io.File -> java.nio.file
由于Java SE 7中文件I/O的实现已经完全的重构了,所以你并不能简单地将一种方法换成另一种方法,如果你想要使用java.nio.file包提供地丰富功能,最简单地解决方案就是使用 File.toPath
方法.然而,如果你并不想使用这种方法,或者这种方法对你的需求来说很低效,你必须重写你的文件I/O代码
两种API之间并没有简单地一对一地等效关系,但是下面地表格能够给你一般地思路,告诉你java.io.file
API实现的功能在java.nio.file
对应于哪些API.
java.io.File Functionality | java.nio.file Functionality | Tutorial Coverage |
---|---|---|
java.io.File | java.nio.file.Path | The Path Class |
java.io.RandomAccessFile | The SeekableByteChannel functionality. | Random Access Files |
File.canRead , canWrite , canExecute | Files.isReadable , Files.isWritable , and Files.isExecutable . On UNIX file systems, the Managing Metadata (File and File Store Attributes) package is used to check the nine file permissions. | Checking a File or Directory Managing Metadata |
File.isDirectory() , File.isFile() , and File.length() | Files.isDirectory(Path, LinkOption...) , Files.isRegularFile(Path, LinkOption...) , and Files.size(Path) | Managing Metadata |
File.lastModified() and File.setLastModified(long) | Files.getLastModifiedTime(Path, LinkOption...) and Files.setLastMOdifiedTime(Path, FileTime) | Managing Metadata |
The File methods that set various attributes: setExecutable , setReadable , setReadOnly , setWritable | These methods are replaced by the Files method setAttribute(Path, String, Object, LinkOption...) . | Managing Metadata |
new File(parent, "newfile") | parent.resolve("newfile") | Path Operations |
File.renameTo | Files.move | Moving a File or Directory |
File.delete | Files.delete | Deleting a File or Directory |
File.createNewFile | Files.createFile | Creating Files |
File.deleteOnExit | Replaced by the DELETE_ON_CLOSE option specified in the createFile method. | Creating Files |
File.createTempFile | Files.createTempFile(Path, String, FileAttributes<?>) , Files.createTempFile(Path, String, String, FileAttributes<?>) | Creating Files Creating and Writing a File by Using Stream I/O Reading and Writing Files by Using Channel I/O |
File.exists | Files.exists and Files.notExists | Verifying the Existence of a File or Directory |
File.compareTo and equals | Path.compareTo and equals | Comparing Two Paths |
File.getAbsolutePath and getAbsoluteFile | Path.toAbsolutePath | Converting a Path |
File.getCanonicalPath and getCanonicalFile | Path.toRealPath or normalize | Converting a Path (toRealPath ) Removing Redundancies From a Path (normalize ) |
File.toURI | Path.toURI | Converting a Path |
File.isHidden | Files.isHidden | Retrieving Information About the Path |
File.list and listFiles | Path.newDirectoryStream | Listing a Directory’s Contents |
File.mkdir and mkdirs | Path.createDirectory | Creating a Directory |
File.listRoots | FileSystem.getRootDirectories | Listing a File System’s Root Directories |
File.getTotalSpace , File.getFreeSpace , File.getUsableSpace | FileStore.getTotalSpace , FileStore.getUnallocatedSpace , FileStore.getUsableSpace , FileStore.getTotalSpace | File Store Attributes |
2.5 遍历文件树的例子代码
Find
– Recurses a file tree looking for files and directories that match a particular glob pattern. This example is discussed in Finding Files.Chmod
– Recursively changes permissions on a file tree (for POSIX systems only).Copy
– Recursively copies a file tree.WatchDir
– Demonstrates the mechanism that watches a directory for files that have been created, deleted or modified. Calling this program with the-r
option watches an entire tree for changes. For more information about the file notification service, see Watching a Directory for Changes.
2.5.1 Find
public class Find {
/**
* A {@code FileVisitor} that finds
* all files that match the
* specified pattern.
*/
public static class Finder
extends SimpleFileVisitor<Path> {
private final PathMatcher matcher;
private int numMatches = 0;
Finder(String pattern) {
matcher = FileSystems.getDefault()
.getPathMatcher("glob:" + pattern);
}
// Compares the glob pattern against
// the file or directory name.
void find(Path file) {
Path name = file.getFileName();
if (name != null && matcher.matches(name)) {
numMatches++;
System.out.println(file);
}
}
// Prints the total number of
// matches to standard out.
void done() {
System.out.println("Matched: "
+ numMatches);
}
// Invoke the pattern matching
// method on each file.
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) {
find(file);
return CONTINUE;
}
// Invoke the pattern matching
// method on each directory.
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) {
find(dir);
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file,
IOException exc) {
System.err.println(exc);
return CONTINUE;
}
}
static void usage() {
System.err.println("java Find <path>" +
" -name \"<glob_pattern>\"");
System.exit(-1);
}
public static void main(String[] args)
throws IOException {
if (args.length < 3 || !args[1].equals("-name"))
usage();
Path startingDir = Paths.get(args[0]);
String pattern = args[2];
Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, finder);
finder.done();
}
}
2.5.2 Chmod
public class Chmod {
/**
* Compiles a list of one or more <em>symbolic mode expressions</em> that
* may be used to change a set of file permissions. This method is
* intended for use where file permissions are required to be changed in
* a manner similar to the UNIX <i>chmod</i> program.
*
* <p> The {@code exprs} parameter is a comma separated list of expressions
* where each takes the form:
* <blockquote>
* <i>who operator</i> [<i>permissions</i>]
* </blockquote>
* where <i>who</i> is one or more of the characters {@code 'u'}, {@code 'g'},
* {@code 'o'}, or {@code 'a'} meaning the owner (user), group, others, or
* all (owner, group, and others) respectively.
*
* <p> <i>operator</i> is the character {@code '+'}, {@code '-'}, or {@code
* '='} signifying how permissions are to be changed. {@code '+'} means the
* permissions are added, {@code '-'} means the permissions are removed, and
* {@code '='} means the permissions are assigned absolutely.
*
* <p> <i>permissions</i> is a sequence of zero or more of the following:
* {@code 'r'} for read permission, {@code 'w'} for write permission, and
* {@code 'x'} for execute permission. If <i>permissions</i> is omitted
* when assigned absolutely, then the permissions are cleared for
* the owner, group, or others as identified by <i>who</i>. When omitted
* when adding or removing then the expression is ignored.
*
* <p> The following examples demonstrate possible values for the {@code
* exprs} parameter:
*
* <table border="0">
* <tr>
* <td> {@code u=rw} </td>
* <td> Sets the owner permissions to be read and write. </td>
* </tr>
* <tr>
* <td> {@code ug+w} </td>
* <td> Sets the owner write and group write permissions. </td>
* </tr>
* <tr>
* <td> {@code u+w,o-rwx} </td>
* <td> Sets the owner write, and removes the others read, others write
* and others execute permissions. </td>
* </tr>
* <tr>
* <td> {@code o=} </td>
* <td> Sets the others permission to none (others read, others write and
* others execute permissions are removed if set) </td>
* </tr>
* </table>
*
* @param exprs
* List of one or more <em>symbolic mode expressions</em>
*
* @return A {@code Changer} that may be used to changer a set of
* file permissions
*
* @throws IllegalArgumentException
* If the value of the {@code exprs} parameter is invalid
*/
public static Changer compile(String exprs) {
// minimum is who and operator (u= for example)
if (exprs.length() < 2)
throw new IllegalArgumentException("Invalid mode");
// permissions that the changer will add or remove
final Set<PosixFilePermission> toAdd = new HashSet<PosixFilePermission>();
final Set<PosixFilePermission> toRemove = new HashSet<PosixFilePermission>();
// iterate over each of expression modes
for (String expr: exprs.split(",")) {
// minimum of who and operator
if (expr.length() < 2)
throw new IllegalArgumentException("Invalid mode");
int pos = 0;
// who
boolean u = false;
boolean g = false;
boolean o = false;
boolean done = false;
for (;;) {
switch (expr.charAt(pos)) {
case 'u' : u = true; break;
case 'g' : g = true; break;
case 'o' : o = true; break;
case 'a' : u = true; g = true; o = true; break;
default : done = true;
}
if (done)
break;
pos++;
}
if (!u && !g && !o)
throw new IllegalArgumentException("Invalid mode");
// get operator and permissions
char op = expr.charAt(pos++);
String mask = (expr.length() == pos) ? "" : expr.substring(pos);
// operator
boolean add = (op == '+');
boolean remove = (op == '-');
boolean assign = (op == '=');
if (!add && !remove && !assign)
throw new IllegalArgumentException("Invalid mode");
// who= means remove all
if (assign && mask.length() == 0) {
assign = false;
remove = true;
mask = "rwx";
}
// permissions
boolean r = false;
boolean w = false;
boolean x = false;
for (int i=0; i<mask.length(); i++) {
switch (mask.charAt(i)) {
case 'r' : r = true; break;
case 'w' : w = true; break;
case 'x' : x = true; break;
default:
throw new IllegalArgumentException("Invalid mode");
}
}
// update permissions set
if (add) {
if (u) {
if (r) toAdd.add(OWNER_READ);
if (w) toAdd.add(OWNER_WRITE);
if (x) toAdd.add(OWNER_EXECUTE);
}
if (g) {
if (r) toAdd.add(GROUP_READ);
if (w) toAdd.add(GROUP_WRITE);
if (x) toAdd.add(GROUP_EXECUTE);
}
if (o) {
if (r) toAdd.add(OTHERS_READ);
if (w) toAdd.add(OTHERS_WRITE);
if (x) toAdd.add(OTHERS_EXECUTE);
}
}
if (remove) {
if (u) {
if (r) toRemove.add(OWNER_READ);
if (w) toRemove.add(OWNER_WRITE);
if (x) toRemove.add(OWNER_EXECUTE);
}
if (g) {
if (r) toRemove.add(GROUP_READ);
if (w) toRemove.add(GROUP_WRITE);
if (x) toRemove.add(GROUP_EXECUTE);
}
if (o) {
if (r) toRemove.add(OTHERS_READ);
if (w) toRemove.add(OTHERS_WRITE);
if (x) toRemove.add(OTHERS_EXECUTE);
}
}
if (assign) {
if (u) {
if (r) toAdd.add(OWNER_READ);
else toRemove.add(OWNER_READ);
if (w) toAdd.add(OWNER_WRITE);
else toRemove.add(OWNER_WRITE);
if (x) toAdd.add(OWNER_EXECUTE);
else toRemove.add(OWNER_EXECUTE);
}
if (g) {
if (r) toAdd.add(GROUP_READ);
else toRemove.add(GROUP_READ);
if (w) toAdd.add(GROUP_WRITE);
else toRemove.add(GROUP_WRITE);
if (x) toAdd.add(GROUP_EXECUTE);
else toRemove.add(GROUP_EXECUTE);
}
if (o) {
if (r) toAdd.add(OTHERS_READ);
else toRemove.add(OTHERS_READ);
if (w) toAdd.add(OTHERS_WRITE);
else toRemove.add(OTHERS_WRITE);
if (x) toAdd.add(OTHERS_EXECUTE);
else toRemove.add(OTHERS_EXECUTE);
}
}
}
// return changer
return new Changer() {
@Override
public Set<PosixFilePermission> change(Set<PosixFilePermission> perms) {
perms.addAll(toAdd);
perms.removeAll(toRemove);
return perms;
}
};
}
/**
* A task that <i>changes</i> a set of {@link PosixFilePermission} elements.
*/
public interface Changer {
/**
* Applies the changes to the given set of permissions.
*
* @param perms
* The set of permissions to change
*
* @return The {@code perms} parameter
*/
Set<PosixFilePermission> change(Set<PosixFilePermission> perms);
}
/**
* Changes the permissions of the file using the given Changer.
*/
static void chmod(Path file, Changer changer) {
try {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
Files.setPosixFilePermissions(file, changer.change(perms));
} catch (IOException x) {
System.err.println(x);
}
}
/**
* Changes the permission of each file and directory visited
*/
static class TreeVisitor implements FileVisitor<Path> {
private final Changer changer;
TreeVisitor(Changer changer) {
this.changer = changer;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
chmod(dir, changer);
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
chmod(file, changer);
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
if (exc != null)
System.err.println("WARNING: " + exc);
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("WARNING: " + exc);
return CONTINUE;
}
}
static void usage() {
System.err.println("java Chmod [-R] symbolic-mode-list file...");
System.exit(-1);
}
public static void main(String[] args) throws IOException {
if (args.length < 2)
usage();
int argi = 0;
int maxDepth = 0;
if (args[argi].equals("-R")) {
if (args.length < 3)
usage();
argi++;
maxDepth = Integer.MAX_VALUE;
}
// compile the symbolic mode expressions
Changer changer = compile(args[argi++]);
TreeVisitor visitor = new TreeVisitor(changer);
Set<FileVisitOption> opts = Collections.emptySet();
while (argi < args.length) {
Path file = Paths.get(args[argi]);
Files.walkFileTree(file, opts, maxDepth, visitor);
argi++;
}
}
}
2.5.3 Copy
public class Copy {
/**
* Returns {@code true} if okay to overwrite a file ("cp -i")
*/
static boolean okayToOverwrite(Path file) {
String answer = System.console().readLine("overwrite %s (yes/no)? ", file);
return (answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes"));
}
/**
* Copy source file to target location. If {@code prompt} is true then
* prompt user to overwrite target if it exists. The {@code preserve}
* parameter determines if file attributes should be copied/preserved.
*/
static void copyFile(Path source, Path target, boolean prompt, boolean preserve) {
CopyOption[] options = (preserve) ?
new CopyOption[] { COPY_ATTRIBUTES, REPLACE_EXISTING } :
new CopyOption[] { REPLACE_EXISTING };
if (!prompt || Files.notExists(target) || okayToOverwrite(target)) {
try {
Files.copy(source, target, options);
} catch (IOException x) {
System.err.format("Unable to copy: %s: %s%n", source, x);
}
}
}
/**
* A {@code FileVisitor} that copies a file-tree ("cp -r")
*/
static class TreeCopier implements FileVisitor<Path> {
private final Path source;
private final Path target;
private final boolean prompt;
private final boolean preserve;
TreeCopier(Path source, Path target, boolean prompt, boolean preserve) {
this.source = source;
this.target = target;
this.prompt = prompt;
this.preserve = preserve;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
// before visiting entries in a directory we copy the directory
// (okay if directory already exists).
CopyOption[] options = (preserve) ?
new CopyOption[] { COPY_ATTRIBUTES } : new CopyOption[0];
Path newdir = target.resolve(source.relativize(dir));
try {
Files.copy(dir, newdir, options);
} catch (FileAlreadyExistsException x) {
// ignore
} catch (IOException x) {
System.err.format("Unable to create: %s: %s%n", newdir, x);
return SKIP_SUBTREE;
}
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
copyFile(file, target.resolve(source.relativize(file)),
prompt, preserve);
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
// fix up modification time of directory when done
if (exc == null && preserve) {
Path newdir = target.resolve(source.relativize(dir));
try {
FileTime time = Files.getLastModifiedTime(dir);
Files.setLastModifiedTime(newdir, time);
} catch (IOException x) {
System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
}
}
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("cycle detected: " + file);
} else {
System.err.format("Unable to copy: %s: %s%n", file, exc);
}
return CONTINUE;
}
}
static void usage() {
System.err.println("java Copy [-ip] source... target");
System.err.println("java Copy -r [-ip] source-dir... target");
System.exit(-1);
}
public static void main(String[] args) throws IOException {
boolean recursive = false;
boolean prompt = false;
boolean preserve = false;
// process options
int argi = 0;
while (argi < args.length) {
String arg = args[argi];
if (!arg.startsWith("-"))
break;
if (arg.length() < 2)
usage();
for (int i=1; i<arg.length(); i++) {
char c = arg.charAt(i);
switch (c) {
case 'r' : recursive = true; break;
case 'i' : prompt = true; break;
case 'p' : preserve = true; break;
default : usage();
}
}
argi++;
}
// remaining arguments are the source files(s) and the target location
int remaining = args.length - argi;
if (remaining < 2)
usage();
Path[] source = new Path[remaining-1];
int i=0;
while (remaining > 1) {
source[i++] = Paths.get(args[argi++]);
remaining--;
}
Path target = Paths.get(args[argi]);
// check if target is a directory
boolean isDir = Files.isDirectory(target);
// copy each source file/directory to target
for (i=0; i<source.length; i++) {
Path dest = (isDir) ? target.resolve(source[i].getFileName()) : target;
if (recursive) {
// follow links when copying files
EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
TreeCopier tc = new TreeCopier(source[i], dest, prompt, preserve);
Files.walkFileTree(source[i], opts, Integer.MAX_VALUE, tc);
} else {
// not recursive so source must not be a directory
if (Files.isDirectory(source[i])) {
System.err.format("%s: is a directory%n", source[i]);
continue;
}
copyFile(source[i], dest, prompt, preserve);
}
}
}
}
2.5.4 WatchDir
public class WatchDir {
private final WatchService watcher;
private final Map<WatchKey,Path> keys;
private final boolean recursive;
private boolean trace = false;
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
/**
* Register the given directory with the WatchService
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
if (trace) {
Path prev = keys.get(key);
if (prev == null) {
System.out.format("register: %s\n", dir);
} else {
if (!dir.equals(prev)) {
System.out.format("update: %s -> %s\n", prev, dir);
}
}
}
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException
{
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Creates a WatchService and registers the given directory
*/
WatchDir(Path dir, boolean recursive) throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<WatchKey,Path>();
this.recursive = recursive;
if (recursive) {
System.out.format("Scanning %s ...\n", dir);
registerAll(dir);
System.out.println("Done.");
} else {
register(dir);
}
// enable trace after initial registration
this.trace = true;
}
/**
* Process all events for keys queued to the watcher
*/
void processEvents() {
for (;;) {
// wait for key to be signalled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
Path dir = keys.get(key);
if (dir == null) {
System.err.println("WatchKey not recognized!!");
continue;
}
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (kind == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
// print out event
System.out.format("%s: %s\n", event.kind().name(), child);
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (recursive && (kind == ENTRY_CREATE)) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(child);
}
} catch (IOException x) {
// ignore to keep sample readbale
}
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
break;
}
}
}
}
static void usage() {
System.err.println("usage: java WatchDir [-r] dir");
System.exit(-1);
}
public static void main(String[] args) throws IOException {
// parse arguments
if (args.length == 0 || args.length > 2)
usage();
boolean recursive = false;
int dirArg = 0;
if (args[0].equals("-r")) {
if (args.length < 2)
usage();
recursive = true;
dirArg++;
}
// register directory and process its events
Path dir = Paths.get(args[dirArg]);
new WatchDir(dir, recursive).processEvents();
}
}