苏州再游感悟

大概10多年前,工作没多久的时候,和老婆去过苏州,待了两天,去了北寺塔、也去了寒山寺。记得当时下着濛濛的细雨,举着把旧伞,跟了个团满苏州的景点儿逛,看着个什么都感觉新鲜得很,非要合个影才能安心离开。只是时间所限,当时连苏州标志性的拙政园、虎丘都没有去。但是离开时心里也并不感觉遗憾,因为这样的城市,早晚要再来的,而且毕竟离上海这么近。只是不曾想,这再来,便是10年之后了。

这次住的地方离拙政园比较近,吃过了午饭,沿着平江路,边逛边走十来分钟就到了。平江路在苏州也算得上是一景,沿途有各色文玩、作坊、秀场、小吃,有些看上去倒也别具一格,但我们怕错过了拙政园的参观时间,便一刻也未停留,匆匆赶到了拙政园。然而进了门,看着周遭的黑瓦白墙、水榭连廊,错落的古树怪石,脚下的青砖铺路,衬以各式卵石拼出的花鸟虫鱼,就有一种时空错乱的感觉,仿佛来到了上海豫园,只是进入了自己未曾涉足过的另一个区域。我没有找志愿者讲解,听听每个连廊,每座桥,每间堂,每棵树或许不一样的故事。因为在我看来,故事再动听也只是故事;而且听故事,也并不需要来到这里。来,便是为了亲身感受。遗憾的是,我人来了,却没有生出什么太多感受来。

回客房的路上,还是会穿过那条著名的平江路,只是返程更加从容。再次从那些让人眼前一亮的店铺门前走过。然而我的眼神依然没有亮起来。这些店一般都很小,一张大门上面挂着个牌子,就是它能展示给路人的全部。所以大多数的小店都是把墙及门都做成玻璃的,好让人们能看到更多有意思的东西,还会把最具代表性的产品挂出来,还可能专门派个伙计到门口引导。每家店的风格都很别致,看上去都有点儿意思。但这其实让我很恐慌,因为它们像是以一种最具活力的方式散发着一种死亡的气息:它做的一切都是为了做生意,为了活下去。这得是多么糟糕的环境,才能激发出如此挣扎的求生欲呢?看着那一扇扇大门,仿佛看到一张张血盆大口,而我就是一只迷途的羔羊,要选择从哪只口中穿过。于是我再一次地,匆匆走过,像是逃跑一样。一路上只是看看江上的扁舟,听听摇桨人唱着的民谣。

类似的感觉,不啻于平江路。

一路上,我也回忆过之前来苏州的感觉。思考着为什么找不到那样的感觉。是因为苏州变了?还是我老了?看着我的小女儿,哪怕是捡到一段小树枝,也能开心地玩起来。我想,应该是我老了吧。但是好像又不是,因为人老了,并不应该乐趣变少呀,只是应该会更高级一些。小屁孩吃个糖就开心半天,而我已经不会了。而且更糟糕的事,开心的事儿好像越来越少了呢。

真是有些遗憾呢。

佛系程序员 第五品 如理实见分

这一品我考虑了很久,始终没有开始写。因为这一品虽然很短,但是可以说是整本经文最深奥、难以阐释的部分。我自己都没有完全理解,又何德何能去为人解说呢?但是这一品又不能不讲,最重要的东西不去讲,就失去了写这个系列的意义。这一品,在我看来,是在讲三观。然而这年头,一说三观就特别敏感,我忘了之前在哪里听到过这样的一个人生经验,说:做人、做事,不要输出自己的三观。我想我是基本赞同的,虽然并不严格执行。再盗用另一个更接地气的说法,一个人的思想就像内裤,你穿没穿,穿的啥样的,你自己知道就行了,不用逢人就脱了裤子给人看。我这里主要是想分析一下佛祖的内裤,但是不免会夹杂一些私货。诸位看官自行分辨吧。

我们先来看看本品的经文:

金刚经 第五品 如理实见分

“须菩提!于意云何?可以身相见如来不?”“不也,世尊!不可以身相得见如来。何以故?如来所说身相,即非身相。”佛告须菩提:“凡所有相,皆是虚妄。若见诸相非相,即见如来。”

佛祖最后告诉须菩提的话,大概是《金刚经》最出名的几句话之一了,因为这句话的确涵盖了诸多法门。我对这句话也有很多个方面的理解,个人感觉最重要的一个方面是,这是讲看待一切事物的方法论。“一切事物”有点空,具体点讲,我认为包括从写代码到与人交往等所有事情。在我展开解释之前,我希望各位看官能先自己想一想,你自己是如何理解这句话的。

先说“凡所有相,皆是虚妄”。相,我的理解,是泛指一切具体的或有形态的或是可以名状的东西。为什么说他们都是虚妄的呢?因为那只是表现形式、手法。并不表示真实的内涵。真实的内涵,一般都是抽象的。比如,我问,你能不能简单说说你对面向对象编程的理解。一般人听到这个问题可能会组织一下语言,概括一下。而我见过的一个牛人是这么回答的,抱歉我对面向对象编程的理解没办法简单说说。

他说得是对的。如果真的对面向对象编程理解到了一定程度,是没办法三言两语说清楚的。一个复杂的问题,一定是需要一个同等复杂的语言才能描述清楚,也需要一个同等复杂的理论才能解释和解答。所以,如果有人说,面向对象的主要特性,就是继承、封装、多态。但是又没办法从这三个词引申、延展出多少相关干货出来,那他一定就是个水货。

而且,你如果把三个对面向对象很懂的人叫一起,聊聊面向对象,一定会发现他们的理解是不一样的。甚至有些点会有冲突。最可能的是,他们三个人,没有一个人说的是完全正确的。而且,别说仨,即使你把所有的面向对象专家都叫来搞个轰趴,你也没办法聊出一个绝对正确的东西出来。

找不到绝对真理,这一点儿问题都没有,因为我们解决大多数问题,甚至可以说所有具体的问题,都并不需要先找到一个绝对真理才能解决。

上面这些分析的另一层意思是:无论谁说的什么话,都不会是绝对到位的。甚至连本意都不是。因为一个人说的话,总会是他心中所想的很小的一部分。他为了说给你听,可能只是取了他觉得最重要的一部分讲给你听而已,并不会完全、完整地表达自己的观点。然后你听的时候,漏一部分、误会一部分、再脑补一部分。你的理解,和对方心中真实所想,就差十万八千里了。哪怕说的人毫无保留、听人的虚心接收,这种情况也在所难免。

上面的,都是“相”,所以说“凡所有相,皆是虚妄”。只是佛祖的意思比上面说的这些要更广大得多。所以出家之人都是六根清净、六亲不识的。

我再举一个例子,上面我说,复杂的东西就需要复杂的东西来描述。你听完上面的解释后,现在又是如何理解的呢?沃尔夫勒姆,在2002年出版的《A New Kind of Science》中提出计算等价性理论,认为:

自然界中各种过程实现的计算在复杂程序上都几乎等价。

自然界中的过程有简单,有复杂的。如果他们都是等价的,有没有办法用简单的方式描述、处理复杂的问题呢?

你可以试着回答一下这个问题。一点儿提示是:你随手在一把尺子上划一下,这个划痕所示的刻度,是一个无理数,而任何一个无理数,因其无限性和随机性,都可以被认为包含着人类发展到现在的所有知识的总和。那么你能说,你随手一划,就复制了人类所有的知识吗?

上面是说“相”。下面聊聊“身相”和“见如来”。

前段时间,一个老同学看了我前几篇之后来问了我一个问题。大体是问,佛教所宣扬的教义就是让人超脱轮回,为什么藏传佛教还搞什么转世灵童。我说这个问题有点儿大,我得写个文章才能解释清。其实我四年前在知乎上也问过一个类似的问题:“既然“诸相非相”,那为什么佛院里还要立佛像?”。本质上都是想问佛祖有没有点儿逻辑,自不自洽。怎么感觉说一套做一套呢?

这个问题说起来简单,但是其实很不简单。

简单的回答是:佛教不同于佛法。佛教团队其实是很世俗化的民间或官方组织。再多的我就不讲了。

稍微复杂一点儿地正面回答这个问题:佛法中的跳出轮回,并不是跳出生死的意思。死还是要死的。但是“阿赖耶识”会延续下来。这个名字,是不是有点儿眼熟?对的,就是圣斗士星矢里沙加领悟的第八感。

再复杂点儿的解释,会牵扯到身相的“圣义谛”和“世俗谛”的区别,就太佛学了,我自己水平所限,还没办法解释到这一层。

程序员的价值和末日

最近和一些人聊到现在的程序员这个职业(是啊,程序员和程序员在一起还能聊些什么呢),更加加深我几年来对工程类程序员(泛指一切技术岗位的员工)这个职业的一点儿感受。主要包括这么几个观点:

  1. 程序员越来越不值钱。这是大势。
  2. 程序员的价值,归根结底在于省钱。
  3. 企业用人要求越来越高,学校教育脱节与之越来越严重。
  4. 技术演进速度迅猛。思维理念、知识体系,五年间就能变得陈旧。

这几点看上去没啥直接关系,其实彼此互有关联。我来解释一下。

程序员的Good Old Time

我记得高中的时候,有一次心血来潮,把爷爷年青时评选高级经济师的论文打成Word文档。我记得也就不到一万字,我花了一个小时才打完,都不好意思让人看见。然而我爷爷一直在旁边转悠,看我打完了居然还夸我打得快,说他当年,他也要打出来,但是不会电脑,专门找了打字员,打了半天才打完,打完还请人吃饭。我听得一脸蒙逼。心想怪不得人们都怀念Good Old Time。妈得原来那个年代,会打个字都能混得风声水起的。

然后呢?当我满怀希望地学了计算机,从校门出来的时候,当时公司老大们训话的话风就变成了:“你们不要天天NB哄哄的,把现在你们全开了,我就算只招六指的程序员都能招满。”这位老大这么有底气,是因为那个年代,程序员就已经满大街都是了。而打字员这个职业已经没有了。

当时,离“会打字就能吃香的喝辣的”的那个年代,也就20多年吧。

当然那位老大毕竟还是说笑的,当年学计算机的,的确也还是很吃香的,我刚毕业那会儿,大公司招在校生都是飞来飞去,公司靠谱点儿的,给实习生都是租个别墅住(当然不是一人一栋)。实习工资都够别的专业的正式工资。

