通常Scala被认为比Java要慢,特别是用于函数式编程时。本文会解释为什么这个被广泛接受的假设是错误的。
数据验证
编程中一个常见的问题是数据验证。即我们要确保所有得到的数据处于正确的结构中。我们需要从安全的,编译器验证的数据中找到不安全的外部输入。在一
个典型的WEB应用中,你需要验证每个请求。很明显这会影响你的应用的性能。在本文中我将会比较处理这个问题的两种极不相同的解决方案。Java的
Bean验证API和来自play的统一验证API。后者是一种更为函数式的方法,它具有不变性和类型安全的特性。 Java: Bean 验证API, aka JSR 303 Bean验证规范首发于2009年。此API使用注解为JavaBean设置约束。然后你需要在一个注解实例上调用验证方法来验证这个Bean的有效性。它的最著名的参考实现来自于Hibernate. 这是他们网址上的一个小示例。:
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
private String licensePlate;
@Min(2)
private int seatCount;
// ...
} 这只是用于声明,真实的验证应该像这样(同样来自于他们的网址)。 ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Car car = new Car("Ford", "0xCAFEBABE", 2);
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
所以你将一个实例传递给validator.validate并获得一个包含错误的Set。如果这个Set是空的,那这个对象就是正确的。
通常你需要用此API来验证Json和XML.以下是一个解析和验证Json对象的例子。
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
private String licensePlate;
@Min(2)
private int seatCount;
// getters and setters
}
String json = ... // contains a json string representing a car
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
ObjectMapper mapper = new ObjectMapper();
Car car = mapper.readValue(json, Car.class); // use Jackson to unmarshall the json
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);
Scala: Play 统一验证 API.
这个统一验证API 致力于提供验证任何数据结构所需的核心原语。它的主要目的是替代Json验证API和play框架中的表单验证API。它易于拓展且支持Json验证和非传统的表单验证。
以下是一个Json验证场景的一点。注意这次我们在直接验证Json。
case class Car(manufacturer: String, licensePlate: String, seatCount: Int)
val carValidation = From[JsValue]{ __ =>
((__ \ "manufacturer").read[String] ~
(__ \ "seatCount").read(min(2)) ~
(__ \ "licensePlate").read(minLength(2) |+| maxLength(14)))(Car.apply _)
}
val json: String = ... // contains a json string representing a car
val validationResult = From[JsObject, Car](json)
在实现此API时,我没有太注重性能。我的主要目标是正确性,组合性和类型安全。实例上一些设计选择,例如惰性验证,都会对性能产生影响。接下来我们看下它是如何执行的。
基准测试协议
该基准包括解析和验证存储在文件的JSON对象. 这些数据是从 the Last.fm Dataset 抽取。JSON的结构已经被修改了一下,便于解析 (使用非常棒的 jq). 代码托管在 这里。
通过使用一系列的scala的计量基准测试进行性能测量. 这两个API都使用Jackson 解析JSON。
结果 基准用5000到10000的JSON对象解析并且验证所花费的时间来测量。在两个不同的场景进行测试:
这是结果。较低的执行时间性能更好。 
让人惊讶的是,Scala的API快很多!
Scala的API相比比Java 的API,明显更快。 有无效字段将极大地影响Java API中的性能,而对Scala这边的影响不大。
基准测试不重要 那么我们学到了什么呢?
我们学到了在这特殊设置下,一个用Scala写的特殊库比一个用Java写的特殊哭更快。这并不真的意味着一个Java程序总是比一个Scala程序慢。
从Scala开始以来,我们就看到很多人好奇 是否Scala真的比Java慢.
基准测试得到了更多的关注, 这一个 是特殊的。它演示了一个用cpoll_cppsp 写的返回json的web程序比nodejs应用快4倍,后者每秒‘只’发送228,887次响应。
我写了一个实际的Web应用程序。我也贡献代码的Web框架。这和我有关吗?
不完全是。基准测试不会与实际应用有关系。同一个量级上,在一个节点上每秒228887次反应,比任何实际的应用数据都要多。
你可能会问为什么我花时间写我自己的基准测试?我期待着并希望能证明统一的API能比Java做的更好。与流行的观念相反,Scala程序可以比其对应的Java更快。
一个有趣的问题是:为什么“慢”的语言速度更快?答案其实很简单。一个更好的,可扩展的语言提供了更好的工具来写出更好的程序。
Java在微基准测试上打败Scala也不要紧。因为Java缺乏必要的构造,它强迫你使用的变通的方法,例如使用flect。这些方法不仅影响了你的程序的性能,更重要的是,他们是使得你的程序运行缓慢的原因。
关于正确复合和用户友好的一个案例Java的问题不友好的API
主要问题在于我使用了Java的validation API,(实际上与Java一样)不必要这么复杂。当然这个平凡的例子看起来漂亮的和简单的,但是当你开始钻研它后,事情就变得很有趣了:
让我们看一个简单的例子,跟踪JSR-303的一个实例:
24 | private List<Similar> similars; |
26 | private DateTime timestamp; |
28 | private String artist; |
30 | private List<Tag> tags; |
当然,这些看起来足够检查一个给定的跟踪实例是否是有效的了。
错了。你看,我们并没有明确的验证一个跟踪实例的有效性,还必须检查其Tags and Similars的有效性。你需要用@Valid标注每个属性。
这种行为是不合直觉的。当我不知道一个跟踪实例的成员是不是有效的时,我无法得它是否有效。
校验与分解
JSR 303不处理数据的整理与分解。 他只是对类实例的校验。出于测试目的,我使用Jackson来将Json数据分解为类实例。但问题是,校验时分解的一部分。我们很难在没有检查JSon结构前提下将JSON树转化为类实例。 "age"属性存在吗?它是Integer类型的吗? 考虑到这点,Java 工作流看起来很奇怪。当处理Json时你实际上需要处理3中类型的错误:
JSR-303仅仅帮助你处理后者。
这个AIP同样强制你直接使用不合法的实例。我认为如果一个类实例不合法,你最初就不应该创建它。
难以扩展API
Hibernate校验程序需要手动添加一些列校验规则(如邮件,长度,非空等)。那如果你需要创建新的校验约束条件(比如从hibernate文档中的例子),你该怎么办? 01 | @Target ( { METHOD, FIELD, ANNOTATION_TYPE }) |
03 | @Constraint (validatedBy = CheckCaseValidator. class ) |
05 | public @interface CheckCase { |
06 | String message() default "{com.mycompany.constraints.checkcase}" ; |
07 | Class<?>[] groups() default {}; |
08 | Class<? extends Payload>[] payload() default {}; |
12 | public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> { |
14 | private CaseMode caseMode; |
16 | public void initialize(CheckCase constraintAnnotation) { |
17 | this .caseMode = constraintAnnotation.value(); |
20 | public boolean isValid(String object, ConstraintValidatorContext constraintContext) { |
24 | if (caseMode == CaseMode.UPPER) |
25 | return object.equals(object.toUpperCase()); |
27 | return object.equals(object.toLowerCase()); |
那是大约 25 行代码。21 行基本上是固定的,所谓纯“仪式性”的。有趣的部分仅仅是:
1 | if (caseMode == CaseMode.UPPER) |
2 | return object.equals(object.toUpperCase()); |
4 | return object.equals(object.toLowerCase()); |
我们如何使用统一的校验API进行扩展相同的内容呢? 1 | def checkCase(caseMode: CaseMode) = validateWith( "constraints.checkcase" ) { (s: String) => |
2 | if (caseMode == CaseMode.UPPER) |
是的,这里只有6行代码。我有自信任何人都可以理解它。
Scala福利: 易于并行 作为一个小福利,既然Scala API只是用不可变的数据结构,那么它就是线程安全的。Scala 提供并行集合。让我们花5分钟改一些最初的代码,我提出了一个发挥我多核处理器最大性能的版本。
时间校验API测定Hibernate validator的基准点 - invalids Hibernate validator Unified - invalids Unified Unified w/ par - invalids Unified w/ par 5000 6000 7000 8000 9000 10000 0k 1k 2k 3k 4k 5k Highcharts.com
这一版本比Java的快了5倍。
结论
我认为这篇文章的观点相当明显。当提到选择一个库或者一门语言时,基准测试毫无用处。一个正确实现的算法总是能被优化。一个失败的实现就是失败了。
这一特殊的实例证明即使Scala有一些开销,但是它优秀的设计使得创建一些相比Java不仅简单而且高效的库称为可能。
你可以阅读 这篇文章 来了解更多关于unified API或者从这里检出代码.
现在,如果你有一个min规则,一个max规则,我想创建一个between规则,该怎么做?这很简单,规则组合。
1 | def between(lower: Int, upper: Int) = min(lower) |+| max(upper) |
然而在Java中呢?好吧,我连写这些代码都不想写。这肯定会冗长并且乏味。 |