九、解读String源码
String类的声明(jdk8版本)
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
}
第一,String类是final的,意味着它不能被子类继承。
第二, String类实现了Serializable接口,意味着它可以序列化。
第三,String类实现了Comparable接口,意味着最好不要用 == 来比较两个字符串是否相等,而应该用 compareTO()方法去比较。因为 == 是用来比较两个对象的地址。如果只是比较两个字符串的内容的话,推荐使用String类中的 equals 方法
第四,String和StringBuffer、StringBuilder 一样,都实现了CharSequence接口,所以它们三个属于近亲。由于String是不可变的,所以遇到字符串拼接的时候就可以考虑一下String的另外连个好兄弟,StringBuffer 和 StringBuilder, 它俩是可变的。
equals方法的源码:
java
public boolean equals(Object anObject) {
// 检查是否是同一个对象的引用,如果是,直接返回 true
if (this == anObject) {
return true;
}
// 检查 anObject 是否是 String 类的实例
if (anObject instanceof String) {
String anotherString = (String) anObject; // 将 anObject 强制转换为 String 类型
int n = value.length; // 获取当前字符串的长度
// 检查两个字符串长度是否相等
if (n == anotherString.value.length) {
char v1[] = value; // 当前字符串的字符数组
char v2[] = anotherString.value; // 另一个字符串的字符数组
int i = 0; // 用于遍历字符数组的索引
// 遍历比较两个字符串的每个字符
while (n-- != 0) {
// 如果在任何位置字符不同,则返回 false
if (v1[i] != v2[i])
return false;
i++;
}
// 所有字符都相同,返回 true
return true;
}
}
// 如果 anObject 不是 String 类型或长度不等,则返回 false
return false;
}
1、String底层为什么由char数组优化为byte数组
- 第五,Java 9 以前,String 是用 char 型数组实现的,之后改成了 byte 型数组实现,并增加了 coder 来表示编码。这样做的好处是在 Latin1 字符为主的程序里,可以把 String 占用的内存减少一半。当然,天下没有免费的午餐,这个改进在节省内存的同时引入了编码检测的开销。
Latin1(Latin-1)是一种单字节字符集(即每个字符只使用一个字节的编码方式),也称为 ISO-8859-1(国际标准化组织 8859-1),它包含了西欧语言中使用的所有字符,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等等。在 Latin1 编码中,每个字符使用一个 8 位(即一个字节)的编码,可以表示 256 种不同的字符,其中包括 ASCII 字符集中的所有字符,即 0x00 到 0x7F,以及其他西欧语言中的特殊字符,例如 é、ü、ñ 等等。由于 Latin1 只使用一个字节表示一个字符,因此在存储和传输文本时具有较小的存储空间和较快的速度
下面是jdk11版本中的String类源码,注意和jdk8有所不同
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
}
从
char[]到byte[],最主要的目的是节省字符串占用的内存空间。内存占用减少带来的另外一个好处,就是GC次数也会减少GC,也就是垃圾回收,JVM的时候,会讲。
2、String类的hashCode方法
- "第六,每一个字符串都会有一个 hash 值,这个哈希值在很大概率是不会重复的,因此 String 很适合作为HashMap的键值。
看一下String类中的hashCode方法
java
private int hash; // 缓存字符串的哈希码
public int hashCode() {
int h = hash; // 从缓存中获取哈希码
// 如果哈希码未被计算过(即为 0)且字符串不为空,则计算哈希码
if (h == 0 && value.length > 0) {
char val[] = value; // 获取字符串的字符数组
// 遍历字符串的每个字符来计算哈希码
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i]; // 使用 31 作为乘法因子
}
hash = h; // 缓存计算后的哈希码
}
return h; // 返回哈希码
}
hashCode 方法首先检查是否已经计算过哈希码,如果已经计算过,则直接返回缓存的哈希码。否则,方法将使用一个循环遍历字符串的所有字符,并使用一个乘法和加法的组合计算哈希码。
这种计算方法被称为"31 倍哈希法"。计算完成后,将得到的哈希值存储在 hash 成员变量中,以便下次调用 hashCode 方法时直接返回该值,而不需要重新计算。这是一种缓存优化,称为"惰性计算"。
31 倍哈希法(31-Hash)是一种简单有效的字符串哈希算法,常用于对字符串进行哈希处理。该算法的基本思想是将字符串中的每个字符乘以一个固定的质数 31 的幂次方,并将它们相加得到哈希值。具体地,假设字符串为 s,长度为 n,则 31 倍哈希值计算公式如下:
java
H(s) = (s[0] * 31^(n-1)) + (s[1] * 31^(n-2)) + ... + (s[n-1] * 31^0)
其中,s[i]表示字符串 s 中第 i 个字符的 ASCII 码值,
^表示幂运算。31 倍哈希法的优点在于简单易实现,计算速度快,同时也比较均匀地分布在哈希表中。
我们可以通过一下方法进行模拟String的hashCode方法
java
public class HashCodeExample {
public static void main(String[] args) {
String text = "浩妹";
int hashCode = computeHashCode(text);
System.out.println("字符串 \"" + text + "\" 的哈希码是: " + hashCode);
System.out.println("String 的 hashCode " + text.hashCode());
}
public static int computeHashCode(String text) {
int h = 0;
for (int i = 0; i < text.length(); i++) {
h = 31 * h + text.charAt(i);
}
return h;
}
}
结果
java
字符串 "浩妹" 的哈希码是: 867758096
String 的 hashCode 867758096
3、String类中的substring方法
String 类中还有一个方法比较常用 substring,用来截取字符串的,来看源码。
java
public String substring(int beginIndex) {
// 检查起始索引是否小于 0,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 计算子字符串的长度
int subLen = value.length - beginIndex;
// 检查子字符串长度是否为负数,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 如果起始索引为 0,则返回原字符串;否则,创建并返回新的字符串
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
substring 方法首先检查参数的有效性,如果参数无效,则抛出 StringIndexOutOfBoundsException 异常(后面会细讲)。接下来,方法根据参数计算子字符串的长度。如果子字符串长度小于零,也会抛出 StringIndexOutOfBoundsException 异常。
如果 beginIndex 为 0,说明子串与原字符串相同,直接返回原字符串。否则,使用 value 数组(原字符串的字符数组)的一部分 new 一个新的 String 对象并返回。
下面是几个使用 substring 方法的示例:
提取字符串中的一段子串:
String str = "Hello, world!";
String subStr = str.substring(7, 12); // 从第7个字符(包括)提取到第12个字符(不包括)
System.out.println(subStr); // 输出 "world"
提取字符串中的前缀或者后缀
java
String str = "Hello, world!";
String prefix = str.substring(0, 5); // 提取前5个字符,即 "Hello"
String suffix = str.substring(7); // 提取从第7个字符开始的所有字符,即 "world!"
处理字符串中的空格和分隔符
java
String str = " Hello, world! ";
String trimmed = str.trim();
String[] words = trimmed.split("String str = " Hello, world! ";
String trimmed = str.trim(); // 去除字符串开头和结尾的空格
String[] words = trimmed.split("\\s+"); // 将字符串按照空格分隔成单词数组
String firstWord = words[0].substring(0, 1); // 提取第一个单词的首字母
System.out.println(firstWord); // 输出 "H"
处理字符串的数字和符号
java
String str = "1234-5678-9012-3456";
String[] parts = str.split("-"); // 将字符串按照连字符分隔成四个部分
String last4Digits = parts[3].substring(1); // 提取最后一个部分的后三位数字
System.out.println(last4Digits); // 输出 "456"
总之,substring 方法可以根据需求灵活地提取字符串中的子串,为字符串处理提供了便利。
4、String类的indexOf方法
indexOf 方法用于查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引。来看该方法的源码:
java
/*
* 查找字符数组 target 在字符数组 source 中第一次出现的位置。
* sourceOffset 和 sourceCount 参数指定 source 数组中要搜索的范围,
* targetOffset 和 targetCount 参数指定 target 数组中要搜索的范围,
* fromIndex 参数指定开始搜索的位置。
* 如果找到了 target 数组,则返回它在 source 数组中的位置索引(从0开始),
* 否则返回-1。
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
// 如果开始搜索的位置已经超出 source 数组的范围,则直接返回-1(如果 target 数组为空,则返回 sourceCount)
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
// 如果开始搜索的位置小于0,则从0开始搜索
if (fromIndex < 0) {
fromIndex = 0;
}
// 如果 target 数组为空,则直接返回开始搜索的位置
if (targetCount == 0) {
return fromIndex;
}
// 查找 target 数组的第一个字符在 source 数组中的位置
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
// 循环查找 target 数组在 source 数组中的位置
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
// 如果 source 数组中当前位置的字符不是 target 数组的第一个字符,则在 source 数组中继续查找 target 数组的第一个字符
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
// 如果在 source 数组中找到了 target 数组的第一个字符,则继续查找 target 数组的剩余部分是否匹配
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
// 如果 target 数组全部匹配,则返回在 source 数组中的位置索引
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
// 没有找到 target 数组,则返回-1
return -1;
}
查找子字符串的位置
java
String str = "Hello, world!";
int index = str.indexOf("world"); // 查找 "world" 子字符串在 str 中第一次出现的位置
System.out.println(index); // 输出 7
查找字符串中某个字符的位置
java
String str = "Hello, world!";
int index = str.indexOf(","); // 查找逗号在 str 中第一次出现的位置
System.out.println(index); // 输出 5
查找子字符串的位置(从指定位置开始查找)
java
String str = "Hello, world!";
int index = str.indexOf("l", 3); // 从索引为3的位置开始查找 "l" 子字符串在 str 中第一次出现的位置
System.out.println(index); // 输出 3
查找多个子字符串
java
String str = "Hello, world!";
int index1 = str.indexOf("o"); // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1); // 输出 4
System.out.println(index2); // 输出 8
5、String类的其他方法
比如说 length() 用于返回字符串的长度
比如说 isEmpty() 用于判断字符串是否为空
比如说 charAt() 用于返回指定索引处的字符
比如说 valueOf() 用于将其他类型的数据转换为字符串
java
String str = String.valueOf(1223); //将整数类型1223转换为字符串类型
valueOf 方法的背后其实调用的是包装器的toString方法, 比如说整数转为字符串调用的就是Integer类的toString方法
java
public static String valueOf(int i) {
return Integer.toString(i);
}
而 Integer 类的 toString 方法又调用了 Integer 类的静态方法
toString(int i):
java
public static String toString(int i) {
// 最小值返回 "-2147483648"
if (i == Integer.MIN_VALUE)
return "-2147483648";
// 整数的长度,负数的长度减 1
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
// 把整数复制到字符数组中
char[] buf = new char[size];
// 具体的复制过程
getChars(i, size, buf);
// 通过 new 返回字符串
return new String(buf, true);
}
- 比如说 getBytes() 用于返回字符串的字节数组,可以指定编码格式
java
String str = "浩妹";
System.out.println(Arrays.toString(text.getBytes(StandardCharsets.UTF_8)));
- 比如说 trim() 用于去除返回字符串两侧的空白字符
java
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
- 比如说
toCharArray()用于将字符串转换为字符数组。
java
String str = "浩妹";
char[] chars = str.toCharArray();
System.out.println(Arrays.toString(chars));