【面试准备】关于String的若干细节

大家好,我是被白菜拱的猪。

一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。

假如你喜欢我的文字,欢迎关注公众号“放开这颗白菜让我来”。

你有真正了解 String 吗

前言

每日一钉第2期,关于String那些事。

在正文开始之前,再简单说说写此系列的目的,首先对我来说,将知识点通过我的语言加上我的理解在复述一遍,以便加深对知识的理解,从而达到在面试过程中能够“侃侃而谈”,做到肚子有货,心中不慌。二是通过每日一钉的方式督促自己,告别拖延(可是现在晚上十一点,不会吧,不会刚立的 flag 就要打脸吧)。其次,对于读者,相信列表中也有很多打算找工作的同学,一起努力,一起进步,亡羊补牢,为时不晚,希望我的文字能够帮助你们。

关于文章内容本身,主要针对 Java 面试。每天搞定一个知识点,日积月累,我相信也是一笔很大的收获。好了,接下来跟随我的脚步看看 String 背后我们不常见的一面。

简介

本次主要讲述字符串常量池、字符串的拼接、intern()然后结合相关面试题,通过 JVM 层面进行讲解,深入剖析底层原理。以前对于相关内容,我们可能只是死记硬背,生硬的语言不但难以记忆,时间长了就会面临忘掉的尴尬局面。而我们学习知识我认为要向张无忌学习七伤拳一样,把握好其本质,不必纠结于用词是否跟标准答案一摸一样。而本篇内容也将会在一种“忘我”的状态下进行敲写。

字符串概述

在编码的过程中,没有人不使用字符串吧,那对字符串又有多少了解呢,我想这是一个值得深思的问题,看完这篇文章我相信对于小白而言将会有很大的收获,为什么这么讲,因为我也是一个小白。

字符串最重要的一个特性是不可变性。为什么这么讲?我们可以点开源码看看它的类结构。
在这里插入图片描述

它是用 final 修饰的,不能被继承,然后实际维护的是一个 final 修饰的char 型数组来保存字符串数据。但是在 jdk 9及以后,变成了byte[],这是为什么呢?我们看看官网怎么说。
在这里插入图片描述

大致翻译一下就是说我们用char存储要占两个字节,但是大部分字符是拉丁文,拉丁文只用一个字节就可以存储,所以一半的空间就浪费掉了。因此我们换成了byte来存储字符串,那之前占用两个字节的字符怎么办呢?像中文就占用两个字符,文档提出用一个encoding-flag 一个标记来表明他占两个字符,这样就节省了很多空间。

当然对应的 StringBuilder 和 StringBuffer 也做了相应的改变。

字符串常量池

在内存分配上,字符串则是放在一个叫做字符串常量池的地方。常量池就类似于一个 Java 系统级别提供的缓存,我们需要什么就先看看里面有没有,有就直接拿去使用,没有在去创建,这样保证了运行的速度和内存的节省。

字符串常量池一个很重要的特性就是不会存储相同内容的字符串

jdk 版本的变化

而且需要注意的是 jdk6 与 jdk6以后的版本,字符串常量池所放的位置是不同的。在jdk6以后,字符串常量池由原先的永久代(jdk 8 变成了元空间)区域移动到了堆中。这是一个需要我们注意的点,所放的不同的位置,决定了他最后存储的机制是不一样的。
在这里插入图片描述

在 jdk8中,字符串常量池依然放在堆中,只不过永久代变成了元空间,放在了本地内存而不是 JVM 分配的内存。

插入方式

说了那么多,那么如何将字符串放入到字符串常量池中呢?有两种方式:

  • 第一种是使用字面量的方式,也就是直接使用双引号。直接使用双引号出来的 String 对象会直接存储在常量池中。比如:String info = “ljl is a handsome boy”;
  • 第二种可以使用 String 提供的 intern() 方法,这个后面重点说明

这时就有人问了,那使用 new 关键字创建一个 String对象呢?比如:String info = new String(“ljl is a good man”);

这个就有的说了。

new String(“ljl is a good man”);

首先我们引出一个面试常问的问题。

题目: new String(“ljl is a good man”)会创建几个对象?

看到这个题目有些懵逼,new 这不是 new 一个对象嘛,这么简单的问题还会问,面试官怕不是个弱zhi,假如这么想你就错了,人人都会还会提出这个问题吗?答案是两个。为什么是两个,我们深入底层,看看字节码文件,一看便知。
在这里插入图片描述

一个对象是:new 关键字在堆空间创建的。

另一个对象是:在字符串常量池创建的对象“ljl is a good man”。字节码指令:ldc。

我们可以这样记忆,将new String(“ljl is a good man”);分成两部分,一个是括号里面的“ljl is a good man”,另一个是new String();前者就是我们前面讲的使用字面量的方式也就是直接使用引号,将字符串在字符串常量池存储,而 new 则是在堆中创建一个普通的对象,然后把常量池中的字符串取出来然后对其进行赋值。

但是实际上两种不同的方式维护的确实同一个数组,这点仅做了解即可,我们可以通过反射的方式验证常量池中的“abc”和堆中的“abc”底层保存的数组是同一个。

