Scala 编程风格指南[Databricks ]

Databricks Scala 编程风格指南

声明 (Disclaimer)

The Chinese version of the Databricks Scala Guide is contributed and maintained by community member Hawstein. We do not guarantee that it will always be kept up-to-date.html

本文档翻译自 Databricks Scala Guide,目前由 Hawstein 进行维护。因为是利用业余时间进行翻译并维护,所以该中文文档并不保证老是与原文档同样处于最新版本,不过我会尽量及时地去更新它。java

前言

Spark 有超过 800 位贡献者,就咱们所知,应该是目前大数据领域里最大的开源项目且是最活跃的 Scala 项目。这份指南是在咱们指导,或是与 Spark 贡献者及 Databricks 工程团队一块儿工做时总结出来的。git

代码由做者__一次编写__,而后由大量工程师__屡次阅读并修改__。事实上,大部分的 bug 来源于后人对代码的修改,所以咱们须要长期去优化咱们的代码,提高代码的可读性和可维护性。达到这个目标最好的方式就是编写简单易懂的代码。程序员

Scala 是一种强大到使人难以置信的多范式编程语言。咱们总结出了如下指南,它能够很好地应用在一个高速发展的项目。固然,这个指南并不是绝对,根据团队需求的不一样,能够有不一样的标准。github

目录

  1. 文档历史apache

  2. 语法风格编程

  3. Scala 语言特性

  4. 并发

  5. 性能

  6. 与 Java 的互操做性

  7. 其它

文档历史

  • 2015-03-16: 最第一版本。

  • 2015-05-25: 增长 override 修饰符 一节。

  • 2015-08-23: 把一些规则的严重程度从「不要」降级到「避免」。

  • 2015-11-17: 更新 apply 方法 一节:伴生对象中的 apply 方法应该返回其伴生类。

  • 2015-11-17: 该指南被翻译成中文,由 Hawstein 进行维护,中文文档并不保证老是与原文档同样处于最新版本。

语法风格

命名约定

咱们主要遵循 Java 和 Scala 的标准命名约定。

  • 类,trait, 对象应该遵循 Java 中类的命名约定,即 PascalCase 风格。

    class ClusterManager
    
    trait Expression
  • 包名应该遵循 Java 中包名的命名约定,即便用全小写的 ASCII 字母。

    package com.databricks.resourcemanager
  • 方法/函数应当使用驼峰式风格命名。

  • 常量命名使用全大写字母,并将它们放在伴生对象中。

    object Configuration {
      val DEFAULT_PORT = 10000
    }
  • 枚举命名与类命名一致,使用 PascalCase 风格。

  • 注解也应遵循 Java 中的约定,即便用 PascalCase 风格。注意,这一点与 Scala 的官方指南不一样。

    final class MyAnnotation extends StaticAnnotation

一行长度

  • 一行长度的上限是 100 个字符。

  • 惟一的例外是 import 语句和 URL (即使如此,也尽可能将它们保持在 100 个字符如下)。

30 法则

「若是一个元素包含的子元素超过 30 个,那么极有可能出现了严重的问题」 - Refactoring in Large Software Projects

通常来讲:

  • 一个方法包含的代码行数不宜超过 30 行。

  • 一个类包含的方法数量不宜超过 30 个。

空格与缩进

  • 通常状况下,使用两个空格的缩进。

    if (true) {
      println("Wow!")
    }
  • 对于方法声明,若是一行没法容纳下全部的参数,那么使用 4 个空格来缩进它们。返回类型能够与最后一个参数在同一行,也能够放在下一行,使用两个空格缩进。

    def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
        path: String,
        fClass: Class[F],
        kClass: Class[K],
        vClass: Class[V],
        conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
      // method body
    }
    
    def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
        path: String,
        fClass: Class[F],
        kClass: Class[K],
        vClass: Class[V],
        conf: Configuration = hadoopConfiguration)
      : RDD[(K, V)] = {
      // method body
    }
  • 若是一行没法容纳下类头(即 extends 后面那部分),则把它们放到新的一行,用两个空格缩进,而后在类内空一行再开始函数或字段的定义(或是包的导入)。

    class Foo(
        val param1: String,  // 4 space indent for parameters
        val param2: String,
        val param3: Array[Byte])
      extends FooInterface  // 2 space here
      with Logging {
    
      def firstMethod(): Unit = { ... }  // blank line above
    }
  • 不要使用垂直对齐。它使你的注意力放在代码的错误部分并增大了后人修改代码的难度。

    // Don't align vertically
    val plus     = "+"
    val minus    = "-"
    val multiply = "*"
    
    // Do the following
    val plus = "+"
    val minus = "-"
    val multiply = "*"

