乐趣区

关于java:那些挑逗-Java-程序员的-Scala-绝技

作者:沐风(joymufeng)

起源:my.oschina.net/joymufeng/blog/2251038

有个问题始终困扰着 Scala 社区,为什么一些 Java 开发者将 Scala 捧到了天上,认为它是来自上帝之吻的完满语言;而另外一些 Java 开发者却对它望而生畏,认为它过于简单而难以了解。

同样是 Java 开发者,为何会呈现两种截然不同的态度,我想这其中肯定有误会。Scala 是一粒金子,然而被一些外表上看起来非常复杂的概念或语法包裹的太严实,以至于人们很难在短时间内搞清楚它的价值。

与此同时,Java 也在一直地摸索后退,然而因为 Java 背负了惨重的历史包袱,所以每向前一步都显得异样艰巨。本文次要面向 Java 开发人员,心愿从解决 Java 中理论存在的问题登程,梳理最容易吸引 Java 开发者的一些 Scala 个性。心愿能够帮忙大家疾速找到那些真正能够感动你的点。

值比拟

撩拨指数: 五星

在 Java 中,针对援用类型,== 用于比拟援用相等性,即比拟两个援用变量是否指向同一个对象。所以一个简略的字符串比拟显得十分啰嗦:

String str = new String("Jack");

// 谬误写法
if (str == "Jack") {}

// 正确写法
if (str != null && str.equals("Jack")) {}
// 或简写成
if ("Jack".equals(str)) {}

而在 Scala 中,== 被设计用于值比拟,在底层实现上会调用 == 右边对象上的 equals 办法,并且会主动解决 null 状况。所以在 Scala 中,你能够释怀地应用 == 进行值比拟:

val str = new String("Jack")
if (str == "Jack") {}

在日常开发中,值比拟的需要要远远高于援用比拟,所以 == 用于值比拟更合乎直觉。当然如果你的确须要援用比拟,Scala 提供了 eqne 两个办法:

val str1 = new String("Jack")
val str2 = new String("Jack")
if (str1 eq str2) {}

类型推断

撩拨指数: 四星

咱们晓得,Scala 一贯以弱小的类型推断闻名于世。很多时候,咱们毋庸关怀 Scala 类型推断零碎的存在,因为很多时候它推断的后果跟直觉是统一的。Java 在 2016 年也新增了一份提议 JEP 286,打算为 Java 10 引入局部变量类型推断(Local-Variable Type Inference)。利用这个个性,咱们能够应用 var 定义变量而无需显式申明其类型。很多人认为这是一项激动人心的个性,然而快乐之前咱们要先看看它会为咱们带来哪些问题。

与 Java 7 的钻石操作符抵触

Java 7 引进了钻石操作符,使得咱们能够升高表达式右侧的冗余类型信息,例如:

List<Integer> numbers = new ArrayList<>();

如果引入了 var,则会导致左侧的类型失落,从而导致整个表达式的类型失落:

var numbers = new ArrayList<>();

所以 var 和 钻石操作符必须二选一,鱼与熊掌不可兼得。

容易导致谬误的代码

上面是一段检查用户是否存在的 Java 代码:

public boolean userExistsIn(Set<Long> userIds) {var userId = getCurrentUserId();
    return userIds.contains(userId);
}

请仔细观察上述代码,你能一眼看出问题所在吗?userId 的类型被 var 隐去了,如果 getCurrentUserId() 返回的是 String 类型,上述代码依然能够失常通过编译,却无形中埋下了隐患,这个办法将会永远返回 false,因为 Set<Long>.contains 办法承受的参数类型是 Object。可能有人会说,就算显式申明了类型,不也是于事无补吗?

public boolean userExistsIn(Set<Long> userIds) {String userId = getCurrentUserId();
    return userIds.contains(userId);
}

Java 的劣势在于它的类型可读性,如果显式申明了 userId 的类型,尽管还是能够失常通过编译,然而在代码审查时,这个谬误将会更容易被发现。这种类型的谬误在 Java 中非常容易产生,因为 getCurrentUserId() 办法很可能因为重构而扭转了返回类型,而 Java 编译器却在关键时刻背离了你,没有报告任何的编译谬误。尽管这是因为 Java 的历史起因导致的,然而因为 var 的引入,会导致这个谬误一直的蔓延。

很显然,在 Scala 中,这种低级谬误是无奈逃过编译器法眼的:

def userExistsIn(userIds: Set[Long]): Boolean = {val userId = getCurrentUserId()
    userIds.contains(userId)
}

如果 userId 不是 Long 类型,则下面的程序无奈通过编译。

字符串加强

