The Peris of JavaSchools

本来只想转 Joel 的那篇文章的, 不过不小心就转多了. 于是变成 Java 黑合集. 欢迎补充

maskray 提供了一个汇集了黑 Java 笑话的页面..

注: 本文只是开心的吐槽一下 Java 的一些缺点, 它的优点是值得承认的. 另外去 Google 实习了之后尤其觉得 Java 是可以写的很高端的.

The Peris of JavaSchools - Joel Spolsky

英文原文地址, 中文版地址, 译者阮一峰.


如今的孩子变懒了。

多吃一点苦,又会怎么样呢?

我一定是变老了,才会这样喋喋不休地抱怨和感叹 "如今的孩子"。为什么他们不再愿意、或者说不再能够做艰苦的工作呢。

当我还是孩子的时候,学习编程需要用到穿孔卡片(punched cards)。那时可没有任何类似 "退格" 键(Backspace key)这样的现代化功能,如果你出错了,就没有办法更正,只好扔掉出错的卡片,从头再来。

回想 1991 年,我开始面试程序员的时候。我一般会出一些编程题,允许用任何编程语言解题。在 99% 的情况下,面试者选择 C 语言。

如今,面试者一般会选择 Java 语言。

说到这里,不要误会我的意思。Java 语言本身作为一种开发工具,并没有什么错。

等一等,我要做个更正。我只是在本篇特定的文章中,不会提到 Java 语言作为一种开发工具,有什么不好的地方。事实上,它有许许多多不好的地方,不过这些只有另找时间来谈了。

我在这篇文章中,真正想要说的是,总的来看,Java 不是一种非常难的编程语言,无法用来区分优秀程序员和普通程序员。它可能很适合用来完成工作,但是这个不是今天的主题。我甚至想说,Java 语言不够难,其实是它的特色,不能算缺点。但是不管怎样,它就是有这个问题。

如果我听上去像是妄下论断,那么我想说一点我自己的微不足道的经历。大学计算机系的课程里,传统上有两个知识点,许多人从来都没有真正搞懂过的,那就是指针(pointers)和递归(recursion)。

你进大学后,一开始总要上一门 "数据结构" 课(data structure), 然后会有线性链表(linked list)、哈希表(hash table),以及其他诸如此类的课程。这些课会大量使用 "指针"。它们经常起到一种优胜劣汰的作用。因为这些课程非常难,那些学不会的人,就表明他们的能力不足以达到计算机科学学士学位的要求,只能选择放弃这个专业。这是一件好事,因为如果你连指针很觉得很难,那么等学到后面,要你证明不动点定理(fixed point theory)的时候,你该怎么办呢?

有些孩子读高中的时候,就能用 BASIC 语言在 Apple II 型个人电脑上,写出漂亮的乒乓球游戏。等他们进了大学,都会去选修计算机科学 101 课程,那门课讲的就是数据结构。当他们接触到指针那些玩意以后,就一下子完全傻眼了,后面的事情你都可以想像,他们就去改学政治学,因为看上去法学院是一个更好的出路 [1]。关于计算机系的淘汰率,我见过各式各样的数字,通常在 40% 到 70% 之间。校方一般会觉得,学生拿不到学位很可惜,我则视其为必要的筛选,淘汰那些没有兴趣编程或者没有能力编程的人。

对于许多计算机系的青年学生来说,另一门有难度的课程是有关函数式编程(functional programming)的课程,其中就包括递归程序设计(recursive programming)。MIT 将这些课程的标准提得很高,还专门设立了一门必修课(课程代号 6.001 [2]),它的教材(Structure and Interpretation of Computer Programs,作者为 Harold Abelson 和 Gerald Jay Sussman Abelson,MIT 出版社 1996 年版)被几十所、甚至几百所著名高校的计算系机采用,充当事实上的计算机科学导论课程。(你能在网上找到这本教材的旧版本,应该读一下。)

