From b46f2656fd523a47ac4be55c01cb49e7d588b7e4 Mon Sep 17 00:00:00 2001 From: guide Date: Sat, 16 Jul 2022 18:32:46 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20add]=20io=20=E4=B8=89=E9=83=A8=E6=9B=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- docs/.vuepress/sidebar.ts | 9 +- docs/java/io/io-basis.md | 544 ++++++++++++++++++++++ docs/java/io/io-design-patterns.md | 319 +++++++++++++ docs/java/{basis/io.md => io/io-model.md} | 3 +- 5 files changed, 879 insertions(+), 3 deletions(-) create mode 100755 docs/java/io/io-basis.md create mode 100644 docs/java/io/io-design-patterns.md rename docs/java/{basis/io.md => io/io-model.md} (99%) diff --git a/README.md b/README.md index ea63c1eb..78fea39a 100755 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ * [泛型&序列化详解](docs/java/basis/generics-and-wildcards.md) * [反射机制详解](docs/java/basis/reflection.md) * [Java 代理模式详解](docs/java/basis/proxy.md) -* [IO 模型详解](docs/java/basis/io.md) * [BigDecimal 详解](docs/java/basis/bigdecimal.md) ### 集合 @@ -73,6 +72,12 @@ * [HashMap(JDK1.8)源码+底层数据结构分析](docs/java/collection/hashmap-source-code.md) * [ConcurrentHashMap 源码+底层数据结构分析](docs/java/collection/concurrent-hash-map-source-code.md) +### IO + +* [IO 基础知识总结](docs/java/io/io-basis.md) +* [IO 设计模式总结](docs/java/io/io-design-patterns.md) +* [IO 模型详解](docs/java/io/io-model.md) + ### 并发 **知识点/面试题总结** : (必看 :+1:) diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 36736997..76c48746 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -54,7 +54,6 @@ export const sidebarConfig = defineSidebarConfig({ "generics-and-wildcards", "reflection", "proxy", - "io", "bigdecimal", ], }, @@ -81,6 +80,14 @@ export const sidebarConfig = defineSidebarConfig({ }, ], }, + + { + text: "IO", + prefix: "io/", + icon: "code", + collapsable: true, + children: ["io-basis", "io-design-patterns", "io-model"], + }, { text: "并发编程", prefix: "concurrent/", diff --git a/docs/java/io/io-basis.md b/docs/java/io/io-basis.md new file mode 100755 index 00000000..16c83303 --- /dev/null +++ b/docs/java/io/io-basis.md @@ -0,0 +1,544 @@ +--- +title: Java IO基础知识总结 +category: Java +tag: + - Java IO + - Java基础 +--- + +## IO 流简介 + +IO 即 `Input/Output`,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。 + +Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 + +- `InputStream`/`Reader`: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- `OutputStream`/`Writer`: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +## 字节流 + +### InputStream(字节输入流) + +`InputStream`用于从源头(通常是文件)读取数据(字节信息)到内存中,`java.io.InputStream`抽象类是所有字节输入流的父类。 + +`InputStream` 常用方法 : + +- `read()` :返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 `-1` ,表示文件结束。 +- `read(byte b[ ])` : 从输入流中读取一些字节存储到数组 `b` 中。如果数组 `b` 的长度为零,则不读取。如果没有可用字节读取,返回 `-1`。如果有可用字节读取,则最多读取的字节数最多等于 `b.length` , 返回读取的字节数。这个方法等价于 `read(b, 0, b.length)`。 +- `read(byte b[], int off, int len)` :在`read(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `skip(long n)` :忽略输入流中的 n 个字节 ,返回实际忽略的字节数。 +- `available()` :返回输入流中可以读取的字节数。 +- `close()` :关闭输入流释放相关的系统资源。 + +从 Java 9 开始,`InputStream` 新增加了多个实用的方法: + +- `readAllBytes()` :读取输入流中的所有字节,返回字节数组。 +- `readNBytes(byte[] b, int off, int len)` :阻塞直到读取 `len` 个字节。 +- `transferTo(OutputStream out)` : 将所有字节从一个输入流传递到一个输出流。 + +`FileInputStream` 是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。 + +`FileInputStream` 代码示例: + +```java +try (InputStream fis = new FileInputStream("input.txt)) { + System.out.println("Number of remaining bytes:" + + fis.available()); + int content; + long skip = fis.skip(2); + System.out.println("The actual number of bytes skipped:" + skip); + System.out.print("The content read from file:"); + while ((content = fis.read()) != -1) { + System.out.print((char) content); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +`input.txt` 文件内容: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220419155214614.png) + +输出: + +``` +Number of remaining bytes:11 +The actual number of bytes skipped:2 +The content read from file:JavaGuide +``` + +不过,一般我们是不会直接单独使用 `FileInputStream` ,通常会配合 `BufferedInputStream`(字节缓冲输入流,后文会讲到)来使用。 + +像下面这段代码在我们的项目中就比较常见,我们通过 `readAllBytes()` 读取输入流所有字节并将其直接赋值给一个 `String` 对象。 + +```java +// 新建一个 BufferedInputStream 对象 +BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); +// 读取文件的内容并复制到 String 对象中 +String result = new String(bufferedInputStream.readAllBytes()); +System.out.println(result); +``` + +`DataInputStream` 用于读取指定类型数据,不能单独使用,必须结合 `FileInputStream` 。 + +```java +FileInputStream fileInputStream = new FileInputStream("input.txt"); +//必须将fileInputStream作为构造参数才能使用 +DataInputStream dataInputStream = new DataInputStream(fileInputStream); +//可以读取任意具体的类型数据 +dataInputStream.readBoolean(); +dataInputStream.readInt(); +dataInputStream.readUTF(); +``` + +`ObjectInputStream` 用于从输入流中读取 Java 对象(反序列化),`ObjectOutputStream` 用于将对象写入到输出流(序列化)。 + +```java +ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data")); +MyClass object = (MyClass) input.readObject(); +input.close(); +``` + +另外,用于序列化和反序列化的类必须实现 `Serializable` 接口,对象中如果有属性不想被序列化,使用 `transient` 修饰。 + +### OutputStream(字节输出流) + +`OutputStream`用于将数据(字节信息)写入到目的地(通常是文件),`java.io.OutputStream`抽象类是所有字节输出流的父类。 + +`OutputStream` 常用方法 : + +- `write(int b)` :将特定字节写入输出流。 +- `write(byte b[ ])` : 将数组`b` 写入到输出流,等价于 `write(b, 0, b.length)` 。 +- `write(byte[] b, int off, int len)` : 在`write(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `flush()` :刷新此输出流并强制写出所有缓冲的输出字节。 +- `close()` :关闭输出流释放相关的系统资源。 + +`FileOutputStream` 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。 + +`FileOutputStream` 代码示例: + +```java +try (FileOutputStream output = new FileOutputStream("output.txt")) { + byte[] array = "JavaGuide".getBytes(); + output.write(array); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +运行结果: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220419155514392.png) + +类似于 `FileInputStream`,`FileOutputStream` 通常也会配合 `BufferedOutputStream`(字节缓冲输出流,后文会讲到)来使用。 + +```java +FileOutputStream fileOutputStream = new FileOutputStream("output.txt"); +BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream) +``` + +**`DataOutputStream`** 用于写入指定类型数据,不能单独使用,必须结合 `FileOutputStream` + +```java +// 输出流 +FileOutputStream fileOutputStream = new FileOutputStream("out.txt"); +DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); +// 输出任意数据类型 +dataOutputStream.writeBoolean(true); +dataOutputStream.writeByte(1); +``` + +`ObjectOutputStream` 用于从输入流中读取 Java 对象(`ObjectInputStream`,反序列化)或者将对象写入到输出流(`ObjectOutputStream`,序列化)。 + +```java +ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt") +Person person = new Person("Guide哥", "JavaGuide作者"); +output.writeObject(person); +``` + +## 字符流 + +不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 **那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** + +个人认为主要有两点原因: + +- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。 +- 如果我们不知道编码类型就很容易出现乱码问题。 + +乱码问题这个很容易就可以复现,我们只需要将 上面提到的 `FileInputStream` 代码示例中的 `input.txt` 文件内容改为中文即可,原代码不需要改动。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220419154632551.png) + +输出: + +```java +Number of remaining bytes:9 +The actual number of bytes skipped:2 +The content read from file:§å®¶å¥½ +``` + +可以很明显地看到读取出来的内容已经变成了乱码。 + +因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 + +字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?`utf8` :英文占 1 字节,中文占 3 字节,`unicode`:任何字符都占 2 个字节,`gbk`:英文占 1 字节,中文占 2 字节。 + +### Reader(字符输入流) + +`Reader`用于从源头(通常是文件)读取数据(字符信息)到内存中,`java.io.Reader`抽象类是所有字符输入流的父类。 + +`Reader` 用于读取文本, `InputStream` 用于读取原始字节。 + +`Reader` 常用方法 : + +- `read()` : 从输入流读取一个字符。 +- `read(char[] cbuf)` : 从输入流中读取一些字符,并将它们存储到字符数组 `cbuf`中,等价于 `read(cbuf, 0, cbuf.length)` 。 +- `read(char[] cbuf, int off, int len)` :在`read(char[] cbuf)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `skip(long n)` :忽略输入流中的 n 个字符 ,返回实际忽略的字符数。 +- `close()` : 关闭输入流并释放相关的系统资源。 + +`InputStreamReader` 是字节流转换为字符流的桥梁,其子类 `FileReader` 是基于该基础上的封装,可以直接操作字符文件。 + +```java +// 字节流转换为字符流的桥梁 +public class InputStreamReader extends Reader { +} +// 用于读取字符文件 +public class FileReader extends InputStreamReader { +} +``` + +`FileReader` 代码示例: + +```java +try (FileReader fileReader = new FileReader("input.txt");) { + int content; + long skip = fileReader.skip(3); + System.out.println("The actual number of bytes skipped:" + skip); + System.out.print("The content read from file:"); + while ((content = fileReader.read()) != -1) { + System.out.print((char) content); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +`input.txt` 文件内容: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220419154632551.png) + +输出: + +``` +The actual number of bytes skipped:3 +The content read from file:我是Guide。 +``` + +### Writer(字符输出流) + +`Writer`用于将数据(字符信息)写入到目的地(通常是文件),`java.io.Writer`抽象类是所有字节输出流的父类。 + +`Writer` 常用方法 : + +- `write(int c)` : 写入单个字符。 +- `write(char[] cbuf)` :写入字符数组 `cbuf`,等价于`write(cbuf, 0, cbuf.length)`。 +- `write(char[] cbuf, int off, int len)` :在`write(char[] cbuf)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `write(String str)` :写入字符串,等价于 `write(str, 0, str.length())` 。 +- `write(String str, int off, int len)` :在`write(String str)` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `append(CharSequence csq)` :将指定的字符序列附加到指定的 `Writer` 对象并返回该 `Writer` 对象。 +- `append(char c)` :将指定的字符附加到指定的 `Writer` 对象并返回该 `Writer` 对象。 +- `flush()` :刷新此输出流并强制写出所有缓冲的输出字符。 +- `close()`:关闭输出流释放相关的系统资源。 + +`OutputStreamWriter` 是字符流转换为字节流的桥梁,其子类 `FileWriter` 是基于该基础上的封装,可以直接将字符写入到文件。 + +```java +// 字符流转换为字节流的桥梁 +public class InputStreamReader extends Reader { +} +// 用于写入字符到文件 +public class FileWriter extends OutputStreamWriter { +} +``` + +`FileWriter` 代码示例: + +```java +try (Writer output = new FileWriter("output.txt")) { + output.write("你好,我是Guide。"); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +输出结果: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220419155802288.png) + +## 字节缓冲流 + +IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。 + +字节缓冲流这里采用了装饰器模式来增强 `InputStream` 和`OutputStream`子类对象的功能。 + +举个例子,我们可以通过 `BufferedInputStream`(字节缓冲输入流)来增强 `FileInputStream` 的功能。 + +```java +// 新建一个 BufferedInputStream 对象 +BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt")); +``` + +字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 `write(int b)` 和 `read()` 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。 + +我使用 `write(int b)` 和 `read()` 方法,分别通过字节流和字节缓冲流复制一个 `524.9 mb` 的 PDF 文件耗时对比如下: + +``` +使用缓冲流复制PDF文件总耗时:15428 毫秒 +使用普通字节流复制PDF文件总耗时:2555062 毫秒 +``` + +两者耗时差别非常大,缓冲流耗费的时间是字节流的 1/165。 + +测试代码如下: + +```java +@Test +void copy_pdf_to_another_pdf_with_byte_array_buffer_stream() { + // 记录开始时间 + long start = System.currentTimeMillis(); + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("深入理解计算机操作系统.pdf")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("深入理解计算机操作系统-副本.pdf"))) { + int len; + byte[] bytes = new byte[4 * 1024]; + while ((len = bis.read(bytes)) != -1) { + bos.write(bytes, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒"); +} + +@Test +void copy_pdf_to_another_pdf_with_byte_array_stream() { + // 记录开始时间 + long start = System.currentTimeMillis(); + try (FileInputStream fis = new FileInputStream("深入理解计算机操作系统.pdf"); + FileOutputStream fos = new FileOutputStream("深入理解计算机操作系统-副本.pdf")) { + int len; + byte[] bytes = new byte[4 * 1024]; + while ((len = fis.read(bytes)) != -1) { + fos.write(bytes, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒"); +} +``` + +如果是调用 `read(byte b[])` 和 `write(byte b[], int off, int len)` 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。 + +这次我们使用 `read(byte b[])` 和 `write(byte b[], int off, int len)` 方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下: + +``` +使用缓冲流复制PDF文件总耗时:695 毫秒 +使用普通字节流复制PDF文件总耗时:989 毫秒 +``` + +两者耗时差别不是很大,缓冲流的性能要略微好一点点。 + +测试代码如下: + +```java +@Test +void copy_pdf_to_another_pdf_buffer_stream() { + // 记录开始时间 + long start = System.currentTimeMillis(); + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("深入理解计算机操作系统.pdf")); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("深入理解计算机操作系统-副本.pdf"))) { + int content; + while ((content = bis.read()) != -1) { + bos.write(content); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒"); +} + +@Test +void copy_pdf_to_another_pdf_stream() { + // 记录开始时间 + long start = System.currentTimeMillis(); + try (FileInputStream fis = new FileInputStream("深入理解计算机操作系统.pdf"); + FileOutputStream fos = new FileOutputStream("深入理解计算机操作系统-副本.pdf")) { + int content; + while ((content = fis.read()) != -1) { + fos.write(content); + } + } catch (IOException e) { + e.printStackTrace(); + } + // 记录结束时间 + long end = System.currentTimeMillis(); + System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒"); +} +``` + +### BufferedInputStream(字节缓冲输入流) + +`BufferedInputStream` 从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。 + +`BufferedInputStream` 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组,通过阅读 `BufferedInputStream` 源码即可得到这个结论。 + +```java +public +class BufferedInputStream extends FilterInputStream { + // 内部缓冲区数组 + protected volatile byte buf[]; + // 缓冲区的默认大小 + private static int DEFAULT_BUFFER_SIZE = 8192; + // 使用默认的缓冲区大小 + 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]; + } +} +``` + +缓冲区的大小默认为 **8192** 字节,当然了,你也可以通过 `BufferedInputStream(InputStream in, int size)` 这个构造方法来指定缓冲区的大小。 + +### BufferedOutputStream(字节缓冲输出流) + +`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率 + +```java +try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { + byte[] array = "JavaGuide".getBytes(); + bos.write(array); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +类似于 `BufferedInputStream` ,`BufferedOutputStream` 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 **8192** 字节。 + +## 字符缓冲流 + +`BufferedReader` (字符缓冲输入流)和 `BufferedWriter`(字符缓冲输出流)类似于 `BufferedInputStream`(字节缓冲输入流)和`BufferedOutputStream`(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。 + +## 打印流 + +下面这段代码大家经常使用吧? + +```java +System.out.print("Hello!"); +System.out.println("Hello!"); +``` + +`System.out` 实际是用于获取一个 `PrintStream` 对象,`print`方法实际调用的是 `PrintStream` 对象的 `write` 方法。 + +`PrintStream` 属于字节打印流,与之对应的是 `PrintWriter` (字符打印流)。`PrintStream` 是 `OutputStream` 的子类,`PrintWriter` 是 `Writer` 的子类。 + +```java +public class PrintStream extends FilterOutputStream + implements Appendable, Closeable { +} +public class PrintWriter extends Writer { +} +``` + +## 随机访问流 + +这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 `RandomAccessFile` 。 + +`RandomAccessFile` 的构造方法如下,我们可以指定 `mode`(读写模式)。 + +```java +// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除 +public RandomAccessFile(File file, String mode) + throws FileNotFoundException { + this(file, mode, false); +} +// 私有方法 +private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{ + // 省略大部分代码 +} +``` + +读写模式主要有下面四种: + +- `r` : 只读模式。 +- `rw`: 读写模式 +- `rws`: 相对于 `rw`,`rws` 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。 +- `rwd` : 相对于 `rw`,`rwd` 同步更新对“文件的内容”的修改到外部存储设备。 + +文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。 + +`RandomAccessFile` 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 `RandomAccessFile` 的 `seek(long pos)` 方法来设置文件指针的偏移量(距文件开头 `pos` 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 `getFilePointer()` 方法。 + +`RandomAccessFile` 代码示例: + +```java +RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); +System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); +// 指针当前偏移量为 6 +randomAccessFile.seek(6); +System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); +// 从偏移量 7 的位置开始往后写入字节数据 +randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); +// 指针当前偏移量为 0,回到起始位置 +randomAccessFile.seek(0); +System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer()); +``` + +`input.txt` 文件内容: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220421162050158.png) + +输出: + +``` +读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 +读取之前的偏移量:6,当前读取到的字符G,读取之后的偏移量:7 +读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 +``` + +`input.txt` 文件内容变为 `ABCDEFGHIJK` 。 + +`RandomAccessFile` 的 `write` 方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖掉。 + +```java +RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); +randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); +``` + +假设运行上面这段程序之前 `input.txt` 文件内容变为 `ABCD` ,运行之后则变为 `HIJK` 。 + +`RandomAccessFile` 比较常见的一个应用就是实现大文件的 **断点续传** 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。 + +`RandomAccessFile` 可以帮助我们合并文件分片,示例代码如下: + +![](https://img-blog.csdnimg.cn/20210609164749122.png) + +我在[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中详细介绍了大文件的上传问题。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/image-20220428104115362.png) + +`RandomAccessFile` 的实现依赖于 `FileDescriptor` (文件描述符) 和 `FileChannel` (内存映射文件)。 diff --git a/docs/java/io/io-design-patterns.md b/docs/java/io/io-design-patterns.md new file mode 100644 index 00000000..1547064e --- /dev/null +++ b/docs/java/io/io-design-patterns.md @@ -0,0 +1,319 @@ +--- +title: Java IO设计模式总结 +category: Java +tag: + - Java IO + - Java基础 +--- + +这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。 + +## 装饰器模式 + +**装饰器(Decorator)模式** 可以在不改变原有对象的情况下拓展其功能。 + +装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。 + +对于字节流来说, `FilterInputStream` (对应输入流)和`FilterOutputStream`(对应输出流)是装饰器模式的核心,分别用于增强 `InputStream` 和`OutputStream`子类对象的功能。 + +我们常见的`BufferedInputStream`(字节缓冲输入流)、`DataInputStream` 等等都是`FilterInputStream` 的子类,`BufferedOutputStream`(字节缓冲输出流)、`DataOutputStream`等等都是`FilterOutputStream`的子类。 + +举个例子,我们可以通过 `BufferedInputStream`(字节缓冲输入流)来增强 `FileInputStream` 的功能。 + +`BufferedInputStream` 构造函数如下: + +```java +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]; +} +``` + +可以看出,`BufferedInputStream` 的构造函数其中的一个参数就是 `InputStream` 。 + +`BufferedInputStream` 代码示例: + +```java +try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) { + int content; + long skip = bis.skip(2); + while ((content = bis.read()) != -1) { + System.out.print((char) content); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +这个时候,你可以会想了:**为啥我们直接不弄一个`BufferedFileInputStream`(字符缓冲文件输入流)呢?** + +```java +BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt"); +``` + +如果 `InputStream`的子类比较少的话,这样做是没问题的。不过, `InputStream`的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。 + +如果你对 IO 流比较熟悉的话,你会发现`ZipInputStream` 和`ZipOutputStream` 还可以分别增强 `BufferedInputStream` 和 `BufferedOutputStream` 的能力。 + +```java +BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName)); +ZipInputStream zis = new ZipInputStream(bis); + +BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName)); +ZipOutputStream zipOut = new ZipOutputStream(bos); +``` + +`ZipInputStream` 和`ZipOutputStream` 分别继承自`InflaterInputStream` 和`DeflaterOutputStream`。 + +```java +public +class InflaterInputStream extends FilterInputStream { +} + +public +class DeflaterOutputStream extends FilterOutputStream { +} + +``` + +这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。 + +为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 `InputStream` 和`OutputStream`。 + +对于字符流来说,`BufferedReader` 可以用来增加 `Reader` (字符输入流)子类的功能,`BufferedWriter` 可以用来增加 `Writer` (字符输出流)子类的功能。 + +```java +BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); +``` + +IO 流中的装饰器模式应用的例子实在是太多了,不需要特意记忆,完全没必要哈!搞清了装饰器模式的核心之后,你在使用的时候自然就会知道哪些地方运用到了装饰器模式。 + +## 适配器模式 + +**适配器(Adapter Pattern)模式** 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。 + +适配器模式中存在被适配的对象或者类称为 **适配者(Adaptee)** ,作用于适配者的对象或者类称为**适配器(Adapter)** 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。 + +IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。 + +`InputStreamReader` 和 `OutputStreamWriter` 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。`InputStreamReader` 使用 `StreamDecoder` (流解码器)对字节进行解码,**实现字节流到字符流的转换,** `OutputStreamWriter` 使用`StreamEncoder`(流编码器)对字符进行编码,实现字符流到字节流的转换。 + +`InputStream` 和 `OutputStream` 的子类是被适配者, `InputStreamReader` 和 `OutputStreamWriter`是适配器。 + +```java +// InputStreamReader 是适配器,FileInputStream 是被适配的类 +InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8"); +// BufferedReader 增强 InputStreamReader 的功能(装饰器模式) +BufferedReader bufferedReader = new BufferedReader(isr); +``` + +`java.io.InputStreamReader` 部分源码: + +```java +public class InputStreamReader extends Reader { + //用于解码的对象 + private final StreamDecoder sd; + public InputStreamReader(InputStream in) { + super(in); + try { + // 获取 StreamDecoder 对象 + sd = StreamDecoder.forInputStreamReader(in, this, (String)null); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + // 使用 StreamDecoder 对象做具体的读取工作 + public int read() throws IOException { + return sd.read(); + } +} +``` + +`java.io.OutputStreamWriter` 部分源码: + +```java +public class OutputStreamWriter extends Writer { + // 用于编码的对象 + private final StreamEncoder se; + public OutputStreamWriter(OutputStream out) { + super(out); + try { + // 获取 StreamEncoder 对象 + se = StreamEncoder.forOutputStreamWriter(out, this, (String)null); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + // 使用 StreamEncoder 对象做具体的写入工作 + public void write(int c) throws IOException { + se.write(c); + } +} +``` + +**适配器模式和装饰器模式有什么区别呢?** + +**装饰器模式** 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。 + +**适配器模式** 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 `StreamDecoder` (流解码器)和`StreamEncoder`(流编码器)就是分别基于 `InputStream` 和 `OutputStream` 来获取 `FileChannel`对象并调用对应的 `read` 方法和 `write` 方法进行字节数据的读取和写入。 + +```java +StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { + // 省略大部分代码 + // 根据 InputStream 对象获取 FileChannel 对象 + ch = getChannel((FileInputStream)in); +} +``` + +适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。 + +另外,`FutrueTask` 类使用了适配器模式,`Executors` 的内部类 `RunnableAdapter` 实现属于适配器,用于将 `Runnable` 适配成 `Callable`。 + +`FutureTask`参数包含 `Runnable` 的一个构造方法: + +```java +public FutureTask(Runnable runnable, V result) { + // 调用 Executors 类的 callable 方法 + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +``` + +`Executors`中对应的方法和适配器: + +```java +// 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法 +public static Callable callable(Runnable task, T result) { + if (task == null) + throw new NullPointerException(); + return new RunnableAdapter(task, result); +} +// 适配器 +static final class RunnableAdapter implements Callable { + final Runnable task; + final T result; + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + public T call() { + task.run(); + return result; + } +} +``` + +## 工厂模式 + +工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 `Files` 类的 `newInputStream` 方法用于创建 `InputStream` 对象(静态工厂)、 `Paths` 类的 `get` 方法创建 `Path` 对象(静态工厂)、`ZipFileSystem` 类(`sun.nio`包下的类,属于 `java.nio` 相关的一些内部实现)的 `getPath` 的方法创建 `Path` 对象(简单工厂)。 + +```java +InputStream is Files.newInputStream(Paths.get(generatorLogoPath)) +``` + +## 观察者模式 + +NIO 中的文件目录监听服务使用到了观察者模式。 + +NIO 中的文件目录监听服务基于 `WatchService` 接口和 `Watchable` 接口。`WatchService` 属于观察者,`Watchable` 属于被观察者。 + +`Watchable` 接口定义了一个用于将对象注册到 `WatchService`(监控服务) 并绑定监听事件的方法 `register` 。 + +```java +public interface Path + extends Comparable, Iterable, Watchable{ +} + +public interface Watchable { + WatchKey register(WatchService watcher, + WatchEvent.Kind[] events, + WatchEvent.Modifier... modifiers) + throws IOException; +} +``` + +`WatchService` 用于监听文件目录的变化,同一个 `WatchService` 对象能够监听多个文件目录。 + +```java +// 创建 WatchService 对象 +WatchService watchService = FileSystems.getDefault().newWatchService(); + +// 初始化一个被监控文件夹的 Path 类: +Path path = Paths.get("workingDirectory"); +// 将这个 path 对象注册到 WatchService(监控服务) 中去 +WatchKey watchKey = path.register( +watchService, StandardWatchEventKinds...); +``` + +`Path` 类 `register` 方法的第二个参数 `events` (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。 + +```java +WatchKey register(WatchService watcher, + WatchEvent.Kind... events) + throws IOException; +``` + +常用的监听事件有 3 种: + +- `StandardWatchEventKinds.ENTRY_CREATE` :文件创建。 +- `StandardWatchEventKinds.ENTRY_DELETE` : 文件删除。 +- `StandardWatchEventKinds.ENTRY_MODIFY` : 文件修改。 + +`register` 方法返回 `WatchKey` 对象,通过`WatchKey` 对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。 + +```java +WatchKey key; +while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + // 可以调用 WatchEvent 对象的方法做一些事情比如输出事件的具体上下文信息 + } + key.reset(); +} +``` + +`WatchService` 内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化,简化后的源码如下所示。 + +```java +class PollingWatchService + extends AbstractWatchService +{ + // 定义一个 daemon thread(守护线程)轮询检测文件变化 + private final ScheduledExecutorService scheduledExecutor; + + PollingWatchService() { + scheduledExecutor = Executors + .newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setDaemon(true); + return t; + }}); + } + + void enable(Set> events, long period) { + synchronized (this) { + // 更新监听事件 + this.events = events; + + // 开启定期轮询 + Runnable thunk = new Runnable() { public void run() { poll(); }}; + this.poller = scheduledExecutor + .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS); + } + } +} +``` + +## 参考 + +- Patterns in Java APIs:http://cecs.wright.edu/~tkprasad/courses/ceg860/paper/node26.html +- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式:https://time.geekbang.org/column/article/204845 +- sun.nio 包是什么,是 java 代码么? - RednaxelaFX https://www.zhihu.com/question/29237781/answer/43653953 \ No newline at end of file diff --git a/docs/java/basis/io.md b/docs/java/io/io-model.md similarity index 99% rename from docs/java/basis/io.md rename to docs/java/io/io-model.md index ec43d20e..1ccab6d5 100644 --- a/docs/java/basis/io.md +++ b/docs/java/io/io-model.md @@ -1,7 +1,8 @@ --- -title: IO 模型详解 +title: Java IO模型详解 category: Java tag: + - Java IO - Java基础 ---