撩拨指数: 四星

罕用操作

Scala 针对字符作进行了加强,提供了更多的应用操作:

// 字符串去重
"aabbcc".distinct // "abc"

// 取前 n 个字符,如果 n 大于字符串长度返回原字符串
"abcd".take(10) // "abcd"

// 字符串排序
"bcad".sorted // "abcd"

// 过滤特定字符
"bcad".filter(_ != 'a') // "bcd"

// 类型转换
"true".toBoolean
"123".toInt
"123.0".toDouble

其实你齐全能够把 String 当做 Seq[Char] 应用,利用 Scala 弱小的汇合操作,你能够得心应手地操作字符串。

原生字符串

在 Scala 中,咱们能够间接书写原生字符串而不必进行本义,将字符串内容放入一对三引号内即可:

// 蕴含换行的字符串
val s1= """Welcome here.
   Type "HELP" for help!"""
   
// 蕴含正则表达式的字符串   
val regex = """\d+"""   

字符串插值

通过 s 表达式,咱们能够很不便地在字符串内插值:

val name = "world"
val msg = s"hello, ${name}" // hello, world

汇合操作

撩拨指数: 五星

Scala 的汇合设计是最容易让人着迷的中央,就像毒品一样,一沾上便让人深陷其中难以自拔。通过 Scala 提供的汇合操作,咱们基本上能够实现 SQL 的全副性能,这也是为什么 Scala 可能在大数据畛域独领风骚的重要起因之一。

简洁的初始化形式

在 Scala 中,咱们能够这样初始化一个列表:

val list1 = List(1, 2, 3)

能够这样初始化一个 Map:

val map = Map("a" -> 1, "b" -> 2)

所有的汇合类型均能够用相似的形式实现初始化,简洁而富裕表达力。

便捷的 Tuple 类型

有时办法的返回值可能不止一个,Scala 提供了 Tuple (元组)类型用于长期寄存多个不同类型的值,同时可能保障类型安全性。千万不要认为应用 Java 的 Array 类型也能够同样实现 Tuple 类型的性能,它们之间有着实质的区别。Tuple 会显式申明所有元素的各自类型,而不是像 Java Array 那样,元素类型会被向上转型为所有元素的父类型。
咱们能够这样初始化一个 Tuple:

val t = ("abc", 123, true)
val s: String  = t._1 // 取第 1 个元素
val i: Int     = t._2 // 取第 2 个元素
val b: Boolean = t._3 // 取第 3 个元素

须要留神的是 Tuple 的元素索引从 1 开始。

上面的示例代码是在一个长整型列表中寻找最大值,并返回这个最大值以及它所在的地位:

def max(list: List[Long]): (Long, Int) = list.zipWithIndex.sorted.reverse.head

咱们通过 zipWithIndex 办法获取每个元素的索引号,从而将 List[Long] 转换成了 List[(Long, Int)],而后对其顺次进行排序、倒序和取首元素,最终返回最大值及其所在位置。

链式调用

通过链式调用,咱们能够将关注点放在数据的解决和转换上,而无需思考如何存储和传递数据,同时也防止了创立大量无意义的两头变量,大大加强程序的可读性。其实下面的 max 函数曾经演示了链式调用。上面咱们演示一下如何应用汇合操作实现 SQL 的关联查问性能,待实现的 SQL 语句如下:

SELECT p.name, p.company, c.country FROM people p JOIN companies c ON p.companyId = c.id
WHERE p.age == 20

下面 SQL 语句实现的性能是关联查问 people 和 companies 两张表,返回年龄为 20 岁的所有员工名称、年龄以及其所在公司名称。

对应的 Scala 实现代码如下:

// Entity
case class People(name: String, age: Int, companyId: String)
case class Company(id: String, name: String)

// Entity List
val people    = List(People("jack", 20, "0"))
val companies = List(Company("0", "lightbend"))

// 实现关联查问
people
  .filter(p => p.age == 20)
  .flatMap{ p =>
    companies
      .filter(c => c.id == p.companyId)
      .map(c => (p.name, p.age, c.name))
}
// 后果:List((jack,20,lightbend))

其实应用 for 表达式看起来更加简洁:

for {
  p <- people if p.age == 20
  c <- companies if p.companyId == c.id
} yield (p.name, p.age, c.name)

非典型汇合操作

Scala 的汇合操作十分丰盛,如果要具体阐明足够写一本书了。这里仅列出一些不那么罕用但却十分好用的操作。

去重:

List(1, 2, 2, 3).distinct // List(1, 2, 3)

交加:

Set(1, 2) & Set(2, 3)   // Set(2)

并集:

