字符串,编码及字符串相关功能的抽象

2025/10/01 总结 共 4239 字,约 13 分钟

字符串,编码及字符串抽象

前言

在日常开发中,我们经常需要接触字符串,字符串涉及了许多细微的要点知识,会在意想不到的时候攻击我们,值得总结提醒。同时,关于字符串的不同特性以及功能,不同语言做出了不同的抽象,我们也可以通过它们组合方式,一窥这些语言的设计理念。

编码

编码是 字符集中的字符二进制 的映射。

  • 编码 (Encode): 字符 → 二进制
  • 解码 (Decode): 二进制 → 字符

因此,如果解码没有选择正确的对应编码,那么就不能正确翻译被编码后生成的字节。

演进历史

编码随着时间,技术的发展和业务本身的需求不断演进。

这里还有两个概念,标准实现。如:Unicode 是标准,而 UTF-8 是它的一个实现(其他实现还有 UTF-16)。而 ASCII 既是标准又是实现。

ASCII

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。

当我们操作单字节的 char,甚至做 char 的字面值的算术操作的时候,就是在操作 ASCII。

很明显,128 个字符是不够使用的。

ISO-8859

ASCII收录了空格及94个“可印刷字符”,足以给英语使用。但是,其他使用拉丁字母的语言(主要是欧洲国家的语言),都有一定数量的附加符号字母,故可以使用ASCII及控制字符以外的区域来储存及表示。ISO-8859 是兼容 ASCII 的。

ISO-8859 字符集是不包含中文的。而 Spring 的旧配置中,很多地方默认的 charset 就是 ISO-8859。当我们直接依赖 Spring 组件进行反序列化时,就有可能出现字符集转换的问题。

ISO in spring

同时,中文仍然需要表示和字符集。

GBK

GBK 编码,全称为《汉字内码扩展规范》(”Guojia Biaozhun Kuozhan”,国家标准扩展),是一种在中国大陆广泛使用的字符编码方案。用两个字节来表示一个汉字,兼容 ASCII。

在今天,虽然 UTF-8 已经成为 Web 和现代应用开发的绝对主流,但在一些老的 Windows 系统、特定行业的软件或历史数据中,你仍然可能会遇到 GBK 编码。

我们日常生活中常见的乱码就是 GBK,UTF-8,ISO-8859 的冲突了,他们三者都兼容 ASCII,同时互相之间都会冲突。

Unicode

为了解决这些冲突,我们需要一个统一的标准。

Unicode,全称为Unicode标准(The Unicode Standard),其官方机构Unicode联盟所用的中文名称为统一码,又译作万国码、统一字符码、统一字符编码,是信息技术领域的业界标准,其整理、编码了世界上大部分的文字系统,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。

注意,虽然 unicode 被列在这里,但它是一种标准更甚一个编码,直接使用 Unicode 存储会浪费很多空间。

由它作为标准,才有了后续的许多编码方式,比如我们所广泛使用的 UTF-8。

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,常用大部分汉字占用3个字节,少数占用4个字节。

UTF-8 也是我们当前通用的事实标准,新的项目基本会采用 UTF-8 作为编码。

此外还有一些需要注意的,比如 Mysql 的 utf8mb4,Oracle 的 AL32UTF8,原本这两个数据库各自有最古老版本的 UTF-8 实现(Mysql 固定 3 字节,四字节字符报错;Oracle 用两个 3 字节实现 4 字节),但为了兼容新确定的 UTF-8 标准,包括后来越来越多的 4 字节字符,不得不各自给出新的,兼容原来标准字符集的新字符集。

对比

数据结构

数据结构仍然是核心。不同的语言都是对 C 的字符串,或者说裸 bytes 做了基于自己设计哲学的改进和增强。

这里不得不强调一下,Java JDK8 的 String 内部永远是一个 char[] value,每个元素固定 16 bit(UTF-16 code unit),构造阶段就把输入(无论原来是 ASCII、Latin-1 还是中文)全部按 2 byte/char 展开到这个数组里;从 JDK 9开始,HotSpot 引入了一项叫 Compact Strings 的实现,先尝试当 ISO-8859(即单字符一字节)处理,省不下再退到 UTF-16。

Java,真的很不节约。

维度CJavaGoRust
本质形态'\0' 结尾的 char[];编译期不记录长度恒定不可变的 java.lang.String 对象,内部持 char[] value + int hash只读字节切片 struct{ *data, len },无容量字段只读字节切片 struct{ ptr, len },另有 String (UTF-8) 与 OsStr / OsString (系统编码) 两套类型

