观点 | Opinion
Java之父James Gosling:你需要的软件可靠性越高,静态类型语言的帮助就越大
作者 Evrone 译者 张健欣
James Gosling,通常被称为“Java博士”,是一名加拿大计算机科学家,以Java编程语言之父闻名。他完成了Java的最初设计,实现了Java最初的编译器和虚拟机。日前,Evrone的DevRel、Grigory Petrov对James Gosling进行了采访。InfoQ对访谈内容进行了翻译,以飨读者。
记者:有些语言,比如Go,省略了类和继承,而另一些语言则尝试使用诸如Rust中的traits之类的特性。作为一名语言设计师,您认为一种现代的、通用的、合理的编程语言是怎样的?
James:我想我不会放弃类的。事实上,我发现类对于创作很有效。C语言中有宏,这几乎是一场灾难,因为宏不是语言的一部分,它们是语言之外的东西。Rust设计师尝试在语言中很好地适配宏。
其它语言,例如Lisp家族的所有语言,设法更优雅地适配宏,但它们有一种定义语法的方法,其中语法完全没有语义。在大多数语言中,语法和语义是紧密相连的。作为一个在过去写了很多Lisp代码的人,我对使用Lisp程序来操纵Lisp程序的技术非常着迷。这是我非常非常想念的一点。有些语言允许你用不同的方式来实现这一点,例如在Groovy中,你可以直接使用AST,而Rust则有一些语法集成的宏。但我总觉得这里有一个有意思的研究问题:你能做得更多吗?
我能体会到Lisp对代码片段进行计算来生成新代码的感觉吗?在Java世界里,人们就是这样做的。这是一个更受欢迎的功能,尽管它的层级很低。因为人们将注解和可以使用某些不同语言生成字节码结合使用,这是超级强大的。它会在你意想不到的地方使用,例如Jackson,并通过计算序列化程序获得了很多性能。
一方面,这是一种非常强大的技术。另一方面,它非常难用。事实证明它是可行的,但你能走多远?它们可能是有限制的。所以,你可以看看像Lombok这样的东西,它添加了一系列非常好的Java特性,但另一方面,它也显示出了弱点,因为这是一组应该内置的功能。而Java Community Process丧失了部分本应该有的社区性。我已经不在社区好几年了,但到处都有一些你可以做的事情。
记者:这就是为什么我们准备了关于您创建语言的奇妙体验的问题,而不是现代Java增强提案。我承认,五年前,我操纵了一些Java字节码。当然,这是好事,但是用它来创建特定领域的语言有点棘手。对于Ruby,这就容易得多了。我们在Evrone的团队精通Ruby,我们有几十个Ruby开发人员。Ruby开发人员都很棒,但是他们需要进行很多年的培训来学习所有的DSL“魔法”。
James:像计算代码片段这样的特性,在Java中很难用的原因是,Java试图做编译到机器代码的所有过程,而Ruby总是被解释执行的。当你这样做,但你不想尽你可能获得所有的性能时,事情就会变得很容易。但是如果你想同时获得强大的功能和终极性能,事情就会变得困难得多。
记者:最近,我们采访了Ruby的作者Yukihiro Matsumoto,他提到他用最新的Ruby 3.0主版本进行了一个实验。他试图在不进行破坏性变更的情况下发布这个版本,看看会发生什么。我知道Java对于破坏性更新很谨慎,所有语言在没有不兼容的情况下演化是一个好主意吗?或者,这是一种只能用于特定语言(如Ruby或Java)的有限制的方法?
James:这几乎完全取决于开发者社区的规模,每一次破坏性的更新都会给开发者社区带来痛苦。如果你没有太多开发者,那么破坏性更新并不是一个大问题,但你必须考虑成本效益的平衡。如果你做了一个破坏性更新,它会增加一些痛苦,但也会带来一些好处。如果你将下标运算符从方括号改为圆括号,则它可能完全不会为你带来任何好处,而是会带来巨大的痛苦。因为这是一个愚蠢的主意。
在JDK 9中,存在一个变化,这是引入的为数不多的破坏性更新之一,它造成的破坏是:如果你使用一些隐藏的API,封装机制就会混乱,那些打破封装边界,并以不正确的方式使用工具的人,从8迁移到9就会很痛苦。但一旦我们克服这些,平台就有了更多创新自由。在8到9转换的特殊情况下,这意味着可以对平台进行分割,你实际上可以进行定制打包,从而使Java运行时环境更小。
另一个经常让人感到不适的地方是:如果某个功能存在bug,人们为这个bug采取了变通方法,如果你修复了bug,你可能会打破这些变通方法。在Java世界中,确实有过这样的例子,我们要么决定不修复bug,要么引入一种正确的方法,这甚至体现在硬件上。sin和cos有一个问题,它们有些错误的地方,因此你必须有正确和错误的指导说明。
记者:二十五年前,当我开始自己的软件开发生涯时,我写了很多C和C++代码。我还记得那些每月发生一次的神秘指针错误。调试这些bug是一种痛苦。但现在,作为一名软件开发者,我看到许多工具集成到我们的工作流中,例如静态类型检查器。现代开发者使用集成开发工具,例如NetBeans、Intellij IDEA或者Visual Studio Code。他们编写源代码,一个静态类型检查器解析程序,构建一个抽象语法树,并检查所有它能检查的东西,然后在文本编辑器中高亮显示可能的错误。这些技巧不仅适用于静态类型语言,也适用于Python、Ruby和TypeScript等动态类型语言。您对我们今天使用的这些静态类型检查器有什么看法?它们是我们向编写更好的软件迈出的一步吗?还是我们需要在语言语法中加入更多这样的功能?
James:嗯,两者都有。我非常喜欢使用静态类型系统的语言,因为它们为静态类型检查器和IDE提供了一个框架。我一生的大部分时间都是作为一名软件工程师度过的,对我来说,最不满意的消磨时间的方式就是寻找那些在奇怪时间点发生的模糊bug。在bug浪费我的时间之前,我能做的任何能够使bug消失的事情都是极好的。所以,我非常喜欢IDE可以做的任何能够减少bug可能性的事情。因此,当我们研究动态类型语言(如JavaScript和Python)时,它们的推理框架比较少,因为它们不一定知道任何东西的类型,它们只是猜测而已。强类型语言(如Java)为类型检查器提供了更严格的框架。在另一个层次上,有些东西可以进行全自动的定理证明。所以像Dafny这样的系统,它有一个非常复杂的定理证明器。因此,如果你想构建一个加密算法,你将能够从数学上证明属性。对于某些代码来说,这确实非常有用。
而且这在很大程度上取决于你的目的到底是什么。如果你是一名大学生,你正在努力完成作业,或者你是一名博士生,你正在努力毕业,那么当你写程序时,你的目标是它工作一次就行了,至少一次。因为你必须做一个演示,能够展示它,看看它是否有效。如果你在一个工业环境中,就像我大部分生涯所在的环境一样,那么它需要每次都能工作。一次工作和每次工作的区别是巨大的。因此,如果它只需要工作一次,那么更具动态性的语言就可以相当好地工作。如果你必须确保它能一次又一次地工作,那么所有的静态类型工具能够帮助你建立信心。但是如果你正在做的事情是,比如你是一名物理学家,你想算出一些计算的结果,它只需要运行一次。这取决于你所工作的背景。你所需要的软件可靠性越高,静态类型语言就越有帮助。
记者:谈一谈企业和产业发展。我自己从来没有为机器人编程过,但我在为数百万人开发软件的公司工作过,我可以比较下今天和20-25年前的情况。现在,像GitHub这样的社交编码平台得到了大公司的支持,它们帮助个人开发者和企业/行业软件开发者进行开源开发。那么,我们可以称今天为开源软件的黄金时代吗?您对此怎么看?
James:我对此没有什么想法,你在问一个关于未来的问题。“今天是黄金时代吗”,这个问题暗示:“它从今天走下坡路吗?”如果今天是黄金时代,那么明天就不会是黄金时代了。我认为,我们正在走向黄金时代,无论黄金时代是什么。我认为有很多有趣的改进可以发生。目前,我们有各种各样安全方面的危机,比如应该如何应对网络恐怖主义等。当这种事情发生时,我不认为现在是黄金时代。如果有某种方式,人们通过社区合作可以让网络恐怖主义终结——那将是黄金时代。我们拭目以待。总的来说,这确实是一个伟大的时代,但它可以变得更好。
记者:您使用JIT(just-in-time compilation,即时编译)创建了Java和JVM(Java Virtual Machine,Java虚拟机)。JIT提供了惊人的速度,同时保持了语言语法的功能和高级特性。许多语言都跟随你的脚步,比如C#和JavaScript。在热路径上编译和重新编译代码的速度接近C和C++。但许多其它语言,如Python、Ruby、PHP,都有可选的JIT,但并不流行。为什么不是所有语言都使用JIT来为开发人员提供惊人的速度?
James:要想真正获得性能提升,使用静态类型语言会有很大帮助。对于动态类型语言,比如Python,这样的性能提升非常困难。通常,人们要做的就是在语言中添加注解,这样就可以得到TypeScript这样的语言,它本质上是带有类型注解的JavaScript。这真的很有趣,因为JavaScript本质上是Java删除了类型声明。因此,TypeScript本质上是具有置换语法的Java。它们有Pascal风格的声明。但是,如果你只是用Python快速编写脚本,那么很多人都会觉得声明很烦人,因为考虑它们的变量类型是很烦人的。
在Python和许多其它语言中,通常只有一种数字,那就是双精度浮点数。不存在真正的整数,不存在字节和16位整数等概念上增加复杂性的东西,但这些复杂概念能提高性能。如果你有一个双精度浮点数和一个单精度浮点数,就会存在一个认知负担。要想做出明智的权衡,你必须了解一些数值分析。软件工程师普遍对数值分析一无所知,所以他们宁愿不去想它。如果你是一位使用Python的物理学家,你可能总是想要获取所有精度。当然,除非你需要在内存中放入一个非常大的数组,其中单精度和双精度或一个8位整数之间的差异才会非常重要。
在我有生之年,我修了太多数值分析课程,被劣质的数值分析搞崩溃的次数太多了,因此我很在乎精度。这取决于你在这个领域的位置,而脚本语言世界中的大多数人并不关心这类问题,他们的关注点是完全不同的。而且很多人实际上并不关心性能和数字的细节,他们关心的是:“速度够快吗”?性能有点儿像布尔值:速度够快,还是不够快。对于一些人来说,这更像是调整赛车。如果你能让一辆车每小时多跑两三英里,那么你更有可能赢得比赛。
记者:我记得几个月前,Ruby on Rails(广受欢迎的Web框架之一)的作者David Heinemeier Hansson提到,他的云预算中只有15%用于语言本身。其余的是一些缓存、消息队列、存储等等。他告诉我们,无论Ruby有多“慢”,这都不是很重要,因为即使Ruby快100倍,15%变成了1%,这也不会有多大变化。现代语言已经“足够快”。
James:这很大程度上取决于你的任务在程序空间中的位置。如果你想要完成的事情实际上是由网络和数据库以及其它所有东西所主导,如果你一直在做RPCs,那么你应该做的第一件事是质疑这些RPCs是否都有价值。当人们谈论微服务时,它们是一件好事,但要明白它们至少比方法调用慢一百万倍。仔细想想这其中的含义。对于很多人来说,所有的低级细节都很重要。如果你知道高并发很重要,能够同时驱动数千个进程,进行主要的计算,如果你正在做数据库本身或一个主要的存储服务,那么你真的要非常关心。因此,这完全取决于手头的任务。
记者:最近,我们看到很多语言都采用了协程和async/await方案来处理网络之类比较慢的事情。它被添加到Python、Ruby、JavaScript以及很多其他语言中。但是async/await、协程和线程中的调度器并不是万能的。他们也有自身的复杂性,有时它们会使软件速度变得更慢。那么,你如何看待这种现代的async/await炒作?这是处理网络的一种好方法吗?还是我们误用了它?我们需要看看Erlang和其它解决方案吗?
James:这类问题的上下文很重要。协程非常好,它们从60年代就开始被采用了。最早采用协程的语言是Simula 67。Simula是一种可爱的语言。我仍然怀念它。它没有线程,有协程,但它们执行协程的方式看起来很像线程。协程在真正的并行中神奇地避开了一些问题。对我来说,协程的一个问题是它们实际上不允许让你利用多个处理器,这就是我很长时间没有采用协程的原因。采用协程,你不能做真正的并行。
因此,人们看向具有真正并行的语言(比如Erlang和Java)中的东西。你必须做的事情增加了另一层复杂性。通常情况下,处理这种复杂性的方法是非常仔细地策划原语。在Java中,你可以用ConcurrentHashMap做的事情非常神奇。一旦你使用了一种基于协程的语言,并且你试图利用多个处理器,如果你做了大量协程操作,而你并没有足够的处理器,那么你只会使一个处理器饱和。你真的很想使用多个处理器,因为世界上已经没有单核处理器了,对吧?每样东西都有很多内核,如果你真的想在一个问题上同时使用你所有的计算机处理器,你只需要克服和处理真正多线程所固有的复杂性。
然后是风格问题。想象你可以说“await这”和“await那”的环境,它们做了这个透明的控制反转,你只能被动屈服。虽然能使你的语法看起来非常像真正的线程。但这意味着,在真正的线程中,有许多棘手的问题需要避免。所以,如果你说“a = a + 1”,你知道这是在运算的中间,你不会被打断,所以你不必做同步。但还有一些其它地方,它不再采用这种风格,而是变成了一种事件导向的风格,你做你自己的事情,然后你把一个事件处理器插入到某个东西中来处理事情完成后发生的事情。这往往是JavaScript的主要风格。这很好用,但可能有点笨重。
当我在70年代初发现Simula时,它有一种自然的风格。你只需要编程,你可以把你的计算看做是独立的东西,而其他事物是否与之交织对你来说是透明的。我发现,作为一种概念模型,它比事件编程要干净地多。它很难在幕后实现,但通常更容易思考。
记者:Simula毕竟是第一个面向对象的语言。我从没有机会使用过它,但我看过文档,它看起来很有特色。然而,如果我们查看一些现代语言,比如Ruby,并发模型是复杂的:我们有进程,进程中有单独的解释器,单独的解释器中有线程,线程中有核心例程——就像一个俄罗斯套娃。如果你允许的话,现在问一个非技术性问题。当我们谈论不同的语言时,在你个人看来,现在哪一种语言是适合研究生院或大学教授软件开发新手作为他们第一语言的最佳语言?
James:我显然会有偏见。很长一段时间,Java一直成功地作为软件开发新手的第一语言。但我学的第一种编程语言是PDP-8汇编代码,大致与Fortran并行使用。你可以教人们任何东西。其中一些语言比其它语言更容易理解,但这很大程度上取决于一个人最终的职业道路。如果你想成为一名全面的软件开发人员,构建大型的高性能的系统,那么其它语言很难打败在JVM上运行的语言。实际上,我不在乎你在JVM上使用哪种语言。我的意思是,Scala和Kotlin都很好。Clojure真的很有趣,但你必须用不同的方式思考。如果你是一名物理系学生,Python就可以了。
我认为你选择哪一种语言并没什么大不了的。虽然很多人坚持他们所学的第一语言,并从事相关的工作,但如果你能让人们学习多种语言并反复切换是更好的。我认为,每个大学都应该为学生开设的一门课程是编程语言比较课程。在这学期里,你有五个不同编程语言的作业,让学生们能够习惯快速学习它们,并思考哪一种编程语言更好。很久以前,我参加了其中一门课程,每次作业我都用了最糟糕的语言,用COBOL进行数值计算。那只是娱乐!甚至我还用Fortran进行符号操作。令人惊讶的是,我仍得了个A。
记者:下一个问题是关于模式匹配。最近,它在Python和Ruby中大放异彩,许多提案都有不同的语言版本。我们查看了开发者白皮书,他们并不完全确定模式匹配在现代高级语言中的角色。这种模式匹配思想,您认为它如何适合那些使用Java、Python、Ruby或某种高级语言的普通现代开发人员?我们真的需要模式匹配吗,或者它只是针对特定用例的特定语法?
James:首先,我认为编程语言中的术语“模式匹配”有点误导。因为当我听到“模式匹配”这个短语,我想到的是正则表达式,无论是字符串上的正则表达式,还是树上的正则表达式。也许模式与树的形状匹配,随便什么。但是回到Simula。Simula有一个inspect语句,这个inspect语句几乎完全与许多模式匹配语句相同。也就是说,inspect语句是一个case语句,其中case标签是类型名,你可以说:
Inspect P
When Image do Show;
When Vector do Draw;
你可以将其视为一个case语句,基于类型的cases。大多数模式匹配语言提案都是这种类似的东西。就我个人而言,我很怀念这一点,我真的很喜欢。特别是如果发生的事情有点像C语言中的隐式转换,如果你说“inspect P When Image P do P”,那么case语句中的P现在就是switch标签的类型。在一种类似C的语法的语言中,你总是以强制转换结束。它看起来像:“如果a是x的实例,否则如果a是y的实例,那么……”Simula中的“inspect”语句非常漂亮,我喜欢它,而这些模式匹配提案和语言特性中许多都是这样的。你可以称它为“类型实例(type case)”,但是如果你称它为“模式匹配”,并且它的功能比正则表达式小,那么它就会让人产生误解,有点像虚假广告。但是,作为一种功能,我认为它很棒。
记者:Kotlin和许多其它语言,例如Clojure或者Scala,在您创建的现有Java虚拟机、库和框架的生态系统,以及现有代码的基础上蓬勃发展。这些语言都面临什么挑战?有什么东西能把它们团结起来吗?这对它们来说有困难吗?当它们试图用某种不同的语法热转换Java语法时,它们面临哪些困难?
James:这取决于你想要做什么。Java虚拟机的一个特点是它内置了许多安全性和可靠性的概念。它们主要与内存模型的完整性有关,所以你不能构造一个指针。像C这样的语言,如果你没有构造指针的能力,你就不能实现C。有些虚拟机没有严格的安全模型。如果你有一个安全的虚拟机,有些地方你就无法实现。但是有些人已经建立了不严格安全的虚拟机,没有内存分配模型。如果您想在C和Kotlin之间实现互操作性,您必须放弃一些安全性和可靠性。
所以,这取决于你想要到哪里。当然,在Java诞生之初,我的个人规则之一是:我不想调试另一个可怕的内存损坏bug。我已经浪费了太多时间在晦涩难懂的内存损坏bug上。而且这是循环中的一个逐次错误,恰好从数组末尾的一个条目中删除,直到数百万次操作之后你才会找出问题。所以这取决于你对什么感到舒服。你知道,有些人认为花时间做这些事很有男子气概。也有人喜欢使用vi,它在70年代是一个伟大的编辑器,在80年代也是一个出色的编辑器。加油,伙计们!