Set(1, 2) | Set(2, 3) // Set(1, 2, 3)

差集:

Set(1, 2) &~ Set(2, 3) // Set(1)

排列:

List(1, 2, 3).permutations.toList
//List(List(1, 2, 3), List(1, 3, 2), List(2, 1, 3), List(2, 3, 1), List(3, 1, 2), List(3, 2, 1))

组合:

List(1, 2, 3).combinations(2).toList 
// List(List(1, 2), List(1, 3), List(2, 3))

并行汇合

Scala 的并行汇合能够利用多核优势减速计算过程,通过汇合上的 par 办法,咱们能够将原汇合转换成并行汇合。并行汇合利用分治算法将计算工作分解成很多子工作,而后交给不同的线程执行,最初将计算结果进行汇总。上面是一个简略的示例:

(1 to 10000).par.filter(i => i % 2 == 1).sum

优雅的值对象

撩拨指数: 五星

Case Class

Scala 规范库蕴含了一个非凡的 Class 叫做 Case Class,专门用于畛域层值对象的建模。它的益处是所有的默认行为都通过了正当的设计,开箱即用。上面咱们应用 Case Class 定义了一个 User 值对象:

case class User(name: String, role: String = "user", addTime: Instant = Instant.now())

仅仅一行代码便实现了 User 类的定义,请脑补一下 Java 的实现。

简洁的实例化形式

咱们为 role 和 addTime 两个属性定义了默认值,所以咱们能够只应用 name 创立一个 User 实例:

val u = User("jack")

在创立实例时,咱们也能够命名参数 (named parameter) 语法扭转默认值:

val u = User("jack", role = "admin")

在理论开发中,一个模型类或值对象可能领有很多属性,其实很多属性都能够设置一个正当的默认值。利用默认值和命名参数,咱们能够十分不便地创立模型类和值对象的实例。所以在 Scala 中基本上不须要应用工厂模式或结构器模式创建对象,如果对象的创立过程的确非常复杂,则能够放在伴生对象中创立,例如:

object User {def apply(name: String): User = User(name, "user", Instant.now())
}

在应用伴生对象办法创立实例时能够省略办法名 apply,例如:

User("jack") // 等价于 User.apply("jack")

在这个例子里,应用伴生对象办法实例化对象的代码,与下面应用类结构器的代码齐全一样,编译器会优先选择伴生对象的 apply 办法。

不可变性

Case Class 在默认状况下实例是不可变的,意味着它能够被任意共享,并发拜访时也无需同步,大大地节俭了贵重的内存空间。而在 Java 中,对象被共享时须要进行深拷贝,否则一个中央的批改会影响到其它中央。例如在 Java 中定义了一个 Role 对象:

public class Role {
    public String id = "";
    public String name = "user";
    
