文章目录
前言
String ,即字符串类,是 Java 最重要的几个类之一。相信大家在平时的学习或工作中经常与 String 打交道,对 String 的一些用法都是了然于胸了,那么,大家是否有思考过一个问题,String 到底是怎么实现的呢?不会没关系,接下来我会带领大家脱下 String 的伪装,去看 String 最本质的东西。
定义
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
显然,由 final 修饰的 String 是不可变的,Serializable 接口表示可以序列化,Comparable 接口用于比较两个字符串的大小,而 CharSequence 接口表示是一个有序字符的集合。
字段属性
private final char value[];
private int hash; // Default to 0
很明显,String 的底层是一个字符数组,value 用于存储字符串,hash 则缓存了该字符串的哈希码。
构造方法
我们来看看 String 的构造方法,可以发现我们创建一个 String 对象的方法其实还是相当多的。
下面我们来看下部分构造函数的源码,其实这部分都不难,大家看代码都能看明白的。
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
一些重要的方法
length()
length 方法返回字符串的长度,从源码可见,实际上返回的便是 value 字符数组的长度。
public int length() {
return value.length;
}
isEmpty()
根据字符数组的长度进行判空。
public boolean isEmpty() {
return value.length == 0;
}
charAt(int index)
该方法用于获取字符串指定位置的字符,它会判断你传入的位置是否符合规范,符合则返回字符,否则抛出异常。
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
equals(Object anObject)
循环遍历两个字符数组,只有当数组长度与每一个字符均相同时才返回 true。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
compareTo(String anotherString)
按字母顺序比较两个字符串,若两个字符串某位置不同,会返回这一位置的字符 Unicode 值之差,在满足上面的情况下,会返回两个字符串长度之差。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
hashCode()
哈希码的算法很简单,大概可以抽象为这个公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
indexOf(int ch, int fromIndex)
该方法返回指定字符第一次出现的索引。
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
//一个char占用两个字节,如果ch小于2的16次方,绝大多数字符都在此范围内
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
//当字符大于2的16次方时,处理的少数情况,该方法会首先判断是否是有效字符,然后依次进行比较
return indexOfSupplementary(ch, fromIndex);
}
}
substring(int beginIndex)
该方法返回一个从索引 beginIndex 开始到结尾的子字符串。首先它会判断范围,若范围有问题会直接抛出异常,然后判断开始索引是否为 0,是的话直接返回 this 就好了,否则就新建一个新范围的 String 对象给它。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
concat(String str)
该方法用于连接字符串。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
//创建一个新的字符数组,长度为原字符串和要拼接的字符串之和,前面填充原字符串,后面为空
char buf[] = Arrays.copyOf(value, len + otherLen);
//把拼接的字符串放入新字符串后面为空的位置
str.getChars(buf, len);
return new String(buf, true);
}
replace(char oldChar, char newChar)
该方法用于做字符替换。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
valueOf(Object obj)
该方法是一个静态方法,用于将其他对象转化为字符串对象,由于它首先会对对象进行判空,所以该方法还是比较安全的。
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
如何创建字符串
参考这篇博客:深入理解Java中的String(大坑)
总结
首先我们先来讨论下 String 的不可变问题,String 真的不可变嘛?我们通过前面的学习,知道字符串对象底层实现是一个字符数组,这是被 final 修饰的,但这只能保证引用不变,value 指向的数组才是真实数据,只要我们能对其操作,仍然能改变数据,即使被声明为私有的,我们也能通过反射来进行改变。
那为什么 String 是不可变的呢?我们可以从性能以及安全两方面来进行考虑。
从性能的角度来看,只有 String 不可变,字符串常量池才有意义,它可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,节约堆内存。
从安全的角度来看,首先,若 String 可变,容易引发安全问题,例如用户名、密码等数据的传入,由于 String 的不可变,它们的值自然是不变的,否则可能会被黑客攻击,改变相应的值,从而造成安全漏洞。然后,String 的不可变在多线程环境下不会引发线程的问题,保证了线程安全。最后,String 的不可变保证了 HashCode 的不可变,针对某些容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。