文章来源:http://cucaracha.iteye.com/blog/2041847
对文件来说,可能最常用的操作就是创建、读取和写出。NIO.2 提供了丰富的方法来完成这些任务。本文从简单的小文件操作开始,最后以缓冲和非缓冲流的操作结束。
流分为输入流和输出流(可以输出到任何地方,比如硬盘或内存)。流支持不同类型的数据,比如字符串、字节、原始数据类型、本地化字符、对象等。使用非缓冲流,读和写的操作直接依赖底层文件系统,使用缓冲流,数据从内存的缓冲区读取,只有缓冲区空了之后才会调用本地方法进行读取。同样,缓冲输出流也是先将数据写出缓冲区,当缓冲区满了之后才会使用本地方法写出。如果没有等到缓冲区满就需要写出数据,那么可以使用 flush 方法。
使用标准参数 OpenOption
在 NIO.2 中,创建、读取和写出都支持一个可选的 OpenOption 参数,使用它来设置怎样打开和读取文件。实际上,OpenOption 是 java.nio.file 包中的一个接口,它有两个实现:LinkOption (还记得著名的 NOFOLLOW_LINKS 枚举常量吗?)和 StandardOpenOption 类。StandardOpenOption 提供了以下的枚举常量:
- READ - 打开文件进行读取访问
- WRITE - 打开文件进行写出访问
- CREATE - 如果文件不存在则创建新文件
- CREATE_NEW - 创建新文件,如果文件已存在,则抛出异常
- APPPEND - 在文件末尾添加数据(与 WRITE 和 CREATE 结合使用)
- DELETE_ON_CLOSE - 当流关闭的时候删除文件(用于删除临时文件)
- TRUNCATE_EXISTING - 将文件截断为 0 个字节(与 WRITE 结合使用)
- SPARSE - 新创建的文件是 Sparse 文件
- SYNC - 保持文件内容和元数据同步
- DSYNC - 保持文件内容异步
在本文中,将会演示一些上面的枚举常量的用法。
创建新文件
可以使用 Files.createFile() 方法来创建新文件,这个方法接受一个 Path 类型的参数,并且接受一个可选的 FileAttribute<?> 参数用于在创建文件的时候设置文件属性。调用后返回新创建的文件。下面的代码段将演示在 C:
afaelnadal ournaments2010 目录(此目录必须存在)下创建 SonyEricssonOpen.txt 文件(文件必须不存在,如果已经存在,则会抛出 FileAlreadyExistsException 异常),并使用默认属性。
- Path newfile = FileSystems.getDefault().
- getPath("C:/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");
- …
- try {
- Files.createFile(newfile);
- } catch (IOException e) {
- System.err.println(e);
- }
也可以在创建文件的时候设置属性,下面的代码演示了如何在 POSIX 文件系统上创建文件,并设置访问权限:
- Path newfile = FileSystems.getDefault().
- getPath("/home/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");
- Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
- FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
- try {
- Files.createFile(newfile, attr);
- } catch (IOException e) {
- System.err.println(e);
- }
随后你将会看到,这并不是创建文件的唯一方式。
写出小文件
NIO.2 提供了优雅的方式来写出小的二进制或文本文件。使用 Files.write() 方法写出文件,这个方法会打开文件用于写出(如果文件不存在会先创建文件)或者将常规文件截取为 0 字节后写出。当所有字节和行写完后,这个方法会关闭文件(即使出现 I/O 错误或异常也会关闭)。简单说来,这个方法相当于使用了 CREATE、TRUNCATE_EXISTING、和 WRITE 的 OpenOption 参数。
调用 write() 写出字节
可以使用 Files.write() 来写出字节,这个方法接受一个 Path 类型的参数,一个用于写出的字节数组,和一个可选的文件打开参数(OpenOption)。这个方法会返回写出文件的 Path 对象。
下面的代码段将演示如何写出字节数组,并且使用了默认的打开参数。(文件名为 ball.png,将会被写出到 C:
afaelnadalphotos 目录):
- Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
- …
- byte[] ball_bytes = new byte[]{
- (byte)0x89,(byte)0x50,(byte)0x4e,(byte)0x47,(byte)0x0d,(byte)0x0a,(byte)0x1a,(byte)0x0a,
- (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x0d,(byte)0x49,(byte)0x48,(byte)0x44,(byte)0x52,
- (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,
- (byte)0x08,(byte)0x02,(byte)0x00,
- …
- (byte)0x49,(byte)0x45,(byte)0x4e,(byte)0x44,(byte)0xae,(byte)0x42,(byte)0x60,(byte)0x82
- };
- try {
- Files.write(ball_path, ball_bytes);
- } catch (IOException e) {
- System.err.println(e);
- }
现在,你检查目录,就可以看到创建的 ball.png 文件。
如果你想使用字节的方式来写出字符串(String),那么先要将字符串转换为字节数组,下面的代码将在 C:
afaelnadalwiki 目录下写出 wiki.txt 文件:
- Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
- …
- String rf_wiki = "Rafael "Rafa" Nadal Parera (born 3 June 1986) is a Spanish professional
- tennis " + "player and a former World No. 1. As of 29 August 2011 (2011 -08-29)[update], he is
- ranked No. 2 " + "by the Association of Tennis Professionals (ATP). He is widely regarded as
- one of the greatest players " + "of all time; his success on clay has earned him the nickname
- "The King of Clay", and has prompted " + "many experts to regard him as the greatest clay
- court player of all time. Some of his best wins are:";
- try {
- byte[] rf_wiki_byte = rf_wiki.getBytes("UTF-8");
- Files.write(rf_wiki_path, rf_wiki_byte);
- } catch (IOException e) {
- System.err.println(e);
- }
调用 write() 按行写出
可以使用 Files.write() 方法按行写出文件,方法会在每行的结尾根据系统添加一个行结束符(line.separator 系统属性)。这个方法接受一个 Path 类型的参数,一个可迭代的字符序列集合,一个用于编码的参数和一个可选的文件打开参数。这个方法会返回写出的文件 Path 对象。
下面的代码将演示如何按行写出文件(实际上,会在上节创建的 wiki.txt 文件后面添加内容)。
- Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
- …
- Charset charset = Charset.forName("UTF-8");
- ArrayList<String> lines = new ArrayList<>();
- lines.add(" ");
- lines.add("Rome Masters - 5 titles in 6 years");
- lines.add("Monte Carlo Masters - 7 consecutive titles (2005-2011)");
- lines.add("Australian Open - Winner 2009");
- lines.add("Roland Garros - Winner 2005-2008, 2010, 2011");
- lines.add("Wimbledon - Winner 2008, 2010");
- lines.add("US Open - Winner 2010");
- try {
- Files.write(rf_wiki_path, lines, charset, StandardOpenOption.APPEND);
- } catch (IOException e) {
- System.err.println(e);
- }
读取小文件
NIO.2 提供了快速读取小文件的方法。使用 Files.readAllBytes() 和Files.readAllLines() 方法读取文件。这个方法在读取完成后会自动关闭流(即使出现 I/O 异常或错误也会关闭)。
调用 readAllBytes() 方法读取数据
Files.readAllBytes() 方法将整个文件读入字节数组中。下面的代码将演示读取前面创建的 ball.png 文件(文件必须存在)到字节数组:
- Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
- …
- try {
- byte[] ballArray = Files.readAllBytes(ball_path);
- } catch (IOException e) {
- System.out.println(e);
- }
如果你想验证返回的字节是否正确,你可以将返回的字节写出到相同目录的 bytes_to_ball.png 文件中:
- …
- Files.write(ball_path.resolveSibling("bytes_to_ball.png"), ballArray);
- …
或者可以通过下面的 ImageIO 方式写出 PNG 图片文件:
- BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(ballArray));
- ImageIO.write(bufferedImage, "png", (ball_path.resolveSibling("bytes_to_ball.png")).toFile());
readAllBytes() 方法也可以用于读取文本文件,这时,读取的字节需要转换成字符串,看看下面的例子(你可以使用任何其它编码):
- …
- try {
- byte[] wikiArray = Files.readAllBytes(wiki_path);
- String wikiString = new String(wikiArray, "ISO-8859-1");
- System.out.println(wikiString);
- } catch (IOException e) {
- System.out.println(e);
- }
注意:如果文件太大(超过 2GB),那么字节数组的长度将会超出上限,会抛出 OutOfMemory 异常。这依赖于 JVM 的 Xmx 参数:在 32 位 JVM 上,不能超过 2GB(通常使用默认,不超过 256 MB)。在 64 位 JVM 上,可以大一些——十几 GB。
调用 readAllLines() 方法读取文件
使用 readAllLines() 方法会按行返回 String 类型的 List,方便进行循环取值(传递给这个方法的 Path 对象用于指定被读取文件,CharSet 用于设置解码的编码格式):
- Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
- …
- Charset charset = Charset.forName("ISO-8859-1");
- try {
- List<String> lines = Files.readAllLines(wiki_path, charset);
- for (String line : lines) {
- System.out.println(line);
- }
- } catch (IOException e) {
- System.out.println(e);
- }
按照官方文档,这个方法会识别以下的行结束符:
- u000Au000D - 换行回车
- u000A - 换行
- u000D - 回车
使用缓冲流
对于大多数操作系统来说,进行读写操作都是比较消耗资源的操作。NIO.2 提供了两个方法来进行缓冲区读写:Files.newBufferedReader() 和 Files.newBufferedWriter(),这两个方法都接受 Path 类型的对象,并返回老的 JDK 1.1 中的 BufferedReader 或 BufferedWriter 对象。
使用 newBufferedWriter() 方法
newBufferedWriter() 参数为一个 Path 类型的对象,一个 Charset 对象用于编码,一个可选的文件打开方式选项,返回一个新的 BufferedWriter 对象。这个方法将打开文件用于写出(如果文件不存在将会创建文件)或者将已存在的文件截取为 0 字节。简短说来,这个方法默认情况下相当于使用了 CREATE、TRUNCATE_EXISTING、和 WRITE 的 OpenOption 属性。
下面的代码将在前面创建的 wiki.txt 文件后面添加内容:
- Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
- …
- Charset charset = Charset.forName("UTF-8");
- String text = " Vamos Rafa!";
- try (BufferedWriter writer = Files.newBufferedWriter(wiki_path, charset,
- StandardOpenOption.APPEND)) {
- writer.write(text);
- } catch (IOException e) {
- System.err.println(e);
- }
使用 newBufferedReader() 方法
newBufferedReader() 方法可用于通过缓冲区读取文件。它的参数为一个 Path 类型的对象,一个 Charset 用于解码。它将返回一个 BufferedReader 类型的对象。
下面的代码将演示使用 UTF-8 的方式读取 wiki.txt:
- Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
- …
- Charset charset = Charset.forName("UTF-8");
- try (BufferedReader reader = Files.newBufferedReader(wiki_path, charset)) {
- String line = null;
- while ((line = reader.readLine()) != null) {
- System.out.println(line);
- }
- } catch (IOException e) {
- System.err.println(e);
- }
如果按照前文的例子一步步运行下来,那么运行结果将会是:
- Rafael "Rafa" Nadal Parera (born 3 June 1986) is a Spanish professional tennis player and a
- former World No. 1. As of 29 August 2011 (2011 -08-29)[update], he is ranked No. 2 by the
- Association of Tennis Professionals (ATP). He is widely regarded as one of the greatest
- players of all time; his success on clay has earned him the nickname "The King of Clay", and
- has prompted many experts to regard him as the greatest clay court player of all time. Some
- of his best wins are:
- Rome Masters - 5 titles in 6 years
- Monte Carlo Masters - 7 consecutive titles (2005-2011)
- Australian Open - Winner 2009
- Roland Garros - Winnner 2005-2008, 2010, 2011
- Wimbledon - Winner 2008, 2010
- US Open - Winner 2010
- Vamos Rafa!
使用非缓冲流
NIO.2 提供了使用非缓冲流的方法,可以用于直接读取或通过 java.io 的 API 封装成缓冲流使用。使用非缓冲流的方法是 Files.newInputStream() 和 Files.newOutputStream()。
使用 newOutputStream() 方法
newOutputStream() 参数为一个 Path 对象和一个可选的用于配置文件打开方式的 OpenOption 配置选项。它返回一个新的线程安全的非缓冲输出流 OutputStream。这个方法将打开文件用于写出(如果文件不存在将会创建文件)或者将已存在的文件截取为 0 字节。简短说来,这个方法默认情况下相当于使用了 CREATE、TRUNCATE_EXISTING、和 WRITE 的 OpenOption 属性。
下面的代码将会写出 "Racquet: Babolat AeroPro Drive GT" 文本内容到文件 C:
afaelnadalequipment
acquet.txt,因为没有指定 OpenOption,所以文件不存的话会自动创建:
- Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
- String racquet = "Racquet: Babolat AeroPro Drive GT";
- byte data[] = racquet.getBytes();
- try (OutputStream outputStream = Files.newOutputStream(rn_racquet)) {
- outputStream.write(data);
- } catch (IOException e) {
- System.err.println(e);
- }
另外,如果你决定要使用缓冲流来取代非缓冲流,可以使用 java.io API 提供的方法。看看下面的代码,将会在 racquet.txt 文件(文件必须存在)后面添加“String: Babolat RPM Blast 16” 文本。
- Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
- String string = " String: Babolat RPM Blast 16";
- try (OutputStream outputStream = Files.newOutputStream(rn_racquet, StandardOpenOption.APPEND);
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
- writer.write(string);
- } catch (IOException e) {
- System.err.println(e);
- }
使用 newInputStream() 方法
newInputStream() 的参数为一个 Path 对象和一个可选的用于配置文件打开方式的 OpenOption 配置选项。它返回一个新的线程安全的 InputStream 对象。默认情况下,这个方法是用的是 OpenOption.READ 选项。
下面的代码将读取 racquet.txt 文件(文件必须存在)的内容:
- Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
- …
- int n;
- try (InputStream in = Files.newInputStream(rn_racquet)) {
- while ((n = in.read()) != -1) {
- System.out.print((char)n);
- }
- } catch (IOException e) {
- System.err.println(e);
- }
你也可以使用 InputStream 的 read() 方法将数据读取到用于缓冲的字节数组中。你可以将上面的代码改写为(记住,你的处理对象依然是非缓冲输入流):
- Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
- …
- int n;
- byte[] in_buffer = new byte[1024];
- try (InputStream in = Files.newInputStream(rn_racquet)) {
- while ((n = in.read(in_buffer)) != -1) {
- System.out.println(new String(in_buffer));
- }
- } catch (IOException e) {
- System.err.println(e);
- }
注:调用 read(in_buffer) 方法和调用 read(in_buffer,0,in_buffer.length) 方法是同样的效果。
你也可以通过 java.io API 将非缓冲输入流转换为缓冲输入流,下面的代码和上面的代码运行结果相同,但是更有效率:
- Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
- …
- try (InputStream in = Files.newInputStream(rn_racquet);
- BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
- String line = null;
- while ((line = reader.readLine()) != null) {
- System.out.println(line);
- }
- } catch (IOException e) {
- System.err.println(e);
- }
上面三段代码输出的结果都是相同的:
- Racquet: Babolat AeroPro Drive GT
- String: Babolat RPM Blast 16