这些课程难得惊人。在第一堂课,你就要学完 Scheme 语言 [3] 的几乎所有内容,你还会遇到一个不动点函数(fixed-point function),它的自变量本身就是另一个函数。我读的这门导论课,是宾夕法尼亚大学的 CSE 121 课程,真是读得苦不堪言。我注意到很多学生,也许是大部分的学生,都无法完成这门课。课程的内容实在太难了。我给教授写了一封长长的声泪俱下的 Email,控诉这门课不是给人学的。宾夕法尼亚大学里一定有人听到了我的呼声(或者听到了其他抱怨者的呼声),因为如今这门课讲授的计算机语言是 Java。

我现在觉得,他们还不如没有听见呢。

这就是争议所在。许多年来,像当年的我一样懒惰的计算机系本科生不停地抱怨,再加上计算机业界也在抱怨毕业生不够用,这一切终于造成了重大恶果。过去十年中,大量本来堪称完美的好学校,都百分之百转向了 Java 语言的怀抱。这真是好得没话说了,那些用 "grep" 命令 [4] 过滤简历的企业招聘主管,大概会很喜欢这样。最妙不可言的是,Java 语言中没有什么太难的地方,不会真的淘汰什么人,你搞不懂指针或者递归也没关系。所以,计算系的淘汰率就降低了,学生人数上升了,经费预算变大了,可谓皆大欢喜。

学习 Java 语言的孩子是幸运的,因为当他们用到以指针为基础的哈希表时,他们永远也不会遇到古怪的 "段错误"[5](segfault)。他们永远不会因为无法将数据塞进有限的内存空间,而急得发疯。他们也永远不用苦苦思索,为什么在一个纯函数的程序中,一个变量的值一会保持不变,一会又变个不停!多么自相矛盾啊!

他们不需要怎么动脑筋,就可以在专业上得到 4.0 的绩点。

我是不是有点太苛刻了?就像电视里的 "四个约克郡男人"[6](Four Yorkshiremen)那样,成了老古板?就在这里吹嘘我是多么刻苦,完成了所有那些高难度的课程?