现在又10多年过去了,就算BAT有这么干的吗?没有的。为啥?因为程序员越来越不值钱。虽然相比其它行业还是高不少,但是相比过去,已经差太远了。而那位老大的笑谈,恐怕也快成事实了。

那么程序员为什么越来越不值钱了呢?我先来讨论一下程序员为什么值钱。

程序员的价值

商品的价值,决定于创造这个商品的社会平均劳动生产率。

商品的价格,还会受市场供需关系的影响。

这些话可能有人还记得,我没记错的话,应该是初中思想政治课上就讲到的。我当年觉得政治、经济是最没用的学科,现在反而感觉,这两是九年义务教育里,在成年后的生活中,最能用上的两个学科。

程序员不是商品(好吧,也许是),那程序员的价值在哪儿呢?程序(软件)吗?不,不是软件,你买个Office,装在电脑里,从来不去用它,它产生价值了吗?并没有。你下载个理财App,从来不买理财产品,这个App产生价值了吗?也没有。

为什么用Office就有价值呢?为什么从理财App上买理财产品就有价值了呢?我不用Office可以写在本子上啊,我不用理财App,可以去银行办理啊。

你想想,哪个效率高?哪个成本高?

所以程序员的价值在于:程序员创造出来的东西,能用极低的成本,提高所有(买了这个软件的)人的效率和产出。简言之,程序员是史上最强大魔法系奶妈的角色。

当然这里的程序员是指群体,毕竟不是所有程序员都做具体软件的,但是从宏观角度看,软件行业做的事儿,就是以极低的成本,颠覆其它各个行业的劳动生产率。

所以程序员的平均工资高,因为软件行业创造的价值的确很大。尤其是在节省原有的人力、物力上。所以说,程序员的价值,其实是省钱。

程序员的贬值

然后回到原来问题上,“为什么说,程序员越来越不价值了呢?”。因为软件业,和其它行业最大的不同在于,软件的发展是累积的,不需要每次从头开始做,而且成长速度没有物理量约束。而且,代码是无国界的。

从小的说,基本所有编程语言的库都自带的排序算法,现在的程序员,除了在学校和在某些面试官面前,根本不会去写什么排序算法。

往大里说,现在有Spring Cloud,有Dubbo,有Istio。你说你也会写服务治理框架,这很好,但是不用写了,你只要会用就好。写了也没有真正的价值。因为这个问题已经被大体解决了。在软件行业,除非有技术壁垒或专利保护,一个问题只需要被解决一次。第二次做的价值基本为0。当然,价格不会为0,因为总会有公司愿意为这第二次到第N次的实现掏钱的。

所以你会自己从头写一个服务治理框架,很好,也能拿到高工资,但是没有价值。这只是有价格而已。除非你做得比前人的都好,会贡献出Delta部分的价值。

那么现在程序的价值在哪?是你能把一个东西(无论是自己写的,还是现成的)在一个公司用起来,为公司解决他们业务上的独特的问题。才是有价值的。这时你会发现,用现成的东西的成本会更低,于是利润空间会更大。

随着软件行业的日益成熟,能创造的新的价值空间会越来越小。而程序员的价格(即工资)从长期来看,总是要回归其价值的。所以很不幸的,最终价格,按我的逻辑,会是0。只是可能要个几百年吧。

程序员的脱节

做程序做久了,会发现程序员做的事情一直在发生天翻地覆的变化,变化快到很多程序员根本跟不上这个节奏。于是公司招人也面临一个尴尬的囧境:会写排序算法的,会Copy Paste的,会写CRUD页面的教科书式程序员汗牛充栋。但是想招一个能创造性地用代码高效地解决实际问题的人就凤毛麟角。而且更糟糕的是,CRUD程序员多得已经营造出了种程序员就是CRUD的不良氛围。这和现在国内互联网行业如火如荼,大都处于跑马圈地的阶段不无关系。

但是当潮水退去了呢?

程序员的末日,就要来了。

 

佛系程序员 第四品

多年之前,我当时的BOSS的BOSS来上海,就坐在我边儿上。那几天憋了半天,憋出了一个我感觉挺重要的问题,看他不太忙的时候就问他:“你觉得我们项目上什么是最重要的,或者说,我们遇到了事儿,应该按什么标准、什么方向判断优先级呢?”那位祖师爷转过头来,盯着我看了两秒,就盯得我心里一阵毛,然后严肃地和我讲:“Hugo,没有任何一个原则,是能应对所有问题的。你明白我的意思么。”。然后聊天结束。

其实须菩提问佛祖也是一个手段问题:“云何应住?云何降服其心?”。佛祖的回答也是很简单,和我BOSS的BOSS说得其实是一个意思:

金刚经 第四品 妙行无住分

“复次,须菩提!菩萨于法,应无所住,行于布施,所谓不住色布施,不住声香味触法布施。须菩提!菩萨应如是布施,不住于相。何以故?若菩萨不住相布施,其福德不可思量。须菩提!于意云何?东方虚空可思量不?”“不也,世尊!”“须菩提!南西北方四维上下虚空可思量不?”“不也,世尊!”“须菩提!菩萨无住相布施,福德亦复如是不可思量。须菩提!菩萨但应如所教住。”

本品划重点,非“应无所住”莫属。本系列写到了这第四品,总算到正题了。这四个字就是我在本系列中想表达的重点。这个字面意思很容易懂,但是真用上就不那么简单了。

我说“真用上就不那么简单了”,有人可能体会不到是什么意思。我试举一例。我面试时常遇到这样一类候选人,你问他LinkedList和ArrayList的区别是什么,他能讲得很清楚。然后在这个问题的铺垫下,再让他写个简单的代码,遇到需要使用一个List的时候,他随手就用了ArrayList,然后和他一起现场Review一下,问,这个场景下,会不会有什么性能问题?提示到这种程度,还是想不到这里应该考虑用LinkedList,非得等我指着代码,一一说破,他才恍然大悟,信誓旦旦地表示刚才只是大意忘了这个点而已。

一个最基本的知识,一个自己能讲得清清楚楚的东西。真到应该用的时候,并不能用得上的。什么原因呢?因为他知道的是“知识”,应用“知识”的过程,是一个组合问题,你要把所有已知的相关知识和当前的问题一一对应过之后,才能全面地回答一个问题。这是一个至少是O(n)复杂度的问题,当解空间太大的时候,没人有这个脑力。一个“知识”,只有内化成“见识”、“智慧”之后,才能以O(1)的速度求解所有问题。那么怎么内化呢?

看过《倚天屠龙记》的人应该对张三丰教张无忌太极时的经过印象颇深:

  张三丰一路剑法使完,竟无一人喝采,各人尽皆诧异:“这等慢吞吞、软绵绵的剑法如何能用来对敌过招?”转念又想:“料来张真人有意放慢了招数,好让他瞧得明白。”

只听张三丰问道:“孩儿,你看清楚了没有?”张无忌道:“看清楚了。”张三丰道: “都记得了没有?”张无忌道:“已忘记了一小半。”张三丰道:“好,那也难为了你。你自己去想想罢。”张无忌低头默想。过了一会,张三丰问道:“现下怎样了?”张无忌道: “已忘记了一大半。”

周颠失声叫道:“糟糕!越来越忘记得多了。张真人,你这路剑法很是深奥,看一遍怎能记得?请你再使一遍给我们教主瞧瞧罢。”

张三丰微笑道:“好,我再使一遍。”提剑出招,演将起来。众人只看了数招,心下大奇,原来第二次所使,和第一次使的竟然没一招相同。周颠叫道:“糟糕,糟糕!这可更加叫人胡涂啦。”张三丰画剑成圈,问道:“孩儿,怎样啦?”张无忌道:“还有三招没忘记。”张三丰点点头,收剑归座。

张无忌在殿上缓缓踱了一个圈子,沉思半晌,又缓缓踱了半个圈子,抬起头来,满脸喜色,叫道:“这我可全忘了,忘得干干净净的了。”张三丰道:“不坏不坏!忘得真快,你这就请八臂神剑指教罢!”说着将手中木剑递了给他。张无忌躬身接过,转身向方东白道: “方前辈请。”周颠抓耳搔头,满心担忧。

傻子都看得出来,张三丰在传授剑道、剑意,而不是在传授剑招。我们推衍一下,这里的“道”是什么呢?学编程、写代码,做人、做事儿有没有“道”呢?

佛祖的道,就是无所住。不住‘色’,不住‘声’、‘香’、‘味’、‘触’、‘法’。有人可能想问:这些色啦、法啦都是什么意思呢?这不重要,因为这里只是举例、比喻,佛祖也没办法把所有你可能要住的东西都枚举出来不是?有人可能又想问:那就是什么原则、法则都不要啦,做个不讲原则的人啦?更不是。我只能诡辩说“不讲原则”也是一个原则。因为更深的意思,我也没办法讲出来了。我只能建议说,不要总想着分析具体什么应该做,什么不应该做。那就是“邪道”了。无论你分析出什么结果,都是有所住。

我几年前就常常感觉有点儿对不住我手下的小弟。因为他们有时来问我一些具体的问题,我没办法给到他们一些正面的、直接的、具体的回答。我的回复大都都是方向性的,原则性的。然后他们自己折腾半天。但是如果事情到我这里,他们又会看到我好像什么都能搞得定,而且还挺快。我起初是有些担心,他们会不会觉得我就是故意不告诉他们。后面大家共事久了,就不会有这种担心了。因为他们会发现,我这人向来是知无不言、言无不尽的。只是有些人终归还是不习惯我的风格,喜欢谈具体的东西,不喜欢谈抽象的。这也无可厚非。而我也其实并不总是对所有人都有足够的耐心。这时候就照章办事儿就好。

佛系程序员 第三品

《金刚经》的前面两品都是铺垫,佛祖开始说正事是第三品。正文如下:

金刚经 第三品 大乘正宗分

佛告须菩提:“诸菩萨摩诃萨应如是降伏其心!所有一切众生之类:若卵生、若胎生、若湿生、若化生;若有色、若无色;若有想、若无想、若非有想非无想,我皆令入无余涅盘而灭度之。如是灭度无量无数无边众生,实无众生得灭度者。何以故?须菩提!若菩萨有我相、人相、众生相、寿者相,即非菩萨。”

当年年少中二的时候,读到第一句“我皆令入无余涅盘而灭度之”,就去闭目参悟武功心法去了,后来什么都没悟出来,这经的后半句也没读下去。直到多年之后,再来读的时候才发现,原来佛祖根本不是这个意思。

