FileInputStream、FileOutputStream

FilelnputStream 和 FileOutputStream 可以提供附着在一个磁盘文件上的输入流和输出流,而你只需向其构造器提供文件名或文件的完整路径名即可。

  1. public FileInputStream(String name) throws FileNotFoundException
  2. public FileInputStream(File file) throws FileNotFoundException
  3. // append表示是否追加写,默认为false表示覆盖写
  4. public FileOutputStream(String name) throws FileNotFoundException
  5. public FileOutputStream(String name, boolean append) throws FileNotFoundException
  6. public FileOutputStream(File file) throws FileNotFoundException
  7. public FileOutputStream(File file, boolean append) throws FileNotFoundException

所有在 Java.io 中的类都将相对路径名解释为以用户工作目录开始,可通过 System.getProperty(“user.dir”) 来获得这个信息。

读取文件示例:

  1. public static void readFile(String filePath) {
  2. try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(filePath))) {
  3. byte[] data = new byte[1024];
  4. while (inputStream.read(data) != -1) {
  5. System.out.println(new String(data));
  6. }
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. }

写入文件示例:

  1. public static void writeFile(String filepath, String content) throws IOException {
  2. try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filepath))) {
  3. bufferedOutputStream.write(content.getBytes());
  4. } catch (Exception e) {
  5. e.printStackTrace();
  6. }
  7. }

FileReader、FileWriter

FileWriter 是文件写入的基础类,它包含 5 个构造函数,可以传递一个具体的文件位置或 File 对象,第二个参数表示是否要追加写还是覆盖写,默认值为 false 表示覆盖写。

  1. public FileWriter(String fileName) throws IOException
  2. public FileWriter(String fileName, boolean append) throws IOException
  3. public FileWriter(File file) throws IOException
  4. public FileWriter(File file, boolean append) throws IOException
  5. public FileWriter(FileDescriptor fd)

读取文件示例:

  1. public static void readFile(String filePath) {
  2. try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
  3. char[] data = new char[1024];
  4. while (reader.read(data) != -1) {
  5. System.out.println(new String(data));
  6. }
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. }

有一点需要注意,在默认情况下,FileReader 是以当前机器的默认字符集来读取文件内容的,如果希望指定字符集的话,可以先通过 FileInputStream 读取到字节流,然后使用 InputStreamReader 读取字符流并指定我们想要设置的字符集。

  1. InputStreamReader reader = new InputStreamReader(new FileInputStream("/user/home/file.txt"), StandardCharsets.UTF_8);

平台使用的编码方式可以由静态方法 Charset.defaultCharset 返回。静态方法 Charset.availableCharsets 会返回所有可用的 Charset 实例,返回结果是一个从字符集的规范名称到 Charset 对象的映射表。而 StandardCharsets 类具有类型为 Charset 的静态变量,用于表示每种 Java 虚拟机都必须支持的字符编码。

