Scala 比 Java 还快? 【已翻译100%】

通常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快1.6倍。
  • 当对象包含一个无效的字段, Scala的API比Java API的速度更快,高达2.6倍Scala API is up 2.6x faster 。

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的一个实例:

public class Similar {
   @NotNull
   private String id;
   @Min(0)
   @Max(1)
   private float score;
   // .. getters and setters ...
}

public class Tag {
   @NotNull
   private String id;
   @NotNull
   private String score;
   // .. getters and setters ...
}

public class Track {
   @NotNull
   private String id;
   @NotNull
   private String title;
   @NotNull
   private List<Similar> similars;
   @NotNull
   private DateTime timestamp;
   @NotNull
   private String artist;
   @NotNull
   private List<Tag> tags;

   // .. getters and setters ...
}

当然,这些看起来足够检查一个给定的跟踪实例是否是有效的了。

错了。你看,我们并没有明确的验证一个跟踪实例的有效性,还必须检查其Tags and Similars的有效性。你需要用@Valid标注每个属性。

这种行为是不合直觉的。当我不知道一个跟踪实例的成员是不是有效的时,我无法得它是否有效。

校验与分解

JSR 303不处理数据的整理与分解。 他只是对类实例的校验。出于测试目的,我使用Jackson来将Json数据分解为类实例。但问题是,校验时分解的一部分。我们很难在没有检查JSon结构前提下将JSON树转化为类实例。 "age"属性存在吗?它是Integer类型的吗?

考虑到这点,Java 工作流看起来很奇怪。当处理Json时你实际上需要处理3中类型的错误:

  • Json数据不是结构良好的。
  • Json是有效的,但不是你想要的结构(例如,少了属性,类型不匹配等等)
  • 没有满足你定义的约束

JSR-303仅仅帮助你处理后者。

这个AIP同样强制你直接使用不合法的实例。我认为如果一个类实例不合法,你最初就不应该创建它。

难以扩展API

Hibernate校验程序需要手动添加一些列校验规则(如邮件,长度,非空等)。那如果你需要创建新的校验约束条件(比如从hibernate文档中的例子),你该怎么办?

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
  String message() default "{com.mycompany.constraints.checkcase}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  CaseMode value();
}

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

  private CaseMode caseMode;

  public void initialize(CheckCase constraintAnnotation) {
      this.caseMode = constraintAnnotation.value();
  }

  public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
   if (object == null)
       return true;

   if (caseMode == CaseMode.UPPER)
       return object.equals(object.toUpperCase());
   else
       return object.equals(object.toLowerCase());
  }

}

那是大约 25 行代码。21 行基本上是固定的,所谓纯“仪式性”的。有趣的部分仅仅是:

if (caseMode == CaseMode.UPPER)
    return object.equals(object.toUpperCase());
else
    return object.equals(object.toLowerCase());

我们如何使用统一的校验API进行扩展相同的内容呢?

def checkCase(caseMode: CaseMode) = validateWith("constraints.checkcase") { (s: String) =>
   if (caseMode == CaseMode.UPPER)
      s == s.toUpperCase
   else
      s == s.toLowerCase
}

是的,这里只有6行代码。我有自信任何人都可以理解它。

现在,如果你有一个min规则,一个max规则,我想创建一个between规则,该怎么做?这很简单,规则组合。

def between(lower: Int, upper: Int) = min(lower) |+| max(upper)

然而在Java中呢?好吧,我连写这些代码都不想写。这肯定会冗长并且乏味。

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或者从这里检出代码.

时间: 2024-10-29 07:30:43

Scala 比 Java 还快? 【已翻译100%】的相关文章

创建你自己的 Java 注解类 【已翻译100%】

如果你已经在使用Java编程,并且也使用了任何像Spring和Hibernate这样的流行框架,那么你应该对注解的使用非常地熟悉.使用一个现有框架工作的时候,通常使用它的注解就够了.但是,你是不是也有时候有要创建属于你自己的注解的需求呢? 不久之前,我找到了一个自己创建一个注解的理由,那是一个涉及验证存储在多种数据库中的常用数据的项目. 场景描述 该业务有多种数据库都存储着相同的数据,它们有各自不同的保持数据更新的方法. 该业务曾计划把所有这些数据都整合到一个主数据库中,以减轻涉及到多种数据源所

Docker, Java EE 7, 和 Maven with WebLogic 12.1.3 【已翻译100%】