我先来翻译一下大意:

佛跟须菩提说:“你们应该和他们讲:如果你说‘我把所有众生都度了’。其实就没人被度,因为你有众生相的话,你就连菩萨都不是。(菩萨都不是,你度个毛啊)”

先小科普一下“灭度”,即“度灭”,就是超脱生死的意思。只是一种得了无上智慧后的一种状态,不是指死。涅槃也不是死的意思。没死也可以涅槃。

佛祖这里说的‘度’、‘菩萨’,其实只是一种比喻。而不是单纯地说‘度’或教育‘菩萨’。而是在阐释一种理念。我想了很久,如何用一个比较现代化的比喻来解释这个理念。但是感觉比喻还是太间接,还是直接讲理念是什么比较直接,用比喻的形式来说话,对听众的“慧根”要求太高,可能听了还是听不懂,而一句话说了,听众没听懂,就是白说。

佛祖的这个理念可以有很多种讲法,比如“内容”和“形式”,“动机”和“手段”,“过程”和“结果”。其实都是同一个理念的不同表达形式。

“度众生”,是形式、是手段,是外在的结果。佛祖想说的是,心大于法。如果你动机不纯,过程不正,内容不善,做什么都是徒劳的,不会得正果。(佛祖在这里没说什么“纯”、“正”、“善”,我纯粹为了工整而硬加的。而且佛祖想说的其实也并不是这些,佛祖想说的就是心态。无我,无人,无众生,无寿者。度只是手段。没有一个合适的心态,度也是白度。)

所以做人做事儿,最重要的是你的本心。明朝大哲王守仁创立的心学也表达了类似的观点:

爱问:“至善只求诸心,恐于天下事理有不能尽。”

先生曰:“心即理也。天下又有心外之事、心外之理乎?”

——《传习录 卷一》

这里的心,指的是“道心”,而非“人心”,和佛教里的“阿耨多罗三藐三菩提心”大体是一个意思。至于两者的区别,王守仁自己的解释是:

“只说‘明明德’,而不谈‘亲民’,便似老、佛。”

意思是认为佛学只是让人开悟、明至理,但是普罗大众完全用不上。所以说王守仁的心学理论上是比佛学更接地气并有实用价值的。但是心学IPO才小几百年,没有佛教根基深厚。王守仁的名气也没释迦牟尼大,所以人们平时聊天放屁、插科打诨都是拿佛祖开涮,提王明阳也没人知道啊。

你看哈,很多宗教、理学的经典其基本理念都很类似,按初中政治、历史课本的话说,都是唯心的,都是错的。说到唯心,我就又想插一嘴。我一直就特别纳闷儿,你看哈,量子力学解释不了女人为什么老问男人诸如“我和你妈同时落水你先救谁”之类的问题,也没人说量子力学是错的,但是有些人总是用唯心主义解释不了这个世界运作的原理为由,认定所有的唯心主义学说都是荒谬的。

这个扯远了。

我们回到第三品的经文上,这个在说什么事儿呢。我先引用知乎上的一个问题,结果正义是不是就可以无视过程的正义性。我回答说不是,我说因为你的过程,可能就是别人的结果。而这段佛经更进一步,过程都不够,而且要考量初衷及动机。所以你看出家人总是一本正经、心无旁骛的,因为他心有邪念都不行。佛祖就看不上他。

这段经文还说了什么呢?我们想想之前的问题是什么?须菩提问的是,如何降服善男善女的心,让他们也像如来佛祖一样有大智慧,得以超脱(涅槃)。

那佛祖回答这些是什么意思?其实有两层额外的意思(佛祖从来不说狠话,所以没有直说):

  1. 须菩提,你这是想去度化众生么?你这想法就不对啊。你在分你、别人、众生。
  2. 众生其实是度不了的。如果能度,佛祖就直接说了。而且,众生是不需要度的。佛祖也从来没有想过去度化谁,甚至也不关心有没有人能度。

说白了就是在批评教育须菩提。

至于说原文里的我相、人相、众生相、寿者相都是啥意思,因为我并不是在讲经,这里就不展开了。

佛系程序员 第二品

如何最快地学到新东西呢?一般找对人,然后问对的问题,大概算是比较便捷的方式之一。但是问对的问题其实很不容易,因为想问对的问题,有两个前提:知道自己知道什么、不知道什么,还知道自己想要什么。面试的时候,如果真有人在提问环节什么都不问,我心里基本就把这个人放弃了。因为不提问的人,要么是什么都不知道,要么是对做事儿没兴趣,要么是对事儿没自己的想法,只会人云亦云,要么是对我这个人有成见,懒得和我说话;无论是哪种情况,都不是什么好现象。所以干脆直接拒掉好了。

《金刚经》这么平易近人的作品,写作手法上也是和小说有一拼,真就是师徒一问一答的形式。感觉比《XXX语录》还像是语录。第二品,和第一品一样,看上去又是一堆废话。我们来瞻仰一下原文:

金刚经 第二品 善现启请分

时,长老须菩提在大众中即从座起,偏袒右肩,右膝着地,合掌恭敬而白佛言:“希有!世尊!如来善护念诸菩萨,善付嘱诸菩萨。世尊!善男子、善女人,发阿耨(nuò)多罗三藐三菩提心,云何应住,云何降(xiáng)伏其心?”佛言:“善哉,善哉。须菩提!如汝所说,如来善护念诸菩萨,善付嘱诸菩萨。汝今谛听!当为汝说:善男子、善女人,发阿耨多罗三藐三菩提心,应如是住,如是降伏其心。”“唯然,世尊!愿乐(yào)欲闻。”

其实说白了,就是须菩提问佛祖如何收了世人的心。那么为说这么一堆废话呢?因为多出来的每句话,都是有意义的。我们一起来分析一下。你也可以说我的说法牵强附会。但是佛经这东西啊,比《哈姆雷特》还哈姆雷特,比《红楼》还红楼。照例先翻译一下。

当时,有个叫须菩提的长老,从人群中坐起来。光着右膀子,右腿跪着,作合十礼,毕恭毕敬地向佛祖问道:“好久不见啊,老大哥,觉悟者们喜欢跟菩萨们讲道理,事事叮嘱。老大哥,如果有些有志青年男女,也想成为觉悟者,得到无上的大智慧。那我们菩萨们,得跟他们讲什么作人作事儿的原则?怎么说才能让他们信服呢?”佛说:“没错,没错。须菩提啊!就像你说的,觉悟者们的确喜欢和菩萨们讲道理,叮嘱他们。这回大家都好好听着,我就跟你们掰扯掰扯:‘如果有些有志青年男女,也想成为觉悟者,得到无上的大智慧。那我们菩萨们,就可以这样讲作人作事儿的原则,从而让他们信服。’”。(大伙齐声说:)“就得这句话了,老大哥,我们想听想得高兴得屁颠屁颠的。”

第一句,“时,长老须菩提大众中即从座起”。须菩提是谁?佛祖座下十大弟子里排前三吧。大众呢,就是第一品提到的1250个僧人。“从大众中即从座起”,什么意思?不是从世尊附侧座起。明白了么。第二品,第一句,还是在说平等。

佛经的写法和《圣经》最大的不同点在于,佛经都是讲故事,聊闲天,你从中读出什么看客自便。所以我说佛经比《红楼》还红楼。而《圣经》基本就是上帝说要什么,就是什么。让天下雨天就下雨。上帝说什么你必须听,不听就死去吧,死了还要下地狱。所以我一直不是很理解,上帝心肠这么歹毒的人,还有这么多拥护者。看来人们还真都有点儿“斯德哥尔摩综合征”倾向。

佛就不一样,特别的亲民。然而须菩提在发问前还是把该做的礼节一个不落地都做了。为什么呢?他不注重礼节,佛祖也不会拿他如何——估计顶多也就不搭理他了呗。不过佛祖胸怀这么宽广的,会这么小心眼么?他不是衣食都自理的么?这其实是在描述第二个道理:一个人,如果知道什么是对的,什么是好的,那么照做就是了。不用在意对方或其他人如何看待。这其实不仅仅需要见识,还需要勇气。须菩提对佛的尊敬,并不是做给佛看的,也不是做给其他人看的,而且也不是因为自己觉得对而做的,因为为了对而做,也是错的,甚至不是做给自己的。

再来看须菩提问问题是如何问的。先说,“希有!世尊!”,这是打招呼,就像写信要写“致某某某”,后面要署名,不能只写正文,这些都是规矩。还是礼,还是尊敬。

后面又做了一个铺垫:“如来善护念诸菩萨,善付嘱诸菩萨。”,什么意思呢?问别人问题,如果不想显得唐突,就得写解释一下来龙去脉,说话也得有个起承转合。别人才好接话不是?这可以说是话术,也可以说是礼。

当然啦,佛经原文是梵文,这个版本的《金刚经》是姚秦(即后秦,公元4世纪的一个朝代)时翻译的,那个时代的人很重礼,至于这些繁文缛节是译本才有的特点还是梵文就有的意思?我就不清楚了。但是这重要吗?不重要的。

我们先回到问题上,问题的核心就一句话:“善男子、善女人,发阿耨多罗三藐三菩提心,云何应住,云何降伏其心?”。佛祖听了之后如何回答的呢?

佛祖先是肯定了他的想法和疑问,然后把这个冗长的问题复述了一遍。这简直和现在发布会或演讲中的问答环节别无二致啊。这套路原来已经传承千年了。当然我相信这肯定不是佛祖首创。公共场合讲话,总是要照顾多数人的。然而在佛祖的铺垫中,有几个字是套路之外的。这几个字才是佛祖想着重表达的:

汝今谛听!当为汝说

汝,就是你;今,这回,现在;谛听,好好听、仔细听。然后我才给你讲讲。意思是什么呢?一层意思是,你不好好听,可能根本就听不懂。其实后半句还有一层意思是,如果不是你要听,我也不会讲。你这问了,我便趁这个机会讲讲。

这里的“听”,不是listen的意思,也不是hear的意思,而是comprehend的意思。这三者有什么分别,我在知乎问题《为什么有很多学生在老师讲课的时候什么都懂,但是做题却不会做?》下面有相关的解释。这里就不展开了。