空行

  • 一个空行能够出如今:

    • 连续的类成员或初始化器(initializers)之间:字段,构造函数,方法,嵌套类,静态初始化器及实例初始化器。

      • 例外:连续的两个字段之间的空行是可选的(前提是它们之间没有其它代码),这一类空行主要为这些字段作逻辑上的分组。

    • 在方法体内,根据须要,使用空行来为语句建立逻辑上的分组。

    • 在类的第一个成员以前或最后一个成员以后,空行都是可选的(既不鼓励也不阻止)。

  • 使用一个或两个空行来分隔不一样类的定义。

  • 不鼓励使用过多的空行。

括号

  • 方法声明应该加括号(即便没有参数列表),除非它们是没有反作用(状态改变,IO 操做都认为是有反作用的)的访问器(accessor)。

    class Job {
      // Wrong: killJob changes state. Should have ().
      def killJob: Unit
    
      // Correct:
      def killJob(): Unit
    }
  • 函数调用应该与函数声明在形式上保持一致,也就是说,若是一个方法声明时带了括号,那调用时也要把括号带上。注意这不只仅是语法层面的人为约定,当返回对象中定义了 apply 方法时,这一点还会影响正确性。

    class Foo {
      def apply(args: String*): Int
    }
    
    class Bar {
      def foo: Foo
    }
    
    new Bar().foo  // This returns a Foo
    new Bar().foo()  // This returns an Int!

大括号

即便条件语句或循环语句只有一行时,也请使用大括号。惟一的例外是,当你把 if/else 做为一个单行的三元操做符来使用而且没有反作用时,这时你能够不加大括号。

// Correct:
if (true) {
  println("Wow!")
}

// Correct:
if (true) statement1 else statement2

// Correct:
try {
  foo()
} catch {
  ...
}

// Wrong:
if (true)
  println("Wow!")

// Wrong:
try foo() catch {
  ...
}

长整型字面量

长整型字面量使用大写的 L 做为后缀,不要使用小写,由于它和数字 1 长得很像,经常难以区分。

val longValue = 5432L  // Do this

val longValue = 5432l  // Do NOT do this

文档风格

使用 Java Doc 风格,而非 Scala Doc 风格。

/** This is a correct one-liner, short description. */

/**
 * This is correct multi-line JavaDoc comment. And
 * this is my second line, and if I keep typing, this would be
 * my third line.
 */

/** In Spark, we don't use the ScalaDoc style so this
  * is not correct.
  */

类内秩序

若是一个类很长,包含许多的方法,那么在逻辑上把它们分红不一样的部分并加上注释头,以此组织它们。

class DataFrame {

  ///////////////////////////////////////////////////////////////////////////
  // DataFrame operations
  ///////////////////////////////////////////////////////////////////////////

  ...

  ///////////////////////////////////////////////////////////////////////////
  // RDD operations
  ///////////////////////////////////////////////////////////////////////////

  ...
}

固然,强烈不建议把一个类写得这么长,通常只有在构建某些公共 API 时才容许这么作。

Imports

  • __导入时避免使用通配符__, 除非你须要导入超过 6 个实体或者隐式方法。通配符导入会使代码在面对外部变化时不够健壮。

  • 始终使用绝对路径来导入包 (如:scala.util.Random) ,而不是相对路径 (如:util.Random)。

  • 此外,导入语句按照如下顺序排序:

    • java.*javax.*

    • scala.*

    • 第三方库 (org.*, com.*, 等)

    • 项目中的类 (对于 Spark 项目,即 com.databricks.*org.apache.spark)

  • 在每一组导入语句内,按照字母序进行排序。

  • 你可使用 IntelliJ 的「import organizer」来自动处理,请使用如下配置:

    java
    javax
    _______ blank line _______
    scala
    _______ blank line _______
    all other imports
    _______ blank line _______
    com.databricks  // or org.apache.spark if you are working on spark

