Java IO/NIO多种读写文件方式

上一章节我们提到了Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO,本章节我将列举一些我们常用于读写文件的方式。

我们通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystemjava.io.FileInputStream类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream类,比如:java.io.FileReader

FileInputStream

使用FileInputStream实现文件读取Demo:

  1. package com.anbai.sec.filesystem;
  2. import java.io.*;
  3. /**
  4. * Creator: yz
  5. * Date: 2019/12/4
  6. */
  7. public class FileInputStreamDemo {
  8. public static void main(String[] args) throws IOException {
  9. File file = new File("/etc/passwd");
  10. // 打开文件对象并创建文件输入流
  11. FileInputStream fis = new FileInputStream(file);
  12. // 定义每次输入流读取到的字节数对象
  13. int a = 0;
  14. // 定义缓冲区大小
  15. byte[] bytes = new byte[1024];
  16. // 创建二进制输出流对象
  17. ByteArrayOutputStream out = new ByteArrayOutputStream();
  18. // 循环读取文件内容
  19. while ((a = fis.read(bytes)) != -1) {
  20. // 截取缓冲区数组中的内容,(bytes, 0, a)其中的0表示从bytes数组的
  21. // 下标0开始截取,a表示输入流read到的字节数。
  22. out.write(bytes, 0, a);
  23. }
  24. System.out.println(out.toString());
  25. }
  26. }

输出结果如下:

  1. ##
  2. # User Database
  3. #
  4. # Note that this file is consulted directly only when the system is running
  5. # in single-user mode. At other times this information is provided by
  6. # Open Directory.
  7. #
  8. # See the opendirectoryd(8) man page for additional information about
  9. # Open Directory.
  10. ##
  11. nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
  12. root:*:0:0:System Administrator:/var/root:/bin/sh
  13. daemon:*:1:1:System Services:/var/root:/usr/bin/false
  14. .....内容过长省去多余内容

调用链如下:

  1. java.io.FileInputStream.readBytes(FileInputStream.java:219)
  2. java.io.FileInputStream.read(FileInputStream.java:233)
  3. com.anbai.sec.filesystem.FileInputStreamDemo.main(FileInputStreamDemo.java:27)

其中的readBytes是native方法,文件的打开、关闭等方法也都是native方法:

  1. private native int readBytes(byte b[], int off, int len) throws IOException;
  2. private native void open0(String name) throws FileNotFoundException;
  3. private native int read0() throws IOException;
  4. private native long skip0(long n) throws IOException;
  5. private native int available0() throws IOException;
  6. private native void close0() throws IOException;

java.io.FileInputStream类对应的native实现如下:

  1. JNIEXPORT void JNICALL
  2. Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
  3. fileOpen(env, this, path, fis_fd, O_RDONLY);
  4. }
  5. JNIEXPORT jint JNICALL
  6. Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
  7. return readSingle(env, this, fis_fd);
  8. }
  9. JNIEXPORT jint JNICALL
  10. Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
  11. jbyteArray bytes, jint off, jint len) {
  12. return readBytes(env, this, bytes, off, len, fis_fd);
  13. }
  14. JNIEXPORT jlong JNICALL
  15. Java_java_io_FileInputStream_skip0(JNIEnv *env, jobject this, jlong toSkip) {
  16. jlong cur = jlong_zero;
  17. jlong end = jlong_zero;
  18. FD fd = GET_FD(this, fis_fd);
  19. if (fd == -1) {
  20. JNU_ThrowIOException (env, "Stream Closed");
  21. return 0;
  22. }
  23. if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {
  24. JNU_ThrowIOExceptionWithLastError(env, "Seek error");
  25. } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {
  26. JNU_ThrowIOExceptionWithLastError(env, "Seek error");
  27. }
  28. return (end - cur);
  29. }
  30. JNIEXPORT jint JNICALL
  31. Java_java_io_FileInputStream_available0(JNIEnv *env, jobject this) {
  32. jlong ret;
  33. FD fd = GET_FD(this, fis_fd);
  34. if (fd == -1) {
  35. JNU_ThrowIOException (env, "Stream Closed");
  36. return 0;
  37. }
  38. if (IO_Available(fd, &ret)) {
  39. if (ret > INT_MAX) {
  40. ret = (jlong) INT_MAX;
  41. } else if (ret < 0) {
  42. ret = 0;
  43. }
  44. return jlong_to_jint(ret);
  45. }
  46. JNU_ThrowIOExceptionWithLastError(env, NULL);
  47. return 0;
  48. }

完整代码参考OpenJDK:openjdk/src/java.base/share/native/libjava/FileInputStream.c

FileOutputStream

使用FileOutputStream实现写文件Demo:

  1. package com.anbai.sec.filesystem;
  2. import java.io.File;
  3. import java.io.FileOutputStream;
  4. import java.io.IOException;
  5. /**
  6. * Creator: yz
  7. * Date: 2019/12/4
  8. */
  9. public class FileOutputStreamDemo {
  10. public static void main(String[] args) throws IOException {
  11. // 定义写入文件路径
  12. File file = new File("/tmp/1.txt");
  13. // 定义待写入文件内容
  14. String content = "Hello World.";
  15. // 创建FileOutputStream对象
  16. FileOutputStream fos = new FileOutputStream(file);
  17. // 写入内容二进制到文件
  18. fos.write(content.getBytes());
  19. fos.flush();
  20. fos.close();
  21. }
  22. }

代码逻辑比较简单: 打开文件->写内容->关闭文件,调用链和底层实现分析请参考FileInputStream

RandomAccessFile

Java提供了一个非常有趣的读取文件内容的类: java.io.RandomAccessFile,这个类名字面意思是任意文件内容访问,特别之处是这个类不仅可以像java.io.FileInputStream一样读取文件,而且还可以写文件。

