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





5.00/5 (2投票s)
在本文的前一部分,我们讨论了 Scala 和 Java 之间主要的区别和相似之处。在这里,我们将继续我们的概述。
如果您错过了本文的第一部分——欢迎阅读关于前五个区别的介绍,请在此处 阅读。
6. Scala 中的 Trait 与 Java 中的虚拟扩展方法。更多行为!
Scala 提供了一个出色的机制来扩展您的类并灵活地为其添加新行为。
有趣的是,类既获得了与混合的 trait 相同的类型,以及 trait 中的所有方法和状态(因此 trait 通常被称为 mixin,因为它们将新行为和状态混合到类中)。
技术上讲,Scala 中的 trait 提供了一个接口,并可选择包含实现,而一个类可以“混合”多个 trait。
请注意,虽然您可以混合任意数量的 trait,但 Scala 与 Java 类似,没有多重继承。在 Java 和 Scala 中,子类只能继承一个超类。但 trait 不同。
好处是 Scala 定义了一套清晰的优先级规则,这些规则独立于混合的 trait 数量,来确定在多重继承层次结构中何时以及执行什么。
这些规则为我们提供了多重继承的所有好处,而没有与之相关的任何问题。
那么,Scala 中的 trait 有什么好处呢?它们能够将类组合成这些 trait,并将 trait 作为构建块。
一如既往,让我们看一个例子。这是 Java 中传统的日志记录例程的设置方式。
```
class SomeClass {
//First, to have logging for a class, you must initialize it
final static Logger log = LoggerFactory.getLogger(this.getClass());
...
//For logging to be efficient, you must always check, if logging level for current message is enabled
//BAD, you will waste execution time if log level is error, fatal, etc.
log.debug("Some debug message");
...
//GOOD, it saves execution time for something more useful
if (log.isDebugEnabled()) { log.debug("Some debug message"); }
//BUT looks clunky, and it's tiresome to write this construct every time you want to log something.
}
```
每次都检查日志级别是否启用非常麻烦。
如果能将此例程编写一次并在任何地方、任何类中重用,那将是很好的,对吧?
Scala 中的 trait 使这一切成为可能。
```
trait Logging {
lazy val log = LoggerFactory.getLogger(this.getClass.getName)
//Let's start with info level...
...
//Debug level here...
def debug(msg: => Any) {
if (log.isDebugEnabled) log.info(s"${msg}")
}
def debug(msg: => Any, throwable: => Throwable) {
if (log.isDebugEnabled) log.info(s"${msg}", throwable)
}
...
//Repeat it for all log levels you want to use
}
```
现在我们以一种方便的风格获得了高效的日志记录例程,作为一个可重用块。要为任何类启用日志记录,我们只需混合我们的 Logging trait!
```
//Now that's all it takes to add logging feature to your class
class SomeClass extends Logging {
...
//With logging trait, no need for declaring a logger manually for every class
//And now, your logging rouitine is either efficient and doesn't litter the code!
log.debug("Some debug message")
...
}
```
借助 Scala 中的 trait,您可以以模块化的方式隔离通用功能。您可以将隔离的功能插入到您需要的任何类中。Trait 是可重用的。想象一下无限的可能性!
现在,Java 8 引入了虚拟扩展方法(也称为默认方法),但 VEMs 和 trait 的动机不同。
在 Scala 中,trait 始终被设计为模块化构建块,而 Java 中的 VEMs 主要用于实现 API 演进,使用 VEMs 来创建构建块虽然很方便,但仅限于行为方面的副作用。
那么,到底有什么限制呢?让我们将我们的 Logging trait 映射到 Java 8。
```
interface Logging {
//See? Here's our problem. We can't get a reference to the implementing class.
final static Logger log = LoggerFactory.getLogger(Loggable.class);
//Let's start with info level...
...
//Debug level here...
void debug(String msg) default {
if(log.isDebugEnabled()) log.debug(msg);
}
void debug(String msg, Throwable throwable) default {
if(log.isDebugEnabled()) log.debug(msg, throwable);
}
...
//Repeat it for all log levels you want to use
}
```
正如您所见,由于机制的限制,我们必须使用Loggable接口作为日志记录器,它会记录Loggable下的所有语句,而不是实现类,这极大地减少了您应用程序中的日志控制。一点用处都没有。
好的,但 Java 8 中的 VEMs 的目的是什么?
主要原因是为了向后兼容。例如,对于许多现有接口来说,以高阶函数的 lambda 表达式形式提供支持将非常有益。
您是否期望 Java 8 的java.util.Collection接口提供一个forEach(lambdaExpr)方法,对吧?
如果此类方法在没有默认实现的情况下添加到接口中,所有实现类都必须提供一个,这直接导致兼容性问题。
但是,通过 VEMs,例如.forEach方法可以添加到java.util.Collection中,并包含一个默认实现。因此,所有实现类将自动继承该方法及其实现。没有兼容性问题,每个人都很高兴。如果实现类对默认实现不满意,它可以简单地覆盖它。
可以看出,Java 8 中的 VEMs 是一种技术必需,最初并不是为了让开发人员使用而设计的,而 Scala 中的 trait 正是为了这一点而引入的,让您的软件更轻便、更好。
这就是为什么 Scala 的 trait 在 Java 8 中优于虚拟扩展方法。
7. 类型增强。丰富我的库!
在上一节中,我们讨论了 Scala 中的 trait 和 Java 8 中的虚拟扩展方法(或默认方法)。我们侧重于类组合以及使用可重用块增强类的行为。
我们还涵盖了 Java 8 中的虚拟扩展方法本应处理的问题。
非侵入性地扩展类行为,这在向后兼容性方面非常重要,尤其是在处理语言的标准库类(如集合)时。
然而,我们没有涵盖 Scala 如何处理这种情况。借助隐式类,您可以通过单个导入来定义和为任何类带来新行为。
让我们来看看
```
//For example, we want to bring new functionality to Int class, because why not?
object IntExtensions {
implicit class IntPredicates(i: Int) {
def isEven = i % 2 == 0
def isOdd = !isEven
}
}
import IntExtensions._ // Bring implicit enrichment into scope.
4.isEven // Yay, we now have new functionality available!
```
这种方法在标准库中最显著的用法之一是桥接 Java 和 Scala 集合,通过为标准集合添加.asScala()和.asJava()方法,这在从 Java 代码调用 Scala 类或从 Scala 代码调用 Java 类时非常方便。
您可以为任何您想要的类添加任何您想要的功能。
由于 Java 8 未能掌握隐式对象这一概念,因此您无法如此灵活。
8. 在语言层面支持真正重要的设计模式,无需旧的模式!
您可以看到许多比较 Java 和 Scala 的文章,其中包含“Scala 中的工厂模式”、“Scala 中的命令模式”等部分。
确实,许多 Java 开发人员对 Scala 的做法感到好奇,并且经常关注设计模式的实现,因为这是他们每天都在使用的。
但正如我们在讨论函数式范式时提到的——这些模式只是命令式编程中不断试错的结果,其中许多已被证明是错误甚至是harmful的,一些过去的必备模式现在被认为是反模式。
那么,您真的需要在 Scala 中使用传统的面向对象设计模式吗?
答案是:否。命令式语言的设计模式通常只是为了克服编程语言的技术限制。
它们有缺点,并且由于这是完成工作的唯一可支持方式,这些缺点常常被忽视。
通过函数式编程,语言的技术限制要弱得多,而函数式方法允许您以几乎任何您想要的方式组合您的代码和应用程序。
所以,您绝对不应该被旧的挣扎中产生的东西所束缚。
例如,您不需要 Scala 中的工厂模式,它不像 Java 中那样必要。
借助 Scala 的 Monad、高阶函数、高级模式匹配、类型推断和隐式对象,几乎不存在需要显式查找(和显式构造)处理程序的情况,这些处理程序在技术上有所不同,但在应用程序业务逻辑方面却非常相似。
但对于简单而常见的设计模式,更多由常识而非技术决定的模式呢?例如单例?
单例是面向对象编程中最流行的设计模式。
单例和 Java 8 的主要问题是您仍然必须每次自己实现它。
而且这总是可能出错。
您不应该这样做,而 Scala 很好地实现了这一点。如果您想要 Scala 中的单例——您已经拥有了!它已集成到语言本身中!
只需将您的类声明为 object
```
object MySingleToneObject {
//That's all, here is your singleton!
}
```
至此,我们来到了 Scala 的一个重要且独特的特性,称为“最小代码库”。下一节再见!
9. 最小代码库。无需“IDE 呕吐”。
这是 Scala 的一个众所周知的特性,但 Java 仍未采纳。最臭名昭著的例子是声明一个类。在 Java 中它看起来像什么?
```
public class Person {
private final Integer id;
private String name;
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {..}
public String getName() {..}
public void setName(String newName) {..}
@Override
public boolean equals(Object obj) {..}
@Override
public int hashCode() {..}
}
```
这就是为什么您不像使用 Scala 那样在交互式控制台中摆弄 Java。
哦,您可以说“等等,但我的 IDE 会为我做这些,它为一切都提供了生成器!”。
但如果 IDE 为您生成了所有这些代码,并且如果这是日常例程,那么您或您的 IDE 为什么还要这样做呢?
为什么不让声明更短,为什么强迫开发人员使用庞大的 IDE?
有了如此多的代码开销,您几乎无法在没有样板代码、IDE 及其代码生成器的情况下完成任何事情。这扼杀了所有的乐趣,您不想尝试和探索新事物。
到您描述完所有内容时(特别是对于初始实现),您已经感到疲倦,只想完成任务。
是什么阻止了 Java 8 像 Scala 那样使其更简单?
在 Scala 中声明同一个类的方法如下。
```
case class Person(@BeanProperty val id: Int, @BeanProperty var name: String)
//@BeanProperty annotation makes compiler generate getters and setters java-style, like getId(), setName(..), etc. instead of default getter-setters.
```
就这样,编译器会处理其他所有事情。现在是 21 世纪了,声明一个类不应该是一项工作,它是一件微不足道的事情,而 Scala 很好地实现了这一点。
变量的初始化也是一件微不足道的事情,但在 Java 中仍然是这样。
```
public final String myCoolString = "My cool final string";
```
因为 Java 的运行时类型推断不包括如此微不足道的事情。
在 Scala 中,初始化变量所需的一切就是写
```
val myCoolString = "My cool final string"
```
说到字符串,您在 Java 中仍然使用糟糕的格式化程序。
```
log.debug(String.format("This is %s, %s = %d", SomeClass.someMethod(), someVariable, someStatusCode));
//It looks especially bad when you need to pass a string elsewhere, like to logger.
```
而 Scala 提供了强大的插值功能。
```
log.debug(s"This is ${SomeClass.someMethod}, $someVariable = $someStatusCode")
```
看起来更简洁,信息量也更大。
如果您确实想格式化字符串,可以这样做。
```
"This is %s, %s = %d".format(SomeClass.someMethod, someVariable, someStatusCode)
```
此外,与 Java 不同,Scala 支持多行字符串字面量,这对于存储类似 SQL 语句的内容特别方便。
```
"""
SELECT column1,
column2
FROM table1
WHERE column1 > 0;
"""
```
这并非“语法糖”,而是对开发和实现过程的实际思考。即使在 Java 8 中,您仍然必须使用变通方法来在声明时初始化列表、集合或映射。为什么?
Scala 确保您不会在这些琐碎的日常事务中遇到问题。
10. 代码和运行时兼容性
这是 Scala 的一个众所周知的特性,但仍然非常重要。将您旧 Java 项目中的任何类用 Scala 编写,逐类迁移。并在相同的旧运行时中无缝运行。
有了这个,您就不会错过 Java 的任何性能改进,因为随着 JRE 的更新,Scala 也能获得新 Java 版本的所有性能优化!
任何 Java 库都可以在 Scala 代码中使用。
```
val httpPost = new HttpPost("http://targethost/login")
val response = httpclient.execute(httpPost)
```
反之亦然。Scala 甚至提供了一些辅助工具来构建桥梁并实现代码兼容性。
- 像@BeanProperty这样的注解,这样从您的 Scala 类获取东西在 Java 代码中就不会显得奇怪;
- 像JavaConversions和JavaConverters这样的辅助对象,用于在 Java 和 Scala 集合之间建立桥梁,因此如果您需要从 Java 代码使用 Scala 集合,您就不必在 Scala 代码中导入并处理 Java 集合;
JavaConversions提供了一系列隐式方法,可以在 Java 集合和最接近的相应 Scala 集合之间进行转换,反之亦然。
JavaConverters利用类型增强来“添加”Java 集合的.asScala方法和 Scala 集合的.asJava方法,这些方法返回上述适当的包装器。
如果您使用某些第三方 Scala 库,您也可以从 Java 代码中调用这些转换器。
漂亮而优雅。
结论
我们希望在本文中展示 Scala 不仅仅是一套针对 Java 的语法糖功能,而确实远不止于此。尽管 Java 8 在某些方面试图变得更像 Scala,但 Scala 仍然通过其函数式范式代表了另一种、更好的软件开发方法。
立即仔细看看 Scala 这里! (链接至 )