Java Version : 主流版本JDK 8
看到了吧 , 底层存储是 char 数组
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// the value is used for character storage 存储字符串的值
private final char value[];
// Cache the hash code for the string 缓存字符串的 hash code
private int hash; // Default to 0
// ......
}
挑几个比较重要的
// 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());
}
这里需要提一下的是: 以 StringBuffer 和 StringBuilder 为参数的构造函数容易被忽略,因为String 、 StringBuffer、StringBuilder 这三种数据类型, 通常都是单独使用的哇。 知道就行,反正平常也不这么写
还有其他构造函数 ,大家可以自行看一下
比较两个字符串是否相等
来看下源码
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) { // 循环比对两个字符串的每一个字符
if (v1[i] != v2[i]) // 如果其中有一个字符不相等就直接返回false,否则继续对比,直接到结束
return false;
i++;
}
return true;
}
}
return false;
}
equals() 是String 类型重写的 Object 中的 方法,Object#equals() 方法需要传递一个 Object 类型的参数值所以才有了上面的instanceof 类型判断 。 当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。
【Object#equals()】
public boolean equals(Object obj) {
return (this == obj); // 仅判断的对象引用,即比较的是对象在内存中的地址
}
【instanceof 用法】
Object a= "123";
Object b= 123;
System.out.println(a instanceof String); // true
System.out.println(b instanceof String); // false
另外还有一个 equalsIgnoreCase(), 忽略字符串的大小写之后进行字符串对比。
比较两个字符串
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;
}
从源码总可以看到compareTo() 方法会循环对比所有的字符,当两个字符串中有任意一个字符不相同时,则 return c1 - c2
。
举个例子
“53334433”.compareTo(“3”) ----> 2 【取最小长度,第一个字符 5 和 3 比,转成char 比较, 不相等 返回 5 - 3 = 2】
再来个例子: 两个字符串分别存储的是 1 和 2,返回的值是 -1;如果存储的是 1 和 1,则返回的值是 0 ,如果存储的是 2 和 1,则返回的值是 1。
还有个compareToIgnoreCase 忽略大小写后比较两个字符串。
可以看出 compareTo() 方法和 equals() 方法都是用于比较两个字符串的,但它们有两点不同:
它们都可以用于两个字符串的比较,当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同
从源码中可以知道String是final修饰的?
为啥子嘞?
高司令以前回答过: 他会更倾向于使用 final,因为它能够缓存结果,当你在传参时不需要考虑谁会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。
String 类设计成不可变的另一个原因是安全,当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,这是迫使 String 类设计成不可变类的一个重要原因。
总之,使用 final 修饰的第一个好处是安全;第二个好处是高效
我们以JVM中的常量池来举个例子
String s1 = "java";
String s2 = "java";
只有字符串是不可变时,我们才能实现字符串常量池。
字符串常量池可以为我们缓存字符串,这样的话不用每次都去开辟一块内存地址存放,自然就提高了运行效率。
如果String是可变的,那字符串常量池就歇菜了。。。。。
【==】
Object#equals() 其实就是 ==
public boolean equals(Object obj) {
return (this == obj);
}
String#equal这是重写了父类Object的equals方法,把它修改成了比较两个字符串的值是否相等,分析如上。
简单来说:
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 insert(int offset, String str) {
toStringCache = null;
super.insert(offset, str);
return this;
}
因为它使用了 synchronized 来保证线程安全,所以性能不是很高。
于是在 JDK 1.5 就有了 StringBuilder,它同样提供了 append 和 insert 的拼接方法,但它没有使用 synchronized 来修饰,因此在性能上要优于 StringBuffer,所以在非并发操作的环境下可使用 StringBuilder 来进行字符串拼接。
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public StringBuilder insert(int offset, String str) {
super.insert(offset, str);
return this;
}
当然了,append 和 insert的方法入参有很多,这里仅仅列举出了一个,主要是让你看下 synchronized实现上的区别。
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
JDK 1.7 之后把永久代代换成的元空间,把字符串常量池从方法区移到了 Java 堆上
除此之外编译器还会对 String 字符串做一些优化,例如以下代码
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2);
输出 true
javap -c 反汇编看一下
从编译代码 #2 可以看出,代码 “Ja”+“va” 被直接编译成了 “Java” ,因此 s1==s2 的结果才是 true,这就是编译器对字符串优化的结果。