关于java:Java中那些关于String和字符串常量池你不得不知道的东西

6次阅读

共计 4723 个字符,预计需要花费 12 分钟才能阅读完成。

老套的口试题

在一些老套的口试题中,会要你判断 s1==s2 为 false 还是 true,s1.equals(s2)为 false 还是 true。

String s1 = new String("xyz");
String s2 = "xyz";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));

对于这种题,你总能很快的给出标准答案:== 比拟的是对象地址,equals 办法比拟的是真正的字符数组。所以输入的是 false 和 true。

下面的属于最低阶的题目,没有什么难度。

当初这种老套的题目曾经缓缓隐没了,取而代之的是有一些变形的新题目:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
// 输入什么呢???System.out.println(str1 == str2);

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
// 又输入什么呢???System.out.println(str3 == str4);

难度晋升了一些,但思考一下也不难得出答案是 false 和 true。

明天的文章就是以这几个题目开展的。

String 对象的创立

先简略看一下 String 类的构造:

能够发现,String 外面有一个 value 属性,是真正存储字符的 char 数组。

在执行 String s = "xyz"; 的时候,在堆区创立了一个 String 对象,一个 char 数组对象。

如何证实创立了一个 String 对象和一个 char 数组对象呢?咱们能够通过 IDEA 的 Debug 性能验证:

留神看我截图的地位,在执行完 String s = "xyz"; 之后,再次点击 load classes,Diff 栏的 String 和 char[]别离加了 1,示意在内存中新增了一个 char 数组对象和一个 String 对象。

当初,咱们再来看 String s = new String("xyz"); 创立了几个对象。

从这张 Debug 动图中,咱们能够得出在 String s = new String("xyz"); 之后,创立了两个 String 对象和一个 char 数组对象。

又因为 String s = new String("xyz");s援用只能指向一个对象,能够画出内存分布图:

从图中能够看到,在堆区,有两个 String 对象,这两个 String 对象的 value 都指向同一个 char 数组对象。

那么问题来了,上面的那个 String 对象基本就没被援用,也就是说他没有被用到,那么它到底是干什么的呢?

占了内存空间又不应用,难道这是 JDK 的设计缺点?

很显然不是 JDK 的缺点,JDK 尽管的确有设计缺点,但不至于这么显著,这么愚昧。

那上面的那个 String 对象是干什么的呢?

答案是用于驻留到字符串常量池中去的,留神,这里我用了一个 驻留,并不是间接把对象放到字符串常量池外面去,有什么区别咱们前面再讲。

这里呈现了 字符串常量池 的概念,我在 String s = new String(“xyz”)创立了几个实例你真的能答对吗?中也有过比拟具体的介绍,有趣味的能够去看一下,这里不再反复了。

你只须要晓得,字符串常量池在 JVM 源码中对应的类是 StringTable,底层实现是一个 Hashtable。

那字符串到底是怎么存的呢?

咱们以 String s = new String("xyz"); 为例:

首先去找字符串常量池找,看能不能找到“xyz”字符串对应对象的援用,如果字符串常量池中找不到:

  • 创立一个 String 对象和 char 数组对象
  • 将创立的 String 对象封装成 HashtableEntry,作为 StringTable 的 value 进行存储
  • new String(“xyz”)会在堆区又创立一个 String 对象,char 数组间接指向创立好的 char 数组对象

如果字符串常量池中能找到:

  • new String(“xyz”)会在堆区创立一个对象,char 数组间接指向曾经存在的 char 数组对象

String s = "xyz"; 是怎么样的逻辑:

首先去找字符串常量池找,看能不能找到“xyz”字符串的援用,如果字符串常量池中能找不到:

  • 创立一个 String 对象和 char 数组对象
  • 将创立的 String 对象封装成 HashtableEntry,作为 StringTable 的 value 进行存储
  • 返回创立的 String 对象

如果字符串常量池中能找到:

  • 间接返回找到援用对应的 String 对象

总结而言就是:

对于 String s = new String("xyz"); 这种模式创立字符串对象,如果字符串常量池中能找到,创立一个 String 对象;如果如果字符串常量池中找不到,创立两个 String 对象。

对于 String s = "xyz"; 这种模式创立字符串对象,如果字符串常量池中能找到,不会创立 String 对象;如果如果字符串常量池中找不到,创立一个 String 对象。

所以,在日常开发中,能用 String s = "xyz"; 尽量不必String s = new String("xyz");,因为能够少创立一个对象,节俭一部分空间。

须要强调的是,字符串常量池存的不是字符串也不是 String 对象,而是一个个 HashtableEntry,HashtableEntry 外面的 value 指向的才是 String 对象,为了不让表述变得复杂,我省略了 HashtableEntry 的存在,但不代表它就不存在。

上文提到的驻留就是新建 HashtableEntry 指向 String 对象,并把 HashtableEntry 存入字符串常量池的过程。

在网上一些文章中,一些作者可能是为了让读者更好的了解,省略了一些这些,肯定要留神分别辨别。