我再告诉你一件事。1900 年的时候,拉丁语和希腊语都是大学里的必修课,原因不是因为它们有什么特别的作用,而是因为它们有点被看成是受过高等教育的人士的标志。在某种程度上,我的观点同拉丁语支持者的观点没有不同(下面的四点理由都是如此):"(拉丁语)训练你的思维,锻炼你的记忆。分析拉丁语的句法结构,是思考能力的最佳练习,是真正对智力的挑战,能够很好地培养逻辑能力。" 以上出自 Scott Barker 之口(http://www.promotelatin.org/whylatin.htm)。但是,今天我找不到一所大学,还把拉丁语作为必修课。指针和递归不正像计算机科学中的拉丁语和希腊语吗?

说到这里,我坦率地承认,当今的软件代码中 90% 都不需要使用指针。事实上,如果在正式产品中使用指针,这将是十分危险的。好的,这一点没有异议。与此同时,函数式编程在实际开发中用到的也不多。这一点我也同意。

但是,对于某些最激动人心的编程任务来说,指针仍然是非常重要的。比如说,如果不用指针,你根本没办法开发 Linux 的内核。如果你不是真正地理解了指针,你连一行 Linux 的代码也看不懂,说实话,任何操作系统的代码你都看不懂。

如果你不懂函数式编程,你就无法创造出 MapReduce [7],正是这种算法使得 Google 的可扩展性(scalable)达到如此巨大的规模。单词 "Map"(映射)和 "Reduce"(化简)分别来自 Lisp 语言和函数式编程。回想起来,在类似 6.001 这样的编程课程中,都有提到纯粹的函数式编程没有副作用,因此可以直接用于并行计算(parallelizable)。任何人只要还记得这些内容,那么 MapRuduce 对他来说就是显而易见的。发明 MapReduce 的公司是 Google,而不是微软,这个简单的事实说出了原因,为什么微软至今还在追赶,还在试图提供最基本的搜索服务,而 Google 已经转向了下一个阶段,开发世界上最大的并行式超级计算机 ----Skynet [8] 的 H 次方的 H 次方的 H 次方的 H 次方的 H 次方的 H 次方。我觉得,微软并没有完全明白,在这一波竞争中它落后多远。

除了上面那些直接就能想到的重要性,指针和递归的真正价值,在于那种你在学习它们的过程中,所得到的思维深度,以及你因为害怕在这些课程中被淘汰,所产生的心理抗压能力,它们都是在建造大型系统的过程中必不可少的。指针和递归要求一定水平的推理能力、抽象思考能力,以及最重要的,在若干个不同的抽象层次上,同时审视同一个问题的能力。因此,是否真正理解指针和递归,与是否是一个优秀程序员直接相关。

如果计算机系的课程都与 Java 语言有关,那么对于那些在智力上无法应付复杂概念的学生,就没有东西可以真的淘汰他们。作为一个雇主,我发现那些 100% Java 教学的计算机系,已经培养出了相当一大批毕业生,这些学生只能勉强完成难度日益降低的课程作业,只会用 Java 语言编写简单的记账程序,如果你让他们编写一个更难的东西,他们就束手无策了。他们的智力不足以成为程序员。这些学生永远也通不过 MIT 的 6.001 课程,或者耶鲁大学的 CS 323 课程。坦率地说,为什么在一个雇主的心目中,MIT 或者耶鲁大学计算机系的学位的份量,要重于杜克大学,这就是原因之一。因为杜克大学最近已经全部转为用 Java 语言教学。宾夕法尼亚大学的情况也很类似,当初 CSE 121 课程中的 Scheme 语言和 ML 语言,几乎将我和我的同学折磨至死,如今已经全部被 Java 语言替代。我的意思不是说,我不想雇佣来自杜克大学或者宾夕法尼亚大学的聪明学生,我真的愿意雇佣他们,只是对于我来说,确定他们是否真的聪明,如今变得难多了。以前,我能够分辨出谁是聪明学生,因为他们可以在一分钟内看懂一个递归算法,或者可以迅速在计算机上实现一个线性链表操作函数,所用的时间同黑板上写一遍差不多。但是对于 Java 语言学校的毕业生,看着他们面对上述问题苦苦思索、做不出来的样子,我分辨不出这到底是因为学校里没教,还是因为他们不具备编写优秀软件作品的素质。Paul Graham 将这一类程序员称为 "Blub 程序员"[9](www.paulgraham.com/avg.html)。

Java 语言学校无法淘汰那些永远也成不了优秀程序员的学生,这已经是很糟糕的事情了。但是,学校可以无可厚非地辩解,这不是校方的错。整个软件行业,或者说至少是其中那些使用 grep 命令过滤简历的招聘经理,确实是在一直叫嚷,要求学校使用 Java 语言教学。

但是,即使如此,Java 语言学校的教学也还是失败的,因为学校没有成功训练好学生的头脑,没有使他们变得足够熟练、敏捷、灵活,能够做出高质量的软件设计(我不是指面向对象式的 "设计",那种编程只不过是要求你花上无数个小时,重写你的代码,使它们能够满足面向对象编程的等级制继承式结构,或者说要求你思考到底对象之间是 "has-a" 从属关系,还是 "is-a" 继承关系,这种 "伪问题" 将你搞得烦躁不安)。你需要的是那种能够在多个抽象层次上,同时思考问题的训练。这种思考能力正是设计出优秀软件架构所必需的。

你也许想知道,在教学中,面向对象编程(object-oriented programming,缩写 OOP)是否是指针和递归的优质替代品,是不是也能起到淘汰作用。简单的回答是:"不"。我在这里不讨论 OOP 的优点,我只指出 OOP 不够难,无法淘汰平庸的程序员。大多数时候,OOP 教学的主要内容就是记住一堆专有名词,比如 "封装"(encapsulation)和 "继承"(inheritance)",然后再做一堆多选题小测验,考你是不是明白" 多态 "(polymorphism)和" 重载 "(overloading)的区别。这同历史课上,要求你记住重要的日期和人名,难度差不多。OOP 不构成对智力的太大挑战,吓不跑一年级新生。据说,如果你没学好 OOP,你的程序依然可以运行,只是维护起来有点难。但是如果你没学好指针,你的程序就会输出一行段错误信息,而且你对什么地方出错了毫无想法,然后你只好停下来,深吸一口气,真正开始努力在两个不同的抽象层次上,同时思考你的程序是如何运行的。

顺便说一句,我有充分理由在这里说,那些使用 grep 命令过滤简历的招聘经理真是荒谬可笑。我从来没有见过哪个能用 Scheme 语言、Haskell 语言和 C 语言中的指针编程的人,竟然不能在二天里面学会 Java 语言,并且写出的 Java 程序,质量竟然不能胜过那些有 5 年 Java 编程经验的人士。不过,人力资源部里那些平庸的懒汉,是无法指望他们听进去这些话的。

再说,计算机系承担的发扬光大计算机科学的使命该怎么办呢?计算机系毕竟不是职业学校啊!训练学生如何在这个行业里工作,不应该是计算机系的任务。这应该是社区高校和政府就业培训计划的任务,那些地方会教给你工作技能。计算机系给予学生的,理应是他们日后生活所需要的基础知识,而不是为学生第一周上班做准备。对不对?

还有,计算机科学是由证明(递归)、算法(递归)、语言(λ 演算 [10])、操作系统(指针)、编译器(λ 演算)所组成的。所以,这就是说那些不教 C 语言、不教 Scheme 语言、只教 Java 语言的学校,实际上根本不是在教授计算机科学。虽然对于真实世界来说,有些概念可能毫无用处,比如函数的科里化(function currying)[11],但是这些知识显然是进入计算机科学研究生院的前提。我不明白,计算机系课程设置委员会中的教授为什么会同意,将课程的难度下降到如此低的地步,以至于他们既无法培养出合格的程序员,甚至也无法培养出合格的能够得到哲学博士 PhD 学位 [12]、进而能够申请教职、与他们竞争工作岗位的研究生。噢,且慢,我说错了。也许我明白原因了。

实际上,如果你回顾和研究学术界在 "Java 大迁移"(Great Java Shift)中的争论,你会注意到,最大的议题是 Java 语言是否还不够简单,不适合作为一种教学语言。

我的老天啊,我心里说,他们还在设法让课程变得更简单。为什么不用匙子,干脆把所有东西一勺勺都喂到学生嘴里呢?让我们再请助教帮他们接管考试,这样一来就没有学生会改学 "美国研究"[13](American studies)了。如果课程被精心设计,使得所有内容都比原有内容更容易,那么怎么可能期望任何人从这个地方学到任何东西呢?看上去似乎有一个工作小组(Java task force)正在开展工作,创造出一个简化的 Java 的子集,以便在课堂上教学 [14]。这些人的目标是生成一个简化的文档,小心地不让学生纤弱的思想,接触到任何 EJB/J2EE 的脏东西 [15]。这样一来,学生的小脑袋就不会因为遇到有点难度的课程,而感到烦恼了,除非那门课里只要求做一些空前简单的计算机习题。

计算机系如此积极地降低课程难度,有一个理由可以得到最多的赞同,那就是节省出更多的时间,教授真正的属于计算机科学的概念。但是,前提是不能花费整整两节课,向学生讲解诸如 Java 语言中 int 和 Integer 有何区别 [16]。好的,如果真是这样,课程 6.001 就是你的完美选择。你可以先讲 Scheme 语言,这种教学语言简单到聪明学生大约只用 10 分钟,就能全部学会。然后,你将这个学期剩下的时间,都用来讲解不动点。

唉。

说了半天,我还是在说要学 1 和 0。

(你学到了 1?真幸运啊!我们那时所有人学到的都是 0。)

Talk at Yale(excerpt) - Joel Spolsky

Joel 在耶鲁的演讲, 英文版. 中文译者阮一峰. 其中有两段黑 Java..

以下是另一段..

Revenge of the Nerds(excerpt) - Paul Graham

英文版, 中文版译者阮一峰.



这篇文章的附录里, Graham 比较了语言的表述能力:

为了解释我所说的语言编程能力不一样,请考虑下面的问题。我们需要写一个函数,它能够生成累加器,即这个函数接受一个参数 n,然后返回另一个函数,后者接受参数 i,然后返回 n 增加(increment)了 i 后的值。

Common Lisp 的写法如下:

(defun foo (n)
  (lambda (i) (incf n i)))

Ruby 的写法几乎完全相同:

def foo (n)
  lambda {|i| n += i } end

Perl 5 的写法则是:

sub foo {
  my ($n) = @_;
  sub {$n += shift}
}

这比 Lisp 和 Ruby 的版本,有更多的语法元素,因为在 Perl 语言中,你不得不手工提取参数。

Smalltalk 的写法稍微比 Lisp 和 Ruby 的长一点:

foo: n
  |s|
  s := n.
  ^[:i| s := s+i. ]

因为在 Smalltalk 中,局部变量(lexical variable)是有效的,但是你无法给一个参数赋值,因此不得不设置了一个新变量,接受累加后的值。

Javascript 的写法也比 Lisp 和 Ruby 稍微长一点,因为 Javascript 依然区分语句和表达式,所以你需要明确指定 return 语句,来返回一个值:

function foo(n) {
  return function (i) {
    return n += i } }

(实事求是地说,Perl 也保留了语句和表达式的区别,但是使用了典型的 Perl 方式处理,使你可以省略 return。)

如果想把 Lisp/Ruby/Perl/Smalltalk/Javascript 的版本改成 Python,你会遇到一些限制。因为 Python 并不完全支持局部变量,你不得不创造一种数据结构,来接受 n 的值。而且尽管 Python 确实支持函数数据类型,但是没有一种字面量的表示方式(literal representation)可以生成函数(除非函数体只有一个表达式),所以你需要创造一个命名函数,把它返回。最后的写法如下:

def foo(n):
  s = [n]
  def bar(i):
    s[0] += i
    return s[0]
  return bar

Python 用户完全可以合理地质疑,为什么不能写成下面这样:

def foo(n):
  return lambda i: return n += i

或者:

def foo(n):
  lambda i: n += i

我猜想,Python 有一天会支持这样的写法。(如果你不想等到 Python 慢慢进化到更像 Lisp,你总是可以直接......)

在面向对象编程的语言中,你能够在有限程度上模拟一个闭包(即一个函数,通过它可以引用由包含这个函数的代码所定义的变量)。你定义一个类(class),里面有一个方法和一个属性,用于替换封闭作用域(enclosing scope)中的所有变量。这有点类似于让程序员自己做代码分析,本来这应该是由支持局部作用域的编译器完成的。如果有多个函数,同时指向相同的变量,那么这种方法就会失效,但是在这个简单的例子中,它已经足够了。

Python 高手看来也同意,这是解决这个问题的比较好的方法,写法如下:

def foo(n):
  class acc:
    def __init__(self, s):
      self.s = s
    def inc(self, i):
      self.s += i
      return self.s
  return acc(n).inc

或者

class foo:
  def __init__ (self, n):
    self.n = n
  def __call__ (self, i):
    self.n += i
    return self.n

我添加这一段,原因是想避免 Python 爱好者说我误解这种语言。但是,在我看来,这两种写法好像都比第一个版本更复杂。你实际上就是在做同样的事,只不过划出了一个独立的区域,保存累加器函数,区别只是保存在对象的一个属性中,而不是保存在列表(list)的头(head)中。使用这些特殊的内部属性名(尤其是__call__),看上去并不像常规的解法,更像是一种破解。

在 Perl 和 Python 的较量中,Python 黑客的观点似乎是认为 Python 比 Perl 更优雅,但是这个例子表明,最终来说,编程能力决定了优雅。Perl 的写法更简单(包含更少的语法元素),尽管它的语法有一点丑陋。

其他语言怎么样?前文曾经提到过 Fortran、C、C++、Java 和 Visual Basic,看上去使用它们,根本无法解决这个问题。Ken Anderson 说,Java 只能写出一个近似的解法:

public interface Inttoint {
 public int call (int i);
}
public static Inttoint foo (final int n) {
 return new Inttoint () {
  int s = n;
  public int call (int i) {
  s = s + i;
  return s;
  }
};
}

这种写法不符合题目要求,因为它只对整数有效。

当然,我说使用其他语言无法解决这个问题,这句话并不完全正确。所有这些语言都是图灵等价的,这意味着严格地说,你能使用它们之中的任何一种语言,写出任何一个程序。那么,怎样才能做到这一点呢?就这个小小的例子而言,你可以使用这些不那么强大的语言,写一个 Lisp 解释器就行了。

这样做听上去好像开玩笑,但是在大型编程项目中,却不同程度地广泛存在。因此,有人把它总结出来,起名为 "格林斯潘第十定律"(Greenspun's Tenth Rule):

"任何 C 或 Fortran 程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是 bug 的、运行速度很慢的 Common Lisp 实现。"

如果你想解决一个困难的问题,关键不是你使用的语言是否强大,而是好几个因素同时发挥作用(a)使用一种强大的语言,(b)为这个难题写一个事实上的解释器,或者(c)你自己变成这个难题的人肉编译器。在 Python 的例子中,这样的处理方法已经开始出现了,我们实际上就是自己写代码,模拟出编译器实现局部变量的功能。

这种实践不仅很普遍,而且已经制度化了。举例来说,在面向对象编程的世界中,我们大量听到 "模式"(pattern)这个词,我觉得那些 "模式" 就是现实中的因素(c),也就是人肉编译器。 当我在自己的程序中,发现用到了模式,我觉得这就表明某个地方出错了。程序的形式,应该仅仅反映它所要解决的问题。代码中其他任何外加的形式,都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码,通过宏的扩展自动实现。

Quotes

这个时候我已经初步理解了编译器前端的一些知识,但是后端 —— 譬如代码生成和垃圾收集 —— 却还是一知半解。不过这并不妨碍我用好的前端知识和烂的后端知识来做出一个东西来。当时我简单看了一下 Java 语言的语法,把我不喜欢的那些东西砍掉,然后给他加上了泛型。Java 那个时候的泛型实现好像也是刚刚出现的,但是我不知道,我也从来没想过泛型要怎么实现。所以当时我想来想去做了一个决定,泛型只让编译器去检查就好了,编译的时候那些 T 都当成 object 来处理,然后就把东西做出来了。我本来以为我这种偷工减料拆东墙补西墙忽悠傻逼用户的方法是业界所不容的,不过后来发现 Java 竟然也是那么做的,让我觉得我一定要黑他一辈子。

If Java had true garbage collection, most programs would delete themselves upon execution.

Robert Sewell

Like the creators of sitcoms or junk food or package tours, Java’s designers were consciously designing a product for people not as smart as them.

Paul Graham

java 使无数三流程序员有了二流程序员的生产力,这难道不是一件伟大的事情吗?

lispc

这句话也可以不理解成黑... 因为这点确实很伟大..

Jokes

我不歧视任何编程语言和 Java.

Java, write once, run away!

另外著名博主 Steve Yegge 有一篇搞笑的《Execution in the Kingdom of Nouns》.. 是批评 Java 没有 First-Class Function 的.. 英文版, 中文版

据说这个图的名字叫做: "一张图看懂 Java 的垃圾回收机制"

Comments