String类应该是我们用的最多的一个类了。String类的方法很多,每个都写太麻烦了,也没有必要。我就捡我觉得比较重要的和应该注意的点写了。
1、定义
public final class String
implements java.io.Serializable, Comparable, CharSequence
可以看到有final修饰,说明String是不可变的,实现了Serializable、Comparable接口,说明String可以序列化,可以进行比较,实现了CharSequence接口,说明String是一个有序字符的集合。
2、字段
/** The value is used for character storage. */
// 存储字符串的字符数组
private final char value[];
/** Cache the hash code for the string */
// 缓存字符串的哈希码
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
// 序列化的UID
private static final long serialVersionUID = -6849794470754667710L;
// 忽略大小写的比较器
public static final Comparator CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
/**
* 很多人不知道这字段是干嘛的,其实和transient一样都是控制字段的序列化的,
* 只不过transient修饰的字段不被序列化,而serialPersistentFields数组里
* 的字段需要序列化,如果某个字段在serialPersistentFields数组里,且被transient
* 修饰,则以serialPersistentFields为准,serialPersistentFields优先级高于
* transient。
* 这里是一个空数组,所以String类的所有字段都不会序列化
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
3、构造方法
构造方法有很多,就不写了,贴个图吧😁
4、equals方法
String类里的equals方法:
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;
}
对比的是每个字符是否相等,还有一个equalsIgnoreCase方法,是忽略大小写对比每个字符是否相等
5、hashCode方法
String类里的hashCode方法:
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;
}
算法不难,就是for循环里的h = 31 * h + val[i],就是这个公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
其中s[i]代表字符串的第i个字符,n代表字符串的长度
我们注意到这里有个31,为什么选31不选别的数呢?这里直接给出原因,具体可以参考这篇文章
31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一
31可以被 JVM 优化,31 * i = (i << 5) - i。因为移位运算比乘法运算更快(PS:hashMap的length为2的整数次方也是基于性能的考虑,hash(key)%length=hash(key)&(length-1),&比%快)
6、toString方法
String类里的toString方法:
public String toString() {
return this;
}
返回字符串本身
7、字符串常量池
为了字符串的复用,减少空间浪费,JVM会维护一个字符串常量池,常量池中的字符串不可重复,并且对长度有限制不能超过65535个字节(注意这里是字节,而不是字符)。有的同学可能有疑问,我有的类里面的
String变量有几十万个字符长,为啥不报错呢??因为那些String变量没放入常量池,有兴趣的可以参考我的这篇博客:String的长度限制
那什么样的字符串会放入常量池呢?
字面量创建的字符串String str1 = "abc"或者由字符串常量拼接出的字符串String str2 = "abc"+"def"会直接放入常量池(当然前提是常量池中不存在该字符串)
调用intern方法,如果常量池中没有该对象,则将该对象添加到池中,并返回池中的引用
对于字符串常量池的详细介绍可以参考我的这篇博客:字符串常量池
8、intern方法
String类里的intern方法:
public native String intern();
可以看到这是一个本地方法。当一个String对象调用intern()方法时,如果常量池中已经有同样的字符串了,则返回该对象的引用(在堆中就返回堆中的引用,在池中就返回池中的引用),如果没有,则将该对象添加到池中,并返回池中的引用。
做个小测试:
public class Test {
public static void main(String[] args) {
String s1 = "liu"; // 这里将"liu"放入常量池
String s2 = s1.intern(); // 因为s1本身就在常量池所以直接返回s1的引用
System.out.println(s1 == s2); // true
String s3 = new String("whut"); // 这里会在堆上创建对象
String s4 = s3.intern(); // 因为s3在堆上,不在常量池中,所以将"whut"添加到池中,此时s4为池中"whut"的引用
System.out.println(s3 == s4); // s3在堆上,s4在常量池中,故false
String s5 = "whut"; // 上面已经将"whut"添加到池中了
System.out.println(s5 == s4); // true
}
}
结果:
9、String 真的不可变吗
what?被final修饰难道可变?
final只是限制引用不能改变,就是限制内存地址不变,但限制不了我把地址上的内容改变。(。・∀・)ノ我们知道String真正的内容放在value数组里,同样value也是被final修饰的,但依然限制不了我改
value数组里的内容。通过反射就能改,我们试一下:
public class Test {
public static void main(String[] args) throws Exception{
String s = "hello world";
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
char[] value1 = (char[])value.get(s);
value1[0] = 'H';
System.out.println(s);
}
}
结果:
可见s被改变了,不过基本不会去改String的值,如果面试官问你String是否可变,你答出这点来是不是就稳了,哈哈哈。