写入文件示例:

  1. public static void writeFile(String filePath, String content) throws IOException {
  2. try (FileWriter fileWriter = new FileWriter(filePath)) {
  3. fileWriter.write(content);
  4. // 如果是追加写
  5. // fileWriter.append(content);
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }

PrintWriter

PrintWriter 这个类拥有以文本格式打印字符串和数字的方法。为了打印文件,需要用文件名和字符编码方式来构建一个 PrintWriter 对象:

  1. public PrintWriter(String fileName) throws FileNotFoundException
  2. public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException
  3. public PrintWriter(File file) throws FileNotFoundException
  4. public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException

为了输出到打印写出器,需要使用与使用 System.out 时相同的 printprintlnprintf 方法。你可以用这些方法来打印数字(int、short、long、float、double)、字符、boolean 值、字符串和对象。其中 println 方法会在每一行中添加对目标系统来说恰当的行结束符(Windows 系统是 “\r\n”,UNIX 系统是 “\n”),也就是通过调用 System.getProperty(“line.separator”) 而获得的字符串。

如果写出器设置为自动冲刷模式,那么只要 println 被调用,缓冲区中的所有字符都会被发送到它们的目的地(打印写出器总是带缓冲区的)。默认情况下,自动冲刷机制是禁用的,可以通过使用 PrintWriter(Writer writer, boolean autoFlush) 来启用或禁用自动冲刷机制。

RandomAccessFile

RandomAccessFile 类可以在文件中的任何位置查找或写入数据,对于磁盘文件来说都是随机访问的,但对于网络套接字通信的输入、输出流却不是。我们可以打开一个用来随机访问的文件,mode 字符串有以下模式:

  • r:只读模式
  • rw:读、写模式
  • rws:每次更新时,都对数据和元数据的写磁盘操作进行同步的读、写模式
  • rwd:每次更新时,只对数据的写磁盘操作进行同步的读、写模式
  1. public RandomAccessFile(String name, String mode) throws FileNotFoundException
  2. public RandomAccessFile(File file, String mode) throws FileNotFoundException

随机访问文件有一个表示下一个将被读入或写出的字节所处位置的文件指针。seek 方法可以用来将这个文件指针设置到文件中的任意字节位置。seek 方法的参数是一个 long 类型的整数,它的值位于 0 到文件按照字节来度量的长度之间。getFilePointer 方法将返回文件指针的当前位置。

  1. public void seek(long pos) throws IOException
  2. public native long getFilePointer() throws IOException;

Files

在 JDK 7 中提供了一个新的文件操作类 Files 来实现文件的写入,它提供了大量处理文件的便捷方法,例如文件复制、读取、写入、获取文件属性、快捷遍历文件目录等,这些方法极大的方便了文件的操作。

1. Path

Path 既可以表示一个目录,也可以表示一个文件,就像 File 那样。实际上,Path 就是用来取代 File 的,这两个类之间也可以通过 toPath()toFile() 进行相互转换。下面简单介绍下 Path 使用方法:

可以通过 Paths.get() 创建一个 Path 对象,此时 Path 并没有真正在物理磁盘上创建。get() 方法的参数既可以是一个文件名,也可以是一个目录名,并且绝对路径或相对路径均可。创建 Path 对象后,还可以通过 toAbsolutePath() 来查看 Path 的绝对路径。

  1. public static Path get(String first, String... more)

如果想要创建文件或目录,可以通过 Files.createFile()Files.createDirectory() 来操作,通过该方法创建的文件或目录已经在物理磁盘上创建成功,可通过资源管理器查看到。

  1. public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException
  2. public static Path createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException

创建完成后,可通过 Files.notExists() 来确认 Path(目录或者文件) 是否已经创建完成。

  1. public static boolean notExists(Path path, LinkOption... options)

2. 处理目录

NIO 2.0 新增的 java.nio.file.DirectoryStream 接口可以非常方便地查找目录中符合某种规则的文件,下面示例展示了要查找 Download 目录下以 .txt 后缀结尾的文件:

  1. public static void searchFile() {
  2. Path dir = Paths.get("/Users/Downloads");
  3. try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.txt")) {
  4. for (Path entry : stream) {
  5. System.out.println(entry.getFileName());
  6. }
  7. } catch (IOException e) {
  8. e.printStackTrace();
  9. }
  10. }

Files.newDirectoryStream() 方法会返回一个过滤后的 DirectoryStream,它被称为目录流,是 Iterable 的子接口,因此我们可以方便地使用 foreach 语法来遍历目录。并且 DirectoryStream 还继承了 Closeable 接口,所以它可以配合 try-with-resources 语法写出更安全简洁的代码。

此外,如果一个目录里既有文件也有子目录,我们想遍历这一整棵目录树并操作符合条件的文件的话,则可以通过 Files.walkFileTree() 方法进行操作。

  1. public static Path walkFileTree(Path start,
  2. FileVisitor<? super Path> visitor) throws IOException

第二个参数 FileVisitor 被称为文件访问器接口,它实现起来非常复杂,但幸好 JDK 的设计者提供了一个默认的实现类 SimpleFileVisitor,如果只想从目录树中找到以 .txt 后缀结尾的文件,可以这样做:

  1. public static void searchFile() {
  2. Path dir = Paths.get("/Users/Downloads");
  3. try {
  4. Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
  5. @Override
  6. public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
  7. if (file.toString().endsWith(".txt")) {
  8. System.out.println(file.getFileName());
  9. }
  10. return FileVisitResult.CONTINUE;
  11. }
  12. });
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }

3. 文件操作

创建一个文件或目录:

  1. public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException
  2. public static Path createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException
  3. public static Path createDirectories(Path dir, FileAttribute<?>... attrs) throws IOException

当使用 createDirectory 方法时,路径中除最后一个目录外,其他目录都必须是已存在的。如果要自动创建路径中不存在的中间目录,则应该使用 createDirectories 方法。有些便捷方法还可以用来在给定位置或者系统指定位置创建临时文件或临时目录:

  1. Path newPath = Files.createTempFile(dir, prefix, suffix);
  2. Path newPath = Files.createTempFile(prefix, suffix);
  3. Path newPath = Files.createTempDirectory(dir, prefix);
  4. Path newPath = Files.createTempDirectory(prefix);

