乐趣区

关于java:工作两年如果你连java中的这些String都不知道那你有点危险了

前言

最近在搞面试题的时候,老是遇到 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 架构实战电子书等等!
全副收费分享给大家,只心愿你多多反对!要是有须要的敌人关注公众号:前程有光,回复材料自行下载!

退出移动版