前言

最近在搞面试题的时候,老是遇到string的问题,容易搞混,就给它整理一下。

String介绍

字符串不变; 它们的值在创建后不能被更改。 字符串缓冲区支持可变字符串。 因为String对象是不可变的,它们可以被共享。

String和==的关系

        String s1 = "愉快的菠萝";
        String s2 = "愉快的";
        String s3 = "菠萝";
        String s4 = "愉快的菠萝";
        String s5 = s2 + s3;
        System.out.println(s1==s4);
        System.out.println((s1=="愉快的"+"菠萝"));
        System.out.println(s1==s5);
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s4));
        System.out.println(System.identityHashCode(s5));

identityHashCode( )
返回与默认方法hashCode()返回的给定对象相同的哈希码,无论给定对象的类是否覆盖了hashCode()。 空引用的哈希码为零。不用string类的hashcode方法是因为string重写了hashcode方法,只要是字符串的值一样,它的hashcode返回也是一样的。
结果

true
true
false
1627674070
1627674070
1360875712

首先我们知道
==是直接比较的两个对象的堆内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的
String作为常量,在常量池中,一个常量只会对应一个地址,另外对于基本数据类型(byte,short,char,int,float,double,long,boolean)来说,他们也是作为常量在方法区中的常量池里面。

  • 然后就解释的通了s1和s4这两个相同的字符串指向同一个地址所以是true。
  • 至于第二个为什么true,是因为+在编译时两边都是字符串常量是会有优化,会给它合并到一起,它也是“愉快的菠萝”这个字符串,所以它是一个地址。
  • 第三个是false,s2+s3 在+两边的是个变量,编译时不知道具体的值,不能优化,而且它会转化为StringBuilder类型通过append方法拼接,可以通过反编译查看,它是一个新的对象所以地址不同
 String s5 = (new StringBuilder()).append(s2).append(s3).toString();

final String

        final String fs1 = "愉快的菠萝";
        final String fs2 = "愉快的";
        final String fs3 = "菠萝";
        final String fs4 = "愉快的菠萝";
        final String fs5 = fs2 + fs3;
        System.out.println(fs1==fs4);
        System.out.println((fs1=="愉快的"+"菠萝"));
        System.out.println(fs1==fs5);
        System.out.println(System.identityHashCode(fs1));
        System.out.println(System.identityHashCode(fs4));
        System.out.println(System.identityHashCode(fs5));

加了final之后,都变成常量,前面两个结果都是一样的,但是第三个却是true了,是因为fs2,fs3都是字符串常量了,编译时也会优化,会合并成一个字符串。

true
true
true
1627674070
1627674070
1627674070

特例

    public static final String fs2 ; // 常量A
    public static final String fs3 ;    // 常量B
    static {
        fs2 = "愉快";
        fs3 = "菠萝";
    }
    public static void main(String[] args) {
        final String fs1 = "愉快的菠萝";
        final String fs4 = "愉快的菠萝";
        final String fs5 = fs2 + fs3;
        System.out.println(fs1==fs4);
        System.out.println((fs1=="愉快的"+"菠萝"));
        System.out.println(fs1==fs5);
        System.out.println(System.identityHashCode(fs1));
        System.out.println(System.identityHashCode(fs4));
        System.out.println(System.identityHashCode(fs5));
    }

fs2和fs3虽然被定义为常量,但是它们都没有马上被赋值。在运算出fs5的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此fs2和fs3在被赋值之前,性质类似于一个变量。那么fs5就不能在编译期被确定,而只能在运行时被创建了。

true
true
false
1627674070
1627674070
1360875712

new String

        String ns1 = new String("愉快的菠萝");
        String ns2 = new String("愉快的");
        String ns3 = new String("菠萝");
        String ns4 = new String("愉快的菠萝");
        String ns5 = ns2 + ns3;
        System.out.println(ns1==ns4);
        System.out.println((ns1=="愉快的"+"菠萝"));
        System.out.println(ns1==ns5);
        System.out.println(System.identityHashCode(ns1));
        System.out.println(System.identityHashCode(ns4));
        System.out.println(System.identityHashCode(ns5));

