[NIO.2] 第二十七篇 新建、读取和写出文件

对文件来说,可能最常用的操作就是创建、读取和写出。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:\rafaelnadal\tournaments\2010 目录(此目录必须存在)下创建 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:\rafaelnadal\photos 目录):

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:\rafaelnadal\wiki 目录下写出 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("\n");
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); 
}


按照官方文档,这个方法会识别以下的行结束符:

  •     \u000A\u000D - 换行回车
  •     \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 = "\nVamos 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:\rafaelnadal\equipment\racquet.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 = "\nString: 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


文章来源: http://www.aptusource.org/2014/04/nio-2-creating-reading-and-writing-files/

你可能感兴趣的:(java,Java NIO.2)