我最近面了些人,发现这个基础题都答得不好

你好,我是 yes。

一般面试我都会问一两道很基础的题目,来考察候选人的“地基”是否扎实,有些是操作系统层面的,有些是 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 内存的划分等等。

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

关于面试题,推荐一下我的个人仓库:

面试仓库地址

然后我写的文章汇总,放在牛客的博客了,有兴趣的可以看下。

链接:https://blog.nowcoder.net/n/77142a8f2001435a9743cf7d8d888cb2

欢迎关注我的个人公众号【yes的练级攻略】,每周都会更新原创文章,最近都在更新面试相关的哈~

我是yes,从一点点的到亿点点,我们下篇见~

#春招##学习路径#
全部评论
好多大厂就是喜欢问基础的,考察基本功
15 回复 分享
发布于 2022-02-28 19:02
想答得完善很难啊,很有参考价值👍
7 回复 分享
发布于 2022-03-05 09:41
很好,又复习了一遍,明天又忘了😂
4 回复 分享
发布于 2022-03-27 02:21
阿里巴巴/蚂蚁金服2023届校招,招聘java实习生,我们部门hc充裕,可以加微信聊聊呀yesi123ok。
4 回复 分享
发布于 2022-03-05 22:35
复习
2 回复 分享
发布于 2022-04-03 22:22
String yesB = new String("aaa") + new String("bbb");   String yesC = yesB.intern();   String yesA = "aaabbb";  个人对前两行代码的解读不认同楼主,若有错误希望还各路大佬不吝赐教。 本论述基于JDK1.8,由于1.8之后,字符串常量池也放到了堆区,因此下面我说的常量池区和非常量池区都是在堆区里。 首先说一下new String(),会在非常量池区划一块空间存字符串值,然后若常量池内没有存该字符串,也会向常量池中存一份,但是最终返回的是非常量池中的引用。     楼主没有将字符串加法的原理展开来讲, String yesB = new String("aaa") + new String("bbb");     真正的过程是  String yesB = new StringBuilder().append("aaa").append("bbb").toString();      抛开中间new 的StringBuilder对象不讲,StringBuilder的toString方法源码:     public String toString() {         // Create a copy, don't share the array         return new String(value, 0, count);     } 因此,第一行  yesB 最终可以等效为   String yesB = new String("aaabbb");  上面已经论述了 new String(); 的过程,因此当第一行语句执行完之后, yesB的值为非常量池中"aaabbb"的地址,且常量池中必定有字符串"aaabbb"。 然后解释第二句,  yesB.intern()   效果是:第一步,如果常量池中无"aaabbb",那么会在常量池中划一块空间存"aaabbb";否则,不做操作。 第二步: 将常量池中"aaabbb"的地址给yesB,并返回给yesC。 因此执行第二句后, yesB和yesC都指向常量池中的"aaabbb" 第三行同意楼主。 最后3个字符串引用的值均为常量池中的地址。
2 回复 分享
发布于 2022-03-09 17:11
不理解的可以关注我个人公众号「yes的练级攻略」,里面有我的联系方式,可以讨论讨论
2 回复 分享
发布于 2022-03-06 09:06
校友好强
1 回复 分享
发布于 2022-04-06 02:58
new对象时字符在堆内存,但是使用intern后,字符串放进去了串池,串池里aaabbb对应对象是B,而A拿aaabbb的时候,发现串池有,所以直接拿来用,这就导致A和B的地址是同一个
1 回复 分享
发布于 2022-03-05 23:09
1. String yesB = new String("aaa") + new String("bbb"); 2. String yesC = yesB.intern(); 3. String yesA = "aaabbb"; 结果A==B True,但是改写成下面这种形式后A==B False,说明哪怕堆中存在对象B,字符串常量池中还是创建了A,跟文中描述的似乎不太相符? 1. String yesB = new String("aaa") + new String("bbb"); 3. String yesA = "aaabbb"; 甚至调整下顺序,结果有不一样了 String B = new String("aaa") + new String("bbb"); String A = "aaabbb"; System.out.println(A == B);  //False String C = B.intern();        System.out.println(A == C);  //True System.out.println(B == C);  //False,这是为什么呢还得再研究下🤣
1 回复 分享
发布于 2022-03-02 18:09
麻了
点赞 回复 分享
发布于 2022-04-09 15:57
想要java面试题
点赞 回复 分享
发布于 2022-04-05 01:33
string是java核心类库,不让改,哪怕通过继承扩展也不行,双亲委派也是为了能正确加载核心类
点赞 回复 分享
发布于 2022-03-09 20:54

相关推荐

05-26 22:25
门头沟学院 Java
Java小肖:不会是想叫你过去把你打一顿吧,哈哈哈
点赞 评论 收藏
分享
流浪的神仙:无恶意,算法一般好像都得9硕才能干算法太卷啦
点赞 评论 收藏
分享
评论
173
616
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务