本篇文章主要討論一下NIO中的文件變成,主要是FileChannel的用法。
一、FileChannel常用操作
1.1 獲取FileChannel
有一個文件text.txt,其內容如下:
abcdef
不能直接打開 FileChannel,必須通過 FileInputStream、FileOutputStream 或者 RandomAccessFile 來獲取 FileChannel,它們都有 getChannel 方法
1.1.1 通過 FileInputStream 獲取
public static void main(String[] args) throws Exception {
//使用FileInputStream獲取channel
FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"));
FileChannel channel1 = fileInputStream.getChannel();
ByteBuffer buffer= ByteBuffer.allocate(10);
//channel1.write(buffer);
channel1.read(buffer);
buffer.flip();
System.out.println((print(buffer)));
}
static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
結果:
abcdef
通過 FileInputStream 獲取的 channel 只能讀,如果使用寫入write方法,會拋出異常:
Exception in thread "main" java.nio.channels.NonWritableChannelException
at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:201)
at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:21)
1.1.2 通過 FileOutputStream 獲取
public static void main(String[] args) throws Exception {
//使用FileOutputStream獲取channel
FileOutputStream fileOutputStream = new FileOutputStream(new File("C:\\Users\\P50\\Desktop\\text.txt"),true);
FileChannel channel2 = fileOutputStream.getChannel();
ByteBuffer buffer= ByteBuffer.allocate(10);
buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
buffer.flip();
channel2.write(buffer);
}
static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
文件被寫入,這裏注意FileOutputStream 的屬性append,如果是true,表示追加,否則覆蓋。本文使用的追加。
abcdefhelloworld
通過 FileOutputStream 獲取的 channel 只能寫,如果使用read方法,會拋出異常:
Exception in thread "main" java.nio.channels.NonReadableChannelException
at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:149)
at com.cloud.bssp.nio.FileChannel.GetFileChannel.main(GetFileChannel.java:28)
1.1.3 通過 RandomAccessFile獲取
public static void main(String[] args) throws Exception {
//使用RandomAccessFile獲取channel
RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
FileChannel channel3 = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(15);
//讀取文件內容到buffer
channel3.read(buffer);
buffer.flip();
System.out.println(print(buffer));
// 切換爲寫模式,並且清空buffer
buffer.clear();
//寫入helloworld到文件
buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
buffer.flip();
channel3.write(buffer);
}
// 切換爲寫模式,並且清空buffer
buffer.clear();
//寫入helloworld到文件
buffer.put(StandardCharsets.UTF_8.encode("helloworld"));
buffer.flip();
channel3.write(buffer);
結果:這裏讀取的少了一個字節,因爲我指定的buffer只有15,文檔中是16,只讀取了一次,。
abcdefhelloworl
文檔內容被修改爲如下,將channel讀取到的內容以及新加入的內容拼接在了一起
abcdefhelloworlhelloworld
通過 RandomAccessFile 是否能讀寫根據構造 RandomAccessFile 時的讀寫模式決定,指定rw(讀寫模式)。
1.2 讀取和寫入
1.2.1 讀取
在前面的獲取例子中已經給出了關於讀取的方式,如下所示,會返回int類型,從 channel 讀取數據填充ByteBuffer,返回值表示讀到了多少字節,返回值爲-1 表示到達了文件的末尾。
int readBytes = channel.read(buffer);
仍然使用上面的第一個例子,如果文檔是空的話,則會返回-1
int read = channel1.read(buffer);
System.out.println(read);
-1
1.2.2 寫入
如上一章節的例子,已經演示瞭如何寫入數據,利用write方法,將buffer的數據寫入channel,但是正確的寫入方式應該如下所示:
while(buffer.hasRemaining()) {
channel.write(buffer);
}
hasRemaining()是buffer的一個方法,判斷position是否小於limit,是則返回true,表示buffer仍然有未讀取的數據。
在 while 中調用 channel.write 是因爲 write 方法並不能保證一次將 buffer 中的內容全部寫入 channel。
1.2.3 強制寫入
操作系統出於性能的考慮,會將數據緩存,不是立刻寫入磁盤。可以調用 channel.force(true) 方法將文件內容和元數據(文件的權限等信息)立刻寫入磁盤。
public abstract void force(boolean metaData) throws IOException;
1.3 關閉
像我們上面寫的代碼實際上都沒有去關閉流和channel的,這如果在生產環境都是會產生嚴重的問題。
channel是必須要關閉的,不過調用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close() 方法會間接地調用 channel 的 close 方法。
看下FileInputStream的close方法:
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
1.4 FileChannel的位置
獲取當前位置
long pos = channel.position();
設置當前位置
long newPos = ...;
channel.position(newPos);
如下獲取文件channel:
// 文件內容爲10個字節的helloworld
RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
FileChannel channel = file.getChannel();
打印不同設置時的位置:
// 打印位置,沒有讀取時是0
System.out.println(channel.position());
// 讀取後是文件的長度
ByteBuffer buffer = ByteBuffer.allocate(10);
channel.read(buffer);
System.out.println(channel.position());
// 設置位置後的長度
FileChannel position = channel.position(5);
System.out.println(position.position());
結果:
0
10
5
1.5 獲取文件大小
channel.size();
二、channel的相互傳輸
channel提供兩個用來channel相互傳輸數據的方法:
/**
* 將一個channel的數據傳輸到target這個channel中,其中position,count,都是調用此方法的channel的
* in.transferTo(0, in.size(), out);
*/
transferTo(long position, long count, WritableByteChannel target)
/**
* 一個channel從src這個channel獲取數據,其中position,count,都是src這個channel的
* out.transferFrom(in,0,in.size());
*/
transferFrom(ReadableByteChannel src, long position, long count)
使用例子如下:
public class TestCopyFileByNIO {
public static void fileChannelCopy(String sfPath, String tfPath) {
FileInputStream fi = null;
FileOutputStream fo = null;
FileChannel in = null;
FileChannel out = null;
try {
fi = new FileInputStream(new File(sfPath));
fo = new FileOutputStream(new File(tfPath));
in = fi.getChannel();//得到對應的文件通道
out = fo.getChannel();//得到對應的文件通道
in.transferTo(0, in.size(), out);//連接兩個通道,並且從in通道讀取,然後寫入out通道
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fi.close();
fo.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
String sPath = "E:\\workspace\\comprehend-service.rar";
String tPath = "E:\\workspace\\comprehend-service-" + System.currentTimeMillis() + "-bak.rar";
fileChannelCopy(sPath, tPath);
long end = System.currentTimeMillis();
System.out.println("用時爲:" + (end - start) + "ms");
}
}
結果:
用時爲:194ms
2.1 channel的最大傳輸值
channel的傳輸是有大小限制的,最大爲2個g,超過會導致數據丟失。所以需要使用循環去多次傳輸數據。
public class TestCopyFileByNIO {
public static void fileChannelCopy(String sfPath, String tfPath) {
FileInputStream fi = null;
FileOutputStream fo = null;
FileChannel in;
FileChannel out;
try {
fi = new FileInputStream(new File(sfPath));
fo = new FileOutputStream(new File(tfPath));
in = fi.getChannel();
out = fo.getChannel();
// 總文件大小
long size = in.size();
// left 剩餘文件的數量
for (long left = size; left > 0;){
System.out.println("position = " + (size - left) + ",left = " + left);
// transferTo返回傳輸的數量,剩餘的減去傳輸的,就是當前剩餘的數量
left -= in.transferTo((size -left), left, out);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fi.close();
fo.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
String sPath = "E:\\workspace\\workspace.zip";
String tPath = "E:\\workspace\\workspace-" + System.currentTimeMillis() + "-bak.zip";
fileChannelCopy(sPath, tPath);
long end = System.currentTimeMillis();
System.out.println("用時爲:" + (end - start) + "ms");
}
結果:
position = 0,left = 2925330022
position = 2147483647,left = 777846375
用時爲:13664ms
三、Path 和 Paths 類
jdk7 引入了 Path 和 Paths 類
Path 用來表示文件路徑
Paths 是工具類,用來獲取 Path 實例
// 相對路徑 使用 user.dir 環境變量來定位 1.txt
Path source = Paths.get("1.txt");
// 絕對路徑 代表了 d:\1.txt
Path source = Paths.get("d:\\1.txt");
// 絕對路徑 同樣代表了 d:\1.txt
Path source = Paths.get("d:/1.txt");
// 代表了 d:\data\projects
Path projects = Paths.get("d:\\data", "projects");
-
.
代表了當前路徑 -
..
代表了上一級路徑
例如目錄結構如下
d:
|- data
|- projects
|- a
|- b
代碼
Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
// 正常化路徑
System.out.println(path.normalize());
會輸出
d:\data\projects\a\..\b
d:\data\projects\b
四、Files類
檢查文件是否存在
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
創建一級目錄
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
- 如果目錄已存在,會拋異常 FileAlreadyExistsException
- 不能一次創建多級目錄,否則會拋異常 NoSuchFileException
創建多級目錄用
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
拷貝文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
- 如果文件已存在,會拋異常 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);
- StandardCopyOption.ATOMIC_MOVE 保證文件移動的原子性
刪除文件
Path target = Paths.get("helloword/target.txt");
Files.delete(target);
- 如果文件不存在,會拋異常 NoSuchFileException
刪除目錄
Path target = Paths.get("helloword/d1");
Files.delete(target);
- 如果目錄還有內容,會拋異常 DirectoryNotEmptyException
遍歷目錄文件
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
}
統計 jar 的數目
Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91");
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
if (file.toFile().getName().endsWith(".jar")) {
fileCount.incrementAndGet();
}
return super.visitFile(file, attrs);
}
});
System.out.println(fileCount); // 724
刪除多級目錄
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);
}
});
刪除是危險操作,確保要遞歸刪除的文件夾沒有重要內容
拷貝多級目錄
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);
關於NIO文件編程此處就寫到這了,有幫助的話朋友個點個贊