一道简单的 Java 笔试题,但值得很多人反思!-程序员宅基地

技术标签: 面试  java  数据结构与算法  

前言

面试别人,对我来说是一件新奇事,以前都是别人面试我。我清楚地知道,我在的地域与公司,难以吸引到中国的一流软件人才。所以,我特地调低了期望,很少问什么深入的技术问题,只问一些广泛的、基础的。我只要最终给Leader一句“这个人技术还行/很好/非常好”,就行了。至于其它能力、综合水平,由别人把关。为此,在挑选唯一的一道笔试题时,我特别地上心。

首先,我不敢用网上那些广为流传的,比如Leetcode、《程序员面试宝典》里的题——这些都太难了!正儿八经做,其实很少有人能在1小时内完美做出来,除非之前遇到过。我本人也并非什么思维敏捷的牛人,不然也不会混得这么惨。正所谓己所不欲,勿施于人,我也不希望以后别人考我特别麻烦的算法题,所以自创了一道特别简单的。

其次,对(Android平台的)Java程序员来说,大多数情况下不需要写什么复杂的算法。相反,Java层主要做的是界面控制、业务逻辑、数据流之类的,更提倡代码的简单和可读,尽量用既有的公共类库,不惜损失一些运行效率。拿一道复杂的算法题,考一个Java程序员,多少有点刁难人。

最后,还是那个薪资待遇和人才梯度问题。没有Google的工资,就别考Google的题;没有Google的向心力,就别期待有Google级别的人才来面试。

亮题

以下有一个 static method,类外会调用它,一个个地插入一些元素进入一个List。可以改变这个List内容的,只有这一个method,要求任何时候这个List都是有序的。比如,依次插入3、2、1、2,我希望List的顺序是1、2、2、3。

webp

我会给出15分钟的时间,而其实往往会再多给10分钟。(有兴趣,你可以停在这试试。相信在看文章这种轻松的环境下,理清这道题的思路也就10~30秒。)

(为什么下限是10秒呢?唉……一不小心暴露了我智商的峰值。我实际问过一些同事,他们通常在理解的同时,就立刻给出了正确的思路,过程不足5秒,其中甚至包括一个硬件工程师,和一个只负责沟通和文档的妹子。)

提示

在过程中,我会逐步给出一些提示,从接口到思路,都会主动提供,其它也基本有问必答。如果单纯考算法,C语言才是最合适的,因为它没有什么高级的工具类,什么复杂点的都得自己写。而Java,则有一些“基础”类库是难以记忆的。比如前面出现的java.util.List,就没有多少人能在纸上写出它的常用接口。

我并不想考察什么死记硬背,在这个时代,断网后本来就没几个程序员能正常编程。所以我会主动提供一份List的不完全接口列表。

webp

我没有给出完全的接口,因为给多了无疑是误导人。真正能用上的接口其实也就3个,但我也总不能只给3个,提示得太明显,也限制了对方的思路。所以,给出了可能用得上的这几个。我也没给出注释,因为有声明就已经够了。而且如果对方问起,我也会给出解释。

一开始我想,考一个排序算了。但是转念一想,这也太不负责任了。对面要是背一道冒泡排序的解法上来,达不到考察技术水平的目的,Boss也不会认可。本着“放水不能太明显”的原则,我想考插入排序,并且把题目弄得没多少人见过。

排序是一类基本算法,合格的程序员至少会一种。大多数人都只会入门级的冒泡排序,而我更喜欢插入排序,原因……你会明白的。

插入排序,其实就是把数组或列表在逻辑上分成两部分,一部分是待排序的,一部分是有序的。一开始,有序的部分只有一个元素(或者一个都没有),然后从待排序的部分里一个个抽出来,插入到有序的部分。等元素都插入到了有序的部分,排序过程也就完成了。

你看,也就抽插N次的事。而我这道题,就是只考插入排序算法的一半,会插就行。

在面试过程中,我甚至常常亲自解释插入排序是怎么回事——放水到这个份上,我都不忍心再退步了。

真正的考察点

