我为什么设计了这道面试题

说说你对引用传递和值传递的理解?

追问:结合java语言,说说java是怎么做的?

追问:下面两个case的输出分别是什么以及原因?

### case1
a = "hello"
B(a)
print(a)

// 方法B的逻辑如下:
B(a) {
    a += "world"
}

### case2
// 对Persion 对象的name属性进行赋值
a = new Person("hello")
B(a)
print(a.name)

// 方法B的逻辑如下:
B(a) {
    a.name += "world"
}

上面是我设计的一道面试题,在实际操作过程中,可以很多种语言措辞,但最终都会引导候选人一步一步来到最后的两个case。

[图片]

起因

在百度的时候有幸成为一二面认证面试官,跟着大伙面一批校招生。准备面试题的时候,就比较犯难,因为这些校招生候选人面的不是PHP/Golang研发岗位,而是C++/Java。让一位只在大学学过Java且过去这么多年的人来面这样的岗位候选人,考察对所学编程语言的掌握程度,不犯难才怪呢。

思来想去,便有了上面这道面试题。我认为可以考察候选人对所学编程语言的掌握程度,以及写出的代码会不会存在潜在的bug。而且在每一种编程语言中,都绕不开这两个概念,还可以根据候选人所学编程语言进行简单调整,比如,候选人面Golang,那就是:在Golang中是怎么做的,说说你的理解。一想到这,我嘴角就露出一丝邪恶的微笑,觉得很投机取巧。

后来跳槽到现在的公司,也参与过几次面试,这道题也就被我沿用了下来。因为要招的是Java开发岗位,像我这种刚开始写Java的小白鼠,也没看过什么Java虚拟机原理啥的,一切相当于从零开始。而面对那些在学校专门学Java,或者从事Java开发七八年的候选人,也只能从这样一个点切入了。

我之前说过,一直对两件事耿耿于怀:一个是bug能不能避免,一个是代码质量。这道题在我的预期中,是没有类似八股文那种标准回答的。只要你能在正确的逻辑下由浅入深的说出自己的理解,基本上就可以通过。当然底层的细节阐述越清晰,得分也就越高。那么你对自己写的每一行代码,就会有一个很清晰的认知,那种潜在的bug也就无处藏身。

可以想想,对自己写出的代码,如果最后结果不符合预期,你是能够在浏览一遍代码后,就知晓了问题的大概所在,还是要靠IDE一步步调试半天后,才发现问题所在?我认为这两种情形下的程序员,在能力水平上和看待程序员编码的态度上,有着截然不同的区别。

指令式程序设计的复杂性

相对于函数式程序设计来讲,我们所熟知的编程语言基本上都属于指令式程序设计的范畴。在指令式程序设计中,最重要的一个特点是赋值。一般地,带有赋值的程序会强迫我们去考虑赋值的相对顺序,以及由此带来的复杂性——一个变量不再是单纯的名字,它关联着一个可以存储值的位置,而存储在这里的值是可以改变的,我们必须时刻小心着对这个值的修改,以及时刻留意这里的值是否符合预期。在并发的程序中,复杂性就会变得更加糟糕。这种复杂性的直观表象有两种:一种是我的代码看起来工作正常,一种是我的代码有bug。

以最简单的交换两个变量值的伪代码为例:

tmp = a
a = b
b = tmp

在看到这段代码后,你是否会额外留意这三条语句的次序?或者你在写的时候,会不会稍微思考一下如何安排他们之间的次序?或者会不会思考优先处理哪个变量, a 还是 b

函数调用,也是一个很好的例子。在使用编程语言提供的库函数时,我们需要留意该函数是否会有副作用?我们在将一段代码抽象为一个函数时,也需要考虑该函数的实现是否会存在副作用?而如果对一种编程语言的参数传递机制没有一些理解,很容易写出有bug的代码。

作用域和值类型

作用域在每种编程语言下都是非常基础的概念。我写过一遍关于Golang语言作用域的文章,在文章结尾简单对比了与其他语言的区别。作用域决定了一个变量名的可见性和生存周期。一般地,函数参数、函数内变量,其可见性仅局限在函数内部,不能被函数外部代码所看见(使用),即使外部存在同名变量,也不是同一个。但变量值的生存周期却并不这样,这取决于该变量值类型。

声明一个变量后,值类型有两种:普通类型和引用类型。以Java语言为例,boolean、char、int、long等基础类型的变量值对应普通类型,Object类型的变量值对应引用类型。在我们所熟知的内存模型下,普通类型值存储在栈空间下,引用类型值存储在堆空间下。因此,对于带有自动垃圾回收机制的编程语言来讲,引用类型值的生存周期并不会在声明对应变量的作用域结束之后立即释放,这是引用类型值的一个潜在风险点。引用类型另一个需要小心对待的情况是,将关联引用类型值的变量作为参数传递给函数时。

引用/指针(reference/pointer) OR 别名(alias)

上面讨论的变量值的两种类型,直接影响了程序内部传递这个类型值的方式:是按值传递,还是按引用传递。对于Java来讲,有两大阵营:

  • 1) 按值传递。持这种观点的核心思想在于,认为将一个Object类型的变量传递给方法时,实际上传递的是这个Object的地址拷贝。
  • 2)基础数据类型按值传递,Object类型按引用传递。持这种观点的核心思想在于,认为引用是Object的别名。

引用和别名在概念上很微妙。究竟是哪一种,其实并不是最重要的,最重要的是要能够识别到在函数内部,可以对引用类型值进行修改,以及修改带来的潜在风险。

字符串的特殊性

但凡提供了字符串这种基础数据类型的编程语言,对字符串的特殊性都有一种约定俗成的做法:引用类型和不可变性。由于字符串的不可变性,因此引用类型的风险,在这里并不适用。当在函数内部对传递过来的字符串进行操作时,操作后的结果字符串和通过参数传递过来的字符串并不是同一个,编程语言内部会新建一个字符串作为操作后的结果字符串。

问八股文问题有没有必要

网上有很多对面试官面试问八股文问题嗤之以鼻的人,以前的我也不例外,觉得没意思,简直在浪费生命。但是大多又迫于形势,多少准备一些。在公司带过一些新人后,我的看法有了转变,觉着是有必要的,至少可以检测候选人对一个概念有没有接触、感知,当然能够理解便再好不过了。这种转变的主要原因在于认知。也是我近半年来的最大感触。

在公司带新人做项目时,认知水平的差距带来的沟通效率、代码质量、编程效率尤其明显。同样一句话,不用认知水平的人,理解出的意思可能会大相径庭,甚至会出乎意料之外。在读《大教堂与市集》记的如何有效反馈bug部分,我也有提到认知水平的重要性。

我为什么不(喜欢)用IDE

编程这么多年来,我很少用功能强大的IDE作为主力编码工具,仅仅用带有编码提示和自动补全的编辑器加上终端。为什么不用呢?因为我的建设性懒惰。懒得去折腾那些复杂的功能插件,总觉着有这功夫,我都解决好几个问题了。你一定会很奇怪,最后我为什么以这个小标题内容作为本文的结束部分,好像与上面也不搭啊。主要是因为,我不用IDE还有另外一个原因:编写代码是为了乐趣,还是应付工作?

对于我,我总觉着,太过于依赖功能强大的IDE,会让人的脑袋变得木楞,就像看多了电视剧,没有思考,脑袋懵懵的一样。而不用IDE,可以使我一直保持思考的状态,可以检查自己对编程语言是否有足够的了解,可以检查自己是否理解写出的每一行代码。而这也是开篇那道面试题的深意。

还有就是,可能我没写过太多很复杂的代码。