Java基础疑点解析 String(一系列难点)
1、String底层源码分析
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
private final byte coder;
private int hash; // Default to 0
private boolean hashIsZero; // Default to false;
@java.io.Serial
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
@java.io.Serial
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public String() {
this.value = "".value;
this.coder = "".coder;
}
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
上面是String底层源码,可以看出:
1、String的不变性指的是类值一旦被初始化,就不能被改变。我们从源码出发,可以看到String类由final修饰,即类不能被继承,String中的方法不能被继承重写。
2、String类实现了Serializable,Comparable,CharSequence 接口
3.string 源码中包含一个 不可变(final修饰的)的byte数组来存放字符串数据 private final byte value[];, 说明String它是一个不可变字符串,底层是一个char(byte)类型的数组。
2、String被设计成不可变的主要目的
String被设计成不可变的主要目的是为了安全和高效。
第一,安全方面。String中有些方法比较底层,如果被继承并重写方法会有隐患;同时不可变对象一定是线程安全的,可以在线程之间共享,无需同步。
第二,设计者希望用户用到的String就是JDK中的String,不希望被继承和重写,确保String可控。
第三,性能方面。基于不变性,编译器和JVM可以对String进行优化,提高性能。
String是不可变的,其hashcode值也是固定不变的。String通过hash来缓存当前字符串的hashcode值,保证只需要计算一次。源码如下:
private int hash; // Default to 0
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;
}
方法中首先对hash属性值做了判断,如果计算过hashCode,结果会被缓存到hash属性,不需要重复计算。
**3、String常量池、intern方法
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class StringTest {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #7 会把 a 符号变为 "a" 字符串对象
// ldc #9 会把 b 符号变为 "b" 字符串对象
// ldc #11 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //底层是 new StringBuilder().append("a").append("b").toString() new String("ab")
System.out.println(s3 == s4);//flase
System.out.println(s3.equals(s4));//true
}
}
输出的是 false,true,因为s3被分配到字符串常量池中,而s4是最后new String(“ab”),它是被分配到堆内存中。两者尽管数据一样,但内存地址不一样,是不同的对象。所以为false。而equals方法比较的是两个字符串“是不是包含相同的数据”,所以是true。
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class StringTest {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5="a"+"b";
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
}
}
输出的是false,true,因为这是两个常量”a”、”b”进行拼接,所以javac在编译期间的优化,结果已经在编译期确定为“ab”,它会到串池中直接查询”ab”,所以s3和s5是同一个对象,而s4是两个变量进行拼接,所以它最后是在堆内存进行创建对象。
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class StringTest {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5="a"+"b";
//System.out.println(s3 == s4);
//System.out.println(s3 == s5);
String s = s4.intern();
System.out.println(s3==s);//true
System.out.println(s3==s4);//false
}
}
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class StringTest {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
//System.out.println(s3 == s4);
//System.out.println(s3 == s5);
String s = s4.intern();
String s3 = "ab";
System.out.println(s3==s);//true
System.out.println(s3==s4);//true
}
}
使用intern()方法,jdk1.8以后,会将将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,无论放没放入,都会把串池中的对象返回。
总结:StringTable 特性
常量池中的字符串仅是符号,第一次用到时才变为对象,利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
3、String,StringBuffer,StringBuilder区别
String 跟其他两个类的区别是
String是final类型,每次声明的都是不可变的对象,
所以每次操作都会产生新的String对象,然后将指针指向新的String对象。
StringBuffer,StringBuilder都是在原有对象上进行操作
所以,如果需要经常改变字符串内容,则建议采用这两者。
StringBuffer vs StringBuilder
前者是线程安全的,后者是线程不安全的。
线程不安全性能更高,所以在开发中,优先采用StringBuilder.
StringBuilder > StringBuffer > String
参考资料:
https://zhuanlan.zhihu.com/p/58171343
黑马程序员JVM笔记