模式匹配

  • 若是整个方法就是一个模式匹配表达式,可能的话,能够把 match 关键词与方法声明放在同一行,以此减小一级缩进。

    def test(msg: Message): Unit = msg match {
      case ...
    }
  • 当以闭包形式调用一个函数时,若是只有一个 case 语句,那么把 case 语句与函数调用放在同一行。

    list.zipWithIndex.map { case (elem, i) =>
      // ...
    }

若是有多个 case 语句,把它们缩进而且包起来。

list.map {
  case a: Foo =>  ...
  case b: Bar =>  ...
}

中缀方法

__避免中缀表示法__,除非是符号方法(即运算符重载)。

// Correct
list.map(func)
string.contains("foo")

// Wrong
list map (func)
string contains "foo"

// 重载的运算符应该以中缀形式调用
arrayBuffer += elem

Scala 语言特性

apply 方法

避免在类里定义 apply 方法。这些方法每每会使代码的可读性变差,尤为是对于不熟悉 Scala 的人。它也难以被 IDE(或 grep)所跟踪。在最坏的状况下,它还可能影响代码的正确性,正如你在括号一节中看到的。

然而,将 apply 方法做为工厂方法定义在伴生对象中是能够接受的。在这种状况下,apply 方法应该返回其伴生类的类型。

object TreeNode {
  // 下面这种定义是 OK 的
  def apply(name: String): TreeNode = ...

  // 不要像下面那样定义,由于它没有返回其伴生类的类型:TreeNode
  def apply(name: String): String = ...
}

override 修饰符

不管是覆盖具体的方法仍是实现抽象的方法,始终都为方法加上 override 修饰符。实现抽象方法时,不加 override 修饰符,Scala 编译器也不会报错。即使如此,咱们也应该始终把 override 修饰符加上,以此显式地表示覆盖行为。以此避免因为方法签名不一样(而你也难以发现)而致使没有覆盖到本应覆盖的方法。

trait Parent {
  def hello(data: Map[String, String]): Unit = {
    print(data)
  }
}

class Child extends Parent {
  import scala.collection.Map

  // 下面的方法没有覆盖 Parent.hello,
  // 由于两个 Map 的类型是不一样的。
  // 若是咱们加上 override 修饰符,编译器就会帮你找出问题并报错。
  def hello(data: Map[String, String]): Unit = {
    print("This is supposed to override the parent method, but it is actually not!")
  }
}

解构绑定

解构绑定(有时也叫元组提取)是一种在一个表达式中为两个变量赋值的便捷方式。

val (a, b) = (1, 2)

然而,请不要在构造函数中使用它们,尤为是当 ab 须要被标记为 transient 的时候。Scala 编译器会产生一个额外的 Tuple2 字段,而它并非暂态的(transient)。

class MyClass {
  // 如下代码没法 work,由于编译器会产生一个非暂态的 Tuple2 指向 a 和 b
  @transient private val (a, b) = someFuncThatReturnsTuple2()
}

按名称传参

__避免使用按名传参__. 显式地使用 () => T

背景:Scala 容许按名称来定义方法参数,例如:如下例子是能够成功执行的:

def print(value: => Int): Unit = {
  println(value)
  println(value + 1)
}

var a = 0
def inc(): Int = {
  a + 1
  a
}

print(inc())

在上面的代码中,inc() 以闭包的形式传递给 print 函数,而且在 print 函数中被执行了两次,而不是以数值 1 传入。按名传参的一个主要问题是在方法调用处,咱们没法区分是按名传参仍是按值传参。所以没法确切地知道这个表达式是否会被执行(更糟糕的是它可能会被执行屡次)。对于带有反作用的表达式来讲,这一点是很是危险的。

多参数列表

__避免使用多参数列表__。它们使运算符重载变得复杂,而且会使不熟悉 Scala 的程序员感到困惑。例如:

