String类面试题
String类的面试题,往往是面试的开端,如果String类的面试题都没有答好,那么给面试官的第一印象并不太好了
String 是如何实现的?它有哪些重要的方法?
String类的内部组成
以JDK1.8为例,发现String类实质是一个字符数组
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
//字符串的值
private final char value[];
//字符串的哈希码
private int hash;
//...
}
- 多构造方法
String类有4个构造方法,其中容易忽略StringBuffer
和StringBuilder
为参数的构造方法
//以String字符串为参数的构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//以char[],字符数组为参数构造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//StringBuffer,为参数的构造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
//StringBuilder,为参数的构造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
equals() 比较两个字符串是否相等
public boolean equals(Object anObject) {
//对象引用相同直接返回 true
if (this == anObject) {
return true;
}
//判断需要对比的值是否为 String 类型,如果不是则直接返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
//把两个字符串都转换为 char 数组对比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循环比对两个字符串的每一个字符
while (n-- != 0) {
// 如果其中有一个字符不相等就 true false,否则继续对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String类重写了Object类的equals()
方法,该方法传入一个Object类型的对象,在比较时,先通过instanceof
判断是否为String
类型,如果不是则直接返回false,如果是,则会循环比较2个字符串的每一个字符,当所有字符都相等时,返回true
还有一个和equals()
方法类型的equalsIgnoreCase()
方法,它用于忽略字符大小写的比较
compareTo() 比较两个字符串
compareTo()用于比较2个字符串,返回值为int
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
//获取到两个字符串长度最短的那个 int 值
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;
}
compareTo()
方法中,会循环比较2个字符串的所有字符,当有一个字符不相等时,则返回2个字符的相减值c1 - c2
,如2个字符串分别存储的是1和2,那么返回值为-1,如果存储的值是1和1,那么返回值为0,如果存储的是2和1,那么返回值为1
还有一个和compareTo()
方法类似的compareToIgnoreCase()
方法,用于忽略大小写的字符比较
综上所述,可以看出,compareTo()
和equals()
都用于比较字符串,但它们有2点不同
- equals()方法可以接收一个Object类型的参数,而compareTo()则只能接收一个String类型的参数
- equals()返回值为 Boolean,而compareTo()的返回值则为int
它们都可以用于比较2个字符串的值,equals()
方法返回true,或者compareTo()
方法返回0时,2个字符串完全相同
String类的其他重要方法
- indexOf():查询字符串首次出现的下标位置
- lastIndexOf():查询字符串最后出现的下标位置
- contains():查询字符串中是否包含另一个字符串
- toLowerCase():把字符串全部转换成小写
- toUpperCase():把字符串全部转换成大写
- length():查询字符串的长度
- trim():去掉字符串首尾空格
- replace():替换字符串中的某些字符
- split():把字符串分割并返回字符串数组
- join():把字符串数组转为字符串
为什么 String 类型要用 final 修饰?,== 和 equals 的区别是什么?
== 和 equals 的区别
== 对于基本数据类型来说,比较的是值
是否相等,而对于引用类型来讲,比较的是对象的内存地址,而Object的equals()
方法,使用的就是==进行比较
public boolean equals(Object obj) {
return (this == obj);
}
所以默认对象的equals()
方法比较的就是对象的地址值,而子类可以复写该方法,例如String
类就复写了equals()
方法,比较2个字符串的值是否相等
public boolean equals(Object anObject) {
//对象引用相同直接返回 true
if (this == anObject) {
return true;
}
//判断需要对比的值是否为 String 类型,如果不是则直接返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
//把两个字符串都转换为 char 数组对比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//循环比对两个字符串的每一个字符
while (n-- != 0) {
//如果其中有一个字符不相等就 true false,否则继续对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
final 修饰的好处
String类的源码可以看出,String类是final修饰的不可变类
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
//...
}
主要原因有2点,一个是安全,另外一个是高效,理由如下:
- 安全
- 在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题
- 高效
- 它能够缓存结果,当你在传参时不需要考虑谁会修改它的值,如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失
以JVM常量池来讲
String s1 = "java";
String s2 = "java";
只有字符串时不可变时,我们才能够实现字符串常量池,字符串常量池可以缓存字符串,提高程序的运行效率
如果String类是可变的,当我们修改s1的值后,s2也跟着变了,这样和我们预期就不相符了,因此也没办法实现字符串常量池了
String 和 StringBuilder、StringBuffer 的区别
因为String类是不可变类,所以在字符串拼接时,性能会很低,这时我们需要使用StringBuffer,它提供了append
和insert
,用于字符串拼接,它使用synchronized
关键字来保证线程安全
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
因为它使用synchronized
关键字来保证线程安全,所以它的性能不是很高,于是在JDK 1.5
中,提供了StringBuilder
,它同样提供了append
和insert
,但它没有使用synchronized
关键字来修饰,因此性能优于StringBuffer
,所以在非并发环境下可以使用StringBuilder
String 的 intern() 方法有什么含义?
String类的intern()
方法是一个native方法
public native String intern();
它会先从字符串常量池中查找,如果存在,则直接返回该引用,如果不存在,则创建该字符串,再放到字符串常量池中,再返回
String 和 JVM
String的常见创建方式有2种,new String()和字面量赋值,字面量赋值会先去常量池中查找是否有该值的字符串,如果有则直接把引用地址指向该值,如果没有则先会在常量池中创建,再把引用指向该值。而new String()是直接在堆上创建一个字符串对象,然后再去常量池中查找是否存在该值,不存在则在常量池中创建,再把引用的值指向该字符串
//堆上直接创建一个String类对象
String s1 = new String("Java");
//从常量池中查询,找得到则使用,找不到则创建,再把字符串拷贝到常量池,它和s1不是同一个对象
String s2 = s1.intern();
//字面量声明,从常量池中查找,找到s2,所以s2和s3是同一个对象
String s3 = "Java";
System.out.println(s1 == s2); //false
System.out.println(s2 == s3); //true
除此以外,JVM还会对字符串做优化,在使用+号拼接字符串时,会直接把字面量组合成新的值
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2);//true
结果是true,因为编译器在编译时做了优化,直接把"ja" + "va"编译成了"java",所以s1实际是"java",然后存放在了字符串常量池中,而s2再去常量池中找时,则找到s1,所以他们是同一个对象