佛祖的行事风格是,听众们不想听,或是没想到去听,便不会非讲给你听。几乎所有的佛经,都是这种弟子问然后佛祖答的形式。你不问,我便不说。因为多数情况下,说了其实作用也不大。因为想法上的事儿,得自己有听的意愿,也有理解的意愿,才能理解并接纳新的想法。自己钻牛角尖,陷进去出不来的人,佛祖是连话都懒得讲的。通俗点儿讲,佛祖也是认同“狗改不了吃屎”及“烂泥糊不上墙”的。

须菩提是聪明人,一听就懂。立马表态:“愿乐欲闻。”,比“谛听”的程度还高。当然啦,这里应该是没有拍佛祖马屁的意思的。更多的还是礼节而已。

总结一下第二品,这一品主要讲人和要自己的想法、见识,不受旁人眼光、看法的影响;又要有开放的心态去了解别人的想法,并能接纳别人的想法。没有这个基础,读多少书都没有用。

佛系程序员 第一品

金刚经我读了有不下100遍,基本每一遍都有新的理解。有些感想总是想写下来,且不管有没有人会看。至少经年之后,自己有了更多的感悟,再回来看的时候,也好有个印证。金刚经共有三十二品,此为第一品感悟。

金刚经 第一品 法会因由分

如是我闻,一时,佛在舍卫国祗树给孤独园,与大比丘众千二百五十人俱。尔时,世尊食时,著衣持钵,入舍卫大城乞食。于其城中,次第乞已,还至本处。饭食讫,收衣钵,洗足已,敷座而坐。

我第一次读到这里的时候就停下来去Google “金刚经 原文”,“金刚经 正文”。因为一眼看上去就觉得不正经。就像是某个佛家弟子写的听经文后的自传,而不是释迦摩尼写的经文。但是找了半天,发现这真的就是正文原经的时候,我愣了半天,这和我想象中少林寺困住狮王谢逊的“金刚伏魔圈”的本源之经实在相去太远了。不过能让六祖慧能开悟的经文。我还是耐着性子读了下去。

我先翻译一下:

我是这么听说的,有一回,佛在“舍卫国祗树给孤独园”这边,和250位和尚在一块儿。正赶上佛祖他老人家要吃饭了,于是穿上衣服,拿着要饭的钵盂,到城里去要饭。大伙在城里挨着个地要完饭,回到园子时各自的位置上,把饭吃完,把衣服脱下来和钵盂一起收好,洗了个脚,铺了个地垫坐了下来。

咳,“衣”或许应该翻译成“袈裟”吧?不过这不重要。说重点。佛教的无上经典(我突然感觉应该也可以作为丐帮的),一开头把佛祖的日常起居介绍一下,有什么特别的地方么?没有吧。没有就对了。佛祖本来就没什么特别的。佛祖想表达的也是这个。佛祖也要吃饭,也要穿衣服,也要洗脚,站久了也会累,也没个像样的地方讲经,更没有什么神迹。就是个普通人。但是他也是特别的,因为他是佛祖,百千万亿劫(佛教时间单位,一劫大概等于268亿7680万年)才出一位的无上正觉者(得“阿耨多罗三藐三菩提心”者)。

这在告诉我们一个很简单的道理:所有人,其实都是一样的、平等的。好,这又是一句废话——直到你真正明白它的意思。

我不知道我是否明白了,但是我感觉这是在讲,其实作人呢,并不需要真正地畏惧、敬仰、羡慕、嫉妒、敌视、鄙夷任何人。因为人啊,都是一样一样一样的。所以和人讲话什么的,甭管谁,不用脸红,说错了他又不会打你。哪怕上手打架,泰森站对面也不用怵,反正他也不屑于打你的,对吧?

比如我见过一些人,知道自己在某些技术方面不是特别好,做东西就特没有信心,做得对不对都不敢讲,更不要问做得好不好了。其实在一方面就是没明白一个道理:别人能做到的事儿,你只要不是智力上或是态度上有什么问题,应该终归也是可以的,而且不需要别人的肯定,才能有自信说做得好了。你只要真明白在做什么,自己就会有标准。因为人和人都是一样的。

但是至于说,为什么有人住千亩大平层,有人住桥洞搭窝棚呢?

因为人和人其实是不一样的。而且人和人之间的差距和收入的差距一样,是能差几个数量级的。程序员更甚。

我不是说人是一样的么,怎么就又不一样了呢?我说话有没有逻辑?这不是我的逻辑,这是佛祖的逻辑。想得读懂《金刚经》,你得适应佛祖的逻辑。后面几品《金刚经》里,出现最多的句式就是:佛说XXX,即非XXX,是名XXX。所以我说读懂《金刚经》,就不是说真是读懂《金刚经》,而是称之为、当作读懂《金刚经》。我说“当作”,就不是“当作”,是当作“当作”……

这段还说什么了呢?佛这么NB的人,岁数也这么大了,他也没让人照顾自己吃喝拉撒睡分毫。都是自己做的,自己穿衣服,自己洗脚,而且井井有条。为什么这么讲呢?如果去看下其它上部座经文就知道了。基本上都是这样开头的。(下部座经文像《本愿经》、《法华经》什么的就算了,那里的佛就开始自带主角光环了。)

写这些又是什么意思呢?其实是在讲,吃喝拉撒睡也是经文,吃喝拉撒睡也是修行。好,这是什么意思呢?

这个可以有很多意思。我这里只想说一个,就是,你想修行(或想学习)是可以随时随地的。

有人时常说自己没有XX机会去做XX。比如,没时间看书,没时间学习,没时间做这儿,没时间做那。其实吧,不是真想做罢了。真想做的事儿,总是可以做,而且随时随地。

第一品的东西不多,我就先说这些。开头没别的,就是要把心态放正,建立信心,并切实地做下去。无论做什么事儿,这都是前提。放第一品。非常合适。

注:本文仅仅描述我对经文的理解,并不表示我完全赞同并实行。

佛系程序员

最近面试了大概上百位求职者,和有些人谈得开了,有人会让我给他点儿建议。我大都只是建议他们培养出一种把事儿做好的心态。有缘的,我会建议去读一读《金刚经》。听者基本都是一脸不知所以的表情。倒不是说我之前有从《金刚经》中悟出了什么,只是后来读《金刚经》发现其中有很多理念,与我对写代码的想法不某而合。当然,也可以说是我自己一厢情愿地想当然罢了。下面我简单地介绍一下这些理念。

首先,一说《金刚经》或任何一部佛学经典,人们常常会有的一个刻板的印象可能就是:和尚才要读那东西。我一开始读《金刚经》的动机就不太纯正,我就是想看看佛祖这么牛逼的人是怎么忽悠,啊不,感化人的。后面我读完了就发现自己脸就肿了。《金刚经》中有个四句偈,是这样说的:

“若以色见我,以音声求我,是人行邪道,不能见如来。”

说得很白,我就不解释了。如来为了被人误解,甚至还说:

若人言:如来有所说法,即为谤佛,不能解我所说故。

我十分想知道那些天天烧香拜佛的人听到佛祖这么讲会有什么样的心理活动。

好,如来说他什么都没说,那《金刚经》说了些什么呢?六祖慧能说,这句话可以说是《金刚经》的总旨:

应无所住,而生其心。

这句话能展开解释成很多东西。我这里就不展开了。回到前面说的给求职者的建议上,我给候选人推荐读《金经刚》的意思其实是:你不要依赖别人如何讲、如何做,你要有自己的见识,自己的作为。我说什么,说得再对,再好,也是我的,不是你的。然而,我又不是真的就是要让人按这个原则做。因为按任何原则做,其实都是“有所住”。但是呢,又不是说让人抛弃所有原则。《金刚经》还说:

汝若作是念:“发阿耨多罗三藐三菩提心者,说诸法断灭。”莫作是念!

这大概就是“不可说,说即是错。”的来源吧。

所以呢,只能你自己去看《金刚经》,至于能从中读懂多少,就要看个人的悟性和缘分了。这个在《金刚经》里也给读者打了招呼:

若善男子、善女人,于后末世,有受持读诵此经,所得功德,我若具说者,或有人闻,心即狂乱,狐疑不信。须菩提!当知是经义不可思议,果报亦不可思议。

《金刚经》,在我看来,是本破除愚昧的书。

从用缓存优化函数性能说说第三方框架的使用

导读

前面两篇文章分别从计算机及领域知识编程语言本身的角度聊了聊写代码要考虑的事儿。但是理解了计算机系统、领悟了领域知识、精通了某门编程语言就够了么?我在编程到底难在哪里的回答里说,软件开发是搭积木式的,如果你想搭得好又有价值,一个合理的办法是在已经搭好的基础上继续搭,而不是把所有东西自己重新搭一遍。JDK和.NET Framework都是这种已经搭好了的最基本的架子。想想软件发展了几十年,各式框架已经一应俱全,基本想做个任何事情都已经有现成的框架可用。当然,如果你觉得现在已经搭好的东西就是坨屎,当然可以也应该自己搭个更好的。但是现实的情况大都是:人家明明已经搭好那么多各式各样的建筑并欢迎你去添砖加瓦,结果只是因为自己不知道、不了解那些东西的存在,自己重头搭了个双层火柴盒还觉得不错。软件开发显然比在minecraft里搭积木要复杂且抽象些。随之而来的一个问题就是:可应用框架的地方并不显而易见,尤其是当你觉得自己写一个更快的时候。这也是本文将要讨论的重点。(在校生请不要误会,在校生请夯实基础并从基础搭起,能搭多少搭多少。)

引子

假设我们发现这个下面开平方函数很慢,比如要一秒,我们Profile之后发现函数慢在Math.sqrt这个第三方函数上,遗憾的是下层Math库的作者声称他那边儿没有进一步提速的空间了,而且他已经不再维护了。然而我们死活就是想要提高这个函数的性能,怎么办?

public double sqrt(final double n) {
    return Math.sqrt(n);
}

代码 1

解决方法倒是有不少的,比如升级硬件,比如换一个更快的库,比如开多线程,或者把Math的作者高薪挖过来。只是这些方案的成本都不低。考虑到这个函数是个纯函数,又考虑到内存并不是很贵,如果又能和用户确认一下他们的输入并不是随机的,有比较高的重复度和局域性的话。把这个函数的运算结果保存下来或许是最多快好省的方案了。实现起来也很简单(注:请无视代码2的拆装箱损耗,那不是问题重点)。

private final Map<Double, Double> rootMap = new ConcurrentHashMap<>();