// Avoid this!
case class Person(name: String, age: Int)(secret: String)

一个值得注意的例外是,当在定义底层库时,可使用第二个参数列表来存放隐式(implicit)参数。尽管如此,咱们应该避免使用 implicits

符号方法(运算符重载)

__不要使用符号做为方法名__,除非你是在定义算术运算的方法(如:+, -, *, /),不然在任何其它状况下,都不要使用。符号化的方法名让人难以理解方法的意图是什么,来看下面两个例子:

// 符号化的方法名难以理解
channel ! msg
stream1 >>= stream2

// 下面的方法意图则不言而喻
channel.send(msg)
stream1.join(stream2)

类型推导

Scala 的类型推导,尤为是左侧类型推导以及闭包推导,可使代码变得更加简洁。尽管如此,也有一些状况咱们是须要显式地声明类型的:

  • __公有方法应该显式地声明类型__,编译器推导出来的类型每每会使你大吃一惊。

  • __隐式方法应该显式地声明类型__,不然在增量编译时,它会使 Scala 编译器崩溃。

  • __若是变量或闭包的类型并不是显而易见,请显式声明类型__。一个不错的判断准则是,若是评审代码的人没法在 3 秒内肯定相应实体的类型,那么你就应该显式地声明类型。

Return 语句

__闭包中避免使用 return__。return 会被编译器转成 scala.runtime.NonLocalReturnControl 异常的 try/catch 语句,这可能会致使意外行为。请看下面的例子:

def receive(rpc: WebSocketRPC): Option[Response] = {
  tableFut.onComplete { table =>
    if (table.isFailure) {
      return None // Do not do that!
    } else { ... }
  }
}

.onComplete 方法接收一个匿名闭包并把它传递到一个不一样的线程中。这个闭包最终会抛出一个 NonLocalReturnControl 异常,并在 __一个不一样的线程中__被捕获,而这里执行的方法却没有任何影响。

然而,也有少数状况咱们是推荐使用 return 的。

  • 使用 return 来简化控制流,避免增长一级缩进。

    def doSomething(obj: Any): Any = {
      if (obj eq null) {
        return null
      }
      // do something ...
    }
  • 使用 return 来提早终止循环,这样就不用额外构造状态标志。

    while (true) {
      if (cond) {
        return
      }
    }

递归及尾递归

__避免使用递归__,除非问题能够很是天然地用递归来描述(好比,图和树的遍历)。

对于那些你意欲使之成为尾递归的方法,请加上 @tailrec 注解以确保编译器去检查它是否真的是尾递归(你会很是惊讶地看到,因为使用了闭包和函数变换,许多看似尾递归的代码事实并不是尾递归)。

大多数的代码使用简单的循环和状态机会更容易推理,使用尾递归反而可能会使它更加繁琐且难以理解。例如,下面的例子中,命令式的代码比尾递归版本的代码要更加易读:

// Tail recursive version.
def max(data: Array[Int]): Int = {
  @tailrec
  def max0(data: Array[Int], pos: Int, max: Int): Int = {
    if (pos == data.length) {
      max
    } else {
      max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
    }
  }
  max0(data, 0, Int.MinValue)
}

// Explicit loop version
def max(data: Array[Int]): Int = {
  var max = Int.MinValue
  for (v <- data) {
    if (v > max) {
      max = v
    }
  }
  max
}

Implicits