达成以上共识之后,咱们再回顾一下那个老套的口试题。

String s1 = new String("xyz");
String s2 = "xyz";
// 为什么输入的是 false 呢?System.out.println(s1 == s2);
// 为什么输入的是 true 呢?System.out.println(s1.equals(s2));

有了下面的根底之后,咱们画出对应的内存图,s1 == s2 为什么是 false 就高深莫测了。

因为 equals 办法比拟的真正的 char 数据,而 s1 和 s2 最终指向的都是同一个 char 数组对象,所以 s1.equals(s2)等于 true。

对于他们最终指向的都是同一个 char 数组对象这一观点,也能够通过反射证实:

我批改了 str1 指向的 String 对象的 value,str2 指向的对象也被影响了。

字符串拼接

当初,咱们再来看一下变式题:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
// 为什么输入的是 false
System.out.println(str1 == str2);

对于这个题目,咱们须要先看一下这段代码的字节码。

字节码指令看不懂没有关系,看我用红色框框起来的局部就行了,能够看到竟然呈现了 StringBuilder。

什么意思呢,就是说 String str1 = s1 + s2; 会被编译器会优化成new StringBuilder().append("aa").append("bb").toString();

StringBuilder 外面的 append 办法就是对 char 数组进行操作,那 StringBuilder 的 toString 办法做了什么呢?

从源码中能够看到,StringBuilder 外面的 toString 办法调用的是 String 类外面的 String(char value[], int offset, int count) 构造方法,这个办法做了什么呢?

  • 依据参数复制一份 char 数组对象。复制了一份!
  • 创立一个 String 对象,String 对象的 value 指向复制的 char 数组对象。

留神,并没有驻留到字符串常量池外面去,这个很要害!!!画一个图了解一下:

也就是说 str2 指向的 String 对象并没有驻留到字符串常量池,而 str1 指向的对象驻留到字符串常量池外面去了,且他们并不是同一个对象。所以 str1 == str2 还是 false

因为复制一份 char 数组对象,所以如果咱们扭转其中一个 char 数组的话,另一个也不会造成影响:

把其中 String 变成丑比之后,另一个还是帅比,也阐明了两个 String 对象用的不是同一份 char 数组。

intern 办法

下面说到,调用 StringBuilder 的 toString 办法创立的 String 对象是不会驻留到字符串常量池的,那如果我偏要驻留到字符串常量池呢?有没有方法呢?

有的,String 类的 intern 办法就能够帮你实现这个事件。

以这段代码为例:

String s1 = "aa";
String s2 = "bb";
String str = s1 + s2;
str.intern();

在执行 str.intern(); 之前,内存图是这样的:

在执行 str.intern(); 之后,内存图是这样的:

intern 办法就是创立了一个 HashtableEntry 对象,并把 value 指向 String 对象,而后把 HashtableEntry 通过 hash 定位存到对应的字符串成常量池中。当然,前提是字符串常量池中原来没有对应的 HashtableEntry。

没了,intern 办法,就是这么简略,一句话给你说分明了。

对于 intern 办法,还有一个很乏味的故事,有趣味的能够去看一下 why 神的这篇文章《深刻了解 Java 虚拟机》第 2 版挖的坑终于在第 3 版中被 R 大填平了

编译优化

写到这里,如同只有一个坑没有填。就是这个题为什么输入的是 true。

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
// 为什么输入的是 true 呢???System.out.println(str3 == str4);

这道题和下面那道题相比,有点类似,在原来的根底上加了两个 final 关键字。咱们先看一下这段代码的字节码:

又是一段字节码指令,不须要看懂,你点一下 #4,竟然就能够看到“ccdd”字符串。

原来,用 final 润饰后,JDK 的编译器会辨认优化,会把 String str3 = s3 + s4; 优化成String str3 = "ccdd"

所以原题就相当于:

String str3 = "ccdd";
String str4 = "ccdd";
// 为什么输入的是 true 呢???System.out.println(str3 == str4);

这样的题目还难吗?是不是那不论 str3 和 str4 怎么比,必定是相等的。

总结

String 对于 Java 程序员来说就是“最相熟的陌生人”,你说 String 简略,它的确简略。你说它难,深究起来的确也有难度,但这些题目,只有你脑海里有一副内存图就会很简略。

面试题也只会越来越难,这个行业看起来也越来越内卷,但只有我学的快,内卷就卷不到我。

好了,明天就写到了,我要去打游戏了。

心愿这篇文章,能对你有一点帮忙。

写在最初

我对每一篇收回去的文章负责,文中波及常识实践,我都会尽量在官网文档和权威书籍找到并加以验证。但即便这样,我也不能保障文章中每个点都是正确的,如果你发现错误之处,欢送指出,我会对其修改。

创作不易,为了更好的表白,须要画很多图,这些都是我本人入手用 PPT 画的,画图也很辛苦的!

所以,不要犹豫了,给点正反馈,许可我,非常欢送并感激你的关注

我是 CoderW,一个程序员。

谢谢你的浏览,咱们下期再见!

正文完
 0