通过new 出来的字符串,都是不一样的对象,所以地址都是不一样的

false
false
false
1625635731
1580066828
491044090

String和常量池的关系

全局字符串池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

class文件常量池(class constant pool)

我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。

全局字符串池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

class文件常量池(class constant pool)

我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。

常量池的项目类型
每种不同类型的常量类型具有不同的结构,具体的结构本文就先不叙述了,本文着重区分这三个常量池的概念(读者若想深入了解每种常量类型的数据结构可以查看《深入理解java虚拟机》第六章的内容)。

运行时常量池(runtime constant pool)

当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

举个实例来说明一下:


public class HelloWorld {
    public static void main(String []args) {
        String str1 = "abc"; 
        String str2 = new String("def"); 
        String str3 = "abc"; 
        String str4 = str2.intern(); 
        String str5 = "def"; 
        System.out.println(str1 == str3);//true 
        System.out.println(str2 == str4);//false 
        System.out.println(str4 == str5);//true
    }

上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了,
首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值
然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例。
当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同。
str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值、
最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。

总结

  • 全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
  • class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
  • 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。(引用)

String和对象的关系

如题

        String s1 = "愉快的";
        String s2 = "菠萝";
        String s3 = new String("愉快的菠萝");
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s2));
        System.out.println(System.identityHashCode(s3));
        System.out.println(System.identityHashCode("愉快的菠萝"));

一共创建了4个对象,s1,s2,s3再加上“愉快的菠萝”这个字符串对象

1627674070
1360875712
1625635731
1580066828

总结而言就是:

对于 String s = new String(“愉快的菠萝”); 这种形式创建字符串对象,如果字符串常量池中能找到,创建一个String对象;如果如果字符串常量池中找不到,创建两个String对象。

对于 String s = “愉快的”; 这种形式创建字符串对象,如果字符串常量池中能找到,不会创建String对象;如果如果字符串常量池中找不到,创建一个String对象。

        String s = "愉快的" +"菠萝";
        System.out.println(System.identityHashCode("愉快的"));
        System.out.println(System.identityHashCode("菠萝"));
        System.out.println(System.identityHashCode(s));
        System.out.println(System.identityHashCode("愉快的菠萝"));

一共生成了3个对象,s就等于"愉快的菠萝"

1627674070
1360875712
1625635731
1625635731

一共生成了6个对象,在前三个的基础上加上,new String(“愉快的”),new String(“菠萝”),还有new String(“愉快的”) + new String(“菠萝”)是以StringBuilder的形式拼接出来的一个对象

1627674070
1360875712
1625635731
1580066828
491044090
644117698

java.lang.String.intern()

当调用intern方法时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。
由此可见,对于任何两个字符串s和t , s.intern() == t.intern()是true当且仅当s.equals(t)是true 。

       String s1 =new String("愉快的") +new String("菠萝");
       String s2 =new String("ja") +new String("va");
        System.out.println(s1==s1.intern());
        System.out.println(s2==s2.intern());
        System.out.println(System.identityHashCode(s1));
        System.out.println(System.identityHashCode(s1.intern()));
        System.out.println(System.identityHashCode(s2));
        System.out.println(System.identityHashCode(s2.intern()));

s1和s1.intern的地址都是一样的,因为常量池中没有“愉快的菠萝”这个字符串,所以把字符串加入常量池,并返回它的引用,所以他们是一致的。
但是java是不一样的,因为在初始的class中,就已经存在了java这个字符串了,所以intern返回的是“java”这个字符串的引用,和s2这个新生成的对象的地址是不一样的。(来源深入了解java虚拟机)

true
false
1627674070
1627674070
1360875712
1625635731

总结

在文章的最后作者为大家整理了很多资料!包括java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书等等!
全部免费分享给大家,只希望你多多支持!要是有需要的朋友关注公众号:前程有光,回复资料自行下载!


前程有光
936 声望618 粉丝