__避免使用 implicit__,除非:

  • 你在构建领域特定的语言(DSL)

  • 你在隐式类型参数中使用它(如:ClassTagTypeTag

  • 你在你本身的类中使用它(意指不要污染外部空间),以此减小类型转换的冗余度(如:Scala 闭包到 Java 闭包的转换)。

当使用 implicit 时,咱们应该确保另外一个工程师能够直接理解使用语义,而无需去阅读隐式定义自己。Implicit 有着很是复杂的解析规则,这会使代码变得极其难以理解。Twitter 的 Effective Scala 指南中写道:「若是你发现你在使用 implicit,始终停下来问一下你本身,是否能够在不使用 implicit 的条件下达到相同的效果」。

若是你必需使用它们(好比:丰富 DSL),那么不要重载隐式方法,即确保每一个隐式方法有着不一样的名字,这样使用者就能够选择性地导入它们。

// 别这么作,这样使用者没法选择性地只导入其中一个方法。
object ImplicitHolder {
  def toRdd(seq: Seq[Int]): RDD[Int] = ...
  def toRdd(seq: Seq[Long]): RDD[Long] = ...
}

// 应该将它们定义为不一样的名字:
object ImplicitHolder {
  def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
  def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
}

异常处理,Try 仍是 try

  • 不要捕获 Throwable 或 Exception 类型的异常。请使用 scala.util.control.NonFatal

    try {
      ...
    } catch {
      case NonFatal(e) =>
        // 异常处理;注意 NonFatal 没法匹配 InterruptedException 类型的异常
      case e: InterruptedException =>
        // 处理 InterruptedException
    }

这能保证咱们不会去捕获 NonLocalReturnControl 异常(正如在Return 语句中所解释的)。

  • 不要在 API 中使用 Try,即,不要在任何方法中返回 Try。对于异常执行,请显式地抛出异常,并使用 Java 风格的 try/catch 作异常处理。

背景资料:Scala 提供了单子(monadic)错误处理(经过 TrySuccessFailure),这样便于作链式处理。然而,根据咱们的经验,发现使用它一般会带来更多的嵌套层级,使得代码难以阅读。此外,对于预期错误仍是异常,在语义上经常是不明晰的。所以,咱们不鼓励使用 Try 来作错误处理,尤为是如下状况:

一我的为的例子:

class UserService {
  /** Look up a user's profile in the user database. */
  def get(userId: Int): Try[User]
}

如下的写法会更好:

class UserService {
  /**
   * Look up a user's profile in the user database.
   * @return None if the user is not found.
   * @throws DatabaseConnectionException when we have trouble connecting to the database/
   */
  @throws(DatabaseConnectionException)
  def get(userId: Int): Option[User]
}

第二种写法很是明显地能让调用者知道须要处理哪些错误状况。

Options

  • 若是一个值可能为空,那么请使用 Option。相对于 nullOption 显式地代表了一个 API 的返回值可能为空。

  • 构造 Option 值时,请使用 Option 而非 Some,以防那个值为 null

    def myMethod1(input: String): Option[String] = Option(transform(input))
    
    // This is not as robust because transform can return null, and then
    // myMethod2 will return Some(null).
    def myMethod2(input: String): Option[String] = Some(transform(input))
  • 不要使用 None 来表示异常,有异常时请显式抛出。

  • 不要在一个 Option 值上直接调用 get 方法,除非你百分百肯定那个 Option 值不是 None

单子连接

单子连接是 Scala 的一个强大特性。Scala 中几乎一切都是单子(如:集合,Option,Future,Try 等),对它们的操做能够连接在一块儿。这是一个很是强大的概念,但你应该谨慎使用,尤为是:

  • 避免连接(或嵌套)超过 3 个操做。

  • 若是须要花超过 5 秒钟来理解其中的逻辑,那么你应该尽可能去想一想有没什么办法在不使用单子连接的条件下来达到相同的效果。通常来讲,你须要注意的是:不要滥用 flatMapfold

  • 连接应该在 flatMap 以后断开(由于类型发生了变化)。

经过给中间结果显式地赋予一个变量名,将连接断开变成一种更加过程化的风格,能让单子连接更加易于理解。来看下面的例子:

class Person(val data: Map[String, String])
val database = Map[String, Person]
// Sometimes the client can store "null" value in the  store "address"

// A monadic chaining approach
def getAddress(name: String): Option[String] = {
  database.get(name).flatMap { elem =>
    elem.data.get("address")
      .flatMap(Option.apply)  // handle null value
  }
}

// 尽管代码会长一些,但如下方法可读性更高
def getAddress(name: String): Option[String] = {
  if (!database.contains(name)) {
    return None
  }

  database(name).data.get("address") match {
    case Some(null) => None  // handle null value
    case Some(addr) => Option(addr)
    case None => None
  }
}

并发

Scala concurrent.Map

__优先考虑使用 java.util.concurrent.ConcurrentHashMap 而非 scala.collection.concurrent.Map__。尤为是 scala.collection.concurrent.Map 中的 getOrElseUpdate 方法要慎用,它并不是原子操做(这个问题在 Scala 2.11.16 中 fix 了:SI-7943)。因为咱们作的全部项目都须要在 Scala 2.10 和 Scala 2.11 上使用,所以要避免使用 scala.collection.concurrent.Map

显式同步 vs 并发集合

有 3 种推荐的方法来安全地并发访问共享状态。__不要混用它们__,由于这会使程序变得难以推理,而且可能致使死锁。

  • java.util.concurrent.ConcurrentHashMap:当全部的状态都存储在一个 map 中,而且有高程度的竞争时使用。

    private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
  • java.util.Collections.synchronizedMap:使用情景:当全部状态都存储在一个 map 中,而且预期不存在竞争状况,但你仍想确保代码在并发下是安全的。若是没有竞争出现,JVM 的 JIT 编译器可以经过偏置锁(biased locking)移除同步开销。

    private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
  • 经过同步全部临界区进行显式同步,可用于监视多个变量。与 2 类似,JVM 的 JIT 编译器可以经过偏置锁(biased locking)移除同步开销。

    class Manager {
      private[this] var count = 0
      private[this] val map = new java.util.HashMap[String, String]
      def update(key: String, value: String): Unit = synchronized {
        map.put(key, value)
        count += 1
      }
      def getCount: Int = synchronized { count }
    }

注意,对于 case 1 和 case 2,不要让集合的视图或迭代器从保护区域逃逸。这可能会以一种不明显的方式发生,好比:返回了 Map.keySetMap.values。若是须要传递集合的视图或值,生成一份数据拷贝再传递。

val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])