public double sqrt(final double n) {
    return rootMap.computeIfAbsent(n, Math::sqrt);
}

代码 2

这个实现的一个问题就是它只加不删。如果用户真能保证他们的输入有比较高的重复性的话这个问题倒不大。但是保不齐存在一个2B用户没按约定使用,系统最后就OOM了,然后常见的后果是这个软件员会被拉出来背内存泄漏导致宕机的锅。所以保险起见应该给这个Map设定一个上限。到了就把老化的结果删除。(注:请无视代码中try..finally及iterator的用法,那也不是重点)

private final Map<Double, Double> rootMap = new LinkedHashMap<>();
private final int maximumSize = 1000;

public double sqrt(final double n) {
    try {
        return rootMap.computeIfAbsent(n, Math::sqrt);
    } finally {
        if (rootMap.size() > maximumSize) {
            rootMap.remove(rootMap.keySet().iterator().next());
        }
    }
}

代码 3

如果用户只要求精确到小数点后两位就够了。那么一个简单的提高命中率的改进就是把参数先进行舍入再执行运算。

private final Map<Double, Double> rootMap = new LinkedHashMap<>();
private final int maximumSize = 1000;

public double sqrt(final double n) {
    final double r = round(n, 2);
    try {
        return rootMap.computeIfAbsent(r, Math::sqrt);
    } finally {
        if (rootMap.size() > maximumSize) {
            rootMap.remove(rootMap.keySet().iterator().next());
        }
    }
}

public double round(final double n, final int scale) {
    return new BigDecimal(Double.toString(n))
              .setScale(scale, BigDecimal.ROUND_HALF_UP))
              .doubleValue();
}

代码 4

好了,到目前为止,在不考虑线程安全性的前提下,我对这个函数结果留存的实现方案算是比较满意了。然后才是本文的正题:代码4的问题是什么?如何解决?

问题在哪儿

业务逻辑不明确

想象一下,如果你第一次看到这个代码4,你能否一眼看出哪些代码是业务逻辑的需要,哪些不是?

这还仅仅是为保存一个方法的执行结果,就多出了十几行代码。这还没打Log呢,还没处理异常呢,还没加调用信息统计呢,还没做运行时Profiling呢,还没保证多线程安全呢。

请想象一下,你把上面的这些附加功能都按从代码1到代码4的实现方式加上去,这代码还能看么?

解决思路很简单:业务逻辑应该与非业务逻辑分离

代码冗余、维护成本高

软件开发的一个基本的原则就是写的代码要测试一下。你自己写了个round方法,你就得写相应的测试覆盖这个函数吧。你代码多,测试也会多。这都是实现上的成本。

另一方面,这回的情况是这样:你在实现这个功能的时候,发现需要这个round方法,于是自己写了一个。另一个程序员在界面显示的时候为了做截断可能也会自己写个round方法。大家在同一个Team、同一个代码库上工作,最好是不要出现这种很多人做重复性的工作。怎么解决呢?可以写个utils包,每个人想到什么感觉可能对别人也有用的东西就放在里面。

但是这其实是个悖论,如果一个程序员在实际项目上连类似这种round的方法都去自已写,有两种可能:一、公司什么库都不让用或是用之前要走半年流程什么的,这个可以通过跳槽来解决。二、意味他并没有事先查找可用utils的意识,没有这种意识的话,就算有别的成员放在项目内utils包里他大概也是不会找的。当然,这个情况可以这样解决:谁写了之后,大家开个会广告一下。最终结果大概会是:程序员们为了不开会,非常自觉地都不再写任何utils了。

更好的办法就是,只要有哪个著名的开源库里有的,大家就都不要自己写这种常见的功能。当然这个办法对于一小部分人来说也会有问题:谁知道我要的东西在哪个开源库里有?这个问题只有Google和Stackoverflow能回答。然后Google之后发现了一堆库可以用的,那么用哪个呢?

如果就是不乐意用别人的东西,非想要自己写,也行。在Team里找一两个最NB的人专门负责这些低层框架上的东西。并给所有人做好Training。

最要不得的就是想到什么就写什么。就像代码4那样。它看上去代码质量也挺高,Style也挺不错,目测也没Bug,但是成本高,没法维护、不好拓展。

可重用性差

考虑一下,如果你需要把Math类里的所有函数都做这样的处理呢?我猜会不会有人想用Map<String, Map<String, Object>>什么的?而且只改一个文件就行了呢!

再考虑一下,如果这个函数的调用会抛出异常呢?你是想把异常也保存一下呢?还是想下次重试呢?

上面的Map使用的是最简单的FIFO策略,如果你需要LRU呢?

还有,你这里把round之后的结果当Key,如果有的需要用toString()的结果当Key呢?

你每种情况具体分析分别实现还是为之写个通用框架呢?

在别人的肩膀上搭积木

善用第三方库

这个世界上的肩膀很多,如果想搭好自己的积木就要找到合适的肩膀,最重要的一步是找到那个重复出现的问题是什么,找到那个模式(不限于设计模式,也包括规范、代码模式等)。

限定大小的Map就是个会重复出现的问题。round一个double值也是个会重复出现的问题。显然这些问题Apache和Google都遇到过,并把他们的库公开了出来给大家用。用上之后代码就可以简化很多。(注:Guava也有类似的Map)

import org.apache.commons.math3.util.Precision;
import org.apache.commons.collections4.map.LRUMap;

private final Map<Double, Double> rootMap = new LRUMap<>(1000);

public double sqrt(final double n) {
    return rootMap.computeIfAbsent(Precision.round(n, 2), Math::sqrt);
}

代码 5

这个例子很容易,但是实现中的情况会比这个复杂得多。复杂的不是要实现的功能本身,最难是你能不能找到模式,找到可以重用的现成的东西,然后最重要的:用对。不要拿着锤子看什么都像钉子。比如,如果你觉得Java 8的Stream不错。但是把Stream这样用就不对了。(注:代码来自Stackoverflow的某问题的回答)

// BEING: Wrong usage example
public Double round(final Number src, final int scale) {
    return Optional.ofNullable(src)
                   .map(Number::doubleValue)
                   .map(BigDecimal::new)
                   .map(dbl -> dbl.setScale(scale))
                   .map(BigDecimal::doubleValue)
                   .orElse(null);
}
// END: Wrong usage example

代码 6

这个代码就是在风格或某一特性上走极端了。

剥离非业务逻辑

代码5仅仅是用上了些第三方的库,代码相对简洁了些。但是比代码1还是复杂了3倍。而且你一眼看去,很容易就产生误解,把rootMap当成了核心。而其实Math::sqrt才是。

现在我们目标很明确,就是要让我们的代码只做我们需要做的事情,让库和框架去解决别的和业务本身没关系的问题。代码5做到了一部分,但是不彻底,而且还有侵入性(你要用一个框架就要对现有代码大动干戈的性质)。有没有办法让代码回到代码1的程度呢?

有人可能想到了。AspectJ可以做到。只要在项目里放一个这样的Aspect,然后就直接用代码1就是了。(注:此为示意代码)

@Aspect
@Component
public class StoreMethodResult {
    private final Map<Double, Double> map = new LRUMap<>(10000);

    @Around("execution(* *.Calculator.*(..)) && args(value,..)")
    private Object round(ProceedingJoinPoint pjp, double value) 
        throws Throwable {
        return map.computeIfAbsent(Precision.round(value, 2), v -> {
            return (Double) pjp.proceed(new Object[]{v});
        });
    }
}

public class Calculator {
    public double sqrt(final double n) {
        return Math.Sqrt(n);
    }
}

代码 7

我们解决了代码4的问题,成功把它简化回到了代码1的程度。但是这样搞会引入两个新问题:

  1. StoreMethodResult和Calculator是两个类。单看Calculator你是无法知道Calcuator在执行的时候它的结果会被保存下来。所以你给代码1加一个能力、功能、机制的时候,还是最好在Calculator本身上留下点儿线索。一个代码是注释。更好的办法是用Annotation。按这个思路做下去,就会需要一个Annotation Driven的Aspect。
  2. 代码7本身也存在类似代码4的问题:它也是在重新造轮子。这个轮子叫缓存(Cache)。

通用化方案

这个函数调用结果保存下来的需求实在是太广泛了,肯定已经有现成的通用解决方案了啊。Spring Cache就是其中的一个方法。使用Spring Cache和Spring Boot的实现方式就是这样:

@SpringBootApplication(scanBasePackages = {"your.package.name.*"})
@EnableCaching
public class Application {
    @Bean
    public CacheManager getCachemanager() {
        final SimpleCacheManager simpleCache = new SimpleCacheManager();
        simpleCache.setCaches(Lists.newArrayList(
      new ConcurrentMapCache("math")));

        return simpleCache;
    }

    @Cacheable(value = "math", 
        key = "T(org.apache.commons.math3.util.Precision).round(#n, 2)")
    public double sqrt(final double n) {
        return Math.sqrt(n);
    }
}

代码 8

代码看着好多,但是前面的都是初始化Cache的部分。对于sqrt函数而言,只需要放个@Cachable就行了。

用Spring Cache不是目的,少写代码也不是重点。请注意在这个实现方案下,函数与其附加能力是放在一起的,而与主要业务逻辑又是分离的。这才是重点。库和框架的使用,应该是为了让你更高效地写出更可读、更少Bug的代码。如果你用一个框架之后,发现代码比之前更不好读,你可能得想想你有没有用对,或是你选择的这个框架本身的设计理念是不是合理。但是代码好不好读这个事儿有些主观,有人反而会觉得用了一堆他不会正确使用的框架的代码才是不好读的。我就呵呵了。相关讨论请参考本系列第一篇《从开平方说说写代码应该考虑的事儿》。

JSR-107 JCache

在程序中Cache结果的能力是如此基本,人们早在2001年就开始了把它放在Java框架里的讨论,即JSR-107。但是不知道是不是因为用的人太多,争论过于激烈,这个JSR在2012年才发布了早期预览草案。直到2014年3月才发布了最终版。

Spring Cache是在2011年引入到Spring 3.1 M1的。Spring Cache本身不是缓存框架,它是各种缓存框架与Spring的胶水(虽然它也自带了个实现,但是功能性要差很多)。Spring Cache的实现和JSR中推荐的做法非常相近,再从Spring Cache发布和JSR的时间线看来,或许Spring Cache在推动JSR-107的进程上也发挥了不小的作用。