WebLogic 12.1.3已经发布,并且对于JavaEE7的APIs在数据库支持web应用开发上也是最重要的支持.以下是在发行版本中支持的一些标准: Java Persistence API 2.1 (implemented by EclipseLink) JAX-RS 2.0 (implemented by Jersey) JSON-P 1.0 (implemented by GlassFish subproject jsonp) WebSockets 1.0 (implemented b

Java 多玩家 libgdx 教程 【已翻译100%】

我们如何去做? 在 libgdx的主页修改libgdx样本"superjumper". 使用 AppWarp Cloud将它转化为2个玩家的实时游戏. 本游戏将匹配玩家并且用户需要到达城堡来赢得游戏的胜利. 用户将获得其他用户成绩的实时反馈以增加了游戏的刺激性. Eclipse 项目设置 接下来,您需要从这个git repository下载libgdx游戏样本(superjumper) 项目.. 在Eclipse中打开下载的superjumper解决方案.你将看到项目如下: 为了创建多

Java 8 Lambda 表达式示例 【已翻译100%】

自从我听说Java8将要支持Lambda表达式(或称闭包),我便开始狂热的想要将这些体面的简洁的功能元素应用到我的代码中来.大多开发者普遍的使用匿名内部类来开发事件处理器,比较器,thread/runnable实现等等,一些没有必要的辅助代码将逻辑复杂化,即便一些非常简单的代码也变的复杂不堪.Java8现在加入了Lambda表达式作为语法的一部分将会极大地解决这一类似问题. 它使得开发者可以封装一个单独的行为单元并且传递给其他代码.他更像是一个匿名类(带有一个方法的可推断类型)的语法糖和一个更少

Spring Java 配置之 Session 超时 【已翻译100%】

我们生活在一个美好的时代,在这个时代你可以使用基于java的配置来开发一个Spring应用程序. 再也没有多余的XML代码了,只有纯正的java代码. 本文中我想要讨论一下关于Spring应用程序中的session管理这里流行主题. 更确切的目的是我将会说说java配置风格会话超时配置. 而在我之前的一篇 博文 中, 我已经谈到了如何去管理一个会话的生命周期. 但是那一种方案需要使用web.xml文件,而在基于java的配置中是不需要的. 因为其作用是操作一个扩展了 AbstractAnnota

从 C++ 到 Objective-C 的快速指南 【已翻译100%】

**简介 ** 当我开始为iOS写代码的时候,我意识到,作为一个C++开发者,我必须花费更多的时间来弄清楚Objective-C中怪异的东西.这就是一个帮助C++专家的快速指南,能够使他们快速的掌握Apple的iOS语言. 请注意这绝不是一个完整的指南,但是它让你避免了阅读100页的手册.除此之外,我知道你喜欢我的写作风格. 背景 需要C++的技能,我会比较C++和Objective-C的东西.此外,COM编程也是有用的,因为Objective-C有类似于IUnkown的东西,因此基础的COM编

单元测试检查清单:让你的测试保持有效,避免大的错误 【已翻译100%】

单元测试是测试你代码的一些常用方法集. 一般的操作步骤如下: 1.先写北侧类的API 2.测试对应API的方法名 3.实现对应测试API的方法体 4.运行单元测试 为什么需要单元测试? 它可以测试现有的以及未来的功能模块. 保证代码质量. 它规范你书写具有可测性,低耦合的代码.这比手工回归测试廉价的多. 它将提高代码可行度.协助团队工作. 为啥需要个检查列表? 单元测试在实际操作时可能要复杂一点. 它需要你考虑清楚整个待测对象的框架. 但测试本身应该简单,直接,易用和易维护. 对于测试的开始点和

使用 Protocol Buffers 代替 JSON 的五个原因 【已翻译100%】

在Ruby和Rails开发者中,面向服务(Service-Oriented)架构有一个当之无愧的名声,它是一个缓解程序规模恶性增长的一个强有力的途径,可在大量应用程序中提取关注点.这些新生小巧的服务通常继续使用Rails或Sinatra,并使用JSON在HTTP上通信.尽管JSON作为一个数据相互交换格式,有很多优点:人类可读.可理解,并通常表现出色. 浏览器和JS并不直接处理数据--尤其是遇到内部服务时.我的观点是,结构化格式,例如谷歌的Protocol Buffers,是一个比JSON在编码

50 个 jQuery 插件可将你的网站带到另外一个高度 【已翻译100%】

Web领域一直在发生变化并且其边界在过去的每一天都在发生变化(甚至不能以小时为计),随着其边界的扩展取得了许多新发展.在这些进步之中,开发者的不断工作创造了更大和更好的脚本,这些脚本以插件方式带来更好的终端用户体验,它们比原来更轻量级,还有更强的处理能力. 关键是这些新发展起来的脚本和插件是能构建响应式Web的,而且还不会丧失它们原有的功能特性--除了更优秀和更轻巧(就文件大小而言)之外,它们还不会增加页面加载的时间. 通过浏览文档,掌握JQuery的语法是很容易的.它可以支持选择DOM元素,创