Java IO 一直以来是大厂面试题中的高频考点,本文将从 Java IO 根底运用说起,以事例 + 源码的办法解说文件、字节约、字符流、缓冲流、打印流、随机拜访流等根底内容,再深化到 Java IO 模型与规划形式,然后构建出对 Java IO 的全面认知。

文章不仅合适完全不了解 Java IO 的新同学,也合适具备必定常识储备的老同学。文中的一切事例代码强烈推荐手写复现一遍,以更好地把握 Java IO 编程根底。

文章的结尾处给出了更新日志,每次新更新的内容都会写明,便于同学们快速了解更新的内容是否是自己所需求的常识点。

我信任,友爱的讨论交流会让彼此快速前进!文章难免有疏漏之处,十分欢迎大家在谈论区中批评指正。

文件

文件在程序中是以流的形式来操作的。类联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

创立文件

常用结构办法

常用结构办法 描绘
File(String pathname) 依据途径名构建
File(File parent, String child) 依据父目录文件 + 子途径构建
File(String parent, String child) 依据父目录途径 + 子途径构建

要想真实地在磁盘中创立文件,需求执行 createNewFile() 办法。

Tips

  1. 一切 java.io 中的类的相对途径默许都是从用户作业目录开端的,运用 System.getProperty("user.dir") 能够获取你的用户作业目录。
  2. 在 Windows 体系中的分隔符为 “\\“,在 Linux 体系中分隔符为 “/“,为了确保体系的可移植性,能够经过常量字符串 java.io.File.separator 获取(拜见事例中的运用)。

运用事例

  1. 运用 File(String pathname) 创立文件