这是一份Android平台的开发工作,Boss要求的是能干活、干好活。我给出的建议要求是:

熟悉Java。

有良好的沟通、表达能力。

学习能力强,喜欢不断拓展计算机领域的知识。

有良好的编码习惯,愿意为代码的简洁、优雅而反复修改。

我建议Boss放弃学历和工作年限的要求,技术岗位就应该只考察技术(和其它基本能力),不应该考察技术的间接证明。

Java是Android的基本功(我们不玩Kotlin、Scala、React Native等新花样),这门语言如果不扎实,那至少得带半年。

我没有在Android岗明确地要求考察Android,是因为Android的那些东西相对来说容易学习。即便是毫无经验的新手,要搞清楚什么“四大组件”“五大布局”,也就一两天的事。而如果Java不够扎实,各种肉眼可见的大小bug就会层出不穷,知识盲点一两年都补不完。

沟通是职场基本功。如果话都说不清,那么会显著降低团队的沟通效率。而且,我个人认为,话说不清的人,代码一定写不好。语言条理清晰,逻辑层次分明,体现到代码上,就是简洁、明朗。

学习能力、求知欲,是作为一个程序员的基本素养。因为,大部分人的工作,类似于在一堆按钮中,找到合适的那个按下去;而程序员的工作,往往是闭着眼睛这么干。开发工程师通常是在一堆未知(没读过的代码、不知道的接口)中,把一小部分变成已知(读懂了的代码或接口),进行一些增删改,最后达成外界(产品经理、设计师、测试工程师)赋予的业务目标。

一些职业卖口水,一些职业卖口才。一些职业卖青春,一些职业卖肉体(咳咳,我说的是空姐和搬砖,想歪的去面壁)。一些职业卖知识,一些职业卖能力。

程序员,或者说软件开发工程师,卖的是学习能力(其实也包括青春和肉体),快速学会各种知识,找到那些藏在屏幕外的按钮,并且正确的按下去。比如,像Bash这类Command line工具,就是自己敲命令出来执行,而不是去界面上找功能对应的按钮;而程序设计、实现,就是去发现、或者创造一种解决问题的办法,然后用代码表达出来——你看,都是在干一些反UI、UX设计的事。唯有不断地学习,才能提高效率,把自己从加班中解脱出来,把项目从bug中拯救出来。所以,厌学的人当不了好程序员,也干不长。

编码习惯,相对次之。部分观点认为,这东西伴随一生,如果一开始没有好习惯,这辈子都没办法改了。Boss就是这么认为的,我倒是不这么认为。我相信编码习惯的可塑性是很高的——你不按规范写,我不给你merge,改不改?

但是,编码习惯作为程序员的软技能,还是可以一定程度上看出其技术素养、代码质量的。至于优雅什么的,我其实没有真的敢这么期待。

所以,我这道题其实是考察这四点。

能写出来,并且无明显问题,代表Java基本功扎实。

理解我对题目的描述,和我确认清楚题目的细节,这是看沟通能力。

List接口不知道,我给你啊;插入排序不会,我教你啊;其它还有什么不会,你问啊——这是在考察学习能力。

代码的字里行间,可以明显看出编码习惯。

面试结果

总体来说,我很伤心。

第一位就让我很伤心,当我看了他前两行代码,就不忍心接着往下看:

webp

第一行就编译不过。如果他对Java的一些命名规范有一定的了解,就绝不会把sSorted写成sorted。(当然,sSorted也许并不是合适的命名方式,因为s和m这类前缀有些冗余。我通常遵守Android源码的通用规范,它是有这类前缀的。)

第二行必然抛出NullPointerException,而不知道是该庆幸还是悲伤的是,它永远执行不到。根据我已经给出的一个接口addElement,和可以猜到或者问出来的读取接口,都是不会把sSorted变成null的。这体现了沟通、理解能力的一点问题。

此外,即使sSorted因为什么bug而变成null,这里也不应该做处理,而是任其抛出NullPointerException,或者转义一下,主动抛出IllegalStateException。否则,此处将变成一个不会crash的隐藏bug。不能用正常处理,代替异常处理;当然,也不能用异常处理,代替流程控制。