RandomAccessFile读取文件测试代码:

  1. package com.anbai.sec.filesystem;
  2. import java.io.*;
  3. /**
  4. * Creator: yz
  5. * Date: 2019/12/4
  6. */
  7. public class RandomAccessFileDemo {
  8. public static void main(String[] args) {
  9. File file = new File("/etc/passwd");
  10. try {
  11. // 创建RandomAccessFile对象,r表示以只读模式打开文件,一共有:r(只读)、rw(读写)、
  12. // rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
  13. RandomAccessFile raf = new RandomAccessFile(file, "r");
  14. // 定义每次输入流读取到的字节数对象
  15. int a = 0;
  16. // 定义缓冲区大小
  17. byte[] bytes = new byte[1024];
  18. // 创建二进制输出流对象
  19. ByteArrayOutputStream out = new ByteArrayOutputStream();
  20. // 循环读取文件内容
  21. while ((a = raf.read(bytes)) != -1) {
  22. // 截取缓冲区数组中的内容,(bytes, 0, a)其中的0表示从bytes数组的
  23. // 下标0开始截取,a表示输入流read到的字节数。
  24. out.write(bytes, 0, a);
  25. }
  26. System.out.println(out.toString());
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }

任意文件读取特性体现在如下方法:

  1. // 获取文件描述符
  2. public final FileDescriptor getFD() throws IOException
  3. // 获取文件指针
  4. public native long getFilePointer() throws IOException;
  5. // 设置文件偏移量
  6. private native void seek0(long pos) throws IOException;

java.io.RandomAccessFile类中提供了几十个readXXX方法用以读取文件系统,最终都会调用到read0或者readBytes方法,我们只需要掌握如何利用RandomAccessFile读/写文件就行了。

RandomAccessFile写文件测试代码:

  1. package com.anbai.sec.filesystem;
  2. import java.io.File;
  3. import java.io.IOException;
  4. import java.io.RandomAccessFile;
  5. /**
  6. * Creator: yz
  7. * Date: 2019/12/4
  8. */
  9. public class RandomAccessWriteFileDemo {
  10. public static void main(String[] args) {
  11. File file = new File("/tmp/test.txt");
  12. // 定义待写入文件内容
  13. String content = "Hello World.";
  14. try {
  15. // 创建RandomAccessFile对象,rw表示以读写模式打开文件,一共有:r(只读)、rw(读写)、
  16. // rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。
  17. RandomAccessFile raf = new RandomAccessFile(file, "rw");
  18. // 写入内容二进制到文件
  19. raf.write(content.getBytes());
  20. raf.close();
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }

FileSystemProvider

前面章节提到了JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我们可以利用支持异步的通道(Channel)模式读取文件内容。

FileSystemProvider读取文件内容示例:

  1. package com.anbai.sec.filesystem;
  2. import java.io.IOException;
  3. import java.nio.file.Files;
  4. import java.nio.file.Path;
  5. import java.nio.file.Paths;
  6. /**
  7. * Creator: yz
  8. * Date: 2019/12/4
  9. */
  10. public class FilesDemo {
  11. public static void main(String[] args) {
  12. // 通过File对象定义读取的文件路径
  13. // File file = new File("/etc/passwd");
  14. // Path path1 = file.toPath();
  15. // 定义读取的文件路径
  16. Path path = Paths.get("/etc/passwd");
  17. try {
  18. byte[] bytes = Files.readAllBytes(path);
  19. System.out.println(new String(bytes));
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }

java.nio.file.Files是JDK7开始提供的一个对文件读写取非常便捷的API,其底层实在是调用了java.nio.file.spi.FileSystemProvider来实现对文件的读写的。最为底层的实现类是sun.nio.ch.FileDispatcherImpl#read0

基于NIO的文件读取逻辑是:打开FileChannel->读取Channel内容。

打开FileChannel的调用链为:

  1. sun.nio.ch.FileChannelImpl.<init>(FileChannelImpl.java:89)
  2. sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105)
  3. sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137)
  4. sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148)
  5. sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212)
  6. java.nio.file.Files.newByteChannel(Files.java:361)
  7. java.nio.file.Files.newByteChannel(Files.java:407)
  8. java.nio.file.Files.readAllBytes(Files.java:3152)
  9. com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)

文件读取的调用链为:

  1. sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147)
  2. sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65)
  3. sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109)
  4. sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103)
  5. java.nio.file.Files.read(Files.java:3105)
  6. java.nio.file.Files.readAllBytes(Files.java:3158)
  7. com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)

FileSystemProvider写文件示例:

  1. package com.anbai.sec.filesystem;
  2. import java.io.IOException;
  3. import java.nio.file.Files;
  4. import java.nio.file.Path;
  5. import java.nio.file.Paths;
  6. /**
  7. * Creator: yz
  8. * Date: 2019/12/4
  9. */
  10. public class FilesWriteDemo {
  11. public static void main(String[] args) {
  12. // 通过File对象定义读取的文件路径
  13. // File file = new File("/etc/passwd");
  14. // Path path1 = file.toPath();
  15. // 定义读取的文件路径
  16. Path path = Paths.get("/tmp/test.txt");
  17. // 定义待写入文件内容
  18. String content = "Hello World.";
  19. try {
  20. // 写入内容二进制到文件
  21. Files.write(path, content.getBytes());
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }

文件读写总结

Java内置的文件读取方式大概就是这三种方式,其他的文件读取API可以说都是对这几种方式的封装而已(依赖数据库、命令执行、自写JNI接口不算,本人个人理解,如有其他途径还请告知)。本章我们通过深入基于IO和NIO的Java文件系统底层API,希望大家能够通过以上Demo深入了解到文件读写的原理和本质。