这篇文章对 Rust-1.8.0 nightly 版和 penJDK-1.8.0_60 进行了比较。 我是一名Java开发者,但从我写的其它一些博客文章中或许不太看得出来。我也公开承认我喜欢Java,这或许使我成为少数人中的一部分。 然而,如果你曾读过我写的其它一些文章,你应该很难忽略这样一个事实,我也真的很喜欢用Rust编程。所以,既然我是一个对Rust和Java都懂的人,为什么不对它们进行一下比较,看看能得出什么结论呢? 历史Java在诞生之初叫Oak。早期的版本一直使用Java的这个绰号,直至1995年,1996年1月发布了1.0版。所以现在的Java有20多岁了!Java早期的目标主要是可移植、简单并且健壮。最初将Java设想成为一种低级的支持垃圾收集和字节码解释的面向对象编程语言。早期的版本慢的像蜗牛一样,并且要消耗掉大量的内存。到现在可以说早期的那些问题绝大多数已经解决了。 |
在早期的互联网时代,Java 被宣传为 “Web 的编程语言”,这都是因为 applet 。但这并没有确定的工作和计划,尽管现今的语言被运行在 web 客户端是开始于 ‘Java’。但是,现今的大多数 Web 架构都被运行在 Java 服务器侧的。 Rust 1.0 长期处于开发(大部分历史可以在 GitHub 上看到,那可以回顾到 2010 年)之中,在 2015 年 5 月被发布。Graydon Hoare 开始这个项目实际上可以追溯到 2008 年左右。自从 2010 年开始, Mozilla 就赞助其开发,随之建立 Servo 项目,旨在创建一个在 Rust 中的现代浏览器引擎。 Rust 开发者开始在设计上进行许多迭代,很多东西被不断扔掉。早期的 Rust 版本就有绿色的线程,如果没记错的话,垃圾收集已经在 0.8 版本期间被移除。而快速迭代曾令很多早期用户不快,但拥有一起公开讨论的文化,使它拥有一个非常全面和周到的设计。 |
运行时 我确定的一件事是,在大多数运行条件下,一个典型的Rust 程序比Java 消耗的内存会少上一个数量级。 随着Java 的流行,我们知道它不是单态的(至少在编译时候不是,JIT编译器会重新编译热点代码),Rust是单态的。这样Java 就会拥有更小的编译后的二进制机器码。现在上述优点已经被运行时和大量涌现的流行库消耗殆尽。 Java 的内存回收已经最优化了,并是经过深思熟虑的。但是它还是没有解决所有问题(Rust的ownership系统解决了这些问题,并使程序更少的出错)。在热点代码上,高性能Java 开发者努力不分配内存,以使GC系统可被管理。这有时又会导致代码错综复杂。 |
更新:Reddit的会员pjmlp指出,JAVA有一些领先的编译器,特别是像Android的ART及一些商业JVM所提供的,据我所知,后者并没有被普遍使用。 另一方面,Rust有一个占用0字节的运行时环境,实际上这确实是有一个运行时环境,只不过它由一些安装的“紧急着陆架”组成,甚至可以用如内置的或操作系统的开发替换。 作为一个完全编译的语言,Rust(理论上)不会像JAVA语言一样有着灵活的跨平台性,但Rust基于LLVM,这使得很多(使用Rust的)后端,可以将开销控制在一定范围。此外,由于缺乏一个庞大的运行时环境及其垃圾回收器使得Rust相较JVM而言更加轻量。 生命周期及所有权另一方面,Rust由于其生命周期及所有权规则能在没有GC(垃圾回收器)的情况下获取对象,其对象通过borrow checker(借还检测器)维护管理,这个borrow checker有时被亲切的称之为borrowck。 这是JAVA所没有的特性,同时为Rust引入了一系列的利与弊。前者因为数据竞争、ConcurrentModificationException及其它祸害JAVA代码相关的东西,编译器会确保其自由性;而后者因为在一些情况下,每个人学习Rust的时候都会运行其head脚本并不用borrow checker,然后问Rust之神为什么他们完美手动添加的生命周期的注解不正确。 |
事实上,规则很简单:你可以拥有一个素数(你可以做任何事,当借进来时期望丢弃它),一个可变的亦或像许多一成不变的暂借,只要你喜欢。借以相反的顺序结束,这意味着有时候切换开关语句可以借以检查程序。 当你涉及部分类型时,它会变得更加复杂,因为这些类型在一些生命周期是通用的。我不会在这里深入介绍,已经有短篇和长篇系统阐述它。只需记住,当你看到'a:'b,它意味着a的生命周期比b的要长。 不幸的是,当概念已经存在了至少20年,在这样一个严格正式的形式中,这是全新的。所以,它需要一定时间来了解它们,当错误信息是有用时(对于备案也一样,其作用会随着仿制药的引入而体现)。 即使在陡峭的学习曲线和更长的编译时间,借助检查是一个伟大的胜利,因为,虽然它可能有时过于严格和苛刻,但是它捕获了真正漏洞,即使有一个很出色测试套件你可能忘记在Java里面使用了。 我喜欢添加那些尽管看起来很巨大的差异,Java的存储模型和Rust是惊人的相似。 |
类 型Java的基本类型是Rust的基本类型的子集(Java没有无符号整数类型,Rust还提供其它一些类型如一些SIMD类型)。尽管在实践过程中,通过实现良好的逃逸分析等手段,可以减少了实践中指针引用带来的一些痛苦,但是将所有存储对象作为引用,还是会导致指针过多使用。同时因为对象通常存储于堆中(禁止堆外的东西,已经在某些圈子成为时尚),这也解释了为什么Java使用堆比Rust多得多,。 Java的整型操作均是封装好的(并没有溢出检查),而Rust的在调试模式进行溢出检查,在发布模式下进行封装不做检查。这使得Rust在测试期间能够检查溢出发现问题,并在发布版本不进行检查提高执行效率。 Rust具有内置的元组类型,可以很容易地基本无额外开销的返回多个值。在Java中,返回一个<A,B>对总是有点麻烦(并附带引用追溯的开销)。在Java中,有人建议增加值类型,从而使其同其它低级语言保持一致,但听说这个建议至少推到Java的10版本或者更后的版本中去考虑。 Rust的在数组类型中携带了该数值大小的信息。但是,创建在运行时可以动态确定大小数组,不用一些黑客的手段和技巧是很难做到的。 Rust爱好者倾向于使用Vecs类型,这相当于Java的ArrayList的(这仅仅适合基本类型)。无论Rust和Java均在编译阶段保持他们的泛型。Rust的类型系统比Java的更强大很多;实际上它是具有图灵能力(可以建立除了零大小类型外的其他任意类型结构的能力)。Rust同时也还利用泛型能力进行生命周期信息的交互。 Java有通用的空类型 --- 每一个非基本对象由于它们都是引用的类型因此均可以为空。再次说明,Java包含值类型后,值类型将减少可能的NULL值,但我们需要再等一段时间。 |
Rust 的 enum 是总和类型,而 Java 的 enum 则是简单的类似于整型数的类型。另外,Java的 enum 值在底层实际上就是单例类 —— 因此人们可以在 enum 中定义抽象方法来实现每个值中的差异化, 这样的东西在 Rust 中就需要有一个匹配表达式 (或者是哈希表,其编译时的构造,某些比较进取的 Rust 大牛已经为其构建了一个 crate)来实现。 Java 的原生类型拥有自动扩大的强行机制,因此你可以将一个整形放到一个接收长整型参数的方法中去。Rust 只拥有“解引用强行机制”,意思就是在解引用(会进行隐式的点操作,或者你也可以使用 * 前缀操作符来明确的进行) 时,其特征实现就是从一个使用了一个适当的 Target 类型的 Deref 实现那里查找出来的。 不过, Rust社区已经声明,对此的过度使用是一种反模式,而 Rust 代码一般会有明确的类型转换。 因此,一般无需查找太久就能轻松地把任何表达式的类型定下来,不用进行方法范围广度的类型推断,这在你有时遇到方法签名异乎寻常的长的单行方法时也会有影响。不过我觉得其语义表达得恰如其分。 你一般不需要看得太仔细就能理解一个 Rust 方法,而 Java 代码有时候就表达得不怎么明了了。 总而言之,更加彻底的类型系统、借鉴和其它的检查,以及默认的不可修改特性,还有大量蹩脚特性的缺失,这些都意味着在大致相同的时间内写出来的 Rust 代码要不 Java 代码更加的稳定。换言之: 编写 Rust 代码可能要比 Java 更难,但同时写出不正确的 Rust 代码也要比写出不正确的 Java 代码难很多。 这个在使用 rustc 编译代码时就会有效果,一般第一次就能跑起来。除了 Rust 之外,同样的自信心只有 OCamML 和 Haskell 能给我, 而这两者无一不是处在一个较高的水平。 |
Java 有 Class, Rust 有 TraitJava有class. 我可以说它有类,甚至是很多类。它也有接口,在最新版本里接口甚至可以自定义缺省方法。 我可能不需要重申,Java类里将数据和行为绑定在一起(封装),,控制访问, 继承于其他类, 实现接口等等。Java里的所有东西(除了一些基础数据类型,基本上是空)都是对象 (并且都是某一个类),哪怕它只是一堆静态方法的集合。 类相当于类型。大量的基础类构成了Java结构的中心。 相类似,Rust有traits,和Java8的接口诡异的相似。它也有类型(通常是结构体或者枚举)和类型接口实现。它也有固定实现(他们自身类型)。最后访问域通常是模块(模块类似于Java包,但是Java包只是一些类和子包的集合)决定。 这种数据和行为分离的方式在开始会显得有点奇怪,但是实际上它很巧妙, 因为它是的数据类型的复合变得非常自然,并且也可以在已经存在的类型上增加新的traits,这在java里完全不可能。 Rust也有只存活在模块内部的独立函数,这表示在写程序代码的时候可以少些很多套话。比如不用类似public class HelloWorld。 Rust的数据和操作分离的方式候提供了一种基于数据的编程方法,你可以首先创建你的数据结构,然后再围绕它进行构造操作。
|
模式和SOLID原则
|
控制流下表显示了两种语言不同的控制流结构:
¹ 显然,这只支持简单范围,否则我们可能要写一个while循环。 ² JAVA的switch声明不及Rust的match语句的全解构模式匹配,另惊讶的是case语句之间更加明显(IDE对此进行警告,让我的惊讶有所折扣) 注意这个列表并不完全,及一些并没有完全与所有案例进行比较。大多数Rust的构造器对于不同loop和match声明的不同混合实际上是语法糖(syntactic sugar)。在JAVA中,for-each循环编译为基于迭代的for循环,其结构为for (Iterator<_> i = _; i.hasNext();) { _ v = i.next(); _ }。 所以简而言之,Rust去掉了C语言那样的for循环。因为分配语句没有返回值(a = b = c这样的语句在Rust中不合法),所以if let 和 while let 之类形式的语句要关心这些,这使得这样的语句看起来更加直观, if (x = y) 这样的错误也不大会出现(不过公平的说,大多数JAVA的IDE也会捕捉这些错误)。 Rust的语法糖围绕着异常强大的解构化的match,使得Rust在某些方面比JAVA更高级,但编译器依然设法生成基于语法糖的更紧凑代码,这些代码大多数要感谢LLVM, Rust编译器使用LLVM来生成代码。 |
错误处理Java中的异常分为两种类型:被检查异常和未检查异常。前者意味着可能的失效模式,该异常能够被调用者直接处理或沿着调用链(以在你的方法定义中声明抛出异常的方式)向上抛出。后者意味着程序员错误,通常被视为不可恢复。 未检查异常的实例有可怕的空指针异常,还有唯一没有那么可怕的数组越界异常(因为它指明了错误具体原因):每一个针对间接引用对象的操作都有可能抛出空指针异常。空指针异常是如此的糟糕以至于在Java技术圈中它有个被熟知的缩写“NPE”。大多数情况下,我们能够清楚的知道是什么错误操作造成了空指针异常,但是有时我们需要花费很长的时间才有可能找到造成空指针异常的错误操作。 任何异常都可以被捕获,这样可以引导新手捕获异常,从而摆脱追踪恼人的堆栈信息。哪些是当然错误的实践呢;就想我之前说过的一样,运行时异常不应被捕获。 一些人认为被检查异常是糟糕的并认为所有异常应该都是未检查的。我个人不同意,但是我不愿意在此花费时间解释。 Rust现在有与线程绑定的“panics”可以被视为运行时异常,它会杀死该线程并应该只能够被另外一个线程捕获.在最近的Rust版本中,使用下面类似:"std::panic::recover(_) that can call a closure and return a"的语句能够将结果中的任何“panic”转换成"Error"。但是这个功能还不稳定,只能够在每日更新的Rust开发版本中使用。 |
还有一些语法糖处理 Result 类型,这类似于 Haskell 和其他函数式语言的一元错误处理。 有利的一面是处理错误变得更具体 —— 一眼就可以看到这个表达式可能是一个错误的方向,以前的那些被包装在 try!(_) 宏调用中,最近一个 RFC 的语法糖已经被接受,因此那些看起来像是 _? 的功能将出现在将来的版本中。 可能打开(unwrap)一个 Result 后,转换成任何错误都会带来恐慌。这通常被用于原型设计,但是在生产代码过程(甚至有一个第三方的 lint 反对这样做)中将会产生不悦。 不利的一面是“冒泡(bubbling up)”错误不再像拍击那样简单抛出 SomeExceptionon 函数声明。函数的返回类型必须改变一些 Result<T, E> 类型(如果函数执行失败,并且 T 是原本的结果,那么 E 错在哪里)。所有的错误方向函数必须与 try!(_) 宏(扩展一个 匹配(match) 在 Result 相加之上,在早期出现错误时就会被 返回(return)) 一起被调用,一些错误的类型可能是不相容的,会导致其中一者盒化错误(对象的基本特征是 std::error::Error 特征)或者在包装之后必须解构以了解原因。 在实践中证明,它工作地很好。 |
函数 && 闭包 JAVA终于有了lambdas表达式,但看起来不及Rust的闭包功能强大, Rust的闭包能修改所获环境以与Rust所属规则保持一致。然而,在大部分情况下JAVA还是非常好。函数处理两者是一样的。一个方法的interface(接口)能通过匹配那个方法数据类型的的所有函数自动实现,这挺好的(除了一些小技巧外)。 Rust的函数隐式实现了一些 Fn*() -> _ 类型,因此可以在没有分配栈的情况下,进行不同设置。调用者必须用所给定类型绑定才能正常运行,这通常需要需要一些技巧。然而,某个调用者可以调用比JAVA更加有规则的策略。 JAVA的流提供了低消耗的方式数据并行计算。Rust并没有直接提供支持,但 Rayon 提供了相较低消耗的并行迭代器,然而还有许多其它第三方的库瞄准了并行与并发方面。 JAVA提供可变函数,以内部使用的数组。然而有些小技巧,在某些情况下可以考虑更好的接口。Rust至少可通过宏模拟,或使用切片参数来模拟可变函数,但或许某天Rust也会提供。 JAVA分配函数基于参数类型,内部修改函数名以包括签名,类似如 next()Lllogiq.example.Example 。而Rust并不这样实现:函数永远总是取一系列的参数,尽管泛型可以扩宽可能的签名中的类型集,如 some_func<S: Into<String>>(s: S) 。 |
我认为Rust的设计者说的非常好:不赞成拥有相同名称的方法依据类型不同处理完全不同的事情。 元编程Rust 既有 procedural 又有 procedural。前者是 Rust 程序可以重新令牌树(token trees),而后者是一种 quasiquoting 类型的模板语言。如上描述,类型系统能够被滥用在“有趣”的(例如:破坏他人代码)事情上。 |
其他语言的接口在一个完美的世界,每一种语言都可以简单地被其他种类语言调用。这显然不现实,但是 C 应用二进制接口(ABI)已经合并成一个通用的标准以适应目标语言。Java 也一样,Java 拥有 Java 本地接口(JNI)。有一个 javah 工具,可以用它来生成 C 头文件来满足 JNI 的需求,有一个一直流传着的谣言,它的目的是让开发人员能触及到经常用到的本地代码。也有一些涉及到 GC 的天花板(因为函数不再给对象使用GC,以免对象内存泄漏)。本地代码必须被放在 java 库路径,然后被加载到一个应用。 通过允许定义 extern "C" 函数, Rust 可以有一些接口直接与 C 交互,编译器将承担哪些 C ABI。有一些褶皱涉及到所有权,生命周期和类型,因为本地代码通过定义不能支撑 Rust 的保证,因此,通常还需要一些包装,呈现一个安全和简单的接口。在 Rust 中还有一些允许直接嵌入到 C++ 的子集,但是我既没有时间,也没有必要发现需要测试它。 Rust 明显的好处是层级低,与 C 的接口只需更少的操作。尽管 Java 的设计者真的担心人们会本地化太频繁,但他们的担心是毫无意义的,因为 Java 本身就做得很好。 |
标准库Java 的标准库包含大量的东西,从 Annotations 到 ZipOutputStream,甚至更多。几乎只有 Python 能在内置库程度上与其左右, 你只需使用 java.* 和 javax.* (一些 org.* 也被包括在内),就可以做出很多伟大的东西。 Java 不抛弃不放弃。因此,它的 API 已经有三个 UI 工具集(AWT,SWing 和 JavaFX),有Enumeration(枚举) 和 Iterator (迭代器) 接口(它们做的几乎是同样的事情),还有两个 IO 类(java.io 和 java.nio,尽管我承认后者以前者为基础)集合和其他有趣的东西。 这样做有利的一面是,可以使得 Java 代码非常长寿,在 JavaLand 2015 期间,Marcus Lagergren 展示了一个 Java 1.0 的 applet 程序,它至今仍在运行(尽管在 Java 9 上,这个例子将不能再工作,因为 applet 最终将会被移除)。 Java 的官方 API 倾向于零意外,并且需要极其完整的文档。 由于 Steve Klabnik 被 Mozilla 雇佣为 Rust 的文档专家,也因为他的工作,Rust 的文档也能紧随其后。 Rust 的库是精益和敏锐的。它有一些集合类,包括大量的字符串处理,智能引用和 cell,并支持基本的并发性,还有很多 IO/网络和迷你的 OS 集成。正是因为这样, Rust 的代码将会依赖一些第三方库,不过这些库非常容易获得和管理。这里好的一面是,如果他们只是想要写一个 JSON 解析器,那么就不需要下载一组的 MIDI 类的 - 或者反之亦然。
|
因为 Rust 的底层特性,它也经常得对在Java中作用相同的功能进行区分, 因为这个原因,如果要拥有或者借用某些东西 (或者能够拥有,又或者需要拥有,等等。) – 这会导致一种多样性的迭代, 例如: Java 的 Iterable 拥有一个 .iterator() 方法, Rust 就得要有‘.iter()’ (迭代不可变型借用) 和 .iter_mut() (迭代的可变型借用),而且有时甚至还得有 .drain(..) (对取值进行迭代,可以选择删除或者替换元素)。此外还有一些辅助特性来实现类型系统的大部分东西 (我已经就这些写过一些东西了)。 Rust 的 API 文档允许离线关键词搜索, 这个在你知道自己要寻找是什么的时候很不错。许多的类型通过许多的特性来协调它们自己的行为,这或多或少的阻碍了其可探索性。换句话来讲,一旦你了解了你所要使用的特性,你就能用它们做出令人惊奇的东西来。 人们也经常会将Rust的标准类型组合在一起; 但你不会经常遇到像 Rc<RefCell<Vec<T>>> 这样的东西。类型别名被用在许多地方来减少嵌套的数量。Manish Goregaokar 写过很不错的 一段 来描述如何选择封装类型的正确组合。 人们还担心,其实在当前版本的Rust中,任意大小的数组(尽管不是向量)或者元组还不能被表示出来。作为一种妥协,现在有了对于大多数必需特性的实现,支持大小到32个元素的数组以及12个元素的元组。人们必需创建一个封装类型来实现这些特性,例如一个拥有33个元素的数组。对于这些问题的处理有一些建议,但显然还是需要进行大量工作,而合适的实现到目前还没有出现。 尽管同 Java API 的规模相比有点相形见绌,但 Rust 标准库已经具备了令人惊讶的能力。一部分API被标记为不稳定的, 这意味它只会在一些实验性质的编译器或者某些 #![feature(_)] 注解上有用。这样可以使库的开发团队在快速迭代API设计的同时保证版本的稳定。另外,这也限制诸如 BTreeMap 这样的无用特性, 如此会有相当多的方法在发行版本的 Rust 中被去掉。这种情况非常有可能随着时间的推移得到改善。 |
工具 Java 的工具已经成熟了近10年,因此像预期的一样他非常的出类拔萃。有不计其数的 IDE,构建工具,代码分析工具,部署和操作工具,剖析工具,覆盖测评工具,性能测试框架,文档书写工具,调试工具等等,其中大多数工具都是自由使用的,要么可以公开使用,要么可以私下使用。 社区和开发者值得一提的是 Java 社区是庞大的。许多人在工作中使用 Java ,至少欧洲是这样,你很难找到一块没有一个 Java 开发者的土地。同样 Java 的生态系统中也很出类拔萃:无论你需要什么,很可能已经有人写了一个类库去实现它。Java 的生态系统架构相当的活跃,但是有一定的下滑风险.
|
Java有专业化的氛围,大公司都是用它。它非常好操作,你可以一手编程,一手穿衣服和打领带(我经常这么做)。Java圈子里的人并不意味着没有趣味,相反,我们往往是快乐的一群人,但是一旦谈起业务,我们都会很专注。 在许多讨论中总少不了一些抱怨的声音:java 瘫痪了,java 版本过时了或者说软件太旧。但是,我在这必须要说明的是 Java 用起来非常好。与此同时,也有一些人提出 Rust 的黄金期永远不会到来。但在 crates.io 上多于2500万的下载量说明了 Rust 随时可能兴起. 我非常有兴趣参与 Rust 社区。Rust的社区与Java相比还很小,但是这里有很多熟练使用 Rust 的人,他们友善,乐于助人,而且幽默,在这里的每一次交流都使我感到快乐, 一点都不烦恼。一些人说他们被迫坚守编码行为准则,但是我还没有看到任何人反对特定的代码,我相信最后的结果会为他们发声的. Java 的开发主要受 Oracle 的管控。因为 Java 被许多大公司使用,它的开发节奏还是相当不错的,但因为他们需要关注许多不同的使用场景,使得 Java 的开发速度无法与 Rust 相匹配。各大版本之间的更新滞后,最长的是花了五年时间更新的 1.7,而现在我们看到大约每两年就会有更新版本。与Java 不同,Rust 每6周更新一个版本(尽管每个版本之间的变化不像 Java 一样激动人心)。 在这次比较中 Rust 不被看好,但是就目前来说 Rust 已经做到很好啦。尽管 Rust 的社区规模小,但是 Rust 社区通过敏捷,智慧和专注弥补了人员的不足。在社区成员的支持与帮助下,Rust 成为了一个伟大的语言. 总结Java 还有很多工作要做,并且我可能在很长一段时间内继续使用它。同时,在可预见的未来,我想我将成为 Rust 圈的一员。Java 和 Rust 都有各自的优点和缺点,它们也都有不错的未来,它们的社区还可以相互学习,当然,这是我个人的观点。 |
本文转自:开源中国社区 [http://www.oschina.net]
本文标题:比较 Rust 和 Java
本文地址:https://www.oschina.net/translate/comparing-rust-and-java
参与翻译:混元归一, 无若, imqipan, leoxu, 昌伟兄, kaiqing, hxapp2, 阿采, xufuji456, 边城