在2014年4月,JSR-107最终发布的一个月之后,Spring就在4.1版中提供了对JSR-107的支持

然而JCache毕竟和Spring Cache还不是完全一致的。所以使用JCache实现的版本看上去会有些不一样。(注:CacheManager的创建还是需要的,只是略去了。)

@CacheResult(
        cacheName = "math", 
        cachedExceptions = { IllegalArgumentException.class })
public double sqrt4(@CacheKey final double n) {
    return Math.sqrt(n);
}

public double roundedSqrt(final double n) {
    return sqrt3(Precision.round(n, 2));
}

代码 9

不难发现JCache的Annotation和Spring Cache的用法略有不同。于是产生了下面几个不同:

  1. 由于自定义key表达示的缺失不得不引入一个新的函数来实现Round的行为。
  2. cachedExceptions的出现又让我们相对比较容易地处理异常。
  3. 独立的@CacheKey的用法相对直观,但是灵活度不足。

各有各的优劣。没有哪一个是完美的。在实际使用中应该根据实际需要选择最合适的框架。需要注意的是,这种使用方式上的不同,对于框架的选择一般是次要性因素。其使用方式上的不同,往往是其设计理念与目标的不同的外在体现而已。而设计理念与目标,才是选择框架的主要指标与标准。

 综述

像本文这样举一个例子,说明正确使用库与框架带来的诸多好处,是件很容易的事儿。然而实际这样做起来绝不会像说起来这么简单。因为你不仅仅要完成需求,还要识别出其中的模式与通用部分,查找可用的库或框架,然后再学习这些库的正确使用方式并集成到你的项目中。从代码4到代码8,代码没多,但是其实是比自己写一个更难,所花的时间也更多。(尤其是你还并不知道也还不会用那些框架的时候)。然而这些多出来的步骤都不是障碍,最大的障碍是心魔。

当你千辛万苦调了几天Bug把代码跑通大功告成之后,是觉得这是结束?还是刚刚开始?愿意不愿意想一想有没有别的方法做同样的事儿?愿意不愿意去把各个方案比较一下?愿意不愿意承认自己一开始拍脑袋想到的办法可能不是最合适的?如果是项目工期十分紧张的情况下呢?如果你的同事、你的领导甚至整个公司都并不赏识你的做法,只求一个个的项目能按时上线呢?如果你现在已经处于天天加班,天天调试老代码又不敢大改的窘境了呢?

那些都不是真的困境,那些都是自己的心魔。

从一个小函数说说编程语言的学习

PHP是世界上最好的语言[1]。

——有人说

编程语言层面上的东西根本不是重点,把基本功打好,学一门新的语言就是信手拈来。

——又有人说

这俩梗好老。

——我说

引言

上一篇文章《从开平方说说写代码应该考虑的事儿》讨论了如何熟悉并利用领域知识和计算机知识编写更高质量代码。这一篇也是从一个非常简单的编程问题开始,通过分析针对这个问题的不同实现方式,阐释学习一门语言都要学些什么以及怎么样才算熟悉了一门语言。

注:本文无意嘲讽《21天精通XXXX》系列,毕竟或许还是有人可以做到的,尤其是在现在这种有人只要用过就敢妄称精通的大环境下。本文只是想说明一些显而易见却又被人熟视无睹的事实。

一个简单的问题

不少人可能毕业之后就从来没写过哪怕排序之类的基本算法,这很正常,但是没人用不到各类Containers(Collections)。而且,但凡是代码写得足够多又不勤于复制粘贴的懒人,就少不得写些工具类把一些常用操作独立出来。比如这样一个函数:

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
}

我常常拿这个简单的函数当面试题,基本没有人猜不出来这个函数是要干什么,倒是会有个别人表示不知道前面的<T>是干什么用的。有性子急的可能很迅速地就构思出了如下实现,然后下一秒就发现坑在哪儿了。(简洁起见,本文所有代码略去参数为null之类的边界检查):

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    for (int index : indexes) {
        if (index < list.size())
            list.remove(index);
    }

    return list;
}

代码 1

显然上面的代码结果是不正确的。我们下面就来一步步地分析,看这个函数在Java里应该如何实现,都有哪些坑。主要意图是在这个过程体会,怎样才是充分地并合理地利用一个语言本身的理念与机制去解决问题。我也建议各位看官也先想一想自己心中的答案是什么再往下看。

先说需求到底是什么?

一说到“正确”,一说到“方案”,不少有经验的程序员的标准回答就是“看情况(It depends)”。看什么情况?看到底是要解决什么问题。我曾经也很喜欢这样回答问题,听上去多特么严谨。但是有时候这么说就不合适了,比如面试的时候这么回答就是犯傻,把到手的按自己能力方向展开并引导面试节奏的机会拱手还给了面试官。知道什么情况就说什么。没有具体上下文,要么自己假设一个;要么说自己的实现方式为了通用起见,所以牺牲了这些、这些、这些。即使不是面试,也不要说“看情况”,除非你真打算把情况一一列举出来并分情况讨论,你乐意说可能听的人也没耐心听,因为问问题的人一般只需要其中一个情况,你不嫌累,听者也会嫌烦。

回到问题上来。所以这个函数的需求是什么?需求就是函数签名所描述的那些。就像Array.Sort,它需要知道具体被使用的环境吗?它只要做到最好就是了。

函数签名能告诉你的,并不仅仅是行为,而且可以有实现细节。比如:

  1. 它是一个泛型函数:这大概是一个Util方法。
  2. 它返回一List<T>:所以它应该生成一个新的List<T>,意思应该是不要动参数List。
  3. 下标集合也是List:所以可能有重复,要处理。

等等。

正确性

代码1的问题是,它没有意识到删除了一个元素之后,后面的元素的下标就都变了,而且没有处理参数下标重复的问题。(还有别的问题后面再讨论)

于是一个简单的改进就可以同时解决上面的两个问题。

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    Collections.sort(indexes);
    for (int i = indexes.size() - 1; i >= 0; i++) {
        final int index = indexes.get(i);
        if (index < list.size())
            list.remove(index);
    }

    return list;
}

代码 2

这个没什么难的,所有人都可以做到这一步。遗憾的是不少人走到这一步就不再走了。因为它已经是正确的了(如果先不考虑对参数的改动)。我不知道是不是因为喝了Bert Lance那句“If it ain’t broke, don’t fix it.”毒鸡汤,并成功解释成了“如果它没坏,就不要改进它,不要重构它,不要动它!而且我也只能做到这里了!

正常情况下,如果并不需要复杂设计与编码就可以做到O(n)的函数,还是不要一开始就写成O(n^2)的好吧?代码写成这样,好意思在文件头上留下自己的名字么?(要不要加是另一回事儿。)

线性时间复杂度

在上面的代码基上,一个可以提高性能的改进是这样的:把要排除的元素标记成null。然后找出不是null的元素。

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    for (final int index : indexes) {
        list.set(index, null);
    }
    final List<T> result = new ArrayList<>();
    for (final T value  : list) {
        if (value != null)
            result.add(value);
    }

    return result;
}

代码 3

这个代码有两个问题:

  1. 性能还不是O(n)。
  2. 显然如果参数list里本来就有null就不行了。

问题1后面会展开讨论。对于问题2,理论上,可以用一个自己私有的对象实例代替null。

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final T DELETE_MARKER = new T();    // Compile Error
    for (final int index : indexes) {
        list.set(index, DELETE_MARKER);
    }
    final List<T> result = new ArrayList<>(list.size());
    for (final T value  : list) {
        if (value != DELETE_MARKER)
            result.add(value);
    }

    return result;
}

代码 4

但是很可惜的是,由于Java的类型擦除机制,类似上面的方式在Java里是不可行的,因为T在运行时不存在。想知道T是什么?在Java里你得多传一个Class参数进来。而C#里上面的代码是可行的(当然需要额外泛型约束才能编译通过,但那不是重点,重点是C#可以知道T具体是什么)。

既然这种方式遇到了些障碍。我们就换一个思路:这个函数一共就俩参数,不for这个就只能for那个。(如果你解决问题真是这么个思路还是不要当程序员了。)

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final List <T> result = new LinkedList<>();
    for (int i = 0; i < list.size(); i++) {
        if (!indexes.contains(i)) {
            result.add(list.get(i));
        }
    }

    return result;
}

代码 5

代码5的结果正确,但是性能又退化成O(log(n))或O(n^2)了。想想,为什么是有“或”?为什么有两种情况?

通用性(独立于参数实现)

请思考如下几个问题:

  1. list.contains的复杂度是多少?
  2. list.get的复杂度是多少?

List的某些操作的时间复杂度取决于其实现方式,甚至还会依赖于调用者的使用模式。对于JDK中常见的几个List实现,在本文的使用方式下,他们的理论上的时间复杂度会是这个样子的(有没有哪个跟算法书上说的不一样?请想想为什么):

ListOperations

图1

也就是说,代码5的时间复杂度会依赖于传入参数的类型。这显然不够理想。有没有办法让这个函数的运行效率无关参数类型呢?

问题1出在List的contains方法上,无论在哪种实现方式下,它都是O(n)复杂度的。好像无解啊?那有没有什么Container的contains方法是O(1)的?当然有啊:

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final List <T> result = new LinkedList<>();
    final Set<Integer> indexesSet = new HashSet<>(indexes);
    for (int i = 0; i < list.size(); i++) {
        if (!indexesSet.contains(i)) {
            result.add(list.get(i));
        }
    }

    return result;
}

代码 6

问题2出在get操作上,LinkedList的get慢,但是ArrayList的get是O(1)的啊。所以心够宽的话,大不了可以先把整个list复制到一个新的ArrayList里不就结了?没错,但是那代码太美我不敢贴上来。那有没有别的方法呢?想一想现在的情形:我们无论如何是要遍历整个list,而且应该只需要遍历一遍。

这明明就是Iterator的地盘啊。所以用iterator就好了啊。

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    int index = -1;
    final Iterator<T> iterator = list.iterator();
    final Set<Integer> indexesSet = new HashSet<>(indexes);
    final List<T> result = new LinkedList<>();
    while (iterator.hasNext()) {
        final T item = iterator.next();
        index++;
        if (!indexesSet.contains(index))
            result.add(item);
    }

    return result;
}

代码 7

有经验的程序员其实一开始想的就是这个方法。这就是有经验的程序员和菜鸟的差距。到这一步,这个函数在功能上性能上都达到了预期的要求,而且做到了不依赖具体参数类型。面试的时候,如果有人能在15分钟内做到这一步,我就已经很高兴了。

