Scala与Java 8的10个区别:第一部分






4.21/5 (12投票s)
由于它们完全的运行时兼容性,“Scala vs. Java”是 Java 开发世界中永无止境的争论。
许多 Java 开发人员非常喜欢 Scala,并且总是优先选择它而不是 Java 来处理任何类型的任务:新应用程序、现有应用程序的组件,甚至是现有 Java 模块的性能关键部分。这使得 Scala 越来越受欢迎,并且它已经成功进入了企业领域。
像亚马逊和暴雪这样知名公司已经在使用 Scala,并为其应用程序带来了所有优势。 Scala 和我们之间是一见钟情,我们只想告诉你为什么。
有很多文章已经清楚地表明,尽管 Scala 语言在初看起来有些复杂,但其函数式编程方法乘以绝对的 Java 互操作性,使其远远优于纯粹的 Java 6 或 Java 7。
但 Scala 已经证明了自己的实用性,以至于 Java 的新版本开始实现 Scala 的功能,以及其他函数式语言的功能。通过发布 Java 8,Oracle 已经跟上了向传统命令式 OOP 语言注入函数式编程功能的潮流。Java 的这个版本已经融入了 Scala 中一些最受开发人员欢迎的功能,这些功能减少了开发人员工作中琐碎操作的繁琐。
- 支持 lambda 表达式:不再需要为诸如处理集合等常见操作编写大量不必要的代码;
- 支持高阶函数:使您的代码更具可重用性,不那么字面化,并且更具自解释性;
- 并行集合:无需任何麻烦即可享受多核系统在数据处理方面的一些好处;
- 类型推断:使代码更具可重用性和不那么字面化,但仍然只在运行时级别;
- 虚拟扩展方法(与 Scala 中的 trait 不同):它允许为类添加新行为
仔细观察,Java 8 中引入的许多方法与 Scala 提供的非常相似。但仍然存在区别吗?您是否仍然应该选择 Scala 而不是 Java?
这是我们对 Scala 和 Java 8 的比较,我们将其制作成一个区别性特征列表。
1. 即使有了 Project Lambda,Java 8 仍然是传统的命令式语言
尽管新功能和方法无疑简化了开发人员的生活,但这仍然与函数式编程不完全相同。Java 8 只提供了一个函数式编程的初步体验:最基本的东西。
尽管如此,这仍然是很棒的东西,它得到了 Scala 初学者的赞扬
“你可以在一行中做到这一点!”
“你再也不用做这件事了!”
“在 Java 中,这段代码会是一团糟,而在 Scala 中,它清晰如晴天!”
Oracle 在理解和吸收函数式编程的大部分热门功能方面做得很好,并且在许多情况下几乎与 Scala 相同,这样没有人会问“Scala 一切都那么好,为什么 Java 非要搞砸它?”。
有人甚至可能会想“好吧,Java 现在拥有了所有好东西,我还能从切换到 Scala 中获得什么?”。
这种观点仍然有一个很大的缺点。
这是由于一个简单的误解,因为 Scala 经常成为人们的第一个函数式语言,并且被介绍为上面提到的酷炫功能集。此外,Scala 允许编写命令式代码,因此它经常被用作“带有语法糖和一套酷炫功能的 Java”来考虑。
这是错误的。
Java 8 仍然是具有函数式编程功能的传统语言。
Scala 是一种成熟的函数式编程语言。
在下一节中,我们将尝试解释为什么这不仅对您的日常代码,而且对整个软件开发世界都至关重要。
2. Scala 是一种函数式编程语言
字面意思就是“写得少,做得多”。
在使用函数式范式时,您不再仅仅是一个“建筑工人”,而更像一个“建筑师”。我们敢说,这是命令式方法与函数式方法之间的一个主要区别,对开发人员而言。
命令式编程意味着您需要编写所有步骤才能达到目标。
例如,您的应用程序逻辑要求从一大堆其他string
中选择一些符合您标准的string
。要说明这种任务的命令式方法,最佳方法是按照“旧 Java”(7、6 或更早版本)的风格来实现这种逻辑
```
//We have some list of strings stringsToFilterList
//We know that we need to store filtered strings somewhere, so we make a storage for them;
List<string> filteredList = new LinkedList<string>();
//We know that to filter this list, we must iterate over all of its elements;
for (stringToFilter : stringsToFilterList) {
if (StringUtils.isNotBlank(stringToFilter) && stringToFilter.startsWith("somePredicate")) {
/* The only meaningful operation in this code block,
we match our strings by set of conditions; */
filteredList.add(stringToFilter);
/* And another technical-only operation inside of it,
we know that after one of the iterated strings match our conditions,
we must store it somewhere; */
}
}
```
所以我们有三个技术步骤,只有一个有意义的操作与应用程序逻辑相关。
在像 Scala 这样的函数式语言中,相同的应用程序逻辑实现将被简化为只有有意义的操作
```
//We have some list of strings stringsToFilterList
val filteredList = stringsToFilterList.filter(s => s.nonEmpty && s.startsWith("somePredicate"))
//We still want to store filtered results somewhere, though.
```
是的,这里只剩下与应用程序逻辑相关的有意义的操作。
您刚刚表达了您真正想做的事情,仅此而已。
然而,有一件重要的事情需要记住。没有任何东西可以阻止您将“旧 Java”风格的代码直接映射到 Scala。
这意味着,如果您的思维方式是“我想过滤掉所有非空字符串,但这些字符串以对应用程序逻辑有意义的内容开头”,您的代码将是简洁而优雅的,但如果您的思维方式是“我需要创建一个用于存储过滤数据的存储,遍历初始数据,检查每个元素是否满足条件,如果条件匹配则存储它”,您将得到的代码将非常类似于我们的“旧 Java”示例
```
//Example of bad code. Nothing will prevent you from writing imperative code in Scala.
val filteredList = ListBuffer[String]()
for (stringToFilter <- stringsToFilterList) {
if(stringToFilter.nonEmpty && stringToFilter.startsWith("somePredicate")) {
filteredList += stringToFilter
}
}
```
没有任何东西能阻止您,您也不会明白为什么需要函数式编程语言,它看起来像是“一切都一样”。因此,对于 Scala(以及函数式编程)来说,这真的是“你想什么,就写什么”。
函数式编程提供了许多方法来表达您认为最合适您的算法,就像数学一样。
现在,您可能会说“等等!但 Java 8 正在做得更好!”,那么让我们也看看。
Java 8 已经引入了 lambda 表达式,我们的示例看起来会更小
```
List<string> filteredList = stringsToFilterList.stream().filter
(s -> StringUtils.isNotBlank(s) &&
s.startsWith("somePredicate")).collect(Collectors.toList());
//Notice, even now there are still some technicalities here, like .stream() and .collect()
//From our point of view, code even became less readable than it was.
```
这仍然不一样。Oracle 完全没有改变方法。这仍然是我们的普通 Java,加上新的“拐杖”。他们只是试图解决症状,并引入了“每个人都在谈论的酷炫功能”,这完全没有改变方法。
尽管 Java 8 中常见的集合操作现在不那么字面化了,但这并没有改变它仍然是传统命令式编程语言这一事实,而且在许多情况下,您仍然需要编写大量仅从技术角度来看有意义,而不是从应用程序逻辑角度来看有意义的代码。
函数式编程试图避免这种情况。我们之前说过,函数式编程提供了许多工具来实现简洁的表达,就像数学一样。这就是为什么它不仅改变了您的日常代码,也改变了软件开发过程本身。
事实上,函数式编程在很大程度上是通过使用数学来推理程序。要做到这一点,您需要一个形式化系统,该系统
- 描述程序本身
- 描述如何对它们的属性进行证明
有许多计算模型提供了这样的形式化系统,例如 lambda 演算和图灵机(它们之间存在一定程度的等价性)。那么,关于数学有趣的一点是:形式化系统越不强大,就越容易用它进行证明。
或者,简单地说,很难推理命令式程序(尤其是具有副作用,如可变性)。
因此,命令式编程的概念几乎没有进展。
著名的设计模式不是通过研究得出的,它们没有任何数学依据。相反,它们是多年试错的结果,其中一些已被证明是错误的。
而且谁知道随处可见的其他几十种“设计模式”呢?
与此同时,在函数式编程社区中,情况更加活跃。
例如,Haskell 程序员提出了 Functors、Monads、Co-monads、Zippers、Applicatives、Lenses——几十个具有数学依据的概念,最重要的是,它们是代码如何组合成程序的实际模式。这些东西可以用来推理您的程序,增加可重用性,并提高应用程序逻辑的实现正确性。
函数式方法为您提供了编写更好程序的手段,而不是编写算法某些步骤的更好方法。
我们将在接下来的部分中介绍其中一些手段,即 Scala 作为函数式编程语言所特有的那些。
3. Monads (单子)
Monad 是一个从数学传入函数式编程的概念。
它们通常被称为“可编程的分号”,因为在许多命令式编程语言中(显然,Java 也不例外),分号通常用于将单个语句链接在一起。因此,“可编程”分号意味着在语句之间会执行一些额外的代码。
它是一种表示计算结构,计算被定义为一系列步骤:具有 monad 结构的类型定义了如何链接操作,或者如何将该类型的函数嵌套在一起。
这允许程序员构建处理数据的管道,其中每个操作都被 monad 提供的附加处理规则所装饰。
根据定义,monads 非常适合链接事物,而 Scala 则充分利用了这一点。
在这篇文章中,我们既不介绍 monad 的正式定义,也不介绍什么条件才能被称为真正的 monad。
我们将讨论 monad 在编程中的好处以及 Scala 如何理解和处理这个概念。
在初学者中,Scala 中最受欢迎的 monads 是
1. **“Maybe”** Monad。“Maybe” monads 通常被称为“Optional Types”。
在 Scala 中,maybe monad 以 Option[T]
类实现,它可以处于两种明确的状态:Some[T]
(当其中存储了类型 T 的实际值时)和 None
(当它不存储任何内容时)。
您始终可以使用 Scala 将内容包装到 Option
中
```
val maybeString = Option(someOtherString)
```
在 Java 中,您需要自己检查其中是否有实际值,但在 Scala 中,Option
会为您完成这项工作,并且由于其不可变性,您无需一次又一次地检查。
Scala 为“maybe” monad 提供了非常方便的辅助方法
```
//Direct matching of state
maybeString match {
case Some(s) => { //do something with this string }
case None => { //do what you need to do in case there is nothing inside }
}
//Get value stored from monad, or use default one if there is nothing inside
maybeString.getOrElse("defaultValueOfThisString")
//Get value stored from monad, or execute a function if there is nothing inside
//Very useful if some additional stuff must be made other from assigning a default value
maybeString.orElse({
//Here goes our function for when there is nothing inside
log.warn("Using default value!!!")
//Not the best reason to have a function here, but hey.
Some("defaultValueOfThisString")
}).get //Thanks to orElse we'll never receive a None.get here
//To quickly check if there is something inside without actually grabbing it
if (maybeString.isEmpty) { ... }
//To quickly check that there is something inside AND it meets some conditions without actually grabbing it
if (maybeString.exists(_.startsWith("somePredicate"))) { ... }
```
太方便了!我们敢打赌,您已经想象到了这给您的代码带来的可能性!
Maybe monad 非常容易实现,因此它如今很受欢迎(请参阅这篇 Swift vs. Objective-C 文章),以及其他函数式编程特性,但出于某种原因,Java 8 仍未将其纳入。
2. **“Try”**。严格来说,这不是一个 monad,因为它不满足其中一个正式条件。尽管如此,它的行为很像一个 monad,您可以将其视为常规 try
/catch
例程的替代品。
```
Try({
//...some code here
})
...
//You can call some other class, too
Try(SomeClass.someMethod())
...
```
好了,现在我们来谈谈精彩的部分。Try
也有两个明确的状态:Success
(当一切顺利时)和 Failure
(例如,当发生异常时,或者结果不满足您的某些条件时)。
让我们来看看
```
//direct matching of state
Try(SomeClass.someMethod()) match {
case Success(s) => { //do something with result }
case Failure(e) => { //do something regarding an exception that was raised }
}
//When you want to do something for failure
Try(SomeClass.someMethod()).recover({ //some fail-safe operations })
//When there are additional conditions that must be met for Try to be a Success.
Try(SomeClass.someMethod()).filter({ //some checks to mark try as Success })
//When you just care about result and want to look at it as a "maybe" monad
Try(SomeClass.someMethod()).toOption
//Quick way to get value if everything went fine or a default value on failure, just like in option.
Try(SomeClass.someMethod()).getOrElse("somethingDefault")
//Or if you need to do something more than just return a default value
Try(SomeClass.someMethod()).orElse({
log.error("We couldn't get that thing for you, enjoy your default value")
Success("somethingDefault")
}).get
//if you just want to know if everything went fine or not
Try(SomeClass.someMethod()).isSuccess
Try(SomeClass.someMethod()).isFailure
```
而且,一如既往地使用 monad,您可以根据需要组合以上所有内容。
与此相比,常规的 try
/ catch
例程显得苍白无力。在 Java 或其他命令式语言中,您无法在应用程序工作流中拥有如此灵活的控制,只有函数式方法才允许这样做。
是的,如果您出于某种原因想要一个简单的 try
/ catch
,它仍然可用。
3. **“Future”**。这是最有趣的一个,因为它为初学者开发人员开启了异步编程的全新世界。
Scala 的并发模型充分利用了这种 monad,这使其如此强大和健壮。您现在可以让代码片段独立且非阻塞,并且仍然可以阻塞并等待结果。这有多酷?
```
val futureResponseBody: String = future {
//we make an http request here and return response body as String
}
//This line of code will be executed without waiting for request to complete
println("Yay! Wasn't blocked by request!")
```
Now, how we can handle it?
```
//direct state matching
futureResponseBody onComplete {
case Success(responseBody) => { //do something with it }
case Failure(e) => { //do something regarding and exception that was raised }
}
//If you want, you can block and wait for the future with some timeout
//(it will be resolved as Failure after that)
futureResponseBody.map(body => //do something with response body)
Await.result(futureResponseBody, 30 seconds)
//do something with successfully resolved future
futureResponseBody onSuccess { ... }
//do something with failed future
futureResponseBody onFailure { ... }
//If you want to know if future is resolved without grabbing what's inside
futureResponseBody.isCompleted
//If you want to get instant value of the future as an Option
futureResponseBody.value
```
如您所见,Scala 中的 monads 符合一组共享规则,并且可以根据开发人员的便利性轻松地相互链接和转换。
Scala 中的 Monads 支持了那些使传统上繁琐的事情变得快速而健壮的方法。
同样,这使您可以专注于实现应用程序逻辑,而不是将时间浪费在纯粹的技术细节上。
4. 不可变性 (Immutability)
不可变性是函数式编程能够实现的首要因素,正如我们试图说明的那样,函数式方法在软件开发人员的体验方面带来了所有不同。
不可变性是一种范式,即任何对象在创建后都不能更改其状态。如果您需要更改不可变对象的某个状态,您将创建一个副本并更改所需的属性。
对于 Java 开发人员来说,不可变类型的最简单例子是 String
。每个 Java 开发人员都知道,在 Java 中,string
是不可变的,并且每次更改都会创建一个副本。
不可变性是函数式编程的关键,因为它符合最小化变化部分的目标,从而更容易理解这些部分。
那么,这有什么好处呢?
- 无副作用。 您不必担心某些东西会更改您传递给某处的东西。但这有什么好处呢?请继续阅读。
- 提高执行速度。 使用不可变对象,基于可变性的成功或失败一旦对象被构造就永远解决了。这意味着运行时检查更少,编译器优化更多,从而更快地执行您的代码。
- 100% 线程安全。 您不必担心锁,因为共享对象永远不会改变。您不必仔细检查代码并找出两个线程可能同时尝试更改同一对象的所有位置,并且您不必编写更多代码来防止可能由此引起的问题。
- 代码更简洁。 对象不可变这一事实将使使用它的代码更加简洁易读。
但是,从 Java 开发的角度来看,不可变性有什么好处呢?
不可变性可以消除 Java 中大量通常令人头疼的问题
- 通过严格限制可变性来隔离发生变化的地方,您可以创建一个更小的错误发生空间,并且需要测试的地方更少。
- 由于异常,不可变对象永远不会处于未知或不期望的状态。所有初始化都发生在构造时,这在 Java 中是原子的,因此任何异常都会在您获得对象实例之前发生。
- 如上所述,不可变对象也是自动线程安全的,并且没有同步问题。这对 Java 的并发模型来说是一个巨大的好处。如果没有任何对象方法可以修改其状态,无论它们被调用多少次,无论多频繁,它们都将在堆栈的自己的内存空间中运行。
- 由于更改仅在构造时发生,因此编写不可变类的单元测试非常简单。您不需要复制构造函数,也永远不必担心实现
clone()
方法的复杂细节。 - 虽然有些老生常谈,但不可变对象是作为
Maps
或Sets
的键的良好候选者。Java 中字典集合的键在使用作键时不能更改其值,因此不可变对象非常适合作为键。
好的,但是 Scala 到底在哪些方面做得更好,以至于您想使用它而不是 Java 呢?
Java 的方法:一切都是可变的,除非特别指定,或者简单地说:“您可以选择使某个东西不可变,但您怎么知道您需要这样做?”。
在许多情况下,没有人会费心这样做。是的,有最佳实践和指南可用,但开发人员有很多日常任务要处理,通常没有时间去探索和学习他们正在使用的编程语言的新功能和最佳实践(尽管他们实际上需要这样做)。
考虑到这一点
1. 您必须知道如何在 Java 中创建不可变对象,并有兴趣这样做,这不像一个关键字那么简单
- 类的所有字段都必须是
final
。 - 除了构造函数外,类不能有任何修改方法。
- 类必须是
final
,因此不能被覆盖。 - 这意味着没有默认的无参数构造函数。
- 这意味着您应该有一个初始化所有字段的构造函数。
- 这意味着您可能需要辅助实体(可能是 DTO),它们也应该是不可变的。
从 Java 开发人员的角度来看,这已经看起来很麻烦了,特别是当您意识到最终您的类将是茫茫可变对象中的一个孤立的不可变对象时。
2. 最小化生产时间始终是软件开发中的首要任务,正如您所见,在 Java 中采用不可变性需要时间和精力;
3. 随着机器成本的不断降低,企业软件的规模可以不断扩大,因此在现有项目中付出这种努力的成本可能高于一两台额外的服务器。
您会发现,大多数高级开发人员都不 bother 于不可变性,而初级开发人员则不关心,只是复制代码项目已有的内容。
Scala 的方法不同:使用不可变对象,直到您明确需要某个东西是可变的。虽然从技术上讲,您仍然应该显式声明对象的可变性/不可变性,但作为函数式编程语言,Scala 在技术上可行的地方默认使用不可变对象,并始终尝试将程序员的思维方式引导至此,甚至通过编译器警告(“嘿,这个东西可以是不可变的!”)。
好的,但这个 Scala 方法将如何帮助现有的 Java 项目?答案是:很简单。
Scala 具有完全的 Java 互操作性,因此您可以编写一个用 Scala 编写的组件、模块或单个类,并从您的 Java 代码中使用它,同样您也可以从 Scala 代码调用任何 Java 类。
因此,使用 Scala,您可以获得不可变性的所有好处,而无需将其集成到传统命令式编程语言(其中可变性是默认值)中的弊端,并且可以在您现有的项目中实现。
这有多酷?
5. 开箱即用的安全简便的并发
Java 8 标准库仅提供传统的线程模型并发。
通过这种模型,程序的执行被分成并发运行的任务。
这类似于在共享内存池中运行同一程序的多个副本。
线程和传统并发方法的问题是众所周知的
- 线程创建本身就是一项相当繁重的操作。
- 线程之间的通信是一件令人头疼的事。
- 总的来说,由于线程带来的问题(尤其是在处理可变类型时),开发人员很难从线程中获益。并发修改、线程饥饿、死锁,等等。
与线程模型相对的是更简单、更高效的 actor 模型,Scala 标准库对此提供了支持。
Actor 模型采用了不同的并发方法,应避免由线程和锁引起的问题。顾名思义,该模型将每个对象定义为一个 actor(一个实体),该实体具有一个*邮箱*和一个*行为*。
这会处理
- 线程通信。消息可以通过它们的邮箱轻松地在 actors 之间交换。
- 线程控制。对于每条消息,都可以定义自定义行为。这包括将消息发送给其他 actors、创建 actors 并为下一条接收到的消息采用新行为;
- 由于所有通信都通过消息进行,因此 actors 之间没有共享状态,因此没有与之相关的问题。
Scala 还通过其不可变性方法在一定程度上消除了锁定带来的麻烦。
Scala 标准库开箱即用地支持 actor 模型。它们可通过 scala.actors
库获得。
Actors 的实现是 Scala 表现力的绝佳证明:包括的所有功能、运算符和其他语言构造都是用纯 Scala 实现的,作为库,而无需更改 Scala 本身。让我们仔细看看。
Scala 区分了基于线程和基于事件的 actors。
基于线程的 actors 每个都运行在自己的 JVM 线程中。它们由 Java 线程调度器调度,Java 线程调度器使用抢占式优先级调度器。
当 actor 进入接收(receive)块时,线程将被阻塞,直到消息到达。基于线程的 actors 使得可以在 actors 中执行长时间运行的计算或阻塞 I/O 操作,而不会妨碍其他 actors 的执行。
基于事件的 actors 摆脱了另一个传统的线程问题——巨大的线程开销,这极大地限制了可以创建的线程数量,从而从根本上减少了多线程的好处。
那么,基于事件的 actors 究竟有何不同?
基于事件的 actors 不是通过每个 actor 一个线程来实现的,而是它们运行在同一个线程上。等待接收消息的 actor 不是由阻塞的线程表示,而是由一个闭包表示。这个闭包捕获了 actor 的状态,这样在接收到消息后就可以继续其计算。这个闭包的执行发生在发送者的线程上。
这样,基于事件的 actors 就提供了一种比基于线程的 actors 更轻量级的替代方案,允许大量并发运行的 actors(例如,在家用机器上运行 100,000 个 actors)。然而,基于事件的 actors 不适合并行处理:由于所有 actors 都在同一个线程上执行,因此不存在调度公平性。
要在 Scala 中使用基于事件的 actor,您应该使用 “react
” 块而不是 “receive
” 块。
使用基于事件的 actors 还有一个很大的限制:进入 “react
” 块后,控制流永远无法返回到封闭的 actor。
Scala 允许程序员在同一个程序中混合使用基于线程的 actors 和基于事件的 actors,因此您可以根据需要使用可扩展、轻量级的基于事件的 actors,或支持并行的基于线程的 actors。
要将某物变成 Scala 中的 actor,您只需扩展 Actor
trait 并实现 receive
或 react
块
```
class SampleActor extends Actor with ActorLogging {
def receive = {
...
}
}
```
创建新的 actor
也非常简单
```
//first we must init an actor system
val sampleActorSystem = ActorSystem("SampleActorSystem")
//now, create an actor
val sampleActor = system.actorOf(Props[SampleActor], name = "sampleActor")
```
发送消息也易如反掌
```
//Structure of SampleMessage class
//case class SampleMessage(msg: String)
sampleActor ! SampleMessage("It's a test message")
```
定义消息的行为也非常简单
```
class SampleActor extends Actor with ActorLogging {
def receive = {
case SampleMessage(msg) = log.info(f"Got sample message: $msg")
}
}
```
如果出于某种原因,您想使用传统的 Java 并发,您也可以使用它。
采用这种简单性和灵活性,加上默认使用不可变类型的优势,您就会发现 Scala 的并发模型远比 Java 8 优越。