// This is broken!
def values: Iterable[String] = map.values

// Instead, copy the elements
def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }

显式同步 vs 原子变量 vs @volatile

java.util.concurrent.atomic 包提供了对基本类型的无锁访问,好比:AtomicBoolean, AtomicIntegerAtomicReference

始终优先考虑使用原子变量而非 @volatile,它们是相关功能的严格超集而且从代码上看更加明显。原子变量的底层实现使用了 @volatile

优先考虑使用原子变量而非显式同步的状况:(1)一个对象的全部临界区更新都被限制在单个变量里而且预期会有竞争状况出现。原子变量是无锁的而且容许更为有效的竞争。(2)同步被明确地表示为 getAndSet 操做。例如:

// good: 明确又有效地表达了下面的并发代码只执行一次
val initialized = new AtomicBoolean(false)
...
if (!initialized.getAndSet(true)) {
  ...
}

// poor: 下面的同步就没那么明晰,并且会出现没必要要的同步
val initialized = false
...
var wasInitialized = false
synchronized {
  wasInitialized = initialized
  initialized = true
}
if (!wasInitialized) {
  ...
}

私有字段

注意,private 字段仍然能够被相同类的其它实例所访问,因此仅仅经过 this.synchronized(或 synchronized)来保护它从技术上来讲是不够的,不过你能够经过 private[this] 修饰私有字段来达到目的。

// 如下代码仍然是不安全的。
class Foo {
  private var count: Int = 0
  def inc(): Unit = synchronized { count + 1 }
}

// 如下代码是安全的。
class Foo {
  private[this] var count: Int = 0
  def inc(): Unit = synchronized { count + 1 }
}

隔离

通常来讲,并发和同步逻辑应该尽量地被隔离和包含起来。这实际上意味着:

  • 避免在 API 层面、面向用户的方法以及回调中暴露同步原语。

  • 对于复杂模块,建立一个小的内部模块来包含并发原语。

性能

对于你写的绝大多数代码,性能都不该该成为一个问题。然而,对于一些性能敏感的代码,如下有一些小建议:

Microbenchmarks

因为 Scala 编译器和 JVM JIT 编译器会对你的代码作许多神奇的事情,所以要写出一个好的微基准程序(microbenchmark)是极其困难的。更多的状况每每是你的微基准程序并无测量你想要测量的东西。

