欢迎你来读这篇博客,这篇博客主要是关于Guava IO 进阶:Source、Sink 与 Streams。 其中包括 CharSource、CharSink、ByteSource、ByteSink、CharStreams、ByteStreams 的核心思想、源码设计、模板方法模式、流复制底层实现,以及在 Java 后端文件处理中的实战用法。
序言 上一章我们讲了 Guava 的 Files 工具类。
Files 很适合做 IO 入门,因为它直接面向文件操作:
1 2 3 4 5 6 Files.write(...) Files.copy(...) Files.move(...) Files.readLines(...) Files.equal(...) Files.getFileExtension(...)
这些 API 很直观。
但如果只停留在 Files,还不算真正理解 Guava IO 的设计思想。
Guava IO 真正值得学习的是它对 IO 的抽象:
1 2 Source:数据从哪里来 Sink:数据写到哪里去
再细分为:
1 2 3 4 5 ByteSource:字节输入来源 ByteSink:字节输出目标 CharSource:字符输入来源 CharSink:字符输出目标
有了这套抽象之后,文件不再是唯一的数据来源。
数据可以来自:
文件;
内存;
URL;
classpath 资源;
网络输入流;
字节数组;
字符串;
其他自定义来源。
输出目标也可以是:
文件;
内存;
字节流;
字符流;
黑洞输出流;
其他自定义目标。
这一章对应课程结构:
1 2 3 4 第 5 章:Guava IO 进阶:Source、Sink 与 Streams 5.1 CharSource 和 CharSink 源码剖析 5.2 ByteSource 和 ByteSink 源码剖析 5.3 CharStreams 和 ByteStreams 源码剖析
对应原课程:
1 2 3 11 Guava 之 CharSource 和 CharSink 源码剖析 12 Guava 之 ByteSource 和 ByteSink 源码剖析 13 Guava 之 CharStreams 和 ByteStreams 源码剖析
这篇文章的目标不是让你背 API,而是理解 Guava IO 背后的设计:
把 IO 操作从具体文件、具体流中抽象出来,用 Source 表示输入,用 Sink 表示输出,再用 Streams 工具类完成底层复制和读取。
这个思想在后端工程中非常重要。
因为业务代码最怕被各种 IO 细节淹没。
好的 IO 代码应该表达清楚:
1 2 3 4 5 6 从哪里读 写到哪里 用什么编码 是否按字符还是按字节 是否一次性读取 是否流式复制
正文 chapter 1:从 Files 到 Source / Sink 上一章我们使用过:
1 2 3 4 Files.asCharSource(file, StandardCharsets.UTF_8) Files.asCharSink(file, StandardCharsets.UTF_8) Files.asByteSource(file) Files.asByteSink(file)
这些方法表面上属于 Files 工具类。
但它们返回的对象已经不是简单工具方法,而是 Guava IO 的核心抽象:
1 2 3 4 CharSource CharSink ByteSource ByteSink
例如:
1 2 CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);String content = source.read();
这里的重点不再是:
而是:
同理:
1 2 CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8); sink.write("hello" );
重点变成:
这就是抽象的力量。
文件只是 Source / Sink 的一种实现。
chapter 2:Source / Sink 思想 Guava IO 把 IO 抽象成四个基本概念:
抽象
含义
面向数据类型
ByteSource
字节输入来源
byte
ByteSink
字节输出目标
byte
CharSource
字符输入来源
char / String
CharSink
字符输出目标
char / String
可以这样理解:
1 2 3 4 Source = 我能从这里读 Sink = 我能往这里写 Byte = 二进制 Char = 文本字符
比如:
1 2 3 4 文件 -> 可以是 ByteSource,也可以是 CharSource 字符串 -> 可以是 CharSource byte[] -> 可以是 ByteSource 文件输出 -> 可以是 ByteSink,也可以是 CharSink
这种抽象可以让业务代码摆脱具体 IO 类型。
例如一个方法只关心“读取文本”:
1 2 3 public String loadConfig (CharSource source) throws IOException { return source.read(); }
它可以读取:
文件;
字符串;
classpath 资源;
URL 资源;
测试中的内存数据。
这比写死:
1 public String loadConfig (File file)
更灵活。
chapter 3:为什么 Source / Sink 比直接使用 File 更灵活 如果方法参数是 File:
1 2 public void importUsers (File file) { }
那么调用方必须提供文件。
如果测试时不想创建真实文件,就不方便。
但如果参数是 CharSource:
1 2 public void importUsers (CharSource source) { }
测试时可以直接使用内存字符串:
1 CharSource source = CharSource.wrap("1,Mario\n2,Luigi" );
生产中可以使用文件:
1 CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);
这样业务逻辑不关心数据来自哪里。
这就是 Source 抽象的价值。
同理,如果输出参数是 CharSink:
1 2 public void exportUsers (CharSink sink) { }
生产中可以写文件。
测试中可以写到内存或自定义 Sink。
这种设计比到处传 File、InputStream、OutputStream 更有表达力。
chapter 4:CharSource 是什么 CharSource 表示字符输入来源。
它的核心职责是:
打开一个 Reader,并基于这个 Reader 提供高级读取方法。
你可以把它理解为:
1 CharSource = 一个可重复打开的字符数据来源
常见创建方式:
1. 从文件创建 1 CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);
2. 从字符串创建 1 CharSource source = CharSource.wrap("hello guava" );
3. 空字符来源 1 CharSource source = CharSource.empty();
CharSource 的常见方法包括:
1 2 3 4 5 6 7 8 openStream() openBufferedStream() read() readFirstLine() readLines() copyTo(...) isEmpty() length()
不同版本 Guava 中部分方法可能有差异,但核心思想一致。
chapter 5:CharSource 基本用法 从字符串创建 CharSource:
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.io.CharSource;public class CharSourceWrapDemo { public static void main (String[] args) throws Exception { CharSource source = CharSource.wrap("hello\nworld" ); String content = source.read(); System.out.println(content); } }
输出:
读取第一行:
1 String firstLine = source.readFirstLine();
读取所有行:
1 List<String> lines = source.readLines();
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.google.common.io.CharSource;import java.util.List;public class CharSourceReadLinesDemo { public static void main (String[] args) throws Exception { CharSource source = CharSource.wrap("Mario\nLuigi\nPeach" ); List<String> lines = source.readLines(); System.out.println(lines); } }
输出:
chapter 6:从文件创建 CharSource 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.io.CharSource;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FileCharSourceDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/users.txt" ); Files.createParentDirs(file); Files.asCharSink(file, StandardCharsets.UTF_8) .write("1,Mario\n2,Luigi\n3,Peach" ); CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8); System.out.println(source.read()); } }
这里:
1 Files.asCharSource(file, StandardCharsets.UTF_8)
把一个文件包装成字符来源。
读取时,CharSource 内部会打开 Reader。
使用者不需要手动管理 FileInputStream、InputStreamReader、BufferedReader 这些细节。
chapter 7:CharSource 的模板方法设计 CharSource 的源码设计很典型。
它通常会把最底层的打开流方法设计成抽象方法:
1 public abstract Reader openStream () throws IOException;
然后在父类中基于 openStream() 实现高级方法:
1 2 3 4 5 public String read () throws IOException { }
1 2 3 4 5 public List<String> readLines () throws IOException { }
这就是典型的模板方法模式:
子类负责提供底层 Reader,父类负责固定读取流程。
例如:
文件 CharSource 的 openStream() 打开文件 Reader;
字符串 CharSource 的 openStream() 打开 StringReader;
资源 CharSource 的 openStream() 打开资源 Reader。
父类不关心具体来源。
它只要能拿到 Reader,就能实现读取、按行读取、复制等高级能力。
chapter 8:CharSource 自定义实现 我们可以自己实现一个简单 CharSource。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.google.common.io.CharSource;import java.io.Reader;import java.io.StringReader;public class CustomStringCharSource extends CharSource { private final String content; public CustomStringCharSource (String content) { this .content = content; } @Override public Reader openStream () { return new StringReader (content); } }
使用:
1 2 3 4 5 6 7 8 public class CustomCharSourceDemo { public static void main (String[] args) throws Exception { CharSource source = new CustomStringCharSource ("hello custom source" ); System.out.println(source.read()); } }
这个例子说明:
只要你实现了 openStream,CharSource 父类就能提供一系列通用读取能力。
这就是源码设计中模板方法的价值。
chapter 9:CharSource 复制到 Appendable CharSource 可以把内容复制到 Appendable。
例如 StringBuilder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.google.common.io.CharSource;public class CharSourceCopyToAppendableDemo { public static void main (String[] args) throws Exception { CharSource source = CharSource.wrap("hello char source" ); StringBuilder builder = new StringBuilder (); source.copyTo(builder); System.out.println(builder); } }
输出:
这里 StringBuilder 实现了 Appendable。
这体现了 Guava 设计里的“面向抽象”思想。
chapter 10:CharSource 复制到 CharSink CharSource 可以复制到 CharSink。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.google.common.io.CharSink;import com.google.common.io.CharSource;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class CharSourceCopyToSinkDemo { public static void main (String[] args) throws Exception { CharSource source = CharSource.wrap("hello copy to sink" ); File targetFile = new File ("guava-io-demo/copy-char.txt" ); Files.createParentDirs(targetFile); CharSink sink = Files.asCharSink(targetFile, StandardCharsets.UTF_8); source.copyTo(sink); System.out.println("复制完成" ); } }
这段代码表达非常清楚:
不需要关心 Reader / Writer 的细节。
chapter 11:CharSink 是什么 CharSink 表示字符输出目标。
它的核心职责是:
打开一个 Writer,并基于这个 Writer 提供高级写入方法。
你可以理解为:
1 CharSink = 一个可以写入字符数据的目的地
常见创建方式:
1 CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);
常用方法:
1 2 3 4 5 openStream() openBufferedStream() write(CharSequence) writeLines(Iterable) writeFrom(Readable)
CharSink 和 CharSource 是一对。
CharSource 负责读;
CharSink 负责写。
chapter 12:CharSink 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.io.CharSink;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class CharSinkBasicDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/charsink-basic.txt" ); Files.createParentDirs(file); CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8); sink.write("hello CharSink" ); System.out.println("写入完成" ); } }
写多行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.google.common.io.CharSink;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;import java.util.List;public class CharSinkWriteLinesDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/charsink-lines.txt" ); Files.createParentDirs(file); CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8); sink.writeLines(List.of("Mario" , "Luigi" , "Peach" )); System.out.println("多行写入完成" ); } }
chapter 13:CharSink 追加写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import com.google.common.io.CharSink;import com.google.common.io.FileWriteMode;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class CharSinkAppendDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/append.txt" ); Files.createParentDirs(file); CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8); sink.write("line1\n" ); CharSink appendSink = Files.asCharSink( file, StandardCharsets.UTF_8, FileWriteMode.APPEND ); appendSink.write("line2\n" ); System.out.println("追加写入完成" ); } }
FileWriteMode.APPEND 表示追加。
如果不指定,默认会覆盖文件内容。
chapter 14:CharSink 的模板方法设计 CharSink 的源码结构也很典型。
底层抽象方法类似:
1 public abstract Writer openStream () throws IOException;
父类基于 openStream() 实现高级写入:
1 2 3 4 5 public void write (CharSequence charSequence) throws IOException { }
1 2 3 4 5 public void writeLines (Iterable<? extends CharSequence> lines) throws IOException { }
这同样是模板方法思想:
子类负责打开 Writer,父类负责统一写入流程。
如果写入目标是文件,openStream() 打开文件 Writer。
如果写入目标是内存,openStream() 可以打开内存 Writer。
业务代码只依赖 CharSink,不依赖具体目标。
chapter 15:CharSource / CharSink 实战:文本转换器 需求:
从一个 CharSource 读取文本;
转成大写;
写入一个 CharSink。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.google.common.io.CharSink;import com.google.common.io.CharSource;import java.io.IOException;import java.util.Locale;public class TextUppercaseConverter { public void convert (CharSource source, CharSink sink) throws IOException { String content = source.read(); String upper = content.toUpperCase(Locale.ROOT); sink.write(upper); } }
使用文件作为输入输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class TextUppercaseConverterDemo { public static void main (String[] args) throws Exception { File sourceFile = new File ("guava-io-demo/input.txt" ); File targetFile = new File ("guava-io-demo/output.txt" ); Files.createParentDirs(sourceFile); Files.asCharSink(sourceFile, StandardCharsets.UTF_8) .write("hello guava io" ); TextUppercaseConverter converter = new TextUppercaseConverter (); converter.convert( Files.asCharSource(sourceFile, StandardCharsets.UTF_8), Files.asCharSink(targetFile, StandardCharsets.UTF_8) ); System.out.println(Files.asCharSource(targetFile, StandardCharsets.UTF_8).read()); } }
测试时不一定要用真实文件:
1 CharSource source = CharSource.wrap("hello test" );
这就是 Source/Sink 抽象带来的测试便利。
chapter 16:ByteSource 是什么 ByteSource 表示字节输入来源。
它的核心职责是:
打开一个 InputStream,并基于这个 InputStream 提供高级读取方法。
常见创建方式:
1 2 ByteSource source = Files.asByteSource(file);ByteSource source = ByteSource.wrap(byteArray);
常见方法:
1 2 3 4 5 6 7 8 9 10 openStream() openBufferedStream() read() copyTo(...) size() isEmpty() hash(...) contentEquals(...) slice(...) asCharSource(charset)
ByteSource 用于二进制数据。
例如:
图片;
PDF;
压缩包;
上传文件;
加密数据;
任意 byte 数组。
chapter 17:ByteSource 基本用法 从 byte 数组创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.google.common.io.ByteSource;import java.nio.charset.StandardCharsets;public class ByteSourceWrapDemo { public static void main (String[] args) throws Exception { byte [] data = "hello byte source" .getBytes(StandardCharsets.UTF_8); ByteSource source = ByteSource.wrap(data); byte [] read = source.read(); System.out.println(new String (read, StandardCharsets.UTF_8)); } }
从文件创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.google.common.io.ByteSource;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class FileByteSourceDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/bytesource.txt" ); Files.createParentDirs(file); Files.write("hello file byte source" .getBytes(StandardCharsets.UTF_8), file); ByteSource source = Files.asByteSource(file); System.out.println(new String (source.read(), StandardCharsets.UTF_8)); } }
chapter 18:ByteSource 转 CharSource 字节数据加上字符集,就可以成为字符数据。
1 CharSource charSource = byteSource.asCharSource(StandardCharsets.UTF_8);
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.google.common.io.ByteSource;import com.google.common.io.CharSource;import java.nio.charset.StandardCharsets;public class ByteSourceAsCharSourceDemo { public static void main (String[] args) throws Exception { ByteSource byteSource = ByteSource.wrap("hello 中文" .getBytes(StandardCharsets.UTF_8)); CharSource charSource = byteSource.asCharSource(StandardCharsets.UTF_8); System.out.println(charSource.read()); } }
这体现了 ByteSource 和 CharSource 的关系:
1 ByteSource + Charset = CharSource
也就是说:
ByteSource 是原始字节;
CharSource 是按某种编码解释后的字符。
chapter 19:ByteSource 计算哈希 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.google.common.hash.HashCode;import com.google.common.hash.Hashing;import com.google.common.io.ByteSource;import java.nio.charset.StandardCharsets;public class ByteSourceHashDemo { public static void main (String[] args) throws Exception { ByteSource source = ByteSource.wrap("hello hash" .getBytes(StandardCharsets.UTF_8)); HashCode hashCode = source.hash(Hashing.sha256()); System.out.println(hashCode); } }
这比直接面对 InputStream 更清晰。
业务上常用于:
文件去重;
上传文件校验;
下载完整性校验;
生成文件指纹。
chapter 20:ByteSource 内容比较 ByteSource 可以判断内容是否一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.google.common.io.ByteSource;import java.nio.charset.StandardCharsets;public class ByteSourceContentEqualsDemo { public static void main (String[] args) throws Exception { ByteSource source1 = ByteSource.wrap("hello" .getBytes(StandardCharsets.UTF_8)); ByteSource source2 = ByteSource.wrap("hello" .getBytes(StandardCharsets.UTF_8)); ByteSource source3 = ByteSource.wrap("world" .getBytes(StandardCharsets.UTF_8)); System.out.println(source1.contentEquals(source2)); System.out.println(source1.contentEquals(source3)); } }
输出:
如果来源是文件,也可以比较文件内容。
chapter 21:ByteSource 切片 slice ByteSource 支持切片:
1 ByteSource slice = source.slice(offset, length);
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.google.common.io.ByteSource;import java.nio.charset.StandardCharsets;public class ByteSourceSliceDemo { public static void main (String[] args) throws Exception { ByteSource source = ByteSource.wrap("hello guava" .getBytes(StandardCharsets.UTF_8)); ByteSource slice = source.slice(6 , 5 ); System.out.println(new String (slice.read(), StandardCharsets.UTF_8)); } }
输出:
切片适合:
读取文件头;
读取魔数;
分段处理;
大文件部分读取;
简单协议解析。
chapter 22:ByteSink 是什么 ByteSink 表示字节输出目标。
它的核心职责是:
打开一个 OutputStream,并基于这个 OutputStream 提供高级写入方法。
常见创建方式:
1 ByteSink sink = Files.asByteSink(file);
常见方法:
1 2 3 4 5 openStream() openBufferedStream() write(byte []) writeFrom(InputStream) asCharSink(charset)
适合写入二进制数据。
chapter 23:ByteSink 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.io.ByteSink;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class ByteSinkBasicDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/bytesink.txt" ); Files.createParentDirs(file); ByteSink sink = Files.asByteSink(file); sink.write("hello byte sink" .getBytes(StandardCharsets.UTF_8)); System.out.println("ByteSink 写入完成" ); } }
从输入流写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import com.google.common.io.ByteSink;import com.google.common.io.Files;import java.io.ByteArrayInputStream;import java.io.File;import java.nio.charset.StandardCharsets;public class ByteSinkWriteFromDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/write-from.txt" ); Files.createParentDirs(file); ByteSink sink = Files.asByteSink(file); ByteArrayInputStream inputStream = new ByteArrayInputStream ( "hello writeFrom" .getBytes(StandardCharsets.UTF_8) ); sink.writeFrom(inputStream); System.out.println("writeFrom 完成" ); } }
chapter 24:ByteSink 转 CharSink 字节输出目标加上字符集,可以变成字符输出目标。
1 CharSink charSink = byteSink.asCharSink(StandardCharsets.UTF_8);
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.google.common.io.ByteSink;import com.google.common.io.CharSink;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class ByteSinkAsCharSinkDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/byte-to-char-sink.txt" ); Files.createParentDirs(file); ByteSink byteSink = Files.asByteSink(file); CharSink charSink = byteSink.asCharSink(StandardCharsets.UTF_8); charSink.write("hello from char sink" ); System.out.println("写入完成" ); } }
这体现了 ByteSink 和 CharSink 的关系:
1 ByteSink + Charset = CharSink
chapter 25:ByteSource / ByteSink 统一文件、内存、URL 资源 ByteSource 的价值在于统一输入来源。
1. 文件来源 1 ByteSource source = Files.asByteSource(file);
2. 内存来源 1 ByteSource source = ByteSource.wrap(bytes);
3. URL 或 classpath 资源 Guava 的 Resources 可以把资源抽象为 ByteSource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.google.common.io.ByteSource;import com.google.common.io.Resources;import java.net.URL;public class ResourceByteSourceDemo { public static void main (String[] args) throws Exception { URL url = Resources.getResource("demo.txt" ); ByteSource source = Resources.asByteSource(url); System.out.println(source.size()); } }
这样业务逻辑可以统一接收:
而不用关心数据来自文件、内存还是资源路径。
chapter 26:ByteSource / ByteSink 实战:二进制复制器 1 2 3 4 5 6 7 8 9 10 11 import com.google.common.io.ByteSink;import com.google.common.io.ByteSource;import java.io.IOException;public class BinaryCopyService { public long copy (ByteSource source, ByteSink sink) throws IOException { return source.copyTo(sink); } }
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class BinaryCopyServiceDemo { public static void main (String[] args) throws Exception { File sourceFile = new File ("guava-io-demo/source.bin" ); File targetFile = new File ("guava-io-demo/target.bin" ); Files.createParentDirs(sourceFile); Files.asByteSink(sourceFile) .write("binary content" .getBytes(StandardCharsets.UTF_8)); BinaryCopyService service = new BinaryCopyService (); long copied = service.copy( Files.asByteSource(sourceFile), Files.asByteSink(targetFile) ); System.out.println("复制字节数:" + copied); } }
这个服务不依赖 File。
它只依赖:
因此更容易复用和测试。
chapter 27:CharSource / CharSink 与 ByteSource / ByteSink 的关系 可以这样理解:
1 2 3 4 5 ByteSource:读原始字节 ByteSink:写原始字节 CharSource:按字符集读文本 CharSink:按字符集写文本
二者的桥梁是:
关系如下:
1 2 ByteSource + Charset -> CharSource ByteSink + Charset -> CharSink
所以:
如果你处理的是文本,用 CharSource / CharSink;
如果你处理的是二进制,用 ByteSource / ByteSink;
如果你从字节转字符,必须明确 Charset;
如果你从字符转字节,也必须明确 Charset。
不要依赖默认字符集。
chapter 28:CharStreams 是什么 CharStreams 是 Guava 提供的字符流工具类。
包名:
1 com.google.common.io.CharStreams
它主要围绕:
1 2 3 4 Reader Writer Readable Appendable
提供工具方法。
常见方法包括:
1 2 3 4 5 6 copy(Readable from, Appendable to) toString(Readable readable) readLines(Readable readable) skipFully(Reader reader, long n) nullWriter() asWriter(Appendable target)
CharSource 和 CharSink 的很多高级方法,底层会用到 CharStreams。
可以理解为:
CharSource / CharSink 是高层抽象,CharStreams 是底层字符流工具。
chapter 29:CharStreams.copy 字符流复制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.google.common.io.CharStreams;import java.io.StringReader;import java.io.StringWriter;public class CharStreamsCopyDemo { public static void main (String[] args) throws Exception { StringReader reader = new StringReader ("hello char streams" ); StringWriter writer = new StringWriter (); long copied = CharStreams.copy(reader, writer); System.out.println("复制字符数:" + copied); System.out.println(writer.toString()); } }
输出:
1 2 复制字符数:18 hello char streams
底层本质是:
1 2 3 4 创建 char buffer 循环 reader.read(buffer) writer.append/write(buffer) 直到读到 -1
chapter 30:CharStreams.toString 把 Readable 读取成字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.google.common.io.CharStreams;import java.io.StringReader;public class CharStreamsToStringDemo { public static void main (String[] args) throws Exception { StringReader reader = new StringReader ("hello toString" ); String content = CharStreams.toString(reader); System.out.println(content); } }
注意:
toString 会把全部内容读入内存。
它适合小文本。
不适合大文件。
chapter 31:CharStreams.readLines 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.google.common.io.CharStreams;import java.io.StringReader;import java.util.List;public class CharStreamsReadLinesDemo { public static void main (String[] args) throws Exception { StringReader reader = new StringReader ("a\nb\nc" ); List<String> lines = CharStreams.readLines(reader); System.out.println(lines); } }
输出:
如果要处理大文件,还是建议使用流式处理或 LineProcessor。
chapter 32:CharStreams.nullWriter nullWriter() 返回一个黑洞 Writer。
写进去的数据会被丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.google.common.io.CharStreams;import java.io.Writer;public class NullWriterDemo { public static void main (String[] args) throws Exception { Writer writer = CharStreams.nullWriter(); writer.write("this content will be ignored" ); writer.flush(); System.out.println("写入 nullWriter 完成" ); } }
常见用途:
测试;
统计写入流程耗时;
丢弃不需要的输出;
适配必须传 Writer 的接口。
它类似 Linux 里的:
chapter 33:ByteStreams 是什么 ByteStreams 是 Guava 提供的字节流工具类。
包名:
1 com.google.common.io.ByteStreams
它围绕:
1 2 3 4 InputStream OutputStream ReadableByteChannel WritableByteChannel
提供常见工具方法。
常见方法包括:
1 2 3 4 5 6 7 copy(InputStream from, OutputStream to) toByteArray(InputStream in) limit(InputStream in, long limit) skipFully(InputStream in, long n) readFully(InputStream in, byte [] b) exhaust(InputStream in) nullOutputStream()
ByteSource 和 ByteSink 的很多高级方法,底层会用到 ByteStreams。
可以理解为:
ByteSource / ByteSink 是高层抽象,ByteStreams 是底层字节流工具。
chapter 34:ByteStreams.copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.google.common.io.ByteStreams;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.nio.charset.StandardCharsets;public class ByteStreamsCopyDemo { public static void main (String[] args) throws Exception { ByteArrayInputStream inputStream = new ByteArrayInputStream ( "hello byte streams" .getBytes(StandardCharsets.UTF_8) ); ByteArrayOutputStream outputStream = new ByteArrayOutputStream (); long copied = ByteStreams.copy(inputStream, outputStream); System.out.println("复制字节数:" + copied); System.out.println(outputStream.toString(StandardCharsets.UTF_8)); } }
输出:
1 2 复制字节数:18 hello byte streams
底层本质是:
1 2 3 4 创建 byte buffer 循环 inputStream.read(buffer) outputStream.write(buffer) 直到读到 -1
chapter 35:ByteStreams.toByteArray 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.ByteStreams;import java.io.ByteArrayInputStream;import java.nio.charset.StandardCharsets;public class ByteStreamsToByteArrayDemo { public static void main (String[] args) throws Exception { ByteArrayInputStream inputStream = new ByteArrayInputStream ( "hello toByteArray" .getBytes(StandardCharsets.UTF_8) ); byte [] bytes = ByteStreams.toByteArray(inputStream); System.out.println(new String (bytes, StandardCharsets.UTF_8)); } }
注意:
toByteArray 会把全部字节读入内存。
适合小文件、小响应体。
不适合大文件。
大文件应该使用流式复制:
1 ByteStreams.copy(inputStream, outputStream)
chapter 36:ByteStreams.nullOutputStream nullOutputStream() 返回一个黑洞 OutputStream。
写进去的数据会被丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.ByteStreams;import java.io.OutputStream;import java.nio.charset.StandardCharsets;public class NullOutputStreamDemo { public static void main (String[] args) throws Exception { OutputStream outputStream = ByteStreams.nullOutputStream(); outputStream.write("ignored bytes" .getBytes(StandardCharsets.UTF_8)); outputStream.flush(); System.out.println("写入 nullOutputStream 完成" ); } }
常见用途:
丢弃输出;
测试复制性能;
只想消费输入流;
适配必须传 OutputStream 的 API。
chapter 37:ByteStreams.limit ByteStreams.limit 可以限制最多读取多少字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.io.ByteStreams;import java.io.ByteArrayInputStream;import java.io.InputStream;import java.nio.charset.StandardCharsets;public class ByteStreamsLimitDemo { public static void main (String[] args) throws Exception { ByteArrayInputStream inputStream = new ByteArrayInputStream ( "hello guava limit" .getBytes(StandardCharsets.UTF_8) ); InputStream limited = ByteStreams.limit(inputStream, 5 ); byte [] bytes = ByteStreams.toByteArray(limited); System.out.println(new String (bytes, StandardCharsets.UTF_8)); } }
输出:
这种能力适合:
限制读取大小;
防止超大输入;
读取文件头;
简单安全保护;
协议解析。
但不要把它当成完整的上传文件大小限制。
上传限制应该在网关、Servlet 容器、应用层一起做。
chapter 38:ByteStreams.exhaust exhaust 用于把输入流读完并丢弃内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.google.common.io.ByteStreams;import java.io.ByteArrayInputStream;import java.nio.charset.StandardCharsets;public class ByteStreamsExhaustDemo { public static void main (String[] args) throws Exception { ByteArrayInputStream inputStream = new ByteArrayInputStream ( "some content" .getBytes(StandardCharsets.UTF_8) ); long count = ByteStreams.exhaust(inputStream); System.out.println("消耗字节数:" + count); } }
适合:
只想消费流;
丢弃响应体;
测试读取速度;
清空剩余输入。
chapter 39:流复制的底层实现思想 无论是 CharStreams.copy 还是 ByteStreams.copy,底层思想都非常朴素:
1 2 3 4 1. 准备一个缓冲区 2. 从输入流读取一批数据 3. 写入输出流 4. 重复直到读取结束
伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 byte [] buffer = new byte [BUFFER_SIZE];long total = 0 ;while (true ) { int read = input.read(buffer); if (read == -1 ) { break ; } output.write(buffer, 0 , read); total += read; }
字符流类似:
1 2 3 4 5 char [] buffer = new char [BUFFER_SIZE];while ((read = reader.read(buffer)) != -1 ) { writer.write(buffer, 0 , read); }
这里有几个关键点:
1. 不要一个字节一个字节复制 这样性能很差。
2. 使用缓冲区批量读写 缓冲区可以显著减少系统调用次数。
3. 注意返回值 read 最后一次读取可能不足 buffer 大小。
所以写入时必须使用:
1 output.write(buffer, 0 , read);
不能写:
否则可能写入脏数据。
4. 注意关闭资源 工具类通常不一定负责关闭你传入的流。
谁打开,谁关闭。
Source / Sink 的高级方法通常会帮你打开并关闭内部流。
chapter 40:Streams 工具类和 Source / Sink 的关系 可以这样理解:
1 2 Source / Sink:更高层 IO 抽象 Streams:更底层流工具
例如:
1 ByteSource.copyTo(ByteSink)
内部大概会:
1 2 3 4 打开 InputStream 打开 OutputStream 调用 ByteStreams.copy 关闭流
再例如:
内部大概会:
1 2 3 打开 Reader 调用 CharStreams.toString 关闭 Reader
所以:
业务代码更推荐用 Source / Sink;
底层流操作可以用 CharStreams / ByteStreams;
如果你已经有 InputStream / OutputStream,就用 ByteStreams;
如果你已经有 Reader / Writer,就用 CharStreams;
如果你还没打开流,只是有文件、URL、字节数组,优先抽象成 Source / Sink。
chapter 41:实战:统一文本导入服务 需求:
输入可以来自文件,也可以来自内存字符串;
统一使用 CharSource;
按行读取;
过滤空行;
返回有效行列表。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.google.common.io.CharSource;import java.io.IOException;import java.util.ArrayList;import java.util.List;public class TextImportService { public List<String> importLines (CharSource source) throws IOException { List<String> lines = source.readLines(); List<String> result = new ArrayList <>(); for (String line : lines) { if (line != null && !line.isBlank()) { result.add(line.trim()); } } return result; } }
使用内存字符串测试:
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.io.CharSource;public class TextImportMemoryDemo { public static void main (String[] args) throws Exception { CharSource source = CharSource.wrap("Mario\n\n Luigi \nPeach" ); TextImportService service = new TextImportService (); System.out.println(service.importLines(source)); } }
使用文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class TextImportFileDemo { public static void main (String[] args) throws Exception { File file = new File ("guava-io-demo/import-users.txt" ); Files.createParentDirs(file); Files.asCharSink(file, StandardCharsets.UTF_8) .write("Mario\n\nLuigi\nPeach" ); TextImportService service = new TextImportService (); System.out.println(service.importLines( Files.asCharSource(file, StandardCharsets.UTF_8) )); } }
同一个业务服务可以支持不同数据来源。
这就是 CharSource 的价值。
chapter 42:实战:统一二进制存储服务 需求:
输入是任意 ByteSource;
保存到任意 ByteSink;
计算 SHA-256;
返回复制字节数和哈希。
结果对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class BinaryStoreResult { private final long copiedBytes; private final String sha256; public BinaryStoreResult (long copiedBytes, String sha256) { this .copiedBytes = copiedBytes; this .sha256 = sha256; } public long getCopiedBytes () { return copiedBytes; } public String getSha256 () { return sha256; } @Override public String toString () { return "BinaryStoreResult{" + "copiedBytes=" + copiedBytes + ", sha256='" + sha256 + '\'' + '}' ; } }
服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.google.common.hash.HashCode;import com.google.common.hash.Hashing;import com.google.common.io.ByteSink;import com.google.common.io.ByteSource;import java.io.IOException;public class BinaryStoreService { public BinaryStoreResult store (ByteSource source, ByteSink sink) throws IOException { HashCode hashCode = source.hash(Hashing.sha256()); long copiedBytes = source.copyTo(sink); return new BinaryStoreResult ( copiedBytes, hashCode.toString() ); } }
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import com.google.common.io.ByteSource;import com.google.common.io.Files;import java.io.File;import java.nio.charset.StandardCharsets;public class BinaryStoreDemo { public static void main (String[] args) throws Exception { ByteSource source = ByteSource.wrap( "hello binary store" .getBytes(StandardCharsets.UTF_8) ); File targetFile = new File ("guava-io-demo/store.bin" ); Files.createParentDirs(targetFile); BinaryStoreService service = new BinaryStoreService (); BinaryStoreResult result = service.store( source, Files.asByteSink(targetFile) ); System.out.println(result); } }
这个服务不关心输入来自哪里,也不关心输出写到哪里。
这就是 ByteSource / ByteSink 的威力。
chapter 43:Source / Sink 的设计优势总结 1. 把 IO 来源和目标抽象化 业务方法不再写死 File。
2. 测试更简单 测试可以用:
1 2 CharSource.wrap(...) ByteSource.wrap(...)
不一定创建真实文件。
3. 资源管理更清晰 Source / Sink 的高级方法会打开并关闭内部流。
4. 代码表达更清楚
比手写一堆流复制更表达意图。
5. 统一文件、内存、资源等来源 文件只是来源之一。
6. 和模板方法模式结合得很好 子类只需要提供 openStream()。
父类提供通用读取/写入能力。
chapter 44:使用建议 1. 处理文本优先 CharSource / CharSink 1 2 CharSource source = Files.asCharSource(file, UTF_8);CharSink sink = Files.asCharSink(file, UTF_8);
2. 处理二进制优先 ByteSource / ByteSink 1 2 ByteSource source = Files.asByteSource(file);ByteSink sink = Files.asByteSink(file);
3. 已经有流时使用 Streams 工具类 1 2 ByteStreams.copy(inputStream, outputStream); CharStreams.copy(reader, writer);
4. 小文件可以 read / toByteArray 大文件不要一次性读入内存。
5. 谁打开流,谁负责关闭 如果你自己传入 InputStream,就要自己管理关闭。
6. 不要依赖默认字符集 字符和字节转换必须指定:
7. Spring Boot 上传下载中可以借鉴 Source / Sink 思想 即使不用 Guava,也可以把输入和输出抽象出来。
8. 不要为了抽象而抽象 简单文件读写用 JDK Files 就可以。
当你需要统一多种来源、多种目标时,Source / Sink 的价值更明显。
chapter 45:一句话总结 这一章的核心是:
Guava IO 的精髓不是 Files 工具方法,而是 Source / Sink 抽象:用 Source 表示数据来源,用 Sink 表示数据目标,再通过 Streams 工具类完成底层流复制。
CharSource 表示字符输入来源。
CharSink 表示字符输出目标。
ByteSource 表示字节输入来源。
ByteSink 表示字节输出目标。
CharStreams 处理 Reader / Writer / Readable / Appendable。
ByteStreams 处理 InputStream / OutputStream。
它们之间的关系可以总结为:
1 2 3 4 5 6 7 文件 / 内存 / URL / 资源 ↓ Source ↓ Streams 底层读取复制 ↓ Sink
好的 IO 设计不是到处传 FileInputStream 和 FileOutputStream。
好的 IO 设计应该让业务代码表达:
1 2 3 4 从这个来源读 写到那个目标 文本用这个编码 二进制按字节处理
Source / Sink 这套思想非常值得学习。
即使你在新项目中更多使用 JDK NIO,也可以把这种抽象思路带到自己的代码设计中。
参考资料
Google Guava API: CharSource.
Google Guava API: CharSink.
Google Guava API: ByteSource.
Google Guava API: ByteSink.
Google Guava API: CharStreams.
Google Guava API: ByteStreams.
Google Guava API: Files.
Google Guava API: Resources.
Java Documentation: Reader.
Java Documentation: Writer.
Java Documentation: InputStream.
Java Documentation: OutputStream.
Java Documentation: StandardCharsets.
启示录 富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。