@Test
public void test1() throws Exception{
    String s1 = "abc";
    String s2 = new String("abc");
    Field field = String.class.getDeclaredField("value");
    //将字段设置为可访问的
    field.setAccessible(true);
    char[] arr = (char[]) field.get(s1);
    arr[0] = 'l';
    System.out.println(s1);//输出是lbc
    System.out.println(s2);//输出也是lbc
}

为了避免头脑风暴,我们可以简单的想堆中有一个对象,值为abc,常量值也有一个对象值也是abc。

字符串拼接

常量拼接

字符串拼接主要有两种方式,一个是常量与常量的拼接:
在这里插入图片描述

通过字节码发现,“a” + “b” + “c” 实际上等同于“abc”,这是因为编译器的优化,在编译的时候就将“a” + “b” + “c”拼接完成。所以s1,s2都是指向字符串常量池同一个地址,s1 == s2的结果为true。

变量拼接

另一种是变量与常量的拼接,或者说结果的对象其中有一个是对象,那么拼接的结果就在堆中,而非前面所说的在常量池中,我们通过字节码看一看拼接的原理。
在这里插入图片描述

我们发现变量拼接是创建了一个 StringBuilder 对象,然后通过 StringBuilder 的append方法进行字符串拼接,然后最后调用 toString() 方法。

然后我们点开 StringBuilder的toString()方法。
在这里插入图片描述

我们发现创建了一个String对象,但是这里跟前面我们将的 new String(“abc”)不同,StringBuilder的toString只创建了一个对象,没有ldc命令,也就是说没有在常量池创建对象,这一点很重要。

最后我们比较 == 是否相等时,自然不会是true了,因为一个是堆中的地址,一个是字符串常量池中的地址。

拓展

既然说到拼接,我们思考 new String(“a”) + new String(“b”)会创建几个对象呢?

首先根据前面讲过 new String(“a”)的例子,他会创建两个对象,所以两个 new 会创建4个对象,最后加上拼接会创建一个StringBuilder所以是五个对象,最后又调用了StringBuilder.toString() 注意这里的 toString() 里面的new String(),只创建了堆中的对象,常量池里面没有 “ab” 的。所以总共应该是六个对象。下面看字节码:
在这里插入图片描述

 /* new String("a") + new String("b")呢?
 *  对象1:new StringBuilder()
 *  对象2: new String("a")
 *  对象3: 常量池中的"a"
 *  对象4: new String("b")
 *  对象5: 常量池中的"b"
 *
 *  深入剖析: StringBuilder的toString():
 *      对象6 :new String("ab")
 *       强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
 */

intern()

在这里插入图片描述

通过阅读文档,我们知道可以将intern()的作用简单概括:

比如 String s1 = new String(“abc”);String s2 = s1.intern();

s1.intern()的作用就是检查字符串常量池中是否含有“abc”,假如有就将字符串常量池该字符串的地址返回,没有则在字符串常量池中创建“abc”,然后将此地址返回,所以 s2 指向的是“abc”在字符串常量池中的地址。这样确保字符串在内存里只有一份拷贝,可以节约内存空间。多次创建new String(“abc”).intern(),这样避免了在堆中重复创建对象。

版本区别

intern()随着 jdk 版本的不同,具体的实现也不同,我们知道自 jdk7 以后字符串常量池由方法区(永久代)移动到了堆,因为new 的对象是在堆中创建的,然后使用intern(),就会导致堆中有两份空间内容实质上是一样的,所以就造成空间的浪费,而 jdk6,方法区与堆是不同的地方,所以就无法做这个节省。针对这两种情况,有下面两种方式:

总结 String 的 intern() 的使用

  • jdk 1.6 中,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址。
  • jdk 1.7 ,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
    • 如果没有,会把此对象的引用地址复制一份,放入串池,并返回串池中的对象地址。

jdk1.6:
在这里插入图片描述

jdk1.7起:
在这里插入图片描述

面试题

@Test
public void test5() {
    //String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
    String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
    s1.intern();
    String s2 = "ab";
    System.out.println(s1 == s2);
}

s1 有两种形式:

​ 一是 new String(“ab”);

​ 二是 new String(“a”) + new String(“b”);

它们的区别在前面讲过,在于字符串常量池中是否含有"ab",然后调用intern(),会根据jdk的版本发生变化,从而结果也会发生变化。

总结

看完这篇文章,正如开头所说,是不是对 String 有了不一样的看法了呢?高中数学老师常说概念,概念。任其题目发生千万变化,最后还是要回归最初的本质,要学会知其然也要知其所以然,这里推荐去看看尚硅谷康师傅的 JVM 教程,你会对 Java 有一个不一样的认识,之前模棱两可的内容都会有清晰的认识。

正如身边的人和物,你是否又真正的了解呢?

这篇文章,花费了不少时间,假如对你有所帮助,不如点个在看加个关注呀。听说今天上证指数 3600了,心中着实有些小慌张。

你知道的越多,你不知道的越多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值