07. 详解 Java 字符串
字符编码基础
ASCII 码
最高位设置为 0,用剩下的 7 位表示字符。这 7 位可以看作数字 0~127。
数字 32~126 表示的字符都是可打印字符,0~31 和 127 表示不可以打印的字符,这些字符一般用于控制目的,这些字符中大部分都是不常用的。数字 32~126 的含义,除了中文之外,我们平常用的字符基本都涵盖了,键盘上的字符大部分也都涵盖了。
ISO 8859-1
最高位为 1,ISO8859-1又称 Latin-1,它也是使用一个字节表示一个字符,其中 0~127 与 ASCII 一样,128~255 规定了不同的含义。在 128~255 中,128~159 表示一些控制字符,这些字符也不常用,就不介绍了。160~255 表示一些西欧字符。
Windows-1252
ISO8859-1 虽然号称是标准,用于西欧国家,但它连欧元(€)这个符号都没有,因为欧元比较晚,而标准比较早。实际中使用更为广泛的是 Windows-1252 编码,这个编码与 ISO8859-1 基本是一样的,区别只在于数字 128~159。Windows-1252 使用其中的一些数字表示可打印字符。这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为,ISO 8859-1 已被 Windows-1252 取代,在很多应用程序中,即使文件声明它采用的是 ISO 8859-1 编码,解析的时候依然被当作 Windows-1252 编码。
我国内地的三个主要编码 GB2312、GBK、GB18030 有时间先后关系,表示的字符数越来越多,且后面的兼容前面的,GB2312 和 GBK 都是用两个字节表示,而 GB18030 则使用两个或四个字节表示。
我国香港特别行政区和我国台湾地区的主要编码是 Big5。
如果文本里的字符都是 ASCII 码字符,那么采用以上所说的任一编码方式都是一样的。
但如果有高位为 1 的字符,除了 GB2312、GBK、GB18030 外,其他编码都是不兼容的。比如,Windows-1252 和中文的各种编码是不兼容的,即使 Big5 和 GB18030 都能表示繁体字,其表示方式也是不一样的,而这就会出现所谓的乱码。
Unicode 编码
Unicode 给世界上每个字符分配了一个编号,编号范围为 0x000000~0x10FFFF。编号范围在 0x0000~0xFFFF 的字符为常用字符集,即 65 536个数字之内,称 BMP(Basic Multilingual Plane)字符。编号范围在 0x10000~0x10FFFF 的字符叫做增补字符(supplementary character)。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。大部分中文的编号范围为 U+4E00~U+9FFF,例如,“马”的 Unicode 是 U+9A6C。
Unicode 主要规定了编号,但没有规定如何把编号映射为二进制。UTF-16 是一种编码方式,或者叫映射方式,它将编号映射为 2 或 4 个字节,对 BMP 字符,它直接用 2 个字节表示,对于增补字符,使用 4 个字节表示,前两个字节叫高代理项(high surrogate),范围为 0xD800~0xDBFF,后两个字节叫低代理项(low surrogate),范围为 0xDC00~0xDFFF。UTF-16 定义了一个公式,可以将编号与 4 字节表示进行相互转换。
Java 内部采用 UTF-16 编码,char 表示一个字符,但只能表示 BMP 中的字符,对于增补字符,需要使用两个 char 表示,一个表示高代理项,一个表示低代理项。
那编号怎么对应到二进制表示呢?有多种方案,主要有 UTF-32、UTF-16 和 UTF-8。
1. UTF-32
这个最简单,就是字符编号的整数二进制形式,4 个字节。但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian, BE),否则,就叫“小端”(Little Endian, LE)。对应的编码方式分别是 UTF-32BE 和 UTF-32LE。可以看出,每个字符都用 4 个字节表示,非常浪费空间,实际采用的也比较少。
2. UTF-16
UTF-16使用变长字节表示:
1)对于编号在 U+0000~U+FFFF 的字符(常用字符集),直接用两个字节表示。
2)字符值在 U+10000~U+10FFFF 的字符(也叫做增补字符集),需要用 4 个字节表示。前两个字节叫高代理项,范围是 U+D800~U+DBFF;后两个字节叫低代理项,范围是 U+DC00~U+DFFF。数字编号和这个二进制表示之间有一个转换算法,这里就不介绍了。
区分是两个字节还是 4 个字节表示一个字符就看前两个字节的编号范围,如果是 U+D800~U+DBFF,就是4个字节,否则就是两个字节。
UTF-16 也有和 UTF-32 一样的字节序问题,如果高位存放在前面就叫大端(BE),编码就叫 UTF-16BE,否则就叫小端,编码就叫UTF-16LE。
UTF-16 常用于系统内部编码,UTF-16 比 UTF-32 节省了很多空间,但是任何一个字符都至少需要两个字节表示,对于美国和西欧国家而言,还是很浪费的。
3. UTF-8
UTF-8 使用变长字节表示,每个字符使用的字节个数与其 Unicode 编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为 1~4 不等。
小于 128 的,编码与 ASCII 码一样,最高位为 0。其他编号的第一个字节有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他字节都以 10 开头。
Java中 Character、String、StringBuilder 等类用于文本处理,它们的基础都是 char。
String 类
Java 中的字符串是由双引号括起来的多个字符,下面示例都是表示字符串常量:
1 | String str = "Hello World" |
String 常用的构造方法
1 | String():使用空字符串创建并初始化一个新的 String 对象。 |
关于 String的实现原理,String 类内部用一个字符数组表示字符串。在Java 9对String的实现进行了优化,它的内部不是 char 数组,而是 byte 数组,如果字符都是 ASCII 字符,它就可以使用一个字节表示一个字符,而不用 UTF-16BE 编码,节省内存。
String 的查找
在给定的字符串中查找字符或字符串是比较常见的操作。在 String 类中提供了 indexOf 和 lastIndexOf 方法用于查找字符或字符串,返回值是查找的字符或字符串所在的位置,-1 表示没有找到。这两个方法有多个重载版本:
1 | int indexOf(int ch):从前往后搜索字符 ch,返回第一次找到字符 ch 所在处的索引。 |
String 的比较
- 比较相等
String 提供的比较字符串相等的方法:
boolean equals(Object anObject)
:比较两个字符串中内容是否相等。boolean equalsIgnoreCase(String anotherString)
:类似 equals 方法,只是忽略大小写。
2. 比较大小
有时不仅需要知道是否相等,还要知道大小,String 提供的比较大小的方法:
- int compareTo(String anotherString):按字典顺序比较两个字符串(字典中顺序事实上就它的 Unicode 编码)。如果参数字符串等于此字符串,则返回值 0;如果此字符串小于字符串参数,则返回一个小于 0 的值;如果此字符串大于字符串参数,则返回一个大于 0 的值。
- int compareToIgnoreCase(String str):类似 compareTo,只是忽略大小写。
3. 比较前缀和后缀
- boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束。
- boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始。
String 的字符串截取
1 | // 从指定索引 beginIndex 开始截取一直到字符串结束的子字符串。 |
trim() 返回一个前后不含任何空格的调用字符串的副本
字符串分割
String 还提供了字符串分割方法 split
方法,参数是分割字符串,返回值 String[]。
字符串替换
String 还提供了字符串分割方法 replace
、replaceAll
、replaceFirsh
方法。
String 的 + 和 += 运算符
Java 中,String 可以直接使用 + 和 += 运算符,这是 Java 编译器提供的支持,背后,Java 编译器一般会生成 StringBuilder, + 和 += 操作会转换为 append。
对于简单的情况,可以可以直接使用 String 的 + 和 +=,对于复杂的情况,尤其是有循环的时候,应该直接使用 StringBuilder。
可变字符串 StringBuffer 和 StringBuilder
Java 提供了两个可变字符串类 StringBuffer 和 StringBuilder,中文翻译为“字符串缓冲区”。
StringBuffer 是线程安全的,它的方法是支持线程同步,线程同步会操作串行顺序执行,在单线程环境下会影响效率。StringBuilder 是 StringBuffer 单线程版本,Java 5之后发布的,它不是线程安全的,但它的执行效率很高。
StringBuffer 和 StringBuilder 具有完全相同的 API,即构造方法和方法等内容一样。StringBuilder 的中构造方法有4个:
1 | // 创建字符串内容是空的 StringBuilder 对象,初始容量默认为 16个字符。 |
StringBuffer 的追加、插入、删除和替换
- 字符串追加方法是 append,append 有很多重载方法,可以追加任何类型数据。
- StringBuilder insert(int offset, String str):在字符串缓冲区中索引为 offset 的字符位置之前插入str,insert 有很多重载方法,可以插入任何类型数据。
- delete(int start, int end):在字符串缓冲区中删除子字符串,要删除的子字符串从指定索引 start 开始直到索引 end - 1 处的字符。start 和 end 两个参数与 substring(int beginIndex, int endIndex)方法中的两个参数含义一样。
- replace(int start, int end, String str) 字符串缓冲区中用 str 替换子字符串,子字符串从指定索引 start 开始直到索引 end - 1 处的字符。start 和 end 同 delete(int start, int end)方法。
编码转换
String 内部是按 UTF-16BE 处理字符的,对 BMP 字符,使用一个 char,两个字节,对于增补字符,使用两个 char,四个字节。不同编码可能用于不同的字符集,使用不同的字节数目,以及不同的二进制表示。如何处理这些不同的编码呢?这些编码与 Java 内部表示之间如何相互转换呢?
Java 使用 Charset 类表示各种编码,它有两个常用静态方法:Charset.defaultCharset() 和 Charset.forName(String charsetName)。
String 类提供了如下方法,返回字符串按给定编码的字节表示:getByte(),getByte(String charsetName),getByte(Charset charset)。
字符串乱码问题
乱码有两种常见原因:一种比较简单,就是简单的解析错误;另外一种比较复杂,在错误解析的基础上进行了编码转换。
简单的解析导致的乱码,之所以看起来是乱码,是因为看待或者说解析数据的方式错了。只要使用正确的编码方式进行解读就可以纠正了。
如果怎么改变查看方式都不对,那很有可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。恢复的基本思路是尝试进行逆向操作,假定按一种编码转换方式获取乱码的二进制格式,然后再假定一种编码解读方式解读这个二进制,查看其看上去的形式,这要尝试多种编码,如果能找到看着正常的字符形式,应该就可以恢复。
System.setProperty(“file.encoding”,“UTF-8”) 设置不生效的问题
当 jvm 启动的时候,load class, 最后调用 main 函数之前,defaultCharset 已经初始化好,而很多函数里都掉用了这个方法象 String.getBytes, 还有 InputStreamReader,InputStreamWriter 都是调用了 Charset.defaultCharset()的方法,就不去追查谁先调用了 defaultCharset。
对 defaultCharset,在 jvm 里的语言就是初始话在启动的时候,而且不可被更改,你只能修改系统的charset,或者jvm的启动参数里加上 -Dfile.encoding="UTF-8"
gradle 运行 Java 类报 An exception occurred while executing the Java class. Input length = 1
java.nio.charset.MalformedInputException: Input length = 1
这个也是乱码问题。
记录
java 指定编码 utf8 编译 运行
1 | javac -encoding utf8 Cut.java |
java 命令启动 spring jar 包,指定编码
1 | java -Dfile.encoding=utf-8 -jar xxx.jar |