若是你要写一个微基准程序,请使用 jmh。请确保你阅读了全部的样例,这样你才理解微基准程序中「死代码」移除、常量折叠以及循环展开的效果。

Traversal 与 zipWithIndex

使用 while 循环而非 for 循环或函数变换(如:mapforeach),for 循环和函数变换很是慢(因为虚函数调用和装箱的缘故)。

val arr = // array of ints
// 偶数位置的数置零
val newArr = list.zipWithIndex.map { case (elem, i) =>
  if (i % 2 == 0) 0 else elem
}

// 这是上面代码的高性能版本
val newArr = new Array[Int](arr.length)
var i = 0
val len = newArr.length
while (i < len) {
  newArr(i) = if (i % 2 == 0) 0 else arr(i)
  i += 1
}

Option 与 null

对于性能有要求的代码,优先考虑使用 null 而不是 Option,以此避免虚函数调用以及装箱操做。用 Nullable 注解明确标示出可能为 null 的值。

class Foo {
  @javax.annotation.Nullable
  private[this] var nullableField: Bar = _
}

Scala 集合库

对于性能有要求的代码,优先考虑使用 Java 集合库而非 Scala 集合库,由于通常来讲,Scala 集合库要比 Java 的集合库慢。

private[this]

对于性能有要求的代码,优先考虑使用 private[this] 而非 privateprivate[this] 生成一个字段而非生成一个访问方法。根据咱们的经验,JVM JIT 编译器并不老是会内联 private 字段的访问方法,所以经过使用
private[this] 来确保没有虚函数调用会更保险。

class MyClass {
  private val field1 = ...
  private[this] val field2 = ...

  def perfSensitiveMethod(): Unit = {
    var i = 0
    while (i < 1000000) {
      field1  // This might invoke a virtual method call
      field2  // This is just a field access
      i += 1
    }
  }
}

与 Java 的互操做性

本节内容介绍的是构建 Java 兼容 API 的准则。若是你构建的组件并不须要与 Java 有交互,那么请无视这一节。这一节的内容主要是从咱们开发 Spark 的 Java API 的经历中得出的。

Scala 中缺失的 Java 特性

如下的 Java 特性在 Scala 中是没有的,若是你须要使用如下特性,请在 Java 中定义它们。然而,须要提醒一点的是,你没法为 Java 源文件生成 ScalaDoc。

  • 静态字段

  • 静态内部类

  • Java 枚举

  • 注解

Traits 与抽象类

对于容许从外部实现的接口,请记住如下几点:

  • 包含了默认方法实现的 trait 是没法在 Java 中使用的,请使用抽象类来代替。

  • 通常状况下,请避免使用 trait,除非你百分百肯定这个接口即便在将来也不会有默认的方法实现。

// 如下默认实现没法在 Java 中使用
trait Listener {
  def onTermination(): Unit = { ... }
}

// 能够在 Java 中使用
abstract class Listener {
  def onTermination(): Unit = { ... }
}

类型别名

不要使用类型别名,它们在字节码和 Java 中是不可见的。

默认参数值

不要使用默认参数值,经过重载方法来代替。

// 打破了与 Java 的互操做性
def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }

// 如下方法是 work 的
def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)

多参数列表

不要使用多参数列表。

可变参数

  • 为可变参数方法添加 @scala.annotation.varargs 注解,以确保它能在 Java 中使用。Scala 编译器会生成两个方法,一个给 Scala 使用(字节码参数是一个 Seq),另外一个给 Java 使用(字节码参数是一个数组)。

    @scala.annotation.varargs
    def select(exprs: Expression*): DataFrame = { ... }
  • 须要注意的一点是,因为 Scala 编译器的一个 bug(SI-1459SI-9013),抽象的变参方法是没法在 Java 中使用的。

  • 重载变参方法时要当心,用另外一个类型去重载变参方法会破坏源码的兼容性。

    class Database {
      @scala.annotation.varargs
      def remove(elems: String*): Unit = ...
    
      // 当调用无参的 remove 方法时会出问题。
      @scala.annotation.varargs
      def remove(elems: People*): Unit = ...
    }
    
    // remove 方法有歧义,所以编译不过。
    new Database().remove()