是否可变

C 的字符串可变不加修饰,灵活也造成了很多问题,而这种灵活性在很多场景下实际并无用途,只会带来更高的开发成本与更复杂的问题。因此,后续语言都提供了不变的字符串与其他提供可变功能的方式。

维度CJavaGoRust
是否可变可以,只要数组可写;但容易越界绝对不可变;任何改变都生成新对象不可变;重新赋值只是换指针String: 可变 (Mutable), &str:只读切片
可变构造器无,只能手动 malloc/realloc 扩缓冲区StringBuilder(非线程安全) StringBuffer(同步,线程安全)strings.Builder(底层 []byte 切片,按需 append直接用 String 即可

功能封装

这里只讨论官方库。可以看到,这几种语言中,C 非常依赖三方库,本身基本不提供什么能力;Java 将 String 的功能零零散散的封装在了 java.lang.Stringjava.lang.StringBuilderjava.util.regex等等不同命名的包中,Go 基本在strings中提供,Rust 基本在String中提供。

功能维度CJavaGoRust
不可变字符串无语言级抽象,只有 char *java.lang.String 对象,常量池复用原生类型 string(只读字节切片)原生类型 &str(只读 UTF-8 切片)
可变构造器手动 malloc/reallocjava.lang.StringBuilder(非线程安全)
java.lang.StringBuffer(同步)
strings.Builder原生类型 String(底层 Vec<u8>
查找/替换/大小写转换<string.h>strstrstrtoktoupperjava.lang.String:自带 indexOfreplacetoUpperCasestrings 包:ContainsReplaceToUpperstr 模块:containsreplaceto_uppercase
正则POSIX <regex.h>java.util.regex.Pattern/Matcherregexpregex crate(标准库外,但官方维护)
分段/拼接strtok/sprintfString.join(JDK 8+)、StringJoinerstrings.Split/Joinsplit_whitespace/split/collect::<Vec<_>>
与数字互转atoi/sprintfInteger.parseInt/String.valueOfstrconvAtoiItoaParseFloatto_string/parse::<i32>
Unicode 属性无,需三方库java.lang.CharacterisDigitisLetterunicode包:IsDigitIsLetterToUpperchar 方法:is_numericis_alphabetic
字节/符文转换无,手工移位getBytes(UTF_8)/new String(bytes, UTF_8)强制 []byte(str)/string([]byte)as_bytes()/from_utf8()/chars()
I/O 高效拼接手写缓冲区StringBuilderbytes.Bufferstd::fmt::Write trait + format!

遍历

Go 特意抽象出了rune 这种数据类型来解释单个字符,弃置了 C 中char的习惯命名。

每种语言都在发展中演进出了自己的安全的面向 UTF-8 的遍历方式。

语言CJavaGoRust
合法遍历写法for 循环 (逐字节)for (int i = 0; i < s.length(); ++i)charfor (cp : s.codePoints())for i, r := range sfor ch in s.chars()
默认遍历单位char = 1 byteUTF-16 code unit(charUnicode scalar value(rune=int32)Unicode scalar value(char
是否可直接遍历字节可以,*p 就是 byte不能直接接触 byte;需 getBytes(StandardCharsets.UTF_8) 拿到 byte[] 再遍历可以,强制 []byte(s) 拿到切片再 for _, b := range bytes可以,s.bytes() 返回 u8 迭代器,不做任何检查
编码安全措施(运行时)无,任何 byte 序列都能编译通过;出现非 ASCII 时程序员自己保证多字节边界字符串构造时已保证 UTF-16 合法;如果 new String(bytes, UTF_8) 遇到非法序列,JDK 默认用 ? 替换range 在解码 UTF-8 时同步检查:遇到非法序列会替换成 utf8.RuneError(U+FFFD)并继续,不会 panicstr 本身必须是合法 UTF-8,构造时已验证;bytes() 只是裸读,不会断字,程序员自己负责后续解码

总结

从 string 这个基础数据结构也能看出几种语言不同的设计哲学,在各自设计理念指导下,哪些东西应该聚合。

这里再抛出几个能以这篇文章作为论据的问题:

  • Go 为什么被称为 modern C?
  • Java 在设计哲学上在意效率吗?
  • Java 的面向对象仅仅在官方类库上就导致了什么样的结果?

文档信息

Search

    Table of Contents