前言

最近在搞面试题的时候,老是遇到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返回也是一样的。
后果

truetruefalse162767407016276740701360875712

首先咱们晓得
==是间接比拟的两个对象的堆内存地址,如果相等,则阐明这两个援用理论是指向同一个对象地址的
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都是字符串常量了,编译时也会优化,会合并成一个字符串。

truetruetrue162767407016276740701627674070

特例

    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就不能在编译期被确定,而只能在运行时被创立了。

truetruefalse162767407016276740701360875712

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 进去的字符串,都是不一样的对象,所以地址都是不一样的

falsefalsefalse16256357311580066828491044090

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再加上“欢快的菠萝”这个字符串对象

1627674070136087571216256357311580066828

总结而言就是:

对于 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就等于"欢快的菠萝"

1627674070136087571216256357311625635731

一共生成了6个对象,在前三个的根底上加上,new String(“欢快的”),new String(“菠萝”),还有new String(“欢快的”) + new String(“菠萝”)是以StringBuilder的模式拼接进去的一个对象

1627674070136087571216256357311580066828491044090644117698

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虚拟机)

truefalse1627674070162767407013608757121625635731

总结

在文章的最初作者为大家整顿了很多材料!包含java外围知识点+全套架构师学习材料和视频+一线大厂面试宝典+面试简历模板+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书等等!
全副收费分享给大家,只心愿你多多反对!要是有须要的敌人关注公众号:前程有光,回复材料自行下载!