但是,本文的重点要讲的,还没有开始。上面只是热身:熟悉问题本身。

正题才刚刚开始:那都是Java代码吗?

上面的确都是Java代码,因为它们都符合Java 8语言规范啊,能编译,能运行。但是不觉得有什么问题么?就这么简单的一个问题,怎么需要这么多代码呢?感觉什么地方不太对啊。

感觉不对是因为,这种代码,也不是Java,这是几乎所有OO编程语言及其框架的公共子集。它没有充分利用Java Collections Framework所提供的各类机制,上面的那些代码,放在C#里,放在Python里,放在C++里,仅仅需要修改一下语法和类名就可以同样地正确工作起来。如果学一种语言只是学到这门语言的这样的公共语言部分,其实就可以解决所有编程问题,不再学习任何新的语言点也能写出所有需要写的代码来,只是写起来就像上面一样——繁复。学一门语言,如果只是学到这种“公共子集”程度,的确是可以“一通百通”,信手拈来,几个小时学完。但是这离学会那门语言,还差得很远。

本文要实现的这个函数,大概算是最普通的编程问题了,它所涉及的,是几乎每个Java程序员天天都要使用到的东西。也就是JCF的那些接口,那些类。我说上面的代码没有充分利用Java,所以我们看看JCF的三个主要接口里都有些什么?请仔细看一遍下面两张图,有没有哪个函数名让你眼前一亮或是一头雾水的感觉呢?各位看官看这些方法的时候请思考下面几个问题:

  1. 了解这些API的用法,是否需要依赖工作上的项目提供特别的机会?
  2. 你觉得需要几年的工作经验才能让一个人知道并能在合适的时候正确地使用这些函数?
  3. 知道这些函数的使用,算熟悉Java?精通Java?还是没有直接关系?

JCFInterfaces

图 2

图中,绿色高亮的是Java 8新引入的接口。黄色高亮部分是辅助性函数,就是那种你完全不知道、不使用,也能无障碍地写出Java代码的函数。没高亮的函数,是必须要有的,也必然会用到的,也是那种每个人都不得不知道的,所谓的语言公共子集部分。当然,如果有人说他连remove方法都用不着,或是说从来只用iterator,我也并不觉得奇怪。

充分利用Java

我们看到List接口里有个比较特别的ListIterator或许不是所有人都知道。我们也来看看这个ListIterator和Iterator比有什么特别的地方。注意:ListIterator继承Iterator。

ListIterator

图3

所以稍微对Java多熟悉一些,就可以知道,从ListIterator是可以在遍历一个List的过程中知道当前index而不需要自己维护一个index变量的。于是我们的代码可以简化成:

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final ListIterator<T> iterator = list.listIterator();
    final Set<Integer> indexesSet = new HashSet<>(indexes);
    final List<T> result = new LinkedList<>();
    while(iterator.hasNext()) {
        if (!indexesSet.contains(iterator.nextIndex())) {
            result.add(iterator.next());
        }
    }

    return result;
}

代码 8

如果用Java 8,那也可以通过新的removeIf来实现(想想这个函数线程安全吗?):

private static int index = -1;
public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    index = 0;
    final Set<Integer> indexesSet = new HashSet<>(indexes);
    final List<T> result = new LinkedList<>(list);
    result.removeIf(t -> !indexesSet.contains(index++));

    return result;
}

代码 9

如果喜欢用Stream呢,还可以写成这样:

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final Set<Integer> indexesSet = new HashSet<>(indexes);

    return range(0, list.size())
            .filter(i -> !indexesSet.contains(i))
            .mapToObj(list::get)
            .collect(toList());
}

代码 10

然而不幸的是,这个代码又出性能问题了。因为它依赖了输入参数list的get方法。在前面的代码中,我们说过,总是可以把list先Copy到一个新的ArrayList中。但是如果传入参数本来就是ArrayList就是多余的了。所以要么就先检查一下参数类型是instanceof ArrayList不就结了?但是如果参数是个自定义的List呢?要不要先给参数做个Quick Perf测试来侦测其get方法是不是O(1)的再来决定要不要先Copy到ArrayList中呢?

在一个成熟的框架里写代码的一个好处是:你可以先假设你遇到的一切General的问题都已经有人遇到过了,而且很可能已经在框架层解决了。你只是得:

  1. 先意识到这个是General的问题。
  2. 形式化规范化这个问题。
  3. 然后用正确的姿势Google一下看看有没有人已经解决了。

比如代码10中的特定方法的性能问题,JDK已经意识到这个问题并且帮你解决了,有个专门的无方法接口叫RandomAccess用来标识一个List的get方法的时间复杂度是不是O(1)或比随机多次get快(详参文档)。那么代码中就可以用这个接口做为判断标准:

public static <T> List<T> except(List<T> list, List<Integer> indexes) {
    final Set<Integer> indexesSet = new HashSet<>(indexes);
    final IntStream indexesToSelect = range(0, list.size())
            .filter(i -> !indexesSet.contains(i));

    if (list instanceof RandomAccess) {
        return indexesToSelect.mapToObj(list::get)
                              .collect(toList());
    } else {
        final List<T> accessList = new ArrayList<>(list);
        return indexesToSelect.mapToObj(accessList::get)
                              .collect(toList());
    }
}

代码 11

当然,这个代码正确工作的前提是,那个自己实现了一个List接口的人,也知道正确地使用RandomAccess接口。

好了,代码8到代码11展示了如何充分使用Java框架来解决这个问题。但是不幸的是,有些代码变得更恶心了(脸好疼)。每种方式都有自己的优劣,应当在使用的时候根据实际需要选择合适的方案。

LinkedList vs ArrayList

以上我们分析过参数的具体类型会对函数性能产生的影响。但是还没有具体讨论过以上代码3至代码9中所创建的临时List对象的选择问题。有心的人可能已经发现,有的用的是ArrayList有的用的是LinkedList。那么用得对吗?更进一步,什么叫用对?

Java自带了这两个主要的List的实现。从图1中可以看出他们在性能上的差别。理论上,我们可以根据自己的使用方式,根据是get操作多还是add/remove操作多来决定是用LinkedList还是用ArrayList。这是我们以上分析的主要依据,如果你Google网上的类似问题,这也是大家的建议。

但是问题是,这个依据对吗?add/remove多的时候,用LinkedList就一定好吗?还有什么其它会影响性能的因素呢?

CPU缓存命中率

CPU缓存命中率主要取决于:所需要访问的数据的大小,在内存中的分布及最重要的访问模式。那么具体到这个函数的CPU缓存命中率,主要受List本身(不包括各个List项T本身)在内存中的分布的影响。请试着想象一下他们两者在内存中的分布情况。大体会如下两图所示。

ArrayList内存分配示意图:

ArrayListMemory

图 4

LinkedList内存分配示意图:

LinkedListMemory

图 5

所以最糟糕的情况会是什么样的呢?你每次访问的LinkedList中的一个Node都Miss CPU的所有一、二、三级缓存。当然实际一般不会这么糟糕,但是绝对比ArrayList要糟糕。缓存不命中的后果就是从内存中读取数据。从内存读又怎么了呢?我们得先看看CPU缓存的访问速度与内存访问速度的差距才能回答这个问题。

从下图可以看出各级缓存结构的访问延迟(Core i7):

AccessLatency

图 6

不难看出,根据问题规模的不同,内存访问会比CPU缓存慢3至50倍。那么这会对LinkedList的性能产生多大的影响呢?看访问延迟,估计有最大50倍性能差吧。不过性能上的事儿,必须要测试一下才知道。

在当前所实现的except函数的环境中,使用List的方式是创建一个默认大小的List,然后只增加不删除。我们就来模拟这个使用方式的下,不同List实现随着问题规模增长时性能的变化。结果如下图所示:

AppendBenchmark

图 7

从图中不难看出,在问题规模比较小(小于一两百万数据)的时候,LinkedList有些许优势,而当问题规模大于五百万数据之后,由于数据局部性不足及CPU缓存容量的限制,LinkedList的性能下降得非常快。(据图可以估计出测试平台的CPU L3缓存大小,不是Core i7)

所以简单起见,你可以在所有的情况下都使用ArrayList,而不用太担心其插入、删除的理论性能比较低的事儿,其数据的局部性可以把大O的系数拉到很低。如果你的所预见的使用情况都只有不到几万数据量的话,用LinkedList也是不错的。当然其实这么少的数据,随便你用什么都可以。

上面这个结论对所有语言都是成立的。如果想深入了解,可以参考《Number crunching: Why you should never, ever, EVER use linked-list in your code again》。Bjarne Stroustrup也在其讲座中表达过同样的观点

有人可能会问,那根据输入的大小选择合适的List实现不就结了吗?也行,就是代码会变得貌似一坨屎,而且还得根据CPU型号选取临界值。如果不想自己的母亲时不时被后来看代码的人问候,还得写注释解释清为什么这么做。另外,调用的人也会很不爽,他会面临这么个问题:这个函数有时候返回LinkedList有时候返回ArrayList。我想多数人会希望API的行为是下面几者之一:
  1. 一直返回一种实现。
  2. 参数是什么类型,也返回什么类型。
  3. 不依赖任何List实现,让调用者根据结果自行构建List。

这样这个API的行为才是可控的。

Java有两个List实现!太漂亮了!

这里想说些题外话,我们如果退一步想想,上面代码复杂度问题的根源,其实是Java本身造成的。它自带了两个List的实现,于是有心的Java程序员们在写代码的时候,无时无刻不在想着:我这回应该用哪个实现呢?但是写C#、写Python什么的,你完全不用考虑这个问题。因为它们都只自带了一种List。因为LinkedList实在是没什么存在的必要。大概唯一的好处就是强迫一部分过于懒惰Java程序员们动动脑子。在某人不小心用了LinkedList,搞出个大大的性能问题,经过简单的分析就可以知道他写的那个List处理函数是多么的糟糕:一个不小心就把一个O(n)的函数用出了O(n^2)的效果。自此LinkedList臭名昭著:性能杀手,效果拔群。于是JDK又引入了RandomAccess接口给这俩List擦屁股。说你们应该在写函数时候多多考虑一下非RandomAccess的List的感受嘛,你看我连接口都设计给你了,你不用?怪我喽?