其中,dir 是一个 Path 对象,prefix 和 suffix 是可以为 null 的字符串。例如,调用 Files.createTempFile(null, “.txt”) 可能会返回一个像 /tmp/12344Ð5522364B37194.txt 这样的临时文件。

删除一个文件也同样的简单,不过在删除文件前最好用 Files.exists() 判断文件是否存在,否则文件不存在时会抛出一个 NoSuchFileException。如果使用 Files.deleteIfExists() 则不用进行手动判断:

  1. public static void delete(Path path) throws IOException
  2. public static boolean exists(Path path, LinkOption... options)
  3. public static boolean deleteIfExists(Path path) throws IOException

复制文件:

  1. public static Path copy(Path source, Path target, CopyOption... options) throws IOException
  2. public static long copy(InputStream in, Path target, CopyOption... options) throws IOException
  3. public static long copy(Path source, OutputStream out) throws IOException

如果目标路径已存在,则复制或移动将失败。如果想覆盖已有的目标路径,可以使用 StandardCopyOption 提供的 REPLACE_EXISTING 选项。如果想要复制所有的文件属性,可以使用 COPY_ATTRIBUTES 选项。

移动文件:

  1. public static Path move(Path source, Path target, CopyOption... options) throws IOException

可以将移动操作定义为原子性的,这样就可以保证要么移动操作成功完成,要么源文件继续保持在原来位置。具体可以使用 StandardCopyOption 提供的 ATOMIC_MOVE 选项来实现。

获取文件信息:

  1. // 返回文件字节数
  2. public static long size(Path path) throws IOException
  3. public static boolean isHidden(Path path) throws IOException
  4. public static boolean isReadable(Path path)
  5. public static boolean isWritable(Path path)

写文件,如果想实现文件的追加写,可以指定 StandardOpenOption.APPEND 选项:

  1. public static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException

读取文件内容:

  1. public static List<String> readAllLines(Path path) throws IOException
  2. public static List<String> readAllLines(Path path, Charset cs) throws IOException

该方法会一次性读取文件的所有内容后,放到一个 List 中进行返回。但是这样存在一个问题,如果内存无法容纳这个文件的全部内容,则会产生 OOM。如果我们想要按需读取文件,则可以使用 Files.lines() 方法:

  1. public static Stream<String> lines(Path path) throws IOException
  2. public static Stream<String> lines(Path path, Charset cs) throws IOException

与 readAllLines 方法返回 List 不同,lines 方法返回的是 Stream。这使得我们在需要时可以不断读取、使用文件中的内容,而不是一次性地把所有内容都读取到内存中,因此避免了 OOM。但是要注意,在调用完之后需要进行关闭,或者通过 try-with-resources 自动关闭。

4. 缓冲读写

NIO 2.0 提供了带有缓冲区的读写辅助方法,使用起来也非常简单。可以通过 Files.newBufferedWriter() 获取一个文件缓冲输入流,并通过 write() 方法写入数据;然后通过 Files.newBufferedReader() 获取一个文件缓冲输出流,通过 readLine() 方法读出数据。

代码示例如下:

  1. public static void writeFile() {
  2. Path file = Paths.get("/Users/Downloads/1.txt");
  3. try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
  4. writer.write("Hello File");
  5. } catch (IOException e) {
  6. e.printStackTrace();
  7. }
  8. }
  1. public static void readFile() {
  2. Path file = Paths.get("/Users/Downloads/1.txt");
  3. try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
  4. String line;
  5. while ((line = reader.readLine()) != null) {
  6. System.out.println(line);
  7. }
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. }

FileChannel

FileInputStream/FileOutputStream FileChannel
单向 双向
面向字节的读写 面向 Buffer 读写
不支持 支持内存文件映射
不支持 支持转入或转出其他通道
不支持 支持文件锁
不支持操作文件元信息 不支持操作文件元信息

1. 获取

通过对 FileInputStream、FileOutputStream 调用其 getChannel() 方法来获取对应文件通道:

  1. public FileChannel getChannel()

FileChannel 提供了静态工厂方法 open()

  1. public static FileChannel open(Path path, OpenOption... options) throws IOException

还可以设置文件的操作模式(OpenOption 操作符控制):

  • READ:只读方式;
  • WRITE:只写方式;
  • APPEND:只追加方式;
  • CREATE:创建新文件;
  • CREATE_NEW:创建新文件,如果存在则失败;
  • TRUNCATE_EXISTING:如果以读方式访问文件,它的长度将被清除至 0;


在 JDK 7 中提供的 Files 工具类中,提供了 newByteChannel() 方法获取文件通道:

  1. public static SeekableByteChannel newByteChannel(Path path,
  2. Set<? extends OpenOption> options,
  3. FileAttribute<?>... attrs) throws IOException