一种解决方法是,在可变参数前显式地定义第一个参数:

class Database {
  @scala.annotation.varargs
  def remove(elems: String*): Unit = ...

  // 如下重载是 OK 的。
  @scala.annotation.varargs
  def remove(elem: People, elems: People*): Unit = ...
}

Implicits

不要为类或方法使用 implicit,包括了不要使用 ClassTagTypeTag

class JavaFriendlyAPI {
  // 如下定义对 Java 是不友好的,由于方法中包含了一个隐式参数(ClassTag)。
  def convertTo[T: ClassTag](): T
}

伴生对象,静态方法与字段

当涉及到伴生对象和静态方法/字段时,有几件事情是须要注意的:

  • 伴生对象在 Java 中的使用是很是别扭的(伴生对象 Foo 会被定义为 Foo$ 类内的一个类型为 Foo$ 的静态字段 MODULE$)。

    object Foo
    
    // 等价于如下的 Java 代码
    public class Foo$ {
      Foo$ MODULE$ = // 对象的实例化
    }

若是非要使用伴生对象,能够在一个单独的类中建立一个 Java 静态字段。

  • 不幸的是,没有办法在 Scala 中定义一个 JVM 静态字段。请建立一个 Java 文件来定义它。

  • 伴生对象里的方法会被自动转成伴生类里的静态方法,除非方法名有冲突。确保静态方法正确生成的最好方式是用 Java 写一个测试文件,而后调用生成的静态方法。

    class Foo {
      def method2(): Unit = { ... }
    }
    
    object Foo {
      def method1(): Unit = { ... }  // 静态方法 Foo.method1 会被建立(字节码)
      def method2(): Unit = { ... }  // 静态方法 Foo.method2 不会被建立
    }
    
    // FooJavaTest.java (in test/scala/com/databricks/...)
    public class FooJavaTest {
      public static compileTest() {
        Foo.method1();  // 正常编译
        Foo.method2();  // 编译失败,由于 method2 并无生成
      }
    }
  • 样例对象(case object) MyClass 的类型并非 MyClass。

    case object MyClass
    
    // Test.java
    if (MyClass$.MODULE instanceof MyClass) {
      // 上述条件始终为 false
    }

要实现正确的类型层级结构,请定义一个伴生类,而后用一个样例对象去继承它:

class MyClass
case object MyClass extends MyClass

其它

优先使用 nanoTime 而非 currentTimeMillis

当要计算持续时间或者检查超时的时候,避免使用 System.currentTimeMillis()。请使用 System.nanoTime(),即便你对亚毫秒级的精度并不感兴趣。

System.currentTimeMillis() 返回的是当前的时钟时间,而且会跟进系统时钟的改变。所以,负的时钟调整可能会致使超时而挂起很长一段时间(直到时钟时间遇上先前的值)。这种状况可能发生在网络已经中断一段时间,ntpd 走过了一步以后。最典型的例子是,在系统启动的过程当中,DHCP 花费的时间要比日常的长。这可能会致使很是难以理解且难以重现的问题。而 System.nanoTime() 则能够保证是单调递增的,与时钟变化无关。

注意事项:

  • 永远不要序列化一个绝对的 nanoTime() 值或是把它传递给另外一个系统。绝对的 nanoTime() 值是无心义的、与系统相关的,而且在系统重启时会重置。

  • 绝对的 nanoTime() 值并不保证老是正数(但 t2 - t1 能确保老是产生正确的值)。

  • nanoTime() 每 292 年就会从新计算起。因此,若是你的 Spark 任务须要花很是很是很是长的时间,你可能须要别的东西来处理了:)

优先使用 URI 而非 URL

当存储服务的 URL 时,你应当使用 URI 来表示。

URL相等性检查)实际上执行了一次网络调用(这是阻塞的)来解析 IP 地址。URI 类在表示能力上是 URL 的超集,而且它执行的是字段的相等性检查。

https://github.com/Hawstein/scala-style-guide/blob/master/README-ZH.md

相关文章
相关标签/搜索