2021-07-23

那些挑逗 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.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.idWHERE p.age == 20

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

对应的 Scala 实现代码如下:

// Entitycase class People(name: String, age: Int, companyId: String)case class Company(id: String, name: String)// Entity Listval 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 Currencycase class Dollar(value: Double) extends Currencycase class Euro(value: Double) extends Currencyval 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.futurefor (v <- f) { println(v) } // 3秒后才会执行打印操作//3秒钟之后返回3Thread.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 =......

原文转载:http://www.shaoqun.com/a/893223.html

跨境电商:https://www.ikjzd.com/

海带宝:https://www.ikjzd.com/w/1548

墩煌网:https://www.ikjzd.com/w/189

focalprice:https://www.ikjzd.com/w/1094.html


作者:沐风(joymufeng)来源:my.oschina.net/joymufeng/blog/2251038有个问题一直困扰着Scala社区,为什么一些Java开发者将Scala捧到了天上,认为它是来自上帝之吻的完美语言;而另外一些Java开发者却对它望而却步,认为它过于复杂而难以理解。同样是Java开发者,为何会出现两种截然不同的态度,我想这其中一定有误会。Scala是一粒金子,但是被一些表
拍拍网服装:https://www.ikjzd.com/w/2205
为什么他家香肠要提前两年预定? 临海古城"香肠王"说:挑肉比挑女婿还仔细:http://www.30bags.com/a/225772.html
为什么泰国人妖都那么漂亮?:http://www.30bags.com/a/405465.html
为什么网上订火车票会提示无法登陆?:http://www.30bags.com/a/399981.html
为什么文艺的人,都热爱南方:http://www.30bags.com/a/242891.html
老板叫我穿少一点去他办公室 上司撕开我的裙子和乳罩:http://lady.shaoqun.com/a/248401.html
学长大人上课和我做 学长的巨大在我体内进进出出:http://lady.shaoqun.com/m/a/247506.html
女班长把内裤掀起来给我们看 女同桌让我闻她内裤:http://www.30bags.com/m/a/249777.html
光明农场大观园龙眼采摘时间、价格:http://www.30bags.com/a/517522.html
如何提高亚马逊产品曝光率?:https://www.ikjzd.com/articles/146846
让一个女人对你"死心塌地"的最好方法就是这6个字的谚语:http://lady.shaoqun.com/a/428430.html
博士在线名人性骚扰75名女性患者,聊天内容惊艳,但不一定判刑:http://lady.shaoqun.com/a/428431.html

No comments:

Post a Comment