⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 yes的练级攻略 「是Yes呀」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一般面试我都会问一两道很基础的题目,来考察候选人的“地基”是否扎实,有些是操作系统层面的,有些是 Java 语言方面的,还有些...

最近我都拿一道 Java 语言基础题来考察候选人:

不用反射,能否实现一个方法,调换两个 String 对象的实际值?

String yesA = "a";
String yesB = "b";
//能否实现这个 swap 方法
// 让yesA=b,yesB=a?
swap(yesA, yesB);

别小看这道题,其实可以考察好几个点:

  1. 明确 yesA 和 yesB 是啥
  2. Java 只有值传递
  3. String 是不可变类
  4. 字符串常量池
  5. intern 的理解
  6. JVM内存的划分与改变

基于上面这几个点,其实还能发散出很多面试题,不过今天咱们这篇文章就不发散了,好好消化上面这几个点就可以了。

我们需要明确答案:实现不了这个方法。

按照题意,我相信你很容易能写出以下的 swap 方法实现:

void swap(String yesA, String yesB){
String temp = yesA;
yesA = yesB;
yesB = temp;
}

首先,我们要知道 String yesA = "a"; 这行代码返回的 yesA 代表的是一个引用,这个引用指向堆里面的对象 a。

也就是说变量 yesA 存储的只是一个引用,通过它能找到 a 这个对象,所以表现出来好像 yesA 就是 a,实际你可以理解 yesA 存储是一个“地址”,Java 通过这个地址就找到对象 a。

因此,我们知道了,yesA 存储的值不是 a,是引用(同理,yesB也一样)。

然后,我们都听过 Java 中只有值传递,也就是调用方法的时候 Java 会把变量 yesA 的值传递到方法上定义的 yesA(同理 yesB 也是一样),只是值传递。

根据上面我们已经知道 yesA 存储的是引用,所以我们得知,swap方法 里面的 yesA 和 yesB 拿到的是引用。

然后调用了 swap 方法,调换了 yesA 和 yesB 的值(也就是它的引用)

请问,swap 里的跟我外面的 yesA 和 yesB 有关系吗?显然,没有关系。

因此最终外面的 yesA 指向的还是 a,yesB 指向的还是 b。

不信的话,我们看下代码执行的结果:

现在,我们明确了,Java 只有值传递。

看到这,可能会有同学疑惑,那 int 呢,int 不是对象呀,没引用啊,其实一样的,记住Java 只有值传递。

我们跑一下就知道了:

很显然, int 也无法交换成功,道理是一样的。

外面的 yesA 和 yesB,存储的值是 1 和 2(这里不是引用了,堆里也没有对象,栈上直接分配值)。

调用 swap 时候,传递的值是 1 和 2,你可以理解为拷贝了一个副本过去。

所以 swap 里的 yesA 和 yesB 实际上是副本,它的值也是 1 和 2,然后副本之间进行了交换,那跟正主有关系吗?

显然没有。

像科幻电影里面有克隆人,克隆人死了,正主会死吗?

不会。

记住,Java 只有值传递。

再回到这个面试题,你需要知道 String 是不可变类。

那什么是不可变类呢?

我在之前的文章说过,这边我引用一下:

不可变类指的是无法修改对象的值,比如 String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。

因为无法被修改,所以像执行s += "a"; 这样的方法,其实返回的是一个新建的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。

看下面这幅图应该就很清晰了:

如图所示,每次其实都是新建了一个对象返回其引用,并不会修改以前的对象值。

所以我们常说不要在字符串拼接频繁的场景不要使用 + 来拼接,因为这样会频繁的创建对象,影响性能。

而一般你说出 String 是不可变类的时候,面试官一般都会追问:

不可变类有什么好处?

来,我也为你准备好答案了:

最主要的好处就是安全,因为知晓这个对象不可能会被修改,在多线程环境下也是线程安全的

你想想看,你引用的对象是一个不可变的值,那么谁都无法修改它,那它永远就是不变的,别的线程也休息动它分毫,你可以放心大胆的用。

然后,配合常量池可以节省内存空间,且获取效率也更高(如果常量池里面已经有这个字符串对象了,就不需要新建,直接返回即可)。

所以这里就提到 字符串常量池了。

例如执行了 String yesA = "a" 这行代码,我们现在知道 yesA 是一个引用指向了堆中的对象 a,再具体点其实指向的是堆里面的字符串常量池里的对象 a。