@Test
public void createFile() {
    // 更换成你想要存放的文件途径,默许情况为用户作业目录,能够经过 System.getProperty("user.dir") 显现获取
    String userDir = System.getProperty("user.dir");
    System.out.println("用户作业目录:" + userDir);
    System.out.println("当时操作体系的文件分隔符为:" + File.separator);
    String fileName = "createFile.txt";
    String path = userDir + File.separator + fileName; // 组装途径
    File file = new File(path); // 此刻只是程序中的一个目标
    // File file = new File(fileName); // 默许会创立到用户作业目录中,和上一面的句子创立的文件途径共同。
    try {
        file.createNewFile(); // 执行该办法才会真实地在磁盘中创立文件
        System.out.println("文件创立成功");
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 运用 File(File parent, String child) 创立文件
@Test
public void createFile2() {
    // 更换成你想要存放的文件途径
    File parentFile = new File("/Users/sunnywinter/projects/interviewcode/");
    String fileName = "testFile2.txt";
    File file = new File(parentFile, fileName);
    try {
        file.createNewFile();
        System.out.println("文件创立成功");
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 运用 File(String parent, String child) 创立文件
@Test
public void createFile3() {
    // 更换成你想要存放的文件途径
    String parentFile = System.getProperty("user.dir");
    String fileName = "createFile3.txt";
    File file = new File(parentFile, fileName);
    try {
        file.createNewFile();
        System.out.println("文件创立成功");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

获取文件信息

常用办法

回来值 办法名 描绘
String getName() 获取文件名
String getAbsolutePath() 获取文件绝对途径
String getParent() 获取文件父级目录
long length() 回来文件巨细(字节)
boolean exists() 判别文件是否存在
boolean isFile() 判别是否是一个文件
boolean isDirectory() 判别是否是一个目录

运用事例

@Test
public void getFileInfo() {
    File file = new File("/Users/sunnywinter/projects/interviewcode/testFile.txt");
    System.out.println("文件名:" + file.getName());
    System.out.println("文件绝对途径:" + file.getAbsolutePath());
    System.out.println("文件父级目录:" + file.getParent());
    System.out.println("文件巨细(字节):" + file.length());
    System.out.println("文件是否存在:" + file.exists());
    System.out.println("是否是一个文件:" + file.isFile());
    System.out.println("是否是一个目录:" + file.isDirectory());
}

目录操作与文件删去

运用办法

回来值 办法名 描绘
boolean mkdir() 创立一级目录
boolean mkdirs() 创立多级目录
boolean delete() 删去文件或目录

运用事例

@Test
public void test() {
    String parentPath = "/Users/sunnywinter/projects/interviewcode/";
    String fileName = "testFile.txt";
    String directoryName = "a";
    String mulDirectoryName = "b/c/d";
    // 删去文件
    File file = new File(parentPath, fileName);
    file.delete();
    // 创立一级目录
    File directory = new File(parentPath, directoryName);
    directory.mkdir();
    // 创立多级目录
    File mulDirectory = new File(parentPath, mulDirectoryName);
    mulDirectory.mkdirs();
    // 删去目录
    directory.delete();
}

IO 流概述

IO,Input/Output,即输入/输出。判别输入输出以核算机内存为中心,假如从内存到外部存储便是输出,从外部存储到内存便是输入。数据传输进程类似于水流,因而称为 IO 流。

Java面试必知必会 —— 全面解读 Java IO(基础篇)

在 Java 中,依据操作数据单位的不同,IO 流分为字节约和字符流;依据数据流的流向不同,分为输入流和输出流;依据流的角色不同,分为节点流和处理流。

Java IO 流共涉及 40 多个类,但都是从表中的 4 个笼统基类派生而来,派生的子类名称都是以其父类名作为子类名的后缀。

(笼统基类) 字节约 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

罗列一些常用的类。

Java面试必知必会 —— 全面解读 Java IO(基础篇)

字节约

首先,咱们先学习怎么将数据写入到文件中。

OutputStream(字节输出流)

OutputStream 用于将内存数据(字节信息)写入到文件中,java.io.OutputStream笼统类是一切字节输出流的父类。

常用办法

回来值 办法名 描绘
void write(int b) 将特定字节写入输出流。
void write(byte b[]) 将数组 b 写入到输出流,等价于 write(b, 0, b.length)
void write(byte[] b, int off, int len) write(byte b[]) 办法的根底上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
void flush() 改写此输出流并强制写出一切缓冲的输出字节。
void close() 封闭输出流开释相关的体系资源。

FileOutputStream

FileOutputStream 是最常用的字节输出流子类,可直接指定文件途径,能够直接输出单字节数据,也能够输出指定的字节数组。

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

Tips

java.io.Closeable 接口扩展了 java.lang.AutoCloseable 接口。因而,对任何 Closeable 进行操作时,都能够运用 try-with-resource 句子(声明晰一个或多个资源的 try 句子,能够主动封闭流,具体运用办法拜见运用事例)。 为什么要有两个接口呢?因为 Closeable 接口的 close 办法只抛出了 IOException,而 AutoCloseable.close 办法能够抛出任何异常。

常用结构函数

Java面试必知必会 —— 全面解读 Java IO(基础篇)

append 为 true 时,表明追加写入。

运用事例

需求 1:向 mrpersimmon.txt 文件中写入 Hi,Mrpersimmon!

代码:

@Test
public void testFileOutputStream() {
    // FileOutputStream(String name, boolean append) 追加写入
    // FileOutputStream(String name) 掩盖写入
    try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mrpersimmon.txt"))) {
        String str = "Hi,Mrpersimmon!";
        // write(byte b[]) : 将字节数组 b 写入到输出流,等价于 write(b, 0, b.length)
        bos.write(str.getBytes("UTF-8")); // str.getBytes() 字符串 -> 字节数组
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Tips

  1. FileOutputStream 在运用中要和 BufferedOutputStream 一同运用,功用更好。
  2. try(…OutputStream) 能够主动封闭输出流,无需 try-finally 手动封闭。

运转成果:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

DataOutputStream

DataOutputStream 用于写入指定类型数据,不能独自运用,有必要结合 FileOutputStream,结构函数源码如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

需求:向 mrpersimmon2.txt 写入 Hi,Mrpersimmon!

代码:

@Test
public void testDataOutputStream() {
    try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("mrpersimmon2.txt"))) {
        // 输出恣意输入类型
        dos.writeUTF("Hi,Mrpersimmon!");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

ObjectOutputStream

ObjectOutputStream 用于将目标写入到输出流(序列化)。与之相对反地,ObjectInputStream 用于从输入流中读取 Java 目标(反序列化)。

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
序列化与反序列化

什么是序列化和反序列化? 序列化便是在保存数据时,保存数据的值和数据类型;反序列化便是在康复数据时,康复数据的值和数据类型。

怎么让类支撑序列化机制呢? 有必要让类完结 Serializable 接口(一个符号接口,没有办法)或许 Externalizable 接口(有办法需求完结)。假如类中有特点不想被序列化,需求运用 transient 润饰。

注意事项

  1. 读写次序要共同;
  2. 要求序列化和反序列化的目标,需求完结 Serializable 接口
  3. 序列化的类中主张增加 serialVersionUID 以太高版别的兼容性
  4. 序列化目标时,默许将一切特点进行了序列化,但除了 static 或 transient 润饰的成员
  5. 序列化目标时,要求里面特点的类型也需求完结序列化接口
  6. 序列化具备可继承性,即某个类完结了序列化,那么它的一切子类也默许完结了序列化。
  7. 根本类型对应的包装类都完结了序列化。
运用事例

需求:创立一个支撑序列化的 Blog 类,向 mrpersimmon3.txt 写入一个 Blog 目标。

代码:

Blog 类

public class Blog implements Serializable {
    private static final long serialVersionUID = -4970674810941727545L;
    String name;
    String url;
    public Blog(String name, String url) {
        this.name = name;
        this.url = url;
    }
    @Override
    public String toString() {
        return "Blog{" +
                "name='" + name + '\'' +
                ", url='" + url + '\'' +
                '}';
    }
}

功用代码

@Test
public void testObjectOutputStream() {
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mrpersimmon3.txt"))) {
        Blog blog = new Blog("mrpersimmon", "https://www.mrpersimmon.top");
        oos.writeObject(blog);
        System.out.println("数据写入完结(序列化)");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果

Java面试必知必会 —— 全面解读 Java IO(基础篇)

下面咱们来学习怎么从文件中读取数据信息。


InputStream(字节输入流)

InputStream 用于从文件读取数据(字节信息)到内存中,java.io.InputStream 笼统类是一切字节输入流的父类。

常用办法

回来值 办法名 描绘
JDK 8 ↓ JDK 8 ↓ JDK 8 ↓
int read() 回来输入流中下一个字节的数据。回来的值介于 0 到 255 之间。假如未读取任何字节,则代码回来 -1 ,表明文件完毕。
int read(byte b[ ]) 从输入流中读取一些字节存储到数组 b 中。假如数组 b 的长度为零,则不读取。假如没有可用字节读取,回来 -1。假如有可用字节读取,则最多读取的字节数最多等于 b.length ,回来读取的字节数。这个办法等价于 read(b, 0, b.length)
int read(byte b[], int off, int len) read(byte b[ ]) 办法的根底上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
long skip(long n) 疏忽输入流中的 n 个字节 ,回来实践疏忽的字节数。
int available() 回来输入流中能够读取的字节数。
void close() 封闭输入流开释相关的体系资源。
JDK 9 ↓ JDK 9 ↓ JDK 9 ↓
byte[] readAllBytes() 读取输入流中的一切字节,回来字节数组。
byte[] readNBytes(byte[] b, int off, int len) 阻塞直到读取 len 个字节。
long transferTo(OutputStream out) 将一切字节从一个输入流传递到一个输出流。

FileInputStream

FileInputStream 是一个比较常用的字节输入流子类,可直接指定文件途径,能够直接读取单字节数据,也能够读取至字节数组中

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

需求:读取 mrpersimmon.txt 文件,并将文件内容显现到操控台中。

Java面试必知必会 —— 全面解读 Java IO(基础篇)

办法 1:运用 read() 单个字节读取,功率较低。

代码:

@Test
public void testFileInputStream() {
    // try() 会主动封闭输入流,FileInputStream 与 BufferedInputStream 合作运用
    try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("mrpersimmon.txt"))) {
        // int available() 回来输入流中能够读取的字节数。
        System.out.println("文件中可读取的字节数量:" + bufferedInputStream.available());
        // long skip(long n) 疏忽输入流中的 n 个字节 ,回来实践疏忽的字节数。
        long skipCounts = bufferedInputStream.skip(3); // 疏忽 3 个字节
        System.out.println("疏忽的字节数量:" + skipCounts);
        // read() 回来输入流中下一个字节的数据。
        System.out.print("从文件中读取的字节内容:");
        int content;
        // 回来值为 -1 时,表明读取完毕
        while ((content = bufferedInputStream.read()) != -1) {
            System.out.print((char) content); // 将读出的 int 类型数据强转成 char 类型
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Tips

  1. FileInputStream 在运用中要和 BufferedInputStream 一同运用,功用更好。
  2. try(…InputStream) 能够主动封闭输入流,无需 try-finally 手动封闭。

运转成果:

文件中可读取的字节数量:15
疏忽的字节数量:3
从文件中读取的字节内容:Mrpersimmon!

办法 2:运用 read(byte[] b) 读取文件,进步功率。

代码:

@Test
public void testFileInputStream2() {
    // try() 会主动封闭输入流,FileInputStream 与 BufferedInputStream 合作运用
    try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("mrpersimmon.txt"))) {
        // int available() 回来输入流中能够读取的字节数。
        int bufSize = bufferedInputStream.available();
        System.out.println("文件中可读取的字节数量:" + bufSize);
        byte[] buf = new byte[8]; // 一次读取 8 字节
        // long skip(long n) 疏忽输入流中的 n 个字节 ,回来实践疏忽的字节数。
        long skipCounts = bufferedInputStream.skip(3); // 疏忽 3 个字节
        System.out.println("疏忽的字节数量:" + skipCounts);
        // read(byte b[]) 从输入流中读取一些字节存储到数组 b 中。
        // 假如数组 b 的长度为零,则不读取。
        // 假如没有可用字节读取,回来 -1。
        // 假如有可用字节读取,则最多读取的字节数最多等于 b.length,回来读取的字节数。
        // 这个办法等价于 read(b, 0, b.length)。
        System.out.print("从文件中读取的字节内容:");
        int readLen;
        // 回来值为 -1 时,表明读取完毕
        while ((readLen = bufferedInputStream.read(buf)) != -1) {
            System.out.print(new String(buf, 0, readLen)); // 将字符数组 buf 转化成字符串
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果:

文件中可读取的字节数量:15
疏忽的字节数量:3
从文件中读取的字节内容:Mrpersimmon!

DataInputStream

DataInputStream 用于读取指定类型数据,不能独自运用,有必要结合 InputStream 的一个完结类运用,结构函数源码如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

需求:读取 mrpersimmon2.txt 文件,并将文件内容显现到操控台中。

代码

@Test
public void testDataInputStream() throws IOException {
    // 有必要将一个 InputStream 的完结类作为结构参数才干运用
    try(DataInputStream dis = new DataInputStream(new FileInputStream("mrpersimmon2.txt"))) {
        // 能够读取恣意具体的类型数据
        System.out.println(dis.readUTF()); // 读取已运用 modified UTF-8 格局编码的字符串。
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果

Hi,Mrpersimmon!

ObjectInputStream

ObjectInputStream 用于从输入流中读取 Java 目标(反序列化)。

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

需求:读取 mrpersimmon3.txt 中的 Blog 目标。

代码

@Test
public void testObjectInputStream() {
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mrpersimmon3.txt"))) {
        System.out.println(ois.readObject());
        System.out.println("数据读取完毕(反序列化完结)");
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

运转成果

Blog{name='mrpersimmon', url='https://www.mrpersimmon.top'}
数据读取完毕(反序列化完结)

归纳事例

需求

完结图片的仿制。

代码

@Test
public void testCopyPic() {
    String srcPicPath = "data.png";
    String destPicPath = "data2.png";
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcPicPath));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPicPath))) {
        byte[] buf = new byte[1024];
        int readLen = 0;
        while ((readLen = bis.read(buf)) != -1) {
            // bis 输入流从源图片文件读取数据后,写入到 bos 输出流的目标文件地址
            bos.write(buf, 0, readLen);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

字符流

字符流与字节约的比照

为什么 I/O 流操作要分为字节约操作和字符流操作呢?

  1. 不管是文件读写仍是网络发送接纳,信息的最小存储单元都是字节。因而,字节约是必要的。而字符流是由 Java 虚拟机将字节转化得到的,相比较于字节约愈加耗时。
  2. 字节约在不知道编码类型的情况下很容易出现乱码问题,因而咱们需求字符流来读取文本文件。

何时运用字节约,何时运用字符流?

假如是音频文件、图片等媒体文件运用用字节约会有更好的功用优势;

假如涉及到字符的话(如,文本文件等)运用字符流比较好。

常用字符编码所占字节数?

字符流默许选用的是 Unicode 编码,咱们能够经过结构办法自定义编码。

utf8,英文占 1 字节,中文占 3 字节;

unicode:任何字符都占 2 个字节;

gbk:英文占 1 字节,中文占 2 字节。

Writer(字符输出流)

Writer用于将内存数据(字符信息)写入到文件,java.io.Writer笼统类是一切字符输出流的父类。

常用办法

回来值 办法名 描绘
void write(int c) 写入单个字符。
void write(char[] cbuf) 写入字符数组 cbuf,等价于write(cbuf, 0, cbuf.length)
void write(char[] cbuf, int off, int len) write(char[] cbuf) 办法的根底上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
void write(String str) 写入字符串,等价于 write(str, 0, str.length())
void write(String str, int off, int len) write(String str) 办法的根底上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
Writer append(CharSequence csq) 将指定的字符序列附加到指定的 Writer 目标并回来该 Writer 目标。
Writer append(char c) 将指定的字符附加到指定的 Writer 目标并回来该 Writer 目标.
void flush() 改写此输出流并强制写出一切缓冲的输出字符。
void close() 封闭输出流开释相关的体系资源。

FileWriter

OutputStreamWriter 是字符流转化为字节约的桥梁,其子类 FileWriter 是根据该根底上的封装,能够直接将字符写入到文件。

类图如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

代码

@Test
public void testFileWriter() {
    String filePath = "mrpersimmon-1.txt";
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
        bw.write("Hi,Mrpersimmon!");
        bw.write("\n"); // 增加换行符
        bw.write("欢迎你来到柿子博客".toCharArray(), 0, 3); // toCharArray 能够将字符串转化成字符数组
        bw.write("\n");
        bw.write("欢迎你来到柿子博客", 3, 6);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Tips

  1. FileWriter 要和 BufferedWriter 一同运用,功用更好;
  2. 必定要封闭输出流或许 flush ,否则无法写入到文件中。

运转成果

Java面试必知必会 —— 全面解读 Java IO(基础篇)

Reader(字符输入流)

Reader用于从文件读取数据(字符信息)到内存中,java.io.Reader笼统类是一切字符输入流的父类。

常用办法

回来值 办法名 描绘
int read() 从输入流读取一个字符。
int read(char[] cbuf) 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,等价于 read(cbuf, 0, cbuf.length)
int read(char[] cbuf, int off, int len) read(char[] cbuf) 办法的根底上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
long skip(long n) 疏忽输入流中的 n 个字符,回来实践疏忽的字符数。
void close() 封闭输入流并开释相关的体系资源。

FileReader

InputStreamReader 是字节约转化为字符流的桥梁,其子类 FileReader 是根据该根底上的封装,能够直接操作字符文件。

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)
运用事例

需求:读取 mrpersimmon-1.txt 中的信息

代码

@Test
public void testFileReader1() {
    try (BufferedReader br = new BufferedReader(new FileReader("mrpersimmon-1.txt"))) {
        char[] cbuf = new char[8];
        int readLen = 0;
        while ((readLen = br.read(cbuf)) != -1) {
            System.out.print(new String(cbuf, 0, readLen));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果

Hi,Mrpersimmon!
欢迎你
来到柿子博客

字节/字符缓冲流

IO 操作是很耗费功用的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节/字符,然后防止频频的 IO 操作,进步流的传输功率。

字节缓冲流这儿选用了装修器形式来增强 InputStreamOutputStream子类目标的功用。字符缓冲流同理。

常见的运用办法已在上面的运用事例中给出,运用办法如下:

BufferedReader br = new BufferedReader(new FileReader("mrpersimmon-1.txt"))

字节约和字节缓冲流功用比照

字节约和字节缓冲流的功用不同首要体现在调用 write(int b)read() 这两种一次只写入/读取一个节点的办法时。因为字节缓冲流内部有缓冲区(字节数组),因而,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,进步读取功率。

测验比照 1(单字节处理)

别离运用字节约和字节缓冲流的办法仿制一个 207 MB 的 PDF 文件,查看耗时比照。

代码

1. 运用字节约仿制 PDF 文件

@Test
public void copyFileByStream() {
    System.out.println("运用字节约仿制 PDF 文件测验开端");
    long startTime = System.currentTimeMillis(); // 记载开端时刻
    String srcPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes.pdf";
    String destPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes-stream.pdf";
    try (FileInputStream fis = new FileInputStream(srcPath);
         FileOutputStream fos = new FileOutputStream(destPath)) {
        int content = 0;
        while ((content = fis.read()) != -1) {
            fos.write(content);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long endTime = System.currentTimeMillis(); // 记载完毕时刻
    System.out.println("运用字节约仿制 PDF 文件总耗时 " + (endTime - startTime) + " 毫秒");
}

2. 运用缓冲字节约仿制 PDF 文件

@Test
public void copyFileByBufferStream() {
    System.out.println("运用缓冲字节约仿制 PDF 文件测验开端");
    long startTime = System.currentTimeMillis(); // 记载开端时刻
    // 文件巨细 207 MB
    String srcPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes.pdf";
    String destPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes-buffer-stream.pdf";
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcPath));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath))) {
        int content = 0;
        while ((content = bis.read()) != -1) {
            bos.write(content);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long endTime = System.currentTimeMillis(); // 记载完毕时刻
    System.out.println("运用缓冲字节约仿制 PDF 文件总耗时 " + (endTime - startTime) + " 毫秒");
}
成果比照
运用字节约仿制 PDF 文件总耗时 1052141 毫秒
运用缓冲字节约仿制 PDF 文件总耗时 6521 毫秒

能够看到,两者耗时不同绝大,相较于字节约,运用缓冲字节约节约约 161 倍的耗时。

测验比照 2(字节数组处理)

别离运用字节约+字节数组、字节缓冲流+字节数组的办法仿制一个 207 MB 的 PDF 文件,查看耗时比照。

代码

1. 运用字节约 + 字节数组仿制 PDF 文件

@Test
public void copyFileByByteArrStream() {
    System.out.println("运用字节约+字节数组仿制 PDF 文件测验开端");
    long startTime = System.currentTimeMillis(); // 记载开端时刻
    // 文件巨细 207 MB
    String srcPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes.pdf";
    String destPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes-arr-stream.pdf";
    try (FileInputStream fis = new FileInputStream(srcPath);
         FileOutputStream fos = new FileOutputStream(destPath)) {
        int readLen = 0;
        byte[] b = new byte[8 * 1024];
        while ((readLen = fis.read(b)) != -1) {
            fos.write(b, 0, readLen);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long endTime = System.currentTimeMillis(); // 记载完毕时刻
    System.out.println("运用字节约 + 字节数组仿制 PDF 文件总耗时 " + (endTime - startTime) + " 毫秒");
}

2. 运用缓冲字节约 + 字节数组仿制 PDF 文件

@Test
public void copyFileByByteArrBufferStream() {
    System.out.println("运用缓冲字节约+字节数组仿制 PDF 文件测验开端");
    long startTime = System.currentTimeMillis(); // 记载开端时刻
    // 文件巨细 207 MB
    String srcPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes.pdf";
    String destPath = "/Users/sunnywinter/Downloads/深化分析Kubernetes-arr-buf-stream.pdf";
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcPath));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath))) {
        int readLen = 0;
        byte[] b = new byte[8 * 1024];
        while ((readLen = bis.read(b)) != -1) {
            bos.write(b, 0, readLen);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long endTime = System.currentTimeMillis(); // 记载完毕时刻
    System.out.println("运用缓冲字节约 + 字节数组仿制 PDF 文件总耗时" + (endTime - startTime) + "毫秒");
}
成果比照
运用字节约 + 字节数组仿制 PDF 文件总耗时 478 毫秒
运用缓冲字节约 + 字节数组仿制 PDF 文件总耗时 391 毫秒

能够看到,两者距离不是特别大,但是缓冲字节约仍具有优势

结论

在日常运用时,应当运用缓冲流,以获取更好的功用优势。

字符缓冲流也是同理,限于篇幅,不再供给测验事例,感兴趣的同学能够自行测验。

源码分析

BufferedInputStream

BufferedInputStream 内部保护了一个缓冲区,这个缓冲区是一个字节数组。下面是源码中的一部分内容:

public
class BufferedInputStream extends FilterInputStream {
    // 缓冲区默许巨细
	private static int DEFAULT_BUFFER_SIZE = 8192;
    // 内部缓冲区字节数组
    protected volatile byte buf[];
    // 结构函数,运用默许的缓冲区巨细
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }
    // 结构函数,运用自定义的缓冲区巨细
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
}

BufferedOutputStream

下面是源码中的一部分内容:

public
class BufferedOutputStream extends FilterOutputStream {
    // 内部缓冲区字节数组
    protected byte buf[];
    // 结构函数,默许缓冲区巨细为 8192 
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }
    // 结构函数,运用自定义的缓冲区巨细
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
}

BufferedReader

和 BufferedInputStream 一样,在内部保护了一个缓冲区,不同的是,这儿是字符缓冲区。下面是源码中的一部分内容:

public class BufferedReader extends Reader {
    // 内部缓冲区字符数组
	private char cb[];
    // 默许缓冲区巨细
    private static int defaultCharBufferSize = 8192;
    // 结构函数,运用自定义的缓冲区巨细
    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }
    // 结构函数,运用默许缓冲区巨细
    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }
}

BufferedWriter

下面是源码中的一部分内容:

public class BufferedWriter extends Writer {
	// 内部缓冲区字符数组
    private char cb[];
    // 默许缓冲区巨细
    private static int defaultCharBufferSize = 8192;
    // 结构函数,运用默许缓冲区巨细
    public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
    }
    // 结构函数,运用自定义的缓冲区巨细
    public BufferedWriter(Writer out, int sz) {
        super(out);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.out = out;
        cb = new char[sz];
        nChars = sz;
        nextChar = 0;
        lineSeparator = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction("line.separator"));
    }
}

打印流

打印流只要输出流(内存 -> 文件),没有输入流。

PrintStream(字节打印流)

咱们常常运用的 System.out 便是用于获取一个 PrintStream 目标,System.out.print 办法实践调用的是 PrintStream 目标的 write 办法。

默许情况下,PrintStream 输出数据的方位是规范输出,即显现器。

类图联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

源码

下面是 PrintStream 的部分源码:

public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable
{
    // 调用的 write 办法
    public void print(String s) {
        if (s == null) {
            s = "null";
        }
        write(s);
    }
    private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }
}

PrintWriter(字符打印流)

包装了 FileWriter ,供给了更方便的办法来完结输出。

类图联系如下

Java面试必知必会 —— 全面解读 Java IO(基础篇)

这儿我就给出一个事例来阐明字符打印流要怎么运用。

需求:将 mrpersimmon-1.txt 中内容打印到 mrpersimmon-copy.txt 文件中。

代码

@Test
public void testPrintWriter() {
    try (BufferedReader br = new BufferedReader(new FileReader("mrpersimmon-1.txt"));
         PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("mrpersimmon-copy.txt")))) {
        String line = null;
        while ((line = br.readLine()) != null) { // 一次读取一行内容,为空时代表读取完毕
            pw.println(line); // 换行并打印到指定文件中
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果

Java面试必知必会 —— 全面解读 Java IO(基础篇)

随机拜访流

在本小结,首要介绍支撑随意跳转到文件恣意方位读写RandomAccessFile 类。

类联系如下:

Java面试必知必会 —— 全面解读 Java IO(基础篇)

结构函数

结构函数的源码如下:

// String name: 指定名称的文件
public RandomAccessFile(String name, String mode)
    throws FileNotFoundException
{
    this(name != null ? new File(name) : null, mode);
}
// String file: 指定文件
public RandomAccessFile(File file, String mode)
        throws FileNotFoundException
{
    // 省略...
}

咱们重点介绍输入参数 mode(读写形式)。依据源码中的注释,读写形式共四种:

  1. r: 只读形式;
  2. rw: 读写形式;
  3. rws: 相较于 rw,还需求将对「文件内容」或「元数据」的每次更新同步写入底层存储设备;
  4. rwd: 相较于 rw,还要求对「文件内容」的每次更新都同步写入底层存储设备。
Java面试必知必会 —— 全面解读 Java IO(基础篇)

什么是「文件内容」?什么是「元数据」?

「文件内容」指的是文件中实践保存的数据,「元数据」则是用来描绘文件特点比方文件的巨细信息、创立和修正时刻。

rwd 相较于 rws 来说,能够减少执行 IO 操作次数。

文件指针

RandomAccessFile 中有一个文件指针用于表明下一个将要被写入或许读取的字节所处的方位

咱们能够经过 seek(long pos) 设置文件指针的偏移量(据文件开头 pos 个字节处)。源码如下:

public void seek(long pos) throws IOException {
    if (pos < 0) {
        throw new IOException("Negative seek offset");
    } else {
        seek0(pos);
    }
}
private native void seek0(long pos) throws IOException;

假如想要获取文件指针当时方位的话,能够运用 getFilePointer() 办法。源码如下:

public native long getFilePointer() throws IOException;

常见办法

回来值 常用办法 描绘
long getFilePointer() 获取文件指针当时方位
void set(long pos) 设置文件指针的偏移量
long length() 回来文件的长度
int read() 读取一个字节
int read(byte[] b) 从该文件读取最多 b.length字节的数据到字节数组。
int read(byte[] b, int off, int len) 从该文件读取最多 len个字节的数据到字节数组。
String readLine() 读取下一行文本。
String readUTF() 从该文件读取字符串。
void write(byte[] b) 从指定的字节数组写入 b.length个字节到该文件,从当时文件指针开端。
void write(byte[] b, int off, int len) 从指定的字节数组写入 len个字节,从偏移量 off开端写入此文件。
void write(int b) 将指定的字节写入此文件。
void writeUTF(String str) 以机器无关的办法运用 UTF-8 编码将字符串写入文件。
int skipBytes(int n) 尝试跳过 n 字节的输入,丢弃跳过的字节。

运用事例

@Test
public void testRandomAccessFile() {
    try(RandomAccessFile raf = new RandomAccessFile(new File("mrpersimmon.txt"), "rw")) {
        // readLine() 读取一行文本
        System.out.println("开始文件内容:" + raf.readLine());
        raf.seek(0); // 设置文件指针偏移量为 0,回到开始方位
        // read() 读取一个字节
        // getFilePointer 获取文件指针当时方位
        System.out.println("读取前的偏移量:" + raf.getFilePointer() + ",当时读取的字符:" + (char) raf.read() + ",读取后的偏移量:" + raf.getFilePointer());
        raf.seek(6); // 设置文件指针偏移量为 6
        System.out.println("读取前的偏移量:" + raf.getFilePointer() + ",当时读取的字符:" + (char) raf.read() + ",读取后的偏移量:" + raf.getFilePointer());
        raf.write(new byte[]{'h', 'i'});
        System.out.println("写入后的偏移量:" + raf.getFilePointer() + ",当时读取的字符:" + (char) raf.read() + ",读取后的偏移量:" + raf.getFilePointer());
        raf.seek(0); // 设置文件指针偏移量为 0,回到开始方位
        System.out.println("当时文件的内容为:" + raf.readLine());
        raf.seek(0); // 设置文件指针偏移量为 0,回到开始方位
        raf.write(new byte[]{'A', 'B', 'C'}); // 掩盖数据
        raf.seek(0);
        System.out.println("掩盖后的文件内容为:" + raf.readLine());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

运转成果

开始文件内容:abcdefg
读取前的偏移量:0,当时读取的字符:a,读取后的偏移量:1
读取前的偏移量:6,当时读取的字符:g,读取后的偏移量:7
写入后的偏移量:9,当时读取的字符:,读取后的偏移量:9
当时文件的内容为:abcdefghi
掩盖后的文件内容为:ABCdefghi

运用场景

RandomAccessFile 比较常见的一个运用便是完结大文件的 断点续传

何谓断点续传?简单来说便是上传文件中途暂停或失利(比方遇到网络问题)之后,不需求从头上传,只需求上传那些未成功上传的文件分片即可。分片(先将文件切分红多个文件分片)上传是断点续传的根底。

该部分内容咱们会在后续实战部分中,手写一个断点续传下载器进行详细解说。

归纳运用事例

事例 1:格局化读取写入文本

需求阐明

1. 给定一个 Employee 类。

public class Employee {
    private String name; // 姓名
    private double salary; // 薪水
    private LocalDate hireDay; // 雇佣日期
    public Employee(String n, double s, int year, int month, int day)
    {
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }
    public String getName()
    {
        return name;
    }
    public double getSalary()
    {
        return salary;
    }
    public LocalDate getHireDay()
    {
        return hireDay;
    }
    // 加薪
    public void raiseSalary(double byPercent)
    {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
    @Override
    public String toString()
    {
        return getClass().getName()
                + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
    }
}

2. 咱们需求依照指定格局写入到 Employee.dat 文件中。

榜首行数字是写入的记载数量。

指定格局:姓名|薪水|入职时刻

3
Carl Cracker|75000.0|1987-12-15
Harry Hacker|50000.0|1989-10-01
Tony Tester|40000.0|1990-03-15

3. 从 Employee.dat 文件中读取数据打印到操控台中。

代码

public class Main {
   @Test
    public void test() throws IOException {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
        // 依照指定格局写入到 `Employee.dat` 文件中。
        try (PrintWriter out = new PrintWriter("Employee.dat", String.valueOf(StandardCharsets.UTF_8))) {
            out.println(staff.length);
            for (Employee e : staff) {
                out.println(e.getName() + "|" + e.getSalary() + "|" + e.getHireDay());
            }
        }
        // 从 `Employee.dat` 文件中读取数据打印到操控台中。
        try (Scanner in = new Scanner(new FileInputStream("Employee.dat"), String.valueOf(StandardCharsets.UTF_8))) {
            int n = in.nextInt();
            in.nextLine();
            Employee[] employees = new Employee[n];
            for (int i = 0; i < n; i++) {
                employees[i] = readEmployee(in);
            }
            for (Employee e : employees) {
                System.out.println(e);
            }
        }
    }
    public Employee readEmployee(Scanner in) {
        String line = in.nextLine();
        // split 办法的参数是一个描绘分隔符的正则表达式。
        // 因为 "|" 在正则表达式中有特别含义,所以需求 "\" 来转义,而 "\" 还需求一个 "\" 来转义。
        // 所以,表达式为 "\\|"。
        String[] tokens = line.split("\\|");
        String name = tokens[0];
        double salary = Double.parseDouble(tokens[1]);
        LocalDate hireDate = LocalDate.parse(tokens[2]);
        int year = hireDate.getYear();
        int month = hireDate.getMonthValue();
        int day = hireDate.getDayOfMonth();
        return new Employee(name, salary, year, month, day);
    } 
}

运转成果

  1. 作业目录中出现 Employee.dat 文件,有如下内容:
Java面试必知必会 —— 全面解读 Java IO(基础篇)
  1. 打印到操控台的内容如下:
io.Employee[name=Carl Cracker,salary=75000.0,hireDay=1987-12-15]
io.Employee[name=Harry Hacker,salary=50000.0,hireDay=1989-10-01]
io.Employee[name=Tony Tester,salary=40000.0,hireDay=1990-03-15]

总结

在这一讲中,咱们解说了怎么判别输入、输出流,字节约和字符流的区别和运用场景,缓冲流和普通流的比照试验,什么是打印流,最后介绍了随机拜访流。

在下一讲中,咱们去看看规划形式是怎么在 Java IO 中运用的。

参考资料

  1. JDK 8 API;
  2. JDK 9 API;
  3. Java核心技术卷 II(原书第11版)高档特性;
  4. JavaGuide;

更新日志

时刻 更新内容
2023.5.15 发布文章
2023.5.16 新增: 获取用户作业目录,获取不同操作体系的文件分隔符,try-with-resource 主动封闭流,java.io.Closeable 接口扩展 java.lang.AutoCloseable 接口的原因,归纳运用那里。修正: 修正创立文件的运用事例,完善内容格局。
2023.5.18 新增:Java IO 的规划形式(壹) —— 装修器形式,在「总结」供给了 url 跳转。
2023.5.23 修正:图片更新。