Guava:字符串与基础工具类

欢迎你来读这篇博客,这篇博客主要是关于Guava 字符串与基础工具类
其中包括 Joiner、Splitter、Strings、Charsets、CharMatcher,以及 Base64 Encoding 和 Decoding 的原理与手动实现。

序言

Guava 是 Google 开源的一套 Java 工具类库。

在 Java 8 之后,JDK 自身已经补齐了很多能力,例如 String.joinCollectors.joiningjava.util.Base64StandardCharsetsOptionalStream 等。因此,今天学习 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);
}
}

输出:

1
Mario,Luigi,Peach

如果不用工具类,我们经常会手写 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);

输出:

1
Mario,Peach

如果你希望用默认值替代 null,可以使用:

1
2
3
String result = Joiner.on(",")
.useForNull("UNKNOWN")
.join(names);

输出:

1
Mario,UNKNOWN,Peach

注意,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
Mario,Luigi,Peach

也可以添加前缀和后缀:

1
2
String result = names.stream()
.collect(Collectors.joining(",", "[", "]"));

输出:

1
[Mario,Luigi,Peach]

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);
}
}
}

输出:

1
2
3
Mario
Luigi
Peach

如果希望直接得到 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);

结果是:

1
2

如果希望保留尾部空字段,需要:

1
text.split(",", -1);

第三,trim 和过滤空值要额外写。比如输入:

1
a, b,  c,,

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);
}
}

输出:

1
[a, b, c]

解释:

  • trimResults():去掉每个拆分结果的首尾空白;
  • omitEmptyStrings():忽略空字符串。

一般推荐组合使用:

1
2
3
Splitter.on(",")
.trimResults()
.omitEmptyStrings()

这样空格项会先被 trim 成空字符串,再被忽略。

chapter 9:Splitter limit

有些场景只想拆一次。

例如:

1
key=value=with=equals

希望结果是:

1
2
key
value=with=equals

可以使用 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);
}
}
}

输出:

1
2
3
4
aaa
bbb
ccc
dd

适合固定长度编码、简单报文分段、长字符串切块等场景。

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
" 1, 2, ,3,, 4 "

希望得到:

1
[1, 2, 3, 4]

代码:

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);
}
}

输出:

1
[1, 2, 3, 4]

这类写法在 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
2
3
true
true
false

注意," " 不是空字符串。如果要判断空白字符串,可以使用:

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);
}
}

输出:

1
2
[]
null

这两个方法适合在边界层做数据清洗,比如 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"));
}
}

输出:

1
2
order:create:
.java

常见用途:

  • 比较路径;
  • 比较权限码;
  • 比较命名空间;
  • 提取共同前缀;
  • 判断文件后缀相似性。

chapter 17:Strings.repeat、padStart、padEnd

重复字符串:

1
String line = Strings.repeat("-", 20);

输出:

1
--------------------

左侧补齐:

1
String value = Strings.padStart("7", 3, '0');

输出:

1
007

右侧补齐:

1
String value = Strings.padEnd("abc", 6, '_');

输出:

1
abc___

适合日志格式化、编号补齐、命令行输出、简单报表格式化等场景。

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());
}
}

输出:

1
UTF-8

不过现在新项目更推荐使用 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);
}
}

输出:

1
13812345678

这比手写循环更直观。

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);
}
}

输出:

1
abcd

如果只是去掉首尾空格,用 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
hello world java

如果希望把连续空白压缩成一个空格:

1
2
String result = CharMatcher.whitespace()
.collapseFrom(text, ' ');

例如:

1
hello     world

会变成:

1
hello world

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);
}
}

输出:

1
Mario_123

这种字符级清洗非常适合用 CharMatcher

chapter 26:Base64 是什么

Base64 是一种把二进制数据编码成文本的方式。

它常用于:

  • 图片转文本;
  • 文件内容传输;
  • JWT 的 Header 和 Payload;
  • Basic Auth;
  • 邮件附件;
  • URL token;
  • 简单二进制数据编码。

Base64 的核心思想是:

用 64 个可打印字符表示二进制数据。

标准 Base64 字符表是:

1
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

一共 64 个字符。每个字符能表示 6 bit,因为:

1
2^6 = 64

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

例如:

1
Man

ASCII 字节是:

1
2
3
M = 77  = 01001101
a = 97 = 01100001
n = 110 = 01101110

合并:

1
010011010110000101101110

每 6 bit 一组:

1
010011 010110 000101 101110

对应十进制:

1
19 22 5 46

查 Base64 表:

1
T W F u

所以:

1
Man -> TWFu

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));
}
}

输出:

1
2
aGVsbG8=
hello

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);
}
}

输出:

1
aGVsbG8=

解释几个关键点。

Java 的 byte 是有符号的,范围是:

1
-128 到 127

但 Base64 按无符号字节处理,范围应该是:

1
0 到 255

所以读取 byte 时要写:

1
input[index++] & 0xff

这样可以把有符号 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));
}
}

输出:

1
hello

解码核心是把 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 判断是否写入 b2b3

chapter 32:Base64 URL Safe

标准 Base64 使用 +/=。但在 URL 中:

  • + 可能被当作空格;
  • / 有路径语义;
  • = 可能需要转义。

所以有 URL Safe Base64。它通常把:

1
2
+ 替换成 -
/ 替换成 _

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 不是加密,只是编码。

编码是:

1
换一种表示方式

加密是:

1
没有密钥不能还原

Base64 可以轻松解码:

1
aGVsbG8= -> hello

所以不要用 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

1
String.join(",", list)

或者:

1
Collectors.joining(",")

2. null 处理明显时用 Joiner

1
Joiner.on(",").skipNulls()

或者:

1
Joiner.on(",").useForNull("UNKNOWN")

3. 字符串拆分复杂时用 Splitter

尤其是需要 trim、omit empty、limit、map split、fixed length 时。

4. 字符集优先用 StandardCharsets

新项目推荐:

1
StandardCharsets.UTF_8

5. 字符清洗用 CharMatcher

例如手机号只保留数字、删除空白、保留合法用户名字符。

6. Base64 优先用 JDK

1
java.util.Base64

如果你已经使用 Guava,并且希望统一 Base16、Base32、Base64 API,可以用:

1
BaseEncoding

7. Base64 不要当加密

它只是编码,不是安全机制。

chapter 36:一句话总结

这一章的核心是:

用成熟工具类处理字符串拼接、拆分、清洗、编码,减少手写细节错误。

Joiner 解决的是拼接问题。

Splitter 解决的是拆分问题。

Strings 解决的是空字符串、公共前后缀和简单格式化问题。

Charsets 解决的是字符集常量问题,但新项目更推荐 StandardCharsets

CharMatcher 解决的是字符级清洗和匹配问题。

Base64 解决的是二进制数据文本化问题。

真正写项目时,不要为了用 Guava 而用 Guava,也不要因为 JDK 有类似能力就完全忽略 Guava。

更好的做法是:

了解每个工具的设计意图,在合适的场景选择最清晰、最稳的写法。

好代码不是 API 越新越好,也不是工具类越多越好。

好代码是让维护者一眼看懂:

1
2
3
4
这里在拼接
这里在拆分
这里在清洗
这里在编码

字符串处理不是小事,它往往决定了日志是否清晰、配置是否稳定、接口是否可靠、数据是否正确。

参考资料

  • 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.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava:字符串与基础工具类
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/01guava-string-basic-tools-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议