欢迎你来读这篇博客,这篇博客主要是关于Guava 字符串与基础工具类。
其中包括 Joiner、Splitter、Strings、Charsets、CharMatcher,以及 Base64 Encoding 和 Decoding 的原理与手动实现。
序言
Guava 是 Google 开源的一套 Java 工具类库。
在 Java 8 之后,JDK 自身已经补齐了很多能力,例如 String.join、Collectors.joining、java.util.Base64、StandardCharsets、Optional、Stream 等。因此,今天学习 Guava,不能只停留在“这个 API 怎么调用”,还要理解它的设计思想:它为什么这样设计,它解决了哪些细节问题,以及在现代 Java 项目中什么时候还值得使用。
这一篇是 Guava 学习系列的第 1 章:字符串与基础工具类。
1 2 3 4 5 6
| 第 1 章:字符串与基础工具类 01 Joiner 详细介绍以及和 Java 8 Collector 对比 02 Guava Splitter 详细讲解以及实战练习 03 Strings、Charsets、CharMatcher 04 Base64 原理详解,手动实现 Base64 Encoding 05 Base64 原理详解,手动实现 Base64 Decoding
|
字符串处理看起来很基础,但在后端开发里非常高频。日志拼接、URL 参数、配置解析、手机号清洗、用户输入处理、字符集转换、Base64 token 编解码,几乎每天都会遇到。
很多线上问题也来自字符串处理细节:末尾多了一个逗号、null 被拼成字符串、String.split 把尾部空字段吞了、字符集不一致导致乱码、Base64 padding 处理错误。工具类的意义不是让代码“看起来高级”,而是减少这些重复、隐蔽、容易出错的细节。
正文
chapter 1:Joiner 是什么
Joiner 是 Guava 提供的字符串拼接工具。它主要解决的问题是:
把多个元素按照指定分隔符拼接成字符串,并且优雅处理 null。
最简单的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.google.common.base.Joiner;
import java.util.List;
public class JoinerDemo {
public static void main(String[] args) { List<String> names = List.of("Mario", "Luigi", "Peach");
String result = Joiner.on(",").join(names);
System.out.println(result); } }
|
输出:
如果不用工具类,我们经常会手写 StringBuilder:
1 2 3 4 5
| StringBuilder builder = new StringBuilder();
for (String name : names) { builder.append(name).append(","); }
|
这样很容易在末尾多拼一个逗号。再改成按下标判断是不是最后一个元素,又会让代码变啰嗦。
Joiner.on(",").join(names) 的好处是:代码表达的是“用逗号连接这些元素”,而不是“我如何一步步拼接字符串”。
chapter 2:Joiner 的 null 处理
Joiner 默认不允许元素为 null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import com.google.common.base.Joiner;
import java.util.Arrays; import java.util.List;
public class JoinerNullDemo {
public static void main(String[] args) { List<String> names = Arrays.asList("Mario", null, "Peach");
String result = Joiner.on(",").join(names);
System.out.println(result); } }
|
这会抛出 NullPointerException。这是 Guava 很典型的设计风格:默认让错误尽早暴露,而不是默默吞掉。
如果你希望跳过 null,可以使用:
1 2 3
| String result = Joiner.on(",") .skipNulls() .join(names);
|
输出:
如果你希望用默认值替代 null,可以使用:
1 2 3
| String result = Joiner.on(",") .useForNull("UNKNOWN") .join(names);
|
输出:
注意,skipNulls() 和 useForNull() 不能同时使用。一个表示跳过 null,一个表示替换 null,语义冲突。
chapter 3:Joiner 拼接 Map
Joiner 也可以拼接 Map。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import com.google.common.base.Joiner;
import java.util.LinkedHashMap; import java.util.Map;
public class JoinerMapDemo {
public static void main(String[] args) { Map<String, String> params = new LinkedHashMap<>(); params.put("userId", "1001"); params.put("orderId", "2002"); params.put("status", "PAID");
String result = Joiner.on("&") .withKeyValueSeparator("=") .join(params);
System.out.println(result); } }
|
输出:
1
| userId=1001&orderId=2002&status=PAID
|
这种写法适合:
- 拼接 URL query;
- 拼接日志字段;
- 拼接签名原文;
- 拼接配置项;
- 拼接调试信息。
不过要注意:Joiner 只负责拼接,不负责 URL 编码,也不负责安全转义。如果是 URL 参数,真实项目里还要对 key 和 value 做 URL encode。
chapter 4:Joiner 和 Java 8 Collectors.joining 对比
Java 8 提供了 Collectors.joining()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.util.List; import java.util.stream.Collectors;
public class CollectorsJoiningDemo {
public static void main(String[] args) { List<String> names = List.of("Mario", "Luigi", "Peach");
String result = names.stream() .collect(Collectors.joining(","));
System.out.println(result); } }
|
输出:
也可以添加前缀和后缀:
1 2
| String result = names.stream() .collect(Collectors.joining(",", "[", "]"));
|
输出:
JDK 还提供了 String.join:
1
| String result = String.join(",", names);
|
选择建议如下:
| 场景 |
推荐写法 |
| 简单字符串列表拼接 |
String.join |
| Stream 流处理中拼接 |
Collectors.joining |
| 需要跳过 null |
Joiner.skipNulls() |
| 需要替换 null |
Joiner.useForNull() |
| 拼接 Map |
Joiner.withKeyValueSeparator() |
所以 Java 8 之后不是完全不需要 Joiner,而是要根据场景选择。简单列表拼接用 JDK,null 语义处理和 Map 拼接用 Joiner 仍然很舒服。
chapter 5:Joiner 实战:拼接结构化日志
后端服务中经常输出结构化日志:
1
| biz=order,scene=create,orderId=1001,userId=2002,status=PAID
|
可以这样写:
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.base.Joiner;
import java.util.LinkedHashMap; import java.util.Map;
public class JoinerLogDemo {
public static void main(String[] args) { Map<String, Object> logFields = new LinkedHashMap<>(); logFields.put("biz", "order"); logFields.put("scene", "create"); logFields.put("orderId", 1001); logFields.put("userId", 2002); logFields.put("status", "PAID");
String message = Joiner.on(",") .withKeyValueSeparator("=") .join(logFields);
System.out.println(message); } }
|
输出:
1
| biz=order,scene=create,orderId=1001,userId=2002,status=PAID
|
这种写法比手写字符串拼接更稳定,也更适合统一日志格式。
chapter 6:Splitter 是什么
Splitter 是 Guava 提供的字符串拆分工具。它解决的问题是:
比 String.split 更清晰、更可控地拆分字符串。
最简单的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.google.common.base.Splitter;
public class SplitterDemo {
public static void main(String[] args) { Iterable<String> result = Splitter.on(",") .split("Mario,Luigi,Peach");
for (String item : result) { System.out.println(item); } } }
|
输出:
如果希望直接得到 List:
1 2
| List<String> parts = Splitter.on(",") .splitToList("Mario,Luigi,Peach");
|
chapter 7:String.split 的几个坑
String.split 很常用,但有几个细节容易踩坑。
第一,split 的参数是正则表达式。
1 2 3
| String ip = "192.168.1.1";
String[] parts = ip.split(".");
|
你以为按点号拆分,但 . 在正则里表示任意字符。正确写法是:
1
| String[] parts = ip.split("\\.");
|
而 Guava 写法更直观:
1
| Splitter.on(".").split("192.168.1.1");
|
第二,尾部空字符串默认会被丢弃。
1 2 3 4 5
| String text = "a,b,";
String[] parts = text.split(",");
System.out.println(parts.length);
|
结果是:
如果希望保留尾部空字段,需要:
第三,trim 和过滤空值要额外写。比如输入:
用 String.split 还要自己 trim 和过滤。用 Guava 可以写成:
1 2 3 4
| Splitter.on(",") .trimResults() .omitEmptyStrings() .splitToList(text);
|
chapter 8:Splitter trim 和 omitEmptyStrings
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.google.common.base.Splitter;
import java.util.List;
public class SplitterTrimDemo {
public static void main(String[] args) { String text = " a, ,b,, c ";
List<String> result = Splitter.on(",") .trimResults() .omitEmptyStrings() .splitToList(text);
System.out.println(result); } }
|
输出:
解释:
trimResults():去掉每个拆分结果的首尾空白;
omitEmptyStrings():忽略空字符串。
一般推荐组合使用:
1 2 3
| Splitter.on(",") .trimResults() .omitEmptyStrings()
|
这样空格项会先被 trim 成空字符串,再被忽略。
chapter 9:Splitter limit
有些场景只想拆一次。
例如:
希望结果是:
可以使用 limit(2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.google.common.base.Splitter;
import java.util.List;
public class SplitterLimitDemo {
public static void main(String[] args) { String text = "key=value=with=equals";
List<String> parts = Splitter.on("=") .limit(2) .splitToList(text);
System.out.println(parts); } }
|
输出:
1
| [key, value=with=equals]
|
这个功能在解析配置时非常实用。
chapter 10:Splitter.fixedLength
Splitter.fixedLength(n) 可以按固定长度拆分字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.google.common.base.Splitter;
public class SplitterFixedLengthDemo {
public static void main(String[] args) { Iterable<String> parts = Splitter.fixedLength(3) .split("aaabbbcccdd");
for (String part : parts) { System.out.println(part); } } }
|
输出:
适合固定长度编码、简单报文分段、长字符串切块等场景。
chapter 11:Splitter 拆分 Map
配置字符串:
1
| host=localhost;port=5432;db=test
|
可以拆成 Map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.google.common.base.Splitter;
import java.util.Map;
public class SplitterMapDemo {
public static void main(String[] args) { String config = "host=localhost;port=5432;db=test";
Map<String, String> map = Splitter.on(";") .withKeyValueSeparator("=") .split(config);
System.out.println(map); } }
|
输出:
1
| {host=localhost, port=5432, db=test}
|
如果 value 中可能包含 =,要谨慎。更安全的方式是先按 ; 拆分,再对每段使用 limit(2)。
chapter 12:Splitter 实战:解析 ID 列表
用户输入:
希望得到:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import com.google.common.base.Splitter;
import java.util.List;
public class SplitterUserInputDemo {
public static void main(String[] args) { String input = " 1, 2, ,3,, 4 ";
List<Long> ids = Splitter.on(",") .trimResults() .omitEmptyStrings() .splitToStream(input) .map(Long::valueOf) .toList();
System.out.println(ids); } }
|
输出:
这类写法在 Controller 入参、后台筛选条件、配置解析中很常见。
chapter 13:Strings 工具类
Strings 是 Guava 中处理字符串空值和简单格式化的工具类。
常用方法包括:
1 2 3 4 5 6 7 8
| Strings.isNullOrEmpty(value) Strings.nullToEmpty(value) Strings.emptyToNull(value) Strings.commonPrefix(a, b) Strings.commonSuffix(a, b) Strings.repeat(value, count) Strings.padStart(value, minLength, padChar) Strings.padEnd(value, minLength, padChar)
|
chapter 14:Strings.isNullOrEmpty
1 2 3 4 5 6 7 8 9 10
| import com.google.common.base.Strings;
public class StringsDemo {
public static void main(String[] args) { System.out.println(Strings.isNullOrEmpty(null)); System.out.println(Strings.isNullOrEmpty("")); System.out.println(Strings.isNullOrEmpty(" ")); } }
|
输出:
注意," " 不是空字符串。如果要判断空白字符串,可以使用:
1
| value == null || value.isBlank()
|
isNullOrEmpty 只判断 null 和空字符串,不判断空白。
chapter 15:Strings.nullToEmpty 和 emptyToNull
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.google.common.base.Strings;
public class StringsNullEmptyDemo {
public static void main(String[] args) { String value1 = null; String safeValue = Strings.nullToEmpty(value1);
System.out.println("[" + safeValue + "]");
String value2 = ""; String nullableValue = Strings.emptyToNull(value2);
System.out.println(nullableValue); } }
|
输出:
这两个方法适合在边界层做数据清洗,比如 Controller 入参、配置读取、表单处理、第三方接口返回。
但核心业务里不建议到处混用 null 和空字符串。最好统一规范:字段到底允许 null,还是统一使用空字符串。
chapter 16:Strings 公共前缀和公共后缀
1 2 3 4 5 6 7 8 9 10 11 12
| import com.google.common.base.Strings;
public class CommonPrefixSuffixDemo {
public static void main(String[] args) { String a = "order:create:submit"; String b = "order:create:cancel";
System.out.println(Strings.commonPrefix(a, b)); System.out.println(Strings.commonSuffix("hello.java", "demo.java")); } }
|
输出:
常见用途:
- 比较路径;
- 比较权限码;
- 比较命名空间;
- 提取共同前缀;
- 判断文件后缀相似性。
chapter 17:Strings.repeat、padStart、padEnd
重复字符串:
1
| String line = Strings.repeat("-", 20);
|
输出:
左侧补齐:
1
| String value = Strings.padStart("7", 3, '0');
|
输出:
右侧补齐:
1
| String value = Strings.padEnd("abc", 6, '_');
|
输出:
适合日志格式化、编号补齐、命令行输出、简单报表格式化等场景。
chapter 18:Charsets 和 StandardCharsets
Guava 的 Charsets 提供常见字符集常量。
1 2 3 4 5 6 7 8 9 10 11 12
| import com.google.common.base.Charsets;
import java.nio.charset.Charset;
public class CharsetsDemo {
public static void main(String[] args) { Charset charset = Charsets.UTF_8;
System.out.println(charset.name()); } }
|
输出:
不过现在新项目更推荐使用 JDK 原生的 StandardCharsets:
1 2 3
| import java.nio.charset.StandardCharsets;
byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8);
|
因为 StandardCharsets 已经足够清晰,也不需要额外依赖。
chapter 19:为什么字符集必须显式指定
不要这样写:
1
| byte[] bytes = text.getBytes();
|
它会使用平台默认字符集。不同机器默认字符集可能不同。
推荐:
1
| byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
字符串还原也一样:
1
| String text = new String(bytes, StandardCharsets.UTF_8);
|
字符集不统一会导致中文乱码、文件解析失败、签名结果不一致、Base64 编码结果不同、第三方接口验签失败。
所以编码相关代码一定要显式指定字符集。
chapter 20:CharMatcher 是什么
CharMatcher 是 Guava 中很有特色的字符匹配工具。
它解决的问题是:
对字符串中的字符进行匹配、过滤、删除、替换、压缩。
常见场景:
- 只保留数字;
- 删除所有空白;
- 删除控制字符;
- 替换特殊字符;
- 判断是否全是数字;
- 清洗手机号;
- 清洗用户名;
- 规整多个空格。
chapter 21:CharMatcher 保留数字
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.google.common.base.CharMatcher;
public class CharMatcherDigitDemo {
public static void main(String[] args) { String phone = "138-1234-5678";
String digits = CharMatcher.inRange('0', '9') .retainFrom(phone);
System.out.println(digits); } }
|
输出:
这比手写循环更直观。
chapter 22:CharMatcher 删除空白字符
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.google.common.base.CharMatcher;
public class CharMatcherWhitespaceDemo {
public static void main(String[] args) { String text = "a b\tc\nd";
String result = CharMatcher.whitespace() .removeFrom(text);
System.out.println(result); } }
|
输出:
如果只是去掉首尾空格,用 trim() 或 strip() 就够了。如果要删除所有空白字符,CharMatcher.whitespace().removeFrom() 很方便。
chapter 23:CharMatcher 替换和压缩空白
替换所有空白为一个空格:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import com.google.common.base.CharMatcher;
public class CharMatcherReplaceDemo {
public static void main(String[] args) { String text = "hello\tworld\njava";
String result = CharMatcher.whitespace() .replaceFrom(text, ' ');
System.out.println(result); } }
|
输出:
如果希望把连续空白压缩成一个空格:
1 2
| String result = CharMatcher.whitespace() .collapseFrom(text, ' ');
|
例如:
会变成:
chapter 24:CharMatcher 判断字符串
判断是否全部是数字:
1 2
| boolean result = CharMatcher.inRange('0', '9') .matchesAllOf("123456");
|
判断是否包含数字:
1 2
| boolean result = CharMatcher.inRange('0', '9') .matchesAnyOf("abc1def");
|
判断是否完全不包含数字:
1 2
| boolean result = CharMatcher.inRange('0', '9') .matchesNoneOf("abcdef");
|
chapter 25:CharMatcher 实战:清洗用户名
假设用户名只允许英文字母、数字和下划线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.google.common.base.CharMatcher;
public class UsernameCleanDemo {
private static final CharMatcher USERNAME_ALLOWED = CharMatcher.inRange('a', 'z') .or(CharMatcher.inRange('A', 'Z')) .or(CharMatcher.inRange('0', '9')) .or(CharMatcher.is('_'));
public static void main(String[] args) { String input = "Mario_123!@#";
String clean = USERNAME_ALLOWED.retainFrom(input);
System.out.println(clean); } }
|
输出:
这种字符级清洗非常适合用 CharMatcher。
chapter 26:Base64 是什么
Base64 是一种把二进制数据编码成文本的方式。
它常用于:
- 图片转文本;
- 文件内容传输;
- JWT 的 Header 和 Payload;
- Basic Auth;
- 邮件附件;
- URL token;
- 简单二进制数据编码。
Base64 的核心思想是:
用 64 个可打印字符表示二进制数据。
标准 Base64 字符表是:
1
| ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
|
一共 64 个字符。每个字符能表示 6 bit,因为:
chapter 27:Base64 Encoding 原理
Base64 编码步骤:
1 2 3 4 5
| 1. 原始数据按字节读取,每个字节 8 bit 2. 每 3 个字节组成 24 bit 3. 把 24 bit 拆成 4 组,每组 6 bit 4. 每个 6 bit 数字映射到 Base64 字符表 5. 如果不足 3 字节,用 0 补齐,并用 = 做 padding
|
为什么是 3 字节变 4 字符?
1 2
| 3 字节 = 24 bit 4 个 Base64 字符 = 4 * 6 bit = 24 bit
|
例如:
ASCII 字节是:
1 2 3
| M = 77 = 01001101 a = 97 = 01100001 n = 110 = 01101110
|
合并:
1
| 010011010110000101101110
|
每 6 bit 一组:
1
| 010011 010110 000101 101110
|
对应十进制:
查 Base64 表:
所以:
chapter 28:Base64 padding 规则
如果原始数据长度不是 3 的倍数,就需要 padding。
| 原始字节数 mod 3 |
Base64 padding |
| 0 |
不补 |
| 1 |
补 == |
| 2 |
补 = |
例如:
1 2 3
| M -> TQ== Ma -> TWE= Man -> TWFu
|
= 本身不表示数据,它只是告诉解码器:这里是补齐产生的,不是真实原始字节。
chapter 29:JDK 和 Guava 的 Base64
JDK 写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import java.nio.charset.StandardCharsets; import java.util.Base64;
public class JdkBase64Demo {
public static void main(String[] args) { String text = "hello";
String encoded = Base64.getEncoder() .encodeToString(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded);
byte[] decoded = Base64.getDecoder() .decode(encoded);
System.out.println(new String(decoded, StandardCharsets.UTF_8)); } }
|
输出:
Guava 写法:
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.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class GuavaBase64Demo {
public static void main(String[] args) { String text = "hello";
String encoded = BaseEncoding.base64() .encode(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded);
byte[] decoded = BaseEncoding.base64() .decode(encoded);
System.out.println(new String(decoded, StandardCharsets.UTF_8)); } }
|
现在普通 Base64 更推荐直接使用 JDK 的 java.util.Base64。如果你需要 Base16、Base32、Base64Url 等统一编码 API,Guava 的 BaseEncoding 仍然很方便。
chapter 30:手动实现 Base64 Encoding
下面手写一个简化版 Base64 编码器。
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 30 31 32 33 34 35 36 37 38 39
| import java.nio.charset.StandardCharsets;
public class ManualBase64Encoder {
private static final char[] BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
public static String encode(byte[] input) { StringBuilder builder = new StringBuilder();
int index = 0;
while (index < input.length) { int b1 = input[index++] & 0xff; int b2 = index < input.length ? input[index++] & 0xff : -1; int b3 = index < input.length ? input[index++] & 0xff : -1;
int c1 = b1 >> 2; int c2 = ((b1 & 0x03) << 4) | (b2 == -1 ? 0 : (b2 >> 4)); int c3 = b2 == -1 ? 64 : (((b2 & 0x0f) << 2) | (b3 == -1 ? 0 : (b3 >> 6))); int c4 = b3 == -1 ? 64 : (b3 & 0x3f);
builder.append(BASE64_TABLE[c1]); builder.append(BASE64_TABLE[c2]); builder.append(c3 == 64 ? '=' : BASE64_TABLE[c3]); builder.append(c4 == 64 ? '=' : BASE64_TABLE[c4]); }
return builder.toString(); }
public static void main(String[] args) { String text = "hello";
String encoded = encode(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded); } }
|
输出:
解释几个关键点。
Java 的 byte 是有符号的,范围是:
但 Base64 按无符号字节处理,范围应该是:
所以读取 byte 时要写:
这样可以把有符号 byte 转成无符号 int。
每 3 个字节拆成 4 个 6 bit:
1 2 3 4
| int c1 = b1 >> 2; int c2 = ((b1 & 0x03) << 4) | (b2 >> 4); int c3 = ((b2 & 0x0f) << 2) | (b3 >> 6); int c4 = b3 & 0x3f;
|
如果不足 3 个字节,就用 = 补齐。
chapter 31:手动实现 Base64 Decoding
Base64 解码是编码的反向过程。
步骤:
1 2 3 4 5
| 1. 每 4 个 Base64 字符为一组 2. 每个字符查表得到 6 bit 数值 3. 4 个 6 bit 合并成 24 bit 4. 再拆成 3 个字节 5. 遇到 = padding 时减少输出字节
|
代码:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets;
public class ManualBase64Decoder {
private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static final int[] REVERSE_TABLE = new int[256];
static { for (int i = 0; i < REVERSE_TABLE.length; i++) { REVERSE_TABLE[i] = -1; }
for (int i = 0; i < BASE64_CHARS.length(); i++) { REVERSE_TABLE[BASE64_CHARS.charAt(i)] = i; } }
public static byte[] decode(String input) { if (input.length() % 4 != 0) { throw new IllegalArgumentException("Invalid Base64 input length"); }
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (int i = 0; i < input.length(); i += 4) { char ch1 = input.charAt(i); char ch2 = input.charAt(i + 1); char ch3 = input.charAt(i + 2); char ch4 = input.charAt(i + 3);
int c1 = decodeChar(ch1); int c2 = decodeChar(ch2); int c3 = ch3 == '=' ? 0 : decodeChar(ch3); int c4 = ch4 == '=' ? 0 : decodeChar(ch4);
int b1 = (c1 << 2) | (c2 >> 4); int b2 = ((c2 & 0x0f) << 4) | (c3 >> 2); int b3 = ((c3 & 0x03) << 6) | c4;
output.write(b1);
if (ch3 != '=') { output.write(b2); }
if (ch4 != '=') { output.write(b3); } }
return output.toByteArray(); }
private static int decodeChar(char ch) { if (ch >= 256 || REVERSE_TABLE[ch] == -1) { throw new IllegalArgumentException("Invalid Base64 character: " + ch); }
return REVERSE_TABLE[ch]; }
public static void main(String[] args) { String encoded = "aGVsbG8=";
byte[] decoded = decode(encoded);
System.out.println(new String(decoded, StandardCharsets.UTF_8)); } }
|
输出:
解码核心是把 4 个 6 bit 还原成 3 个 8 bit:
1 2 3
| int b1 = (c1 << 2) | (c2 >> 4); int b2 = ((c2 & 0x0f) << 4) | (c3 >> 2); int b3 = ((c3 & 0x03) << 6) | c4;
|
如果 ch3 == '=',说明只有 1 个原始字节。如果 ch4 == '=',说明只有 2 个原始字节。因此输出时需要根据 padding 判断是否写入 b2 和 b3。
chapter 32:Base64 URL Safe
标准 Base64 使用 +、/、=。但在 URL 中:
+ 可能被当作空格;
/ 有路径语义;
= 可能需要转义。
所以有 URL Safe Base64。它通常把:
JDK 写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import java.nio.charset.StandardCharsets; import java.util.Base64;
public class Base64UrlDemo {
public static void main(String[] args) { String text = "hello";
String encoded = Base64.getUrlEncoder() .withoutPadding() .encodeToString(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded);
byte[] decoded = Base64.getUrlDecoder() .decode(encoded);
System.out.println(new String(decoded, StandardCharsets.UTF_8)); } }
|
Guava 写法:
1 2 3
| String encoded = BaseEncoding.base64Url() .omitPadding() .encode(bytes);
|
常见应用包括 JWT、URL token、短链接参数、前后端传输安全字符串。
chapter 33:Base64 不是加密
Base64 不是加密,只是编码。
编码是:
加密是:
Base64 可以轻松解码:
所以不要用 Base64 保存密码,也不要把 Base64 当作安全机制。
如果要保存密码,应使用 bcrypt、Argon2、PBKDF2 等密码哈希算法。如果要加密数据,应使用 AES、RSA、ChaCha20-Poly1305 等真正的加密方案。
Base64 的作用是把二进制数据变成文本,方便传输和存储。
chapter 34:综合实战:解析配置、清洗输入、编码输出
假设有一个配置字符串:
1
| name= Mario ; phone=138-1234-5678 ; roles= admin, user, dev
|
我们希望:
- 拆成 Map;
- 清洗手机号;
- 拆分 roles;
- 使用 UTF-8 转字节;
- Base64 编码用户名。
代码:
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 30 31 32 33 34 35 36 37 38 39 40 41 42
| import com.google.common.base.CharMatcher; import com.google.common.base.Splitter;
import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map;
public class GuavaStringPracticeDemo {
public static void main(String[] args) { String config = "name= Mario ; phone=138-1234-5678 ; roles= admin, user, dev";
Map<String, String> map = Splitter.on(";") .trimResults() .omitEmptyStrings() .withKeyValueSeparator( Splitter.on("=") .trimResults() .limit(2) ) .split(config);
String name = map.get("name");
String phone = CharMatcher.inRange('0', '9') .retainFrom(map.get("phone"));
List<String> roles = Splitter.on(",") .trimResults() .omitEmptyStrings() .splitToList(map.get("roles"));
String encodedName = Base64.getEncoder() .encodeToString(name.getBytes(StandardCharsets.UTF_8));
System.out.println("name = " + name); System.out.println("phone = " + phone); System.out.println("roles = " + roles); System.out.println("encodedName = " + encodedName); } }
|
输出:
1 2 3 4
| name = Mario phone = 13812345678 roles = [admin, user, dev] encodedName = TWFyaW8=
|
这个例子把本章几个工具串起来了。
chapter 35:Guava 字符串工具类使用建议
1. 简单拼接优先用 JDK
或者:
2. null 处理明显时用 Joiner
1
| Joiner.on(",").skipNulls()
|
或者:
1
| Joiner.on(",").useForNull("UNKNOWN")
|
3. 字符串拆分复杂时用 Splitter
尤其是需要 trim、omit empty、limit、map split、fixed length 时。
4. 字符集优先用 StandardCharsets
新项目推荐:
5. 字符清洗用 CharMatcher
例如手机号只保留数字、删除空白、保留合法用户名字符。
6. Base64 优先用 JDK
如果你已经使用 Guava,并且希望统一 Base16、Base32、Base64 API,可以用:
7. Base64 不要当加密
它只是编码,不是安全机制。
chapter 36:一句话总结
这一章的核心是:
用成熟工具类处理字符串拼接、拆分、清洗、编码,减少手写细节错误。
Joiner 解决的是拼接问题。
Splitter 解决的是拆分问题。
Strings 解决的是空字符串、公共前后缀和简单格式化问题。
Charsets 解决的是字符集常量问题,但新项目更推荐 StandardCharsets。
CharMatcher 解决的是字符级清洗和匹配问题。
Base64 解决的是二进制数据文本化问题。
真正写项目时,不要为了用 Guava 而用 Guava,也不要因为 JDK 有类似能力就完全忽略 Guava。
更好的做法是:
了解每个工具的设计意图,在合适的场景选择最清晰、最稳的写法。
好代码不是 API 越新越好,也不是工具类越多越好。
好代码是让维护者一眼看懂:
字符串处理不是小事,它往往决定了日志是否清晰、配置是否稳定、接口是否可靠、数据是否正确。
参考资料
- Google Guava User Guide: Strings utilities.
- Google Guava API: Joiner.
- Google Guava API: Splitter.
- Google Guava API: Strings.
- Google Guava API: CharMatcher.
- Google Guava API: BaseEncoding.
- Java Documentation: String.
- Java Documentation: Collectors.
- Java Documentation: StandardCharsets.
- Java Documentation: Base64.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。