    public Role(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

如果在两个 User 之间共享 Role 实例就会呈现问题,就像上面这样:

u1.role = new Role("user", "user");
u2.role = u1.role;

当咱们批改 u1.role 时,u2 就会受到影响,Java 的解决形式是要么基于 u1.role 深度克隆一个新对象进去,要么新创建一个 Role 对象赋值给 u2。

对象拷贝

在 Scala 中,既然 Case Class 是不可变的,那么如果想扭转它的值该怎么办呢?其实很简略,利用命名参数能够很容易拷贝一个新的不可变对象进去:

val u1 = User("jack")
val u2 = u1.copy(name = "role", role = "admin")

清晰的调试信息

咱们不须要编写额定的代码便能够失去清晰的调试信息,例如:

val users = List(User("jack"), User("rose"))
println(users)

输入内容如下:

List(User(jack,user,2018-10-20T13:03:16.170Z), User(rose,user,2018-10-20T13:03:16.170Z))

默认应用值比拟相等性

在 Scala 中,默认采纳值比拟而非援用比拟,应用起来更加合乎直觉:

User("jack") == User("jack") // true

下面的值比拟是开箱即用的,无需重写 hashCode 和 equals 办法。

模式匹配

撩拨指数: 五星

更强的可读性

当你的代码中存在多个 if 分支并且 if 之间还会有嵌套,那么代码的可读性将会大大降低。而在 Scala 中应用模式匹配能够很容易地解决这个问题,上面的代码演示货币类型的匹配:

sealed trait Currency
case class Dollar(value: Double) extends Currency
case class Euro(value: Double) extends Currency
val Currency = ...
currency match {case Dollar(v) => "$" + v
    case Euro(v) => "€" + v
    case _ => "unknown"
}

咱们也能够进行一些简单的匹配,并且在匹配时能够减少 if 判断:

use match {case User("jack", _, _) => ...
    case User(_, _, addTime) if addTime.isAfter(time) => ...
    case _ => ...
}

变量赋值

利用模式匹配,咱们能够疾速提取特定局部的值并实现变量定义。咱们能够将 Tuple 中的值间接赋值给变量:

val tuple = ("jack", "user", Instant.now())
val (name, role, addTime) = tuple
// 变量 name, role, addTime 在以后作用域内能够间接应用

对于 Case Class 也是一样:

val User(name, role, addTime) = User("jack")
// 变量 name, role, addTime 在以后作用域内能够间接应用

并发编程

撩拨指数: 五星

在 Scala 中,咱们在编写并发代码时只须要关怀业务逻辑即可,而不须要关注工作如何执行。咱们能够通过显式或隐式形式传入一个线程池,具体的执行过程由线程池实现。Future 用于启动一个异步工作并且保留执行后果,咱们能够用 for 表达式收集多个 Future 的执行后果,从而防止回调天堂:

val f1 = Future{1 + 2}
val f2 = Future{3 + 4}
for {
    v1 <- f1
    v2 <- f2
}{println(v1 + v2) // 10
}

应用 Future 开发爬虫程序将会让你事倍功半,如果你想同时抓取 100 个页面数据,一行代码就能够了:

Future.sequence(urls.map(url => http.get(url))).foreach{contents => ...}

Future.sequence 办法用于收集所有 Future 的执行后果,通过 foreach 办法咱们能够取出收集后果并进行后续解决。

当咱们要实现齐全异步的申请限流时,就须要精密地管制每个 Future 的执行机会。也就是说咱们须要一个管制 Future 的开关,没错,这个开关就是 Promise。每个 Promise 实例都会有一个惟一的 Future 与之相关联:

val p = Promise[Int]()
val f = p.future
for (v <- f) {println(v) } // 3 秒后才会执行打印操作

// 3 秒钟之后返回 3
Thread.sleep(3000)
p.success(3)

跨线程错误处理

Java 通过异样机制处理错误,然而问题在于 Java 代码只能捕捉以后线程的异样,而无奈跨线程捕捉异样。而在 Scala 中,咱们能够通过 Future 捕捉任意线程中产生的异样。
异步工作可能胜利也可能失败,所以咱们须要一种既能够示意胜利,也能够示意失败的数据类型,在 Scala 中它就是 Try[T]。Try[T] 有两个子类型,Success[T]示意胜利,Failure[T]示意失败。就像量子物理学中薛定谔的猫,在异步工作执行之前,你根本无法预知返回的后果是 Success[T] 还是 Failure[T],只有当异步工作实现执行当前后果能力确定下来。

val f = Future{/* 异步工作 */} 

// 当异步工作执行实现时
f.value.get match {case Success(v) => // 解决胜利状况
  case Failure(t) => // 解决失败状况
}

咱们也能够让一个 Future 从谬误中复原:

val f = Future{/* 异步工作 */}
for{result <- f.recover{ case t => /* 处理错误 */}
} yield {// 处理结果}

申明式编程

撩拨指数: 四星

Scala 激励申明式编程,采纳申明式编写的代码可读性更强。与传统的命令式编程相比,申明式编程更关注我想做什么而不是怎么去做。例如咱们常常要实现分页操作,每页返回 10 条数据:

val allUsers = List(User("jack"), User("rose"))
val pageList = 
  allUsers
    .sortBy(u => (u.role, u.name, u.addTime)) // 顺次按 role, name, addTime 进行排序
    .drop(page * 10) // 跳过之前页数据
    .take(10) // 取当前页数据,如有余 10 个则全副返回

你只须要通知 Scala 要做什么,比如说先按 role 排序,如果 role 雷同则按 name 排序,如果 role 和 name 都雷同,再按 addTime 排序。底层具体的排序实现曾经封装好了,开发者无需实现。另外,申明式代码更适宜并行优化,因为它仅指定了后果所满足的模式,而并不指定执行的具体过程。而因为命令式代码指定了具体的执行步骤和程序,仅能依附更高的时钟频率晋升执行速度。

面向表达式编程

撩拨指数: 四星

在 Scala 中,一切都是表达式,包含 if, for, while 等常见的控制结构均是表达式。表达式和语句的不同之处在于每个表达式都有明确的返回值。

val i = if(true){1} else {0} // i = 1
val list1 = List(1, 2, 3)
val list2 = for(i <- list1) yield {i + 1}

不同的表达式能够组合在一起造成一个更大的表达式,再联合上模式匹配将会施展微小的威力。上面咱们以一个计算加法的解释器来做阐明。

一个整数加法解释器

咱们首先定义根本的表达式类型:

abstract class Expr
case class Number(num: Int) extends Expr
case class PlusExpr(left: Expr, right: Expr) extends Expr

下面定义了两个表达式类型,Number 示意一个整数表达式,PlusExpr 示意一个加法表达式。
上面咱们基于模式匹配实现表达式的求值运算:

def evalExpr(expr: Expr): Int = {
  expr match {case Number(n) => n
    case PlusExpr(left, right) => evalExpr(left) + evalExpr(right)
  }
}

咱们来尝试针对一个较大的表达式进行求值:

evalExpr(PlusExpr(PlusExpr(Number(1), Number(2)), PlusExpr(Number(3), Number(4)))) // 10

隐式参数和隐式转换

撩拨指数: 五星

隐式参数

如果每当要执行异步工作时,都须要显式传入线程池参数,你会不会感觉很烦?Scala 通过隐式参数为你解除这个懊恼。例如 Future 在创立异步工作时就申明了一个 ExecutionContext 类型的隐式参数,编译器会主动在以后作用域内寻找适合的 ExecutionContext,如果找不到则会报编译谬误:

implicit val ec: ExecutionContext = ???
val f = Future {/* 异步工作 */}

当然咱们也能够显式传递 ExecutionContext 参数,明确指定应用的线程池:

implicit val ec: ExecutionContext = ???
val f = Future {/* 异步工作 */}(ec)

隐式转换

隐式转换相比拟于隐式参数,应用起来更来灵便。如果 Scala 在编译时发现了谬误,在报错之前,会先对错误代码利用隐式转换规则,如果在利用规定之后能够使得其通过编译,则示意胜利地实现了一次隐式转换。

在不同的库间实现无缝对接

当传入的参数类型和指标类型不匹配时,编译器会尝试隐式转换。利用这个性能,咱们将已有的数据类型无缝对接到三方库上。例如咱们想在 Scala 我的项目中应用 MongoDB 的官网 Java 驱动执行数据库查问操作,然而查问接口承受的参数类型是 BsonDocument,因为应用 BsonDocument 构建查问比拟蠢笨,咱们心愿可能应用 Scala 的 JSON 库构建一个查问对象,而后间接传递给官网驱动的查问接口,而无需扭转官网驱动的任何代码,利用隐式转换能够十分轻松地实现这个性能:

implicit def toBson(json: JsObject): BsonDocument =  ...

val json: JsObject = Json.obj("_id" -> "0")
jCollection.find(json) // 编译器会主动调用 toBson(json)

利用隐式转换,咱们能够在不改变三方库代码的状况下,将咱们的数据类型与其进行无缝对接。例如咱们通过实现一个隐式转换,将 Scala 的 JsObject 类型无缝地对接到了 MongoDB 的官网 Java 驱动的查问接口中,看起就像是 MongoDB 官网驱动真的提供了这个接口一样。

同时咱们也能够将来自三方库的数据类型无缝集成到现有的接口中,也只须要实现一个隐式转换方法即可。

扩大已有类的性能

例如咱们定义了一个美元货币类型 Dollar:

class Dollar(value: Double) {def + (that: Dollar): Dollar = ...
  def + (that: Int): Dollar = ...
}

于是咱们能够执行如下操作:

val halfDollar = new Dollar(0.5)
halfDollar + halfDollar // 1 dollar
halfDollar + 0.5 // 1 dollar

然而咱们却无奈执行像 0.5 + halfDollar 这样的运算,因为在 Double 类型上无奈找到一个适合的 + 办法。

在 Scala 中,为了实现下面的运算,咱们只须要实现一个简略的隐式转换就能够了:

implicit def doubleToDollar(d: Double) = new Dollar(d)

0.5 + halfDollar // 等价于 doubleToDollar(0.5) + halfDollar

更好的运行时性能

在日常开发中,咱们通常须要将值对象转换成 Json 格局以不便数据传输。Java 的通常做法是应用反射,然而咱们晓得应用反射是要付出代价的,要接受运行时的性能开销。而 Scala 则能够在编译时为值对象生成隐式的 Json 编解码对象,这些编解码对象只不过是一般的函数调用而已,不波及任何反射操作,在很大水平上晋升了零碎的运行时性能。

小结

如果你保持读到了这里,我会感觉十分快慰,很大可能上 Scala 的某些个性曾经吸引了你。然而 Scala 的魅力远不止如此,以上列举的仅仅是一些最容易抓住你眼球的一些个性。如果你违心推开 Scala 这扇大门,你将会看到一个齐全不一样的编程世界。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版