共计 10758 个字符,预计需要花费 27 分钟才能阅读完成。
作者:沐风(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 提供了 eq
和 ne
两个办法:
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 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!