如果对此有话题有兴趣,可以参看这个知乎问题下面的前五个回答。为什么那么多公司不用 .NET,而选择 PHP、JSP,是 .NET 有什么缺点吗?(这个问题虽然都没问Java,下面一群回Java的,呵呵)有个回答说的好:“.NET的问题就在于不能有效提高使用者的智商。”。Java不仅能,而且擅长。

想我写了快十年C#代码,其间几乎没有细想过List的实现是用数组更好还是用链表更好这个问题(毕竟也很少直接用List)。然而写Java的第一天我就意识到了。这个问题在Stackoverflow上很火。其中有一个排名不高的的回答有些碍眼。他说,LinkedList的作者Joshua Bloch(此人也是Effective JavaJava Concurrency in Practice两本书的作者),发过这样一条Twitter

Does anyone actually use LinkedList? I wrote it, and I never use it.

看这语气,说像是在说:“我写了个用于IQ测试的Java类,你们通过了吗?”

通过以上二节的分析,主要并不是想解释如何在LinkedList和ArrayList之间做取舍,而是想说明一个事实:这些分析里有计算机本身的约束,也有不少是Java语言本身的一些细节,而不同的语言里,针对同一个问题会有不同的设计,不同的细节。如果不了解这些细节,并不能用合理的方式使用这门语言。而这些才是学习语言的难点。(PHP充斥着各种细节,所以成了最好的语言。)

掌握一门语言的法门,只能靠深学活用。能通过一个下午熟悉的,只有语法而已。而语法本身,常常并不是一门语言最重要的部分。甚至连重要都算不上。

接口隔离原则

前面讨论了Java里两个List的现实之间的取舍问题。一个终级解决办法就是不做取舍,让调用方自己去从结果集构建List,如果他需要一个List的话。意思就是说,我们不要求参数是List,也不返回一个List。而只使用完成这个函数所必须的操作(同时不损失性能)。这就是最小接口依赖原则的含义。哪么哪些接口是我们为了实现这个函数所必须要使用的呢?其实就一个:遍历。于是我们的except函数可以被简化成这样。

public Iterable<T> except(Collection<T> list, Set<Integer> indexes) {
}

indexes的参数变成了Set,也更符合这个操作本身的要求。直接返回一个Iterable,可以让使用者按自己的要求和具体环境选择下一步所需要的具体数据类型。我们来考虑如何实现这个函数,虽然从语法上,直接返回一个List是合法的,但是显然已经不符合我们的目标。所以,在这个版本的实现中,真的需要返回一个纯粹的Iterable才能达到效果。(思考题:如果有个子类重写了这个函数并返回一个具体的List,是否算违反了里氏代换原则?)

public static <T> Iterable<T> except(Collection<T> list, Set<Integer> indexes) {
	final Iterator<T> iterator = list.iterator();
	final Set<Integer> validIndexes = indexes.stream().filter(i -> i < list.size() && i >= 0).collect(toSet());

	return () -> new Iterator<T>() {
		private int cursor = 0;

		@Override
		public boolean hasNext() {
			return cursor < list.size() - validIndexes.size();
		}

		@Override
		public T next() {
			while (validIndexes.remove(cursor))
				move();

			return move();
		}

		private T move() {
			cursor++;
			return iterator.next();
		}
	};
}

代码12

这还是用Java 8写的,都写了这么长,如果用老Java,代码将更长。解决长代码的方式也很简单,就是看看这个代码是不是承担了太多的责任。但是这么简单的操作,你说它承担了太多的责任,这不是搞笑么?但是它的确是的。它做的事情,至少可以分成这两步:

  1. 求indexes的补集。
  2. 按indexes选取元素。

于是这个操作可以被分解成这样:

public static <T> Iterable<T> except(Collection<T> list, Set<Integer> indexes) {
    return select(list.iterator(), 
              range(0, list.size()).filter(i ->
                      !indexes.contains(i)).iterator());
}

public static <T> Iterable<T> select(Iterator<T> list, Iterator<Integer> orderedIndexes) {
    return () -> new Iterator<T>() {
        private int cursor = 0;

        @Override
        public boolean hasNext() {
            assert list.hasNext();
            return orderedIndexes.hasNext();
        }

        @Override
        public T next() {
            while (cursor < orderedIndexes.next()) {
                cursor++;
                list.next();
            }

            return list.next();
        }
    };
}

代码 13

情况看上去更糟糕了-_-,但是这个新的select函数在通用性上显然比except要高得多,它也可以被except之外的其它的集合操作函数所使用。其它的集合操作函数,只是需要先通过纯粹的数学运算计算出要提取的元素的位置,然后最后调用select函数。

但是,即使这样,这个3行的except,跟它所需要解决的问题相比,也还是没必要地复杂了。

Iterable vs List

使用Iterable相比使用List,除了缩小了接口依赖之外,其实还有几个的不小的好处。

  1. 延迟求值。意思是这个函数本身运行时间变成了O(1)。
  2. 内存占用从O(n)减小到了O(1)。因为只返回了一个Iterable,而没有返回任何数据。

但是这些好处不是没有代价的。使用Iterable也会带来几个不小的问题。

  1. 隐式依赖参数列表。
  2. 不可重复使用。
  3. 非线程安全。
  4. 由于3,所以不可并行化。

第一点很明显,就当成思考题吧。第二点给个使用环境当引子。

private static <T> void printTwice(Iterable<T> items) {
    items.forEach(s -> System.out.println(s));
    items.forEach(s -> System.out.println(s));
}

代码12和代码13与之前的代码相比,在上面的使用方式下会具有不同的结果。而这个结果是编码者不想看到的。(C#里近似的接口IEnumerable,由于合理的接口设计,没有这个问题。)

可并行性

对于性能要求比较高的环境,可代码本身的并行性也会一个重要的代码可用性及质量的考量。如果用Java 8的流实现,并且实现成纯函数,好处之一就是可以比较简单地支持多线程并行计算。基于代码10,可以通过一个函数完成并行化。如代码14所示:

public static <T> Collection<T> except(List<T> list, Set<Integer> indexes) {
    return range(0, list.size())
                .parallel()
                .filter(i -> !indexes.contains(i))
                .mapToObj(list::get)
                .collect(toList());
}

代码 14

然而如果想把使用Iterator的代码并行化就没有那么轻松写意了。如果你有兴趣的话,可以写一个可并行化的Iterator试试。

Java vs .NET

写了12个版本的Java代码了,断断续续地花了好几天。代码越来越长,却始终找不到一个简洁、高效、灵活、可扩展的实现方式。想想就觉得火大。C#程序员可能早就憋不住了,这个问题的C#版实现方式不要太简洁。

public static IEnumerable<T> except<T>(IEnumerable<T> items, ISet<int> indexes)
{
    return items.Where((t, i) => !indexes.Contains(i));
}

看着这代码,顿时觉得写代码还是那么的美好,而且它具有一切好处:线性时间、延迟求值、参数无关、最小依赖等。写法也简单到我都不屑于提取出来一个函数。想想真心疼Java程序员们。不过或许这就是Java提高使用者智商的方式呢!再说了,我们都知道语言层面的东西都是不重要的。对吧?(这么想心里会舒服些,毕竟我也在写Java。)

如果你在写C#,可能不自觉地就免费地获得了不少的好处,这是一种幸福。但是这并不表示应该在写代码的时候可以无视上述的那些潜在问题。否则就成了Java程序员所鄙视的那种只会拖拖控件的码农界食物链末端生物。

.NET自动地为程序员们做了很多事情,而且做得很出色。但是做得太好,好到程序员代码写烂了它都能工作得还不错,结果不小心把不少.NET程序员都给惯傻了,傻到出点儿问题都不知道怎么下手处理,然后还反过来骂.NET是垃圾。而Java程序员从一上手写代码开始,就要开始解决这样那样的问题,水平慢慢地就被迫堆高了。其后果就是.NET始终被Java压制,这大概是微软始料未及的吧。

那些事儿

在引言里说过,这篇文章只是想说说那些显而易见的事儿。然而一千个人眼中有一千个哈姆雷特。所以我担心不说得清楚些,还是会有人不知道我在说在什么。本文的目的不是解释这个except函数在Java里怎么写也不是为了展示Java里多样的写法。本文要说的那些事是:

  1. 编程没有简单问题,再简单的问题都有多种解法适用于不同的环境。
  2. 学习一门语言从来不是一个简单的事儿。但是学习到这个语言的公共部分是个很简单的事儿(就像大牛们宣扬的,你基本功扎实的话,一两天的事儿)。
  3. 学习一门语言到公共部分就可以解决你工作上的所有问题,而且在这个层次,的确可以说用什么语言写代码都是无所谓的。(因为你总是可以自己搞定剩下的语言特定部分——大凡都通过重新造轮子的方式。)
  4. 深入学习一个语言,从来不是一个简单的事儿。(那怕简单如Java都是这样,我也没有深入学习过Java,我只是简单了解了一下JCF,然后看了点Best Practice而已。)
  5. 在你把一个语言学到一定深度之前,你可能会看到一些代码看不太懂。这个时候正确的积极的心态是承认自己的水平不够,而不是抱怨这个代码的可读性太差、搞得别人都看不懂。
  6. 最好等真精通了一门或几门语言之后再谈论什么一通百通,否则对于个人成长而言遗患无穷。(以Java为类,感觉对java.lang,java.util,java.io包里的所有最新的类的所有方法都了解并能合理使用才能算熟悉级别的吧。)
  7. 选择一门表达力强并且适用你个人理解力极限的编程语言,可以提高效率、节省时间、延长生命、早下班、赚更多的钱。(没最后两点大概没有人关心吧。)
  8. 公司不幸用了Java你也得适应。

参考书目

Cay S. Horstmann, Gary Cornell. Core Java 2, Volume II – Advanced Features (《Java2核心技术 卷II:高级特性》)

Richard Warburton. Java 8 Lambdas: Functional Programming for the Masses (《Java 8 函数式编程》)

Joshua Bloch. Effective Java Second Edition (《Effective Java中文版》)

Randal E. Bryant, David R. O’Hallaron. Computer Systems – A Programmer’s Perspective Third Edition (《深入理解计算机系统》)

Thomas H. Cormen, Charles E.Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to Algorithms Second Edition(《算法导论》)

Donald E. Knuth. The Art of Computer Programming Vol 1: Fundamental Algorithms Third Edition (《计算机程序设计艺术 卷1:基本算法》)