文章目录
String 类的特点–以final修饰的类
1> 以 JDK1.8 版本来看,String 内部实际存储结构为 char 数组,部分源码:
public final class String implements java.io.Serializable,Comparable<String>,CharSequence {
// 用于存储字符串
private final char value[];
// 缓存字符串的 hashcode
private int hash;
...
}
以上 String 是被final修饰的,是不可被继承的类,为什么要这样设计呢?
Java 语言之父 James Gosling 的回答是:
- 会更倾向于使用 final,因为它能够缓存结果,传参时不需要考虑谁会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。
- 迫使 String 类设计成不可变的另一个原因是安全,在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,这是迫使 String 类设计成不可变类的一个重要原因。
总结来说,使用 final 修饰的第一个好处是安全;第二个好处是高效,以 JVM 中的字符串常量池来举例,如下两个变量:
String s1 = "java";
String s2 = "java";
只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示:
如果 String 是可变的,那当 s1 的值修改之后,s2 的值也跟着改变了,这样就和我们预期的结果不相符了,因此也就没有办法实现字符串常量池的功能了。
2> 多构造方法
// 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(StringBuilfer builder) {
this.value = Arrays.copyOf(builder.getValue(),builder.length());
}
其中,比较容易被我们忽略的是以 StringBuffer 和 StringBuilder 为参数的构造函数,因为这三种数据类型,我们通常都是单独使用的,所以需要关注。
equals() 和 == 比较字符串
1> equals() 方法
在 Object类中 equals() 默认比较的是对象的地址值是否相同,意义不大,一般子类都会重写此方法
public boolean equals(Object obj){
return this == obj;
}
String类重写了 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) {
// 如果其中有一个字符不相等就 false,否则继续对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
重写后的 equals() 方法需要接收一个 Object 类型的参数值,在比较时会先通过 instanceof 判断是否是 String 类型,如果不是则会返回 false ,instanceof 的使用如下:
Object oString = "123";
Object oInt = 123;
System.out.println(oString instanceof String); // 返回 true
System.out.println(oInt instanceof String); // 返回 false
当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。
还有一个和 equals() 比较类似的方法 equalsIgnoreCase(), 忽略字符串的大小写进行字符串比较
2> == 对于基本数据类型来说,就是比较 “值” 是否相等的;而对于引用类型来说,是用于比较引用地址值是否相同,前面我们知道 Object 中的 equals() 方法
public boolean equals(Object obj) {
return (this == obj);
}
可以看出,Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的字面值是否相等。
CompareTo() 比较两个字符串
compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值,源码如下:
public int compareTo(String anotherString) {
int len1 = value.legnth;
int len2 = anotherString.value.legnth;
// 获取到两个字符串长度最短的那个 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;
}
- 取得value数组的长度,取得value数组里面的元素, 如果两个串长一样,则返回值 0;如果不一样:基于字符串中每个字符的Unicode值比较若此字符串按字典顺序小于字符串参数,则返回一个小于 0 的值;若此字符串按字典顺序大于字符串参数,则返回一个大于 0 的值。
- 可以看出当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同。
String 和 StringBuilder、StringBuffer 的区别
由于String 类型是不可变的,所以在字符串拼接的时候如果使用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 来保证了线程安全,所以性能不好,于是在 JDK1.5 的时候有了StringBuilder,它同样提供了append和insert的拼接方法,但没有使用 synchronized,所以性能好,在并发操作的环境下可以使用。
String 和 JVM
String 常见的创建方式有两种,new String() 的方式和直接赋值,直接赋值的方式会先去字符串常量池中查找是否已经有此值,有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而new String() 的方式一定会先再堆上创建一个字符串对象,然后再去常量池查询此字符串的值是否已经存在,如果不存在会先再常量池中创建此字符串,然后把引用的值指向此字符串。
String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
编译器对字符串拼接的优化
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2); // true
虽然 s1 拼接了多个字符串,但对比的结果却是 true,通过发编译工具查看结果如下:
Compiled from "StringDemo.java"
public class StringDeno {
public StringDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Java
2: astore_1
3: ldc #2 // String Java
5: astore_2
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
22: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 22
}
从反编译代码 #2 可以看出,代码 “Ja” + “va” 被直接编译成立 “Java”,因此 s1 == s2 的结果才是true。但是值得注意的是,常量在编译期就被确定了,所以编译器可以这样优化,但如果拼接的是一个变量,结果就不一样了。
int a = 0;
String s1 = "hello" + a;
String s2 = "hello0";
System.out.println(s1 == s2); // false
变量是在运行期才确定的,所以在拼接时候在堆上会产生一个临时对象保存在变量 s1中。
事实上,在做 + 拼接操作,编译器都有一定的优化,首先我们先理解一面一句代码的含义:
eg:
String s1 = new String("hel") + new String("lo");
s1 = intern();
String s2 = new StringBuilder("hel").append("lo").toString();
System.out.println(s1 == s2.intern()); // false
1> String s1 = new String(“hel”) + new String(“lo”);
常量池生成的对象:hel 、lo。注意常量池并没有生成hello对象
堆生成的对象:new String(“hel”)、new String(“lo”)、拼接操作生成的new String(“hello”),并且此时的heelo在堆里面
2> s1 = intern();
字符串常量池中,创建一个引用,指向s1指向的对象new String(“hello”) 即:获取或创建一个字符串对象或引用
3> String s2 = new StringBuilder(“hel”).append(“lo”).toString();
s2 的引用地址指向堆里面再次生成的 new String(“hello”)
4> s2.intern()
s2 的引用地址指向第 2> 步中 s1.intern() 返回的变量(池中"hello"串不存在入池并返回引用,存在则直接返回引用)
- 关于intern() 方法任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
public String intern()
返回值:一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。 - 对于拼接操作,编译器会默认优化成 new String().append().toString()