另外,更令我失望的是,有一位是这么写的:

webp

我问他,如果这个元素不在这个List里存在怎么办?如果这个List是空的怎么办?他顿时一囧,我也一起囧,心想自己是不是太坏了。

还有一位,仿佛听见了我这几个问题,他竟然一一作答:

webp

他想干什么呢?也许是优化性能吧,只能这么帮腔了。另外,他对size的理解,和数组的length相同。

这位算是经验比较丰富(30岁),对Java的理解比较深入的了。他说排序不需要手写,Java里有现成的接口。我说,是这样没错,但接口我没给出,如果你记得,那就写出来吧。

于是他在刚才那一大段“优化”的后面,这么写了:

webp

思路上,插入后再排序,我先不吐槽。我明明说了“记得”再写,这Comparable及其接口int compareTo(T another)如果记不清,我就当看lambda表达式了。可是,他这个?分明是Comparator的int compare(T lhs, T rhs)接口呀!

不过,其实这些我都可以捏着鼻子认了,因为我也手写不出来。但List是没有sort方法的呀!

Arrays和Collections才有各自的sort方法,它俩算是银弹型工具类,而Array和Collection是没有的。这个细节,谁用谁知道,知道了就绝不会记错,尽管就差一个s。

还有一位,他先插入、再冒泡排序,是这么写的:

webp

你没看错,for()里面是,分隔的。

你没看错,temp是从石头缝里蹦出来的。

你没看错,List.get(e)是可以对其赋值的。

你没看错,List.size(i)是可以传参数进去的。

还有两位,直接交白卷放弃了。

其中一位还比较认真,思考了一会儿,说“我不想浪费时间”。

我没乱用词,他确实“比较认真”。另一位在我递过去后,直接看两眼就递回来,“排序我不会”,然后看手机去了。

o(╯□╰)o

参考答案

我自己在纸上写的时候,花了大概5分钟去思考细节,再花5分钟写出来。(唉……一不小心,又暴露了自己奇慢无比的思维,以及奇慢无比的写字速度。)这比我此前预计的时间多了好几倍!

不过,以我给的15~25分钟,应该不算太难为人……吧?

webp

这是我自己在纸上写的答案。(如果有兴趣,可以停在此处,考虑下这是否是最优算法。)

webp

这是 java.util.LinkedList在Android(API 23)上的实现,而反编译Oracle JDK 1.8的实现也大同小异。也就是说,我写的答案虽然看似简洁,但其最坏时间复杂度与先插入再排序也没太大区别,都是O(n2)。

终日打燕,反而被燕啄了眼!(暴露了真实水平。)

我后来又写了一个参考答案,算是勉强在脸上摸了些防晒霜。(大家有兴趣可以想想为什么这是一个改进。当然,一定还有更好的方案。)

webp

(我没有在提示列表中给出迭代器,结果自己也被晃过去了。)

隐藏的杀手锏

面试官在出题考察应聘者时,应聘者也在通过这道题考察这家公司。

为了避免让人觉得这家公司考题太简单、工作内容太无趣、里面的员工(我)水平太低,我还准备了一些后续问题,由浅入深,作为杀手锏。

为什么LinkedList可以赋值给List?

考察多态(polymorphism)。

为什么List<Integer>要写<>内的内容,而LinkedList<>()可以不写?

考察泛型(generic)。

为什么List里面是Integer,但放进去和拿出来的都是int?

(此处有坑,其实拿出来的还是Integer。)

考察基本数据类型的自动装箱、拆箱(auto boxing/unboxing)。

如何在外面有多线程调用时,保证这个唯一的List的正确性?

考察synchronized和volatile。

如何在多线程状态下的每一个线程,各保持一个独立的List?

考察ThreadLocal。

(当然,还有一些和Android相关的问题。)

我真心是没想考算法,所以连算法复杂度的评估都没打算问。实际情况是,我往往没有机会问这些问题,因为没几个人写出来。