2. 文件映射

FileChannel 提供了 map() 方法用于将磁盘文件上的某段映射至系统内存,返回 MappedByteBuffer,具有以下几种映射模式:

  • READ_ONLY:以只读的方式映射,如果发生修改,则抛出 ReadOnlyBufferException;
  • READ_WRITE:读写方式;
  • PRIVATE:对这个 MappedByteBuffer 的修改不写入文件,且其他程序是不可见的;

注意:一旦经过 map 映射后,MappedByteBuffer 将与用于映射的 FileChannel 没有联系,即使 Channel 关闭也对 MappedByteBuffer 没有影响。

当面对超大文件的处理时,使用 map() 方法整体的性能才会提升。对于很小的文件使用 map 处理的性能不一定比传统基于流的读写好,因为直接映射进入内存的代价开销较大。需要在这两者之间进行权衡选择。

  1. public static void copyFileByMappedByteBuffer() {
  2. try (FileChannel inChannel = FileChannel.open(Paths.get("/Users/Downloads/1.txt"), StandardOpenOption.READ);
  3. // CREATE_NEW选项可以直接写入不存在的文件
  4. FileChannel outChannel = FileChannel.open(Paths.get("/Users/Downloads/3.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW)) {
  5. // 内存映射文件
  6. MappedByteBuffer inMapBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
  7. MappedByteBuffer outMapBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
  8. // 直接对缓冲区进行数据的读写操作
  9. byte[] bytes = new byte[inMapBuffer.limit()];
  10. inMapBuffer.get(bytes);
  11. outMapBuffer.put(bytes);
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }

3. 传输

此外,FileChannel 还直接提供了数据传输的相关方法,这样无需借助用户缓冲区就可完成文件的复制:

  1. public abstract long transferFrom(ReadableByteChannel src,
  2. long position, long count) throws IOException;
  3. public abstract long transferTo(long position, long count,
  4. WritableByteChannel target) throws IOException;

使用案例:

  1. public static void copyFileByTransfer() {
  2. try (FileChannel inChannel = FileChannel.open(Paths.get("/Users/Downloads/1.txt"), StandardOpenOption.READ);
  3. FileChannel outChannel = FileChannel.open(Paths.get("/Users/Downloads/2.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ)) {
  4. // 传输(这两种方法的使用)
  5. // inChannel.transferTo(0, inChannel.size(), outChannel);
  6. outChannel.transferFrom(inChannel, 0, inChannel.size());
  7. } catch (IOException e) {
  8. e.printStackTrace();
  9. }
  10. }

通过 transfreTo 方法进行流的复制。在一些操作系统(比如高版本的 Linux 和 UNIX)上可以借助 DMA(直接内存访问)协助进行数据复制,而不需要消耗 CPU 资源。在复制过程中,数据可以从磁盘经过总线直接发送到目标文件或套接字协议栈,无需经过用户态缓存进行数据中转,这样文件复制的效率是最高的。

你可以通过这篇文章,了解 transferTo 方法的更多细节。

注意 transferTo 不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket 发送,同样可以享受这种机制带来的性能和扩展性提高。

4. 文件锁

文件锁是作用在文件区域上的锁,即文件区域是同步资源,多个程序访问时,需要先获取该区域的锁,才能进入访问文件,访问结束释放锁,实现程序串行化访问文件。可以类比 Java 中的对象锁或者 lock 锁理解。

示例:其中 false 意味着独占模式(写锁),true 则对应共享模式(读锁)。

  1. FileChannel channel = fileInputStream.getChannel();
  2. FileLock lock = channel.lock(0L, 23L, false);

文件锁 FileLock 是被整个 Java Vitrual Machine 持有的,即 FileLock 是进程级别的,所以不可用于作为多线程安全控制的同步工具。

虽然上面提到 FileLock 不可用于多线程访问安全控制,但是多线程访问是安全的。如果线程 1 获取了文件锁 FileLock(共享或者独占),线程 2 再来请求获取该文件的文件锁,则会抛出 OverlappingFileLockException 异常。FileLock 的实现依赖于底层操作系统实现的本地文件锁设施,一个程序获取到 FileLock 后是否会阻止另一个程序访问相同文件具重叠内容的部分取决于操作系统的实现,具有不确定性。

以上所说的文件锁的作用域是文件的区域,可以是整个文件内容或者只是文件内容的一部分。独占和共享也是针对文件区域而言。程序(或者线程)获取文件 0 至 23 范围的锁,另一个程序(或者线程)仍然能获取文件 23 至以后的范围。只要作用的区域无重叠,都相互无影响。