文件编程
本课程的重点在于网络编程,因此文件编程只是简单的过一遍,了解即可。
# 1. FileChannel
⚠️ FileChannel 只能工作在阻塞模式下。
# 1.1 获取 FileChannel
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法:
- 通过 FileInputStream 获取的 channel 只能读
- 通过 FileOutputStream 获取的 channel 只能写
- 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
获取 channel 的源头决定了这个 channel 是否能读和是否能写
# 1.2 读取
会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾:
int readBytes = channel.read(buffer);
# 1.3 写入
写入的正确姿势如下:
ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip(); // 切换读模式
while(buffer.hasRemaining()) {
channel.write(buffer);
}
2
3
4
5
6
7
在 while 中调用 channel.write
是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel,因此每次循环需要检查 buffer.hasRemaining()
。
其实 FileChannel 会一次写完,但是 SocketChannel 并不一定,所以最好是按照上面的姿势来写入。
# 1.4 关闭
channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法。
所以可以通过 try-with-resource 的方式来完成关闭。
# 1.5 位置
获取当前位置:
long pos = channel.position();
设置当前位置:
long newPos = ...;
channel.position(newPos);
2
设置当前位置时,如果设置为文件的末尾
- 这时读取会返回 -1
- 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
# 1.6 大小
使用 size()
方法获取文件的大小。
# 1.7 强制写入
操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true)
方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘
# 2. 两个 channel 传输数据
transferTo()
方法将数据从一个 channel 传到另一个 channel 中:
String FROM = "helloword/data.txt";
String TO = "helloword/to.txt";
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
from.transferTo(0, from.size(), to);
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("transferTo 用时:" + (end - start) / 1000_000.0);
2
3
4
5
6
7
8
9
10
11
12
transferTo()
方法效率很高,它底层会利用操作系统的零拷贝进行优化,但一次最多传输 2g 的数据。
如果要对超过 2g 的文件进行传输,可以进行分多次传输:
public class TestFileChannelTransferTo {
public static void main(String[] args) {
try (
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
) {
// 效率高,底层会利用操作系统的零拷贝进行优化
long size = from.size();
// left 变量代表还剩余多少字节
for (long left = size; left > 0; ) {
System.out.println("position:" + (size - left) + " left:" + left);
left -= from.transferTo((size - left), left, to);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3. Path
JDK 7 引入了 Path 和 Paths 类
- Path 用来表示文件路径
- Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt"); // 相对路径 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了 d:\1.txt
Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了 d:\1.txt
Path projects = Paths.get("d:\\data", "projects"); // 代表了 d:\data\projects
2
3
4
5
6
7
两个特殊的路径符号:
.
代表了当前路径..
代表了上一级路径
示例:
Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
System.out.println(path.normalize()); // 正常化路径
2
3
输出:
d:\data\projects\a\..\b
d:\data\projects\b
2
# 4. File
也是 JDK 1.7 新增。
检查文件是否存在:
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
2
创建一级目录:
Path path = Paths.get("helloword/d1");
Files.createDirectory(path); // 只能创建一级目录
2
- 如果目录已存在,会抛异常 FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛异常 NoSuchFileException
创建多级目录用:
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
2
拷贝文件:
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
2
3
4
- 如果文件已存在,会抛异常 FileAlreadyExistsException
如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
移动文件:
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
2
3
4
- StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
删除文件:
Path target = Paths.get("helloword/target.txt");
Files.delete(target);
2
3
- 如果文件不存在,会抛异常 NoSuchFileException
删除目录:
Path target = Paths.get("helloword/d1");
Files.delete(target);
2
3
- 如果目录还有内容,会抛异常 DirectoryNotEmptyException
遍历目录文件(重点关注 walkFileTree()
方法):
public static void main(String[] args) throws IOException {
Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
System.out.println(dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
System.out.println(dirCount); // 133
System.out.println(fileCount); // 1479
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
删除多级目录:
Path path = Paths.get("d:\\a");
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
删除是危险操作,确保要递归删除的文件夹没有重要内容。
拷贝多级目录:
long start = System.currentTimeMillis();
String source = "D:\\Snipaste-1.16.2-x64";
String target = "D:\\Snipaste-1.16.2-x64aaa";
Files.walk(Paths.get(source)).forEach(path -> {
try {
String targetName = path.toString().replace(source, target);
// 是目录
if (Files.isDirectory(path)) {
Files.createDirectory(Paths.get(targetName));
}
// 是普通文件
else if (Files.isRegularFile(path)) {
Files.copy(path, Paths.get(targetName));
}
} catch (IOException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println(end - start);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21