吐槽与建议

首先,喷一下大学扩招……算了,不扯这么远了。那两位放弃做题的,一个是计算机学院的,一个是软件工程学院的。排序写不出来,竟然也是能毕业的!

有两位是某App的开发者。我把他们的App下载下来,发现了一堆bug后,本来想忍忍、就当没看见、码农何苦为难码农,然后手机发热、卡顿、灭屏后几乎点亮不了(内存泄露吃光了RAM,导致系统进程没有内存可用)。过了一阵最终好了,我查看耗电排行,运行10分钟就高居榜首,耗了17%的电——我吓得立刻卸载了。一个第三方App能把系统给卡成这样,一般人还真做不到。

还有两位是“相关专业”的,非计算机、软件工程专业,反而表现最佳,虽然还是没写出来。

他们无一例外,都是在大学以外,又参加过某些Java、Android培训的。这些培训班的水平,可见一斑。问题倒不一定是培训班的教学质量,而是这种大规模提供人才转型服务的形式本身——这个世界上,本来就不是谁,都能当一个好码农,哪怕工作要求只是复制粘贴。

现在,很多码农都戏称自己是在“搬砖”、复制粘贴,但实际上程序员的工作不可能仅止于此。使用别人写好的基本算法,参考别人的实现代码,只是为了集中精力去解决抽象层次更高的业务问题。

“我们不写代码,我们只做代码的搬运工。”——万万不可把这句话当做信条。

还有很多人,在没有Demo的情况下,无论给多么详细的API或其它资料,仍然无法写代码。他们只能在既有的基础上,修修补补,无法凭空创作。


欢迎工作一到五年的Java工程师朋友们加入Java填坑之路:8  6  0  1  1  3  4  8  1
里面提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
 

转载于:https://my.oschina.net/u/3999718/blog/2962436

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_33858249/article/details/92424016

智能推荐

linux 程序溢出,linux-溢出程序-程序员宅基地

文章浏览阅读197次。后门程序: 100描述米特尼克拿到了BAT数据中心的口令后,为了确保口令被更改后仍能登陆数据中心,他从一位小伙伴那拿到了一个后门程序植入进了服务器。这个后门程序没有任何说明,但是米特尼克迅速找到了使用方法。后门程序:http://bctf.cn/files/downloads/backdoor_844d899c6320ac74a471e3c0db5e902e 安装地址:218.2.197.250:..._fff308

开源语音识别软件有哪些?-程序员宅基地

文章浏览阅读403次。开源语音识别软件有很多种, 下面列举几种常见的:Kaldi: 是一种用于语音识别的开源工具包, 支持多种语言, 并且在处理大规模语料库方面有较好的表现.HTK: 是一个用于语音识别、语音合成和语音处理的开源工具包, 支持多种语言.Julius: 是一个用于语音识别的开源软件, 支持日语和英语.Sphinx: 是一个用于语音识别的开源工具包, 支持多种语言.CMUCLMTK: 是..._多说话人语音识别开源工具

Mac os下apache正常启动localhost无法访问服务器_mac apache启动但是访问不到-程序员宅基地

文章浏览阅读8.1k次,点赞3次,收藏2次。由于删除了/private/var/log下面的日志,导致重启电脑后apache无法正常工作。重启电脑后apache无法正常运行,访问localhost或127.0.0.1都会无法找到服务器。探索:问题出现后,由于$ sudo apachectl start不会报任何错,但是访问localhost或127.0.0.1始终失败。可能情况1: 没有监听_mac apache启动但是访问不到

java list(属性方法)_java list exists-程序员宅基地

文章浏览阅读1.2w次,点赞5次,收藏17次。List的方法和属性 方法或属性 作用Capacity 用于获取或设置List可容纳元素的数量。当数量超过容量时,这个值会自动增长。您可以设置这个值以减少容量,也可以调用trin()方法来减少容量以适合实际的元素数目。Count 属性,用于获取数组中当前元素数量Item( ) 通过指定索引获取或设置元素。对于List类来说,它是一个索引器。Add( ) 在List中添加一个对象的公有方法AddRan_java list exists

