String底层详解(包括字符串常量池)

String a = “abc”; ,说一下这个过程会创建什么,放在哪里?

JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。

new String(“abc”) 是去了哪里,仅仅是在堆里面吗?

由于String本身的不可变性(后续分析),在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。接着使用new关键字,在堆内存中创建一个String对象(对象保存在堆中),堆中对象的数据会指向常量池中abc字符串的引用。

如果再通过new String(“abc”)创建一个字符串对象,此时由于字符串常量池已经存在abc,所以只需要在堆内存中创建一个String对象即可。

intern 方法有什么作用?

String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
  • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

具体案例举例及分析

// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true


String str1 = “ab”;
String str2 = “a” + “b”;
//变量a和b都是常量字符串,其中b这个变量,在编译时,由于不存在可变化的因素,所以编译器会直接把变量b赋值为ab(这个是属于编译器优化范畴,也就是编译之后,b会保存到Class常量池中的字面量)。
//对于字符串常量,初始化a时, 会在字符串常量池中创建一个字符串ab并返回该字符串常量池的引用。
//对于变量b,赋值ab时,首先从字符串常量池中查找是否存在相同的字符串,如果存在,则返回该字符串引用。
System.out.print(str1 == str2);//true


String str3 = new String("Hello World"); //创建对象,对象指向常量池的"Hello World“
String str4 = str.intern();//str4指向常量池的"Hello World“
System.out.println(str3 == str4);//false,引用的地址不一样,一个在堆中,一个在常量池中


String str5 = new String("Hello World") + new String("!");//将"Hello World"和"!"都放在常量池,但常量池没有"Hello World!"
String str6 = str5.intern();//在常量池创建"Hello World!",即把str5放入常量池
System.out.println(str5 == str6);//true

StringBuilder sb = new StringBuilder().append(new String("Hello World")).append(new String("!")); 
String str7 = sb.toString();  
String str8 = str.intern();//在常量池创建"Hello World!",即把str7放入常量池
System.out.print(str == str1);//true


String s1 = "a";  
String s2 = "b";  
String s3 = "ab";  
String s4 = s1 + s2;  //等于StringBuilder sb = new StringBuilder().append(s1).append(s2);
System.out.println(s3 == s4);//false,一个在常量池,一个是StringBuilder的对象调用toString()方法

String的equals方法源码

public boolean equals(Object anObject) {
        if (this == anObject) {//先判断地址,地址一样一定相同
            return true;
        }
        if (anObject instanceof String) {//判断是不是String类型
            String anotherString = (String)anObject;//是的话强转
            int n = value.length;
            if (n == anotherString.value.length) {//判断长度是否一致,不一致直接false
                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;
    }

String 类型的变量和常量做“+”运算时发生了什么?(两个字符串相加的底层是如何实现的)

1.如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量。
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

引用的值在程序编译期是无法确定的,编译器无法对其进行优化

2.如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。

字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

String,StringBuilder,StringBuffer有什么区别

1.可变性

String 是不可变的(后面会详细分析原因)。

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 finalprivate 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
  	//...
}

2.线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

3.性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

你可能感兴趣的:(jvm,java,面试)