Java 之父:找Bug最浪费时间,现在不是开源的黄金时代
介绍
90年代初,James Gosling和一群技术人员合作“绿色计划”,创建了一个名为Oak的项目,旨在开发出能够运行于虚拟机的编程语言,开创计算机在家电产品上的嵌入式应用。后来,这项工作就演变为Java。
近日,Evrone的软件工程师、技术编辑Grigory与Java之父James进行了一场线上对话。在采访中,James提出了许多观点,他认为:“对软件的可靠性要求越高,静态类型语言就越有帮助。”他还分享了自己对某些编程语言的看法:反感C语言中的宏、对Lombok又爱又恨、很喜爱Lisp等等。
关于现代编程语言的构建方式
Grigory: 作为软件开发人员和软件顾问,我们试图在俄罗斯组织Python、Ruby、Java和Go社区。您在Java方面的经验和工作可以很好地帮助开发人员。通过这次采访,我们希望能够帮助到其他开发者,一起解决业内的基本问题。
有些语言,比如Go,没有类和继承一说,而另一些语言则尝试使用诸如Rust的特性。作为一名语言设计师,您认为现代通用的、合理的编程语言构建方式应该是什么?
James: 我应该会继续使用类,类对我的编程工作很有用。在C语言中有一些宏,这非常糟糕,因为宏不是语言的一部分,不应该纳入其中。而Rust的工作人员正在尝试恰到好处地使用宏。
对于其他语言,如Lisp家族,人们总是设法更灵活地应用它们。它们有一种定义语法的方法,语法几乎完全与语义无关。在大多数语言中,语法和语义是密切相关的。我曾写了很多Lisp,我真的很喜欢使用Lisp程序。有的语言能让你以不同的方式做到这一点,比如在Groovy中,你可以直接使用AST,Rust有一些语法集成的宏。但我总觉得还有一个问题:除此之外还能做什么呢?
Lisp对代码片段进行运算,再生成新代码,在Java世界里,人们就是这样做的。虽然看似低级,但很受欢迎。人们可以使用注释的组合,用一些不同的语言生成字节码,这是超级强大的。它会给你意想不到的惊喜,比如在Jackson,它通过计算序列化程序延展了性能。
一方面,这是一种非常强大的技术,另一方面,它非常难以驾驭。这个技术充满可能性,但这种可能性是有限的。我对Lombok又爱又恨,因为它添加了一大堆Java特性,这些特性都很不错,但从另一方面来说,也显示出了弱点。JCP的社区职能在下降,我已经离开好几年了,虽然有些事情可以做,但也只能是在纸上谈谈。
Grigory: 这就是为什么我们更想了解您创建语言的经验,而不是一些Java增强建议。五年前,我尝试操控一些Java字节码。我发现,用它来创建特定领域的语言有点困难。但是有了Ruby后,就容易多了。Evrone公司有许多精通Ruby的开发人员。Ruby开发人员很优秀,但是他们需要多年的培训才能真正掌控DSL的魔力。
James: 像代码片段运算这样的特性,在Java中之所以尴尬是因为Java总是试图去编译机器代码,Ruby几乎总是被解释。如果你想同时获得超强功能和终极性能,这一切就会变得很困难。
如何看待那些破坏性的更改?
Grigory: 最近,我们采访了Ruby的创作者Yukihiro Matsumoto,他对最新的Ruby 3.0版本进行了实验,试图在不破坏更改的情况下发布这个版本。我知道Java对“不破坏更改”的态度一直很谨慎。让所有语言在完全兼容的情况下进化,这是合理的吗?还是说这个方法只能用于特定的语言,例如Ruby或Java?
James: 这几乎完全取决于开发人员社区的规模。每一个破坏性的变更都会给开发人员社区带来痛苦。如果你没有很多的开发人员,那么破坏更改并不是一个大问题。
除此之外,你还必须权衡成本效益。如果你做了一个突破性的改变,它会增加你的工作负担,但也可能会带来一些好处。不过,如果你只是将下标操作符从方括号更改为圆括号,这并不会带来任何好处,只会徒增麻烦。
在JDK 9中,出现了一个罕见的破坏性变化:如果你使用的是隐藏API,封装机制就会被打乱。人们打破封装界限,运用了非常规的方式,使用了不应该使用的东西,这种改变是痛苦的。然而,一旦我们彻底改变,平台便有了更多的创新空间。在这种改变下,平台可以被分割,你可以定制打包,Java的运行环境就会更小。
另一个麻烦是:当遇到Bug时,人们会为之做一些变通措施,如果你修复了这个Bug,变通措施就被破坏了。在Java的世界中,确实有这样的例子,我们要么不修复Bug,要么引入一个方法来修正错误,这甚至体现在硬件上。
如何看待静态类型检查器?
Grigory: 25年前,当我开始自己的软件开发生涯时,我写了很多C和C++代码。几乎每个月都会遇到一次错误警报。调试这些错误是一件很痛苦的事情。但是现在,我看到许多工具集成到我们的工作流程中,比如静态类型检查器。现代开发人员使用IDE,如NetBeans、IntelliJ IDEA,甚至Visual Studio。他们编写源代码,编写静态类型检查器解析程序,构造抽象语法树,并进行检查,然后在文本编辑器中标记错误。这些技巧不仅适用于静态类型的语言,也适用于动态类型的语言,在Python、Ruby和TypeScript中皆可使用。
Grigory: 你对静态类型检查器有什么看法?它们能帮助人们编写出更好的程序,还是说需要在语言语法中添加更多内容?
James:我都同意。我非常喜欢使用静态类型系统的语言,因为它们为静态类型检查器和IDE提供了一个框架。作为一名资深软件工程师,寻找那些奇怪的Bug是最浪费时间的。为减少这方面的时间浪费,我会尽力阻止Bug的出现。因此,我非常喜欢IDE,它能够提供减少Bug的方法。而动态类型语言很少有框架来解决这个问题,因为它们不一定能判断所有类型,只能靠猜测。强类型语言(如Java)为类型检查器提供了更严格的框架。在另一个层次上,甚至可以进行自动的定理证明。像Dafny这样的系统,它有一个非常复杂的定理证明器。所以如果你想建立一个加密算法,你将能够用数学方法进行证明。这听上去很夸张,但对于某些代码来说,真的很有用。
这很大程度上取决于你的目标是什么。
- 如果你是一名正在努力完成作业的大学生,或是一名正在努力毕业的博士生,那么当你编写一个程序时,你的目标是让这个程序至少能运行一次,因为你必须要展示成果。
- 如果你在行业环境中,那么每次运行都必须成功。一次运行成功和每次都运行成功之间的差别是巨大的。如果只需要运作一次,那么动态语言会更合适。如果你必须确保它能一次又一次地运行,那么所有的静态类型工具都适用。
- 如果你是一个物理学家,你想得出一些计算结果,那么它只需要运行一次。这取决于你的工作背景。你对软件的可靠性要求越高,静态类型语言就越有帮助。
现在是开源软件的黄金时代吗?
Grigory: 我们来谈谈企业和产业发展吧。我从来没做过机器人,但我曾在大型开发公司工作过,如果将今天和20-25年前进行比较,我们会发现,像GitHub这样的社交编码平台,得到了大公司的支持,他们帮助个人开发者和企业软件开发者进行开源开发。那么,现在是不是开源软件的黄金时代呢?你有何看法?
James: 我不知道。这个问题涉及未来。“现在是黄金时代吗?”背后的意思是,将来要开始走下坡路了吗?如果现在是黄金时代,那么未来就不是黄金时代了吗?
我不知道。这个问题涉及未来。“现在是黄金时代吗?”背后的意思是,将来要开始走下坡路了吗?如果现在是黄金时代,那么未来就不是黄金时代了吗?
为什么有的语言不使用JIT?
Grigory: 您使用JIT(即时编译)创建了Java和JVM。JIT保证了高速,同时保持了高级语法。许多语言都跟随你的脚步,比如C#和JavaScript。其他语言,如Python、Ruby、PHP,都有可选的JIT,但不太常用。许多主流语言也并不使用JIT来提升速度。为什么不是所有的语言都使用JIT呢?他们不想为软件开发人员提供更快的速度吗?
James: 如果开发者想要获得性能改进,那么使用静态类型语言是非常有用的。通常,人们会给语言添加注释,就会得到TypeScript这样的语言,TypeScript的本质就是带有类型注释的JavaScript。而JavaScript的本质是Java,只是去掉了类型声明。所以TypeScript本质上就是带有置换语法的Java。
在Python之类的语言中,通常只有一种数值,那就是双精度浮点数。没有真正的整数,没有字节和16位整数这些东西。如果是双精度浮点数和单精度浮点数,概念认知的过程会很复杂,但你必须掌握数值分析。许多软件工程师对数值分析几乎一无所知,所以他们直接放弃了。
如果你是一个使用Python的物理学家,那么精确度对你来说是最重要的。如果你需要在内存中放入一个非常大的数组,在这种情况下,单精度和双精度或8位整数之间的差异就非常重要。
在我的一生中,我参加过许多数值分析课程,被数值分析弄得焦头烂额,因此我不得不关注数值分析。具体问题具体分析,大多数使用脚本语言的人并不关注这类问题。很多人并不关心具体的性能和数值,他们关心的是:“它够快吗?”性能是一个boolean值,对一些人来说,这更像是调试一辆赛车,如果你的车每小时能多行驶两到三英里,你更有可能获胜。
Grigory: 几个月前,Ruby on Rails(一种广受欢迎的Web框架)的作者David Heinemeier Hansson提到,在他的云计算的应用开发中,性能基本都是由缓存、消息队列、存储等方面决定的,开发语言的影响最多不超过15%。他指出,不管Ruby有多“慢”,这都不是很重要,因为即使Ruby的速度快了100倍,将15%变成了1%,也不会改变什么。
James: 要具体分析你的任务类型。如果你的任务是由网络和数据库等主导的,你一直在做RPC,那么第一步就是质疑这些RPC是否有价值。微服务确实挺不错,但是它们比方法调用慢一百万倍。想想这意味着什么。
对于大多数人来说,他们可以通过清晰的大规模架构来获得更多性能。但对部分人而言,所有的细节都很重要。如果你知道高并发的重要性,知道它能够同时驱动数千个进程,进行大型计算;如果你做的是数据库或存储服务,你会非常在意这些细节。所以这完全取决于你手头的任务。
关于现代async/await方法
Grigory: 许多语言都采用了协程和async/await方法来处理网速慢之类的问题。这个方法被添加到Python、 Ruby、 JavaScript以及其他语言中。但是在一个线程中这个方法并不是无所不能的,并且很复杂,有时还会使软件运行速度变慢。那么,您如何看待这种现代async/await方法呢?这是处理网络的好方法,还是我们误用了它,我们需要寻找其他方法?
James: 这依然取决于实际情况。协程是完美的,它起源于60年代。第一种使用协程的语言是Simula 67。Simula是一种很好的语言,它没有线程,但是有协程,只不过它们做协程的方式看起来很像线程。协程完全避开了并行中的一些难题。对我来说,协程有一个问题:它们不允许使用多个处理器,无法做到真正的并行。因此我已经很久没有使用协程。
所以人们更重视那些真正具有并行性的语言,比如Erlang和Java。
在Java中使用ConcurrentHashMap可以做到很多事情。如果你在进行多协程操作又没有足够的处理器,一旦你使用基于协程的语言并使用多个处理器,最后也只是饱和了一个处理器而已。使用多个处理器也是不得已的,毕竟世界上已经没有单位处理器了。每样东西都有很多内核,如果你想在一个问题上同时使用所有的计算机,你只需要克服多线程所固有的复杂性。
还有样式的问题。透明的控制反转时常发生,而你只能被动接受。这会让你的语法看起来很像线程,但也意味着在真正的线程中会遇到一些问题。例如,你输入“a=a + 1”的语句,你知道这个操作不会被中断,所以你不需要同步。但在其他情况下,它变成了一个event导向的样式,操作过程中,你需要插入一个事件处理程序,去处理后续问题。这就是JavaScript的主要风格。这样也不错,但似乎有点笨拙。
70年代早期,我第一次接触Simula,我发现它很灵活,在编码过程中,运算过程几乎是独立的。作为一个概念模型,它要清晰得多
第一语言的选择
Grigory:我想问一个非技术问题,在你看来,对于现在刚入门的软件开发人员或者是研究生和大学生而言,他们应该选择哪一种语言来作为他们的第一语言?
James: 我回答这个问题可能会带有一点偏见,毕竟 Java 已经成功运行这么多年。我学的第一种编程语言是PDP-8汇编代码,随后是Fortran。大家可以去学习任何语言,有些人的接受能力更强,但这很大程度上取决于一个人最终的职业道路。如果你想成为一个软件开发人员,你要构建大型的、高性能的系统,运行在JVM上的语言最值得去学习,例如Scala和Kotlin,Clojure很有趣。如果你是物理专业的学生,Python是个不错的选择。
其实选哪一种语言都无关紧要,很多人都只是坚持他们学到的第一种语言,如果你能让人们反复学习各种语言,那肯定是最好的。我认为每个大学都应该为学生开设一门“比较编程语言”的课程。用五种不同的程序语言完成作业,这能加快学习进度,并且他们会发现这些语言的区别真的不大,同时也能让他们自己去思考,哪一种语言更好。很久以前我上过一门课,每次作业我都用最不合适的语言,例如,用Cobol语言进行数值计算,以及Fortran中的符号操作。令人惊讶的是,我的成绩依然是A。
如何看待“模式匹配”?
Grigory: 下一个问题是关于模式匹配的。最近,它与Python、Ruby接轨,并且为不同的语言提出了建议。我们查阅了开发人员白皮书发现,目前并不能完全确定模式匹配在现代高级语言中的作用。您认为这种模式匹配思想,如何适用于Java、Python、Ruby或其他高级语言?我们真的需要模式匹配吗,或者它是特殊案例的特定用法?
James: 对于新手来说,我认为编程语言中的“模式匹配”一词会造成误导。当我听到“模式匹配”这个短语时,我想到的是正则表达式。Simula有inspect语句,而inspect语句与许多模式匹配语句几乎完全相同。inspect语句是一个case语句,其中case标签是类型名,你可以执行:
Inspect P
When Image do Show;
When Vector do Draw;
因此可以把它看成是一个case语句,用case处理类型,大多数模式匹配语言的建议都是这样的。就我个人而言,我很喜欢,特别是应对C的隐形强制转换。在C之类的语言中,常常需要进行强制转换。如果你执行“inspect P When Image P do P ”,那么在case语句体中,P就是switch标签的类型,这让一切都变得简单多了。我很喜欢Simula中的inspect语句。我同意所谓“特殊案例”的说法,如果称之为“模式匹配”,但是它又不如正则表达式,就会有误导性,像一则虚假广告。但是抛开这些,我依然认为它的功能很强大。
可以将所有的语言联系到一起吗?
Grigory: 还有最后一个问题。俄罗斯软件开发人员对JetBrains和Kotlin的开发倍感骄傲。Kotlin、Clojure和Scala等语言,在您创建的Java虚拟机、库、框架和现有代码生态系统上蓬勃发展。但是,这些语言都面临着挑战吗?能不能把这些语言都联系到一起呢?当有人试图用不同的语法对Java执行hotswap操作时,会遇到什么困难?
James:这要看你的目的是什么。Java虚拟机内置了许多安全性和可靠性的概念,主要与内存模型的完整性有关,诸如指针之类的东西,因此你不能伪造指针。对于C语言,如果你没有能力伪造,你将无法使用。如果你试图在JVM上实现C语言,并配置了严格的安全虚拟机,有些功能将无法实现。但是有些人所构建的虚拟机并没有严格的安全模型,并且没有内存分配模型。如果想要在C和Kotlin之间实现互操作,就必须放弃一定程度的安全性和可靠性。这就看你的取舍了。
在Java诞生之初,我的原则是:我不想调试内存损坏问题。在内存奔溃问题上我已经浪费了太多的时间,这个问题能消耗你几天的时间。我真的很讨厌追踪内存损坏bug。不过,每个人的喜好不同,可能有人认为花时间做这件事很有价值。也有人喜欢使用vi,在70年代和80年代它都是一个很好的编辑器。
Grigory: 内存安全模型确实很重要,它提供了一些东西,但也造成了一些限制。非常感谢你,James!
结语
读完Java之父的专访,你有哪些收获与见地呢?对你来说,哪种语言是你的第一语言呢?