Golang单元测试和压力测试-程序员宅基地

文章浏览阅读1k次,点赞22次,收藏23次。go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程类似,并不需要学习新的语法,规则和工具。go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终可执行文件中。在*_test.go文件中有三种类型的函数,单元测试函数,基准测试函数和示例函数。

Python 的 Numpy 数值计算_numpy 数组中的值 分组计数-程序员宅基地

文章浏览阅读1k次,点赞4次,收藏5次。文章目录1. Numpy 介绍2. Numpy 数组2.1 介绍2.2 数组属性2.3 创建数组1. Numpy 介绍  Numpy(Numerical Python),是 Python 科学计算的基础包。Mumpy 主要提供了以下内容:快速高效的多维数组对象 ndarray。对数组执行元素级计算以及直接对数组执行科学计算的函数。线性代数、傅里叶变换及随机数生成的功能。将 C、C++..._numpy 数组中的值 分组计数

随便推点

[Python] pyqt6 - 如何实现关闭窗口前弹出确认框_pyqt怎么在退出时显示确定要退出吗-程序员宅基地

文章浏览阅读1.4k次,点赞34次,收藏21次。本文主要介绍了pyqt6中,如何利用窗口的closeEvent函数和QMessageBox的question函数来实现关闭窗口时弹出确认框的功能。_pyqt怎么在退出时显示确定要退出吗

Golang 元素值在经过通道传递时会被复制,这个复制是浅复制_golang channel copy-程序员宅基地

文章浏览阅读1.2k次。1. channel 元素值为切片package mainimport ( "fmt")func main() { ch := make(chan []int, 3) s := []int{1, 3, 4} ch <- s fmt.Printf("s length is %d, cap is %d\n", len(ch), cap(ch)) fmt.Printf("s address is %p\n", &s) r := <-ch r[0] = 2 _golang channel copy

数据结构学习笔记(全)_数据结构笔记整理-程序员宅基地

文章浏览阅读619次,点赞2次,收藏27次。若有两个孩子,其右孩子为q,则将左孩子的最右边结点p删除,并将p放在被删除的根节点的位置,如果被删除的p有左子树,则直接继承在p的位置。从图中的某个顶点V0出发,并在访问此顶点之后 依次访问V0的所有未被访问过的邻接点,之后按这 些顶点被访问的先后次序依次访问它们的邻接点,直 至图中所有和V0有路径相通的顶点都被访问到。先选择一个顶点(无要求),选择权值最短的边,将其与一个新的顶点连接,并将其看为一个新的整体,重复进行”选择权值最短的边,将其与一个新的顶点连接“的操作直至所有顶点相连。_数据结构笔记整理

[开发|C++] 在 Ubuntu20.04系统上编译gcc-4.9.4_ubuntu20.04 gcc9.4.0编译gcc4.9.2-程序员宅基地

文章浏览阅读566次,点赞11次,收藏9次。5. 报错信息如下。7. 报错信息如下。_ubuntu20.04 gcc9.4.0编译gcc4.9.2

3小时零基础入门微信小程序开发2024年最新版-程序员宅基地

文章浏览阅读964次,点赞27次,收藏25次。用大白话讲:变量就是一个装东西的盒子再通俗些讲:变量就是用于存放数据的容器,我们通过变量名获取对应的数据。如上图所示,我们的盒子(变量)可以装名字,布尔类型的true,还可以用来装数字。变量的本质:就是在程序的内存中申请一块用来存放数据的空间。对象只是一种特殊的数据。对象是一组无序的相关属性和方法组成。这里重点要记住属性和方法这两个新概念属性:事物的特征,对象里的属性就是用来表现该对象具备哪些特征方法:事物的行为,对象里方法就是用来表示该对象具备哪些行为。

Bellman-Ford算法详讲_bellmanford算法不能处理什么情况-程序员宅基地

文章浏览阅读272次。Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。适_bellmanford算法不能处理什么情况