如果字符串常量池已经有了 a,那么直接返回其引用,如果没有 a,则会创建 a 对象,然后返回其引用。

这种叫以字面量的形式创建字符串。

还有一种是直接 new String,例如:

String yesA = new String("a")

这种方式又不太一样,首先这里出现了字面量 "a",所以会判断字符串常量池里面是否有 a,如果没有 a 则创建一个 a。

然后会在堆内存里面创建一个对象 a,返回堆内存对象 a 的引用,也就是说返回的不是字符串常量池里面的 a

我们从下面的实验就能验证上面的说法,用字面量创建返回的引用都是一样的,new String 则不一样

至此,你应该已经清晰字面量创建字符串和new String创建字符串的区别了。

讲到这,经常还会伴随一个面试题,也就是 intern

以下代码你觉得输出的值各是啥呢?你可以先思考一下

String yesA = "aaabbb";
String yesB = new String("aaa") + new String("bbb");
String yesC = yesB.intern();
System.out.println(yesA == yesB);
System.out.println(yesA == yesC);

好了,公布答案:

第一个输出是 false 应该没什么疑义,一个是字符串常量的引用,一个是堆内的(实际上还是有门道的,看下面)。

第二个输出是 true 主要是因为这个 intern 方法。

intern 方法的作用是,判断下 yesB 引用指向的值在字符串常量里面是否有,如果没有就在字符串常量池里面新建一个 aaabbb 对象,返回其引用,如果有则直接返回引用。

在我们的例子里,首先通过字面量定义了 yesA ,因此当定义 yesC 的时候,字符串常量池里面已经有 aaabbb 对象(用equals()方法确定是否有对象),所以直接返回常量池里面的引用,因此 yesA == yesC

你以为这样就结束了吗?

我们把上面代码的顺序换一下:

String yesB = new String("aaa") + new String("bbb");
String yesC = yesB.intern();
String yesA = "aaabbb"; // 这里换了
System.out.println(yesA == yesB);
System.out.println(yesA == yesC);

把 yesA 的定义放到 yesC 之后,结果就变了:

是不是有点懵?奇了怪了,按照上面的逻辑不应该啊。

实际上,我最初画字符串常量池的时候,就将其画在堆内,也一直说字符串常量池在堆内,这是因为我是站在 JDK 1.8 的角度来说事儿的。

在 JDK 1.6 的时候字符串常量池是放在永久代的,而 JDK 1.7 及之后就移到了堆中。

这区域的改变就导致了 intern 的返回值有变化了。

在这个认知前提下,我们再来看修改顺序后的代码具体是如何执行的:

  1. String yesB = new String("aaa") + new String("bbb");

此时,堆内会新建一个 aaabbb 对象(对于 aaa 和 bbb 的对象讨论忽略),字符串常量池里不会创建,因为并没有出现 aaabbb 这个字面量。

  1. String yesC = yesB.intern();

此时,会在字符串常量池内部创建 aaabbb 对象?关键点来了。

在 JDK 1.6 时,字符串常量池是放置在永久代的,所以必须新建一个对象放在常量池中。

但 JDK 1.7 之后字符串常量池是放在堆内的,而堆里已经有了刚才 new 过的 aaabbb 对象,所以没必要浪费资源,不用再存储一份对象,直接存储堆中的引用即可。

所以 yesC 这个常量存储的引用和 yesB 一样。

  1. String yesA = "aaabbb";

同理,在 1.7 中 yesA 得到的引用与 yesC 和 yesB 一致,都指向堆内的 aaabbb 对象。

  1. 最终的答案都是 true

现在我们知晓了,在 1.7 之后,如果堆内已经存在某个字符串对象的话,再调用 intern 此时不会在字符串常量池内新建对象,而是直接保存这个引用然后返回。

你看这面试题坑不坑,你还得站在不同的 JDK 版本来回答,不然就是错的,但是面试官并不会在提问的时候跟你说版本的情况。

其实很多面试题都是这样的,看似抛给你一个问题,你好像能直接回答,如果你直接回答,那就错了,你需要先声明一个前提,然后再回答,这样才正确。

最后

你看,就这么一个小小的基础题就可以引出这么多话题,还能延伸到 JVM 内存的划分等等。

这其实很考验基础,也能看出来一个人学习的知识是否串起来,因为这些知识都是有关联性的,给你一个点,就能扩散成面,这样的知识才成体系。

文章目录
  1. 1. 最后