在丑陋的 Java I/O 編程方式誕生多年以后盐捷,Java終于簡(jiǎn)化了文件讀寫(xiě)的基本操作偶翅。
很多學(xué)得比較快的同學(xué)可能學(xué)習(xí)過(guò)Java的文件讀寫(xiě),就是諸如 InputStream, OutputStream 一樣的東西碉渡,如上面所說(shuō)的一樣聚谁,他們既丑陋又難用,學(xué)習(xí)路線非常曲折滞诺。不過(guò)好在 Java 設(shè)計(jì)者終于意識(shí)到了 Java 使用者多年來(lái)的痛苦形导,在 Java7 中對(duì)此引入了巨大的改進(jìn),這些新特性被放在 java.nio.file 包下面习霹。除此之外朵耕,Java8 新增的 streams 與文件結(jié)合使得文件操作變得更加優(yōu)雅易用,可以說(shuō)從此以后我們幾乎再也不需要見(jiàn)到 InputStream 這種難用的東西了淋叶。
首先讓我們將看一下文件操作的兩個(gè)基本概念:
- 文件的路徑
- 文件本身
可能有同學(xué)要問(wèn)我這里為什么只提到了文件而沒(méi)有提到目錄阎曹,我這里說(shuō)的文件是指廣義的文件(File),包括我們認(rèn)為的文件(RegularFile)和目錄(Directory)煞檩,為了避免歧義处嫌,之后盡量用英文說(shuō)明狹義的文件(RegularFile)。
一斟湃、文件的路徑
路徑熏迹,也就是 Path ,例如"C://Windows/Fonts"凝赛、"/Users/tommy0607/Downloads/InputFix.jar"等等都是路徑癣缅。
java.nio.file.Paths 類(lèi)包含一個(gè)重載方法 static get(String first, String... more)
,該方法通過(guò)輸入一系列路徑片段可以返回一個(gè)路徑哄酝。
例如想要獲得 /Users/tommy0607/Downloads 的路徑,可以用下面三種方式:
- 直接用全路徑:
Paths.get("/Users/tommy0607/Downloads")
- 用父目錄的路徑名和文件名字:
Paths.get("/Users/tommy0607","Downloads")
- 將路徑進(jìn)行任意分段:
Paths.get("/Users","tommy0607","Downloads")
當(dāng)然除了絕對(duì)路徑之外祷膳,相對(duì)路徑也是可以的陶衅,例如Path.get("test/abc.png")
就是一個(gè)相對(duì)路徑。
它還有一個(gè)重載方法直晨,接受一個(gè)統(tǒng)一資源定位符URI為參數(shù)搀军,在本節(jié)課中用不到就直接略過(guò)膨俐。
Path 對(duì)象的主要方法如下:
- getParent() 獲取該路徑的父路徑
- isAbsolute() 返回該路徑是否是絕對(duì)路徑
- toAbsolutePath() 獲取該路徑的絕對(duì)路徑
- normalize() 將絕對(duì)路徑正常化罩句,如果當(dāng)前路徑是相對(duì)路徑則會(huì)報(bào)錯(cuò)
- toUri() 將其轉(zhuǎn)換成URI
- resolve(String/Path other) 將當(dāng)前路徑與參數(shù)中的路徑進(jìn)行拼接焚刺,一般用于獲取子目錄的路徑
- getFileName() 獲取路徑的文件名,分段路徑的最右邊那部分就是文件名
關(guān)于normalize()
方法的正趁爬茫化我要特別講解一下:
假設(shè)當(dāng)前目錄是 /Users/tommy0607/Downloads乳愉,現(xiàn)有這樣的對(duì)象Path path = Paths.get("../../test.jar")
,
那么path.toAbsolutePath()
返回的路徑是 /Users/tommy0607/Downloads/../../test.jar屯远,而path.toAbsolutePath().normoalize()
返回的路徑則是 /User/test.jar蔓姚,也就是它會(huì)把絕對(duì)路徑里所有的"../"都去掉,這就是正晨ぃ化
還有要特別注意的就是 Path 類(lèi)進(jìn)行的所有操作都只是基于字符串處理坡脐,也就是說(shuō)如果你輸入一個(gè)不存在的路徑也不會(huì)有任何的報(bào)錯(cuò),這點(diǎn)要多加注意房揭!
二备闲、文件本身
雖然 Java 中也有用來(lái)代表一個(gè)文件(包括RegularFile和Directory)的類(lèi),叫做 File捅暴,但這個(gè)類(lèi)的設(shè)計(jì)有些問(wèn)題恬砂,它與其說(shuō)是文件還不如說(shuō)是路徑,更何況在 Java7 之后就基本用不到這個(gè)類(lèi)了伶唯,現(xiàn)在用得比較多的是 nio 中的 Files 類(lèi)觉既。
Files 工具類(lèi)包含一系列完整的方法用于獲得路徑對(duì)應(yīng)的文件的相關(guān)信息,下面用一個(gè)簡(jiǎn)單的示例來(lái)展示:
import java.nio.file.*;
import java.io.IOException;
public class PathAnalysis {
public static void main(String[] args) throws IOException {
Path p = Paths.get("PathAnalysis.java").toAbsolutePath();
say("該文件存在", Files.exists(p));
say("該文件是目錄", Files.isDirectory(p));
say("該文件可執(zhí)行", Files.isExecutable(p));
say("該文件可讀", Files.isReadable(p));
say("該文件是RegularFile", Files.isRegularFile(p));
say("該文件可寫(xiě)", Files.isWritable(p));
say("該文件不存在", Files.notExists(p));
say("該文件是隱藏的", Files.isHidden(p));
say("文件尺寸", Files.size(p));
say("文件最后修改日期: ", Files.getLastModifiedTime(p));
say("文件擁有者", Files.getOwner(p));
}
static void say(String id, Object result) {
System.out.print(id + ": ");
System.out.println(result);
}
}
三乳幸、文件查找
在查找文件方面瞪讼,Java 也提供了非常方便實(shí)用的API。但在介紹文件查找之前不得不先介紹一下Files.walk()
方法:它可以返回某個(gè)路徑下所有子目錄和文件的路徑的流Stream<Path>
粹断。舉個(gè)例子:
Files.walk(Paths.get("/Users/tommy0607/Downloads"))
.forEach(System.out::println);
它輸出了/Users/tommy0607/Downloads 下面所有子目錄和文件的路徑(包括當(dāng)前目錄本身)
文件查找有兩種模式符欠,glob 和 regex,這里只講解前一種模式瓶埋,后一種是使用正則表達(dá)式希柿,感興趣的同學(xué)可以自學(xué)。
在這里养筒,我們使用 glob
查找以 .tmp
或 .txt
結(jié)尾的所有路徑:
import java.nio.file.*;
public class Find {
public static void main(String[] args) throws Exception {
Path test = Paths.get("test");
// 創(chuàng)建一個(gè)叫dir.tmp的文件夾
Files.createDirectory(test.resolve("dir.tmp"));
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{tmp,txt}");
Files.walk(test)
.filter(matcher::matches)
.forEach(System.out::println);
System.out.println("***************");
PathMatcher matcher2 = FileSystems.getDefault()
.getPathMatcher("glob:*.tmp");
Files.walk(test)
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
System.out.println("***************");
Files.walk(test)
.filter(Files::isRegularFile) //只查找RegularFile
.map(Path::getFileName)
.filter(matcher2::matches)
.forEach(System.out::println);
}
}
/* 輸出:
test\bag\foo\bar\baz\5208762845883213974.tmp
test\bag\foo\bar\baz\File.txt
test\bar\baz\bag\foo\7918367201207778677.tmp
test\bar\baz\bag\foo\File.txt
test\baz\bag\foo\bar\8016595521026696632.tmp
test\baz\bag\foo\bar\File.txt
test\dir.tmp
test\foo\bar\baz\bag\5832319279813617280.tmp
test\foo\bar\baz\bag\File.txt
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
dir.tmp
5832319279813617280.tmp
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
5832319279813617280.tmp
*/
在 matcher
中曾撤,glob
表達(dá)式開(kāi)頭的 **/
表示“當(dāng)前目錄及所有子目錄”,這在不僅僅要匹配當(dāng)前目錄下的路徑時(shí)非常有用晕粪。一個(gè) *
表示“任何字符串”挤悉,然后是一個(gè)點(diǎn),接著是大括號(hào)巫湘,表示一系列的可能性——我們正在尋找以 .tmp
或 .txt
結(jié)尾的路徑装悲。
matcher2
只使用 *.tmp
昏鹃,按理來(lái)說(shuō)應(yīng)該無(wú)法匹配任何路徑的,但注意看map()
方法將全路徑轉(zhuǎn)換成了文件名
注意诀诊,在前兩種情況下洞渤,輸出中都會(huì)出現(xiàn) dir.tmp
,即使它是一個(gè)目錄而不是一個(gè)文件(RegularFile)属瓣。如果只查找文件载迄,必須像在最后的 files.walk()
中那樣對(duì)其進(jìn)行篩選。
四奠涌、文件讀寫(xiě)
到了最后一部分才講到本課程的核心主題——文件讀寫(xiě)宪巨,不過(guò)用起來(lái)其實(shí)非常簡(jiǎn)單方便。
讀文件
如果一個(gè)文件很小溜畅,也就是說(shuō)就算把整個(gè)文件一次性全部讀到內(nèi)存中也沒(méi)問(wèn)題捏卓,那么可以用Files.readAllLines()
和Files.readAllBytes()
,前者是返回所有行的字符串慈格,后者則是獲取返回整個(gè)文件的字節(jié)數(shù)組怠晴。
但如果文件很大,一次性讀取到內(nèi)存中不太可能浴捆,或者說(shuō)你并不需要讀取整個(gè)文本蒜田,Files.lines()
方便地將文件轉(zhuǎn)換為行的 Stream
(也就是說(shuō)流中的元素是文件的每一行文本):
import java.nio.file.*;
public class ReadLineStream {
public static void main(String[] args) throws Exception {
Files.lines(Paths.get("PathInfo.java"))
.skip(13)
.findFirst()
.ifPresent(System.out::println);
}
}
代碼很容易理解,先跳過(guò) PathInfo.java 的前13行选泻,然后讀取接下來(lái)的一行并輸出冲粤。
寫(xiě)文件
寫(xiě)文件就更簡(jiǎn)單了,直接調(diào)用Files.write()
即可页眯,支持寫(xiě)入字節(jié)數(shù)組和一行一行的字符串梯捕;
或者Files.writeString()
,只寫(xiě)入一個(gè)字符串對(duì)象窝撵。這兩個(gè)方法中都有的 Charset 類(lèi)型的參數(shù)是寫(xiě)入的字符串的編碼傀顾,如果不指定的話默認(rèn)就是UTF-8編碼。