本章内容
什么是Scala语言
Java 8与Scala是如何相生相承的
Scala中的函数与Java 8中的函数有哪些区别
类和trait
Scala是一种混合了面向对象和函数式编程的语言。它常常被看作Java的一种替代语言,程序员们希望在运行于JVM上的静态类型语言中使用函数式特性,同时又期望保持Java体验的一致性。和Java比较起来,Scala提供了更多的特性,包括更复杂的类型系统、类型推断、模式匹配(我们在14.4节提到过)、定义域语言的结构等。除此之外,你可以在Scala代码中直接使用任何一个Java类库。
你可能会有这样的疑惑,我们为什么要在一本介绍Java 8的书里特别设计一章讨论Scala。本书的绝大部分内容都在介绍如何在Java中应用函数式编程。Scala和Java 8极其类似,它们都支持对集合的函数式处理(类似于对Stream的操作)、一等函数、默认方法。不过Scala将这些思想向前又推进了一大步:它为实现这些思想提供了大量的特性,这方面它领先了Java 8一大截。我们相信你会发现,对比Scala和Java 8在实现方式上的不同以及了解Java 8目前的局限是非常有趣的。通过这一章,我们希望能针对这些问题为你提供一些线索,解答一些疑惑。
请记住,本章的目的并非让你掌握如何编写纯粹的Scala代码,或者了解Scala的方方面面。不少的特性,比如模式匹配,在Scala中是天然支持的,也非常容易理解,不过这些特性在Java 8中却并未提供,这部分内容我们在这里不会涉及。本章着重对比Java 8中新引入的特性和该特性在Scala中的实现,帮助你更全面地理解该特性。比如,你会发现,用Scala重新实现原先用Java完成的代码更简单,可读性也更好。
本章从对Scala的介绍入手:让你了解如何使用Scala编写简单的程序,以及如何处理集合。紧接着我们会讨论Scala中的函数式,包括一等函数、闭包以及科里化。最后,我们会一起看看Scala中的类,以及一种名为trait的特性,它是Scala中带默认方法的接口。
15.1 Scala简介
本节会简要地介绍Scala的一些基本特性,让你有一个比较直观的感受:到底简单的Scala程序怎么编写。我们从一个略微改动的Hello World示例入手,该程序会以两种方式编写,一种以命令式的风格编写,另一种以函数式的风格编写。接着,我们会看看Scala支持哪些数据结构——List
、Set
、Map
、Stream
、Tuple
以及Option
——并将它们与Java 8中对应的数据结构一一进行比较。最后,我们会介绍trait,它是Scala中接口的替代品,支持在对象实例化时对方法进行继承。
15.1.1 你好,啤酒
让我们看一个简单的例子,这样你能对Scala的语法、语言特性,以及它与Java的差异有一个比较直观的认识。我们对经典的Hello World示例进行了微调,让我们来点儿啤酒。你希望在屏幕上打印输出下面这些内容:
Hello 2 bottles of beerHello 3 bottles of beerHello 4 bottles of beerHello 5 bottles of beerHello 6 bottles of beer
1. 命令式Scala
下面这段代码中,Scala以命令式的风格打印输出这段内容:
object Beer { def main(args: Array[String]){ var n : Int = 2 while( n <= 6 ){ println(s"Hello ${n} bottles of beer") ←─在字符串中插值 n += 1 } }}
如何运行这段代码的指导信息可以在Scala的官方网站找到1 。这段代码看起来和你用Java编写的程序相当类似。它的结构和Java程序几乎一样:它包含了一个名为main
的方法,该方法接受一个由参数构成的数组(类型注释遵循这样的语法s : String
,不像Java那样用String s
)。由于main
方法不返回值,所以使用Scala不需要像Java那样声明一个类型为void
的返回值。
1参见http://www.scala-lang.org/documentation/getting-started.html。
注意 通常而言,在Scala中声明非递归的方法时,不需要显式地返回类型,因为Scala会自动地替你推断生成一个。
转入main
的方法体之前,我们想先讨论下对象的声明。不管怎样,Java中的main
方法都需要在某个类中声明。对象的声明产生了一个单例的对象:它声明了一个对象,比如Bear
,与此同时又对其进行了实例化。整个过程中只有一个实例被创建。这是第一个以经典的设计模式(即单例模式)实现语言特性的例子——尽量不拘一格地使用它!此外,你可以将对象声明中的方法看成静态的,这也是main
方法的方法签名中并未显式地声明为静态的原因。
现在让我们看看main
的方法体。它看起来和Java非常类似,但是语句不需要再以分号结尾了(它成了一种可选项)。方法体中包含了一个while
循环,它会递增一个可修改变量n
。通过预定义的方法println
,你可以打印输出n
的每一个新值。println
这一行还展示了Scala的另一个特性:字符串插值。字符串插值在字符串的字面量中内嵌变量和表达式。前面的这段代码中,你在字符串字面量s"Hello ${n} bottles of beer"
中直接使用了变量n
。字符串前附加的插值操作符s
,神奇地完成了这一转变。而在Java中,你通常需要使用显式的连接操作,比如"Hello " + n + " bottles of beer"
,才能达到同样的效果。
2. 函数式Scala
那么,Scala到底能带来哪些好处呢?毕竟我们在本书里主要讨论的还是函数式。前面的这段代码利用Java 8的新特性能以更加函数式的方式实现,如下所示:
public class Foo { public static void main(String args) { IntStream.rangeClosed(2, 6) .forEach(n -> System.out.println("Hello " + n + " bottles of beer")); }}
如果以Scala来实现,它是下面这样的:
object Beer { def main(args: Array[String]){ 2 to 6 foreach { n => println(s"Hello ${n} bottles of beer") } }}
这种实现看起来和基于Java的版本有几分相似,不过Scala的实现更加简洁。首先,你使用表达式2 to 6
创建了一个区间。这看起来相当特别: 2
在这里并非原始数据类型,在Scala中它是一个类型为Int
的对象。Scala语言里,任何事物都是对象;不像Java那样,Scala没有原始数据类型一说了。通过这种方式,Scala被转变成为了纯粹的面向对象语言。Scala语言中Int
对象支持名为to
的方法,它接受另一个Int
对象,返回一个区间。所以,你还可以通过另一种方式实现这一语句,即2.to(6)
。由于接受一个参数的方法可以采用中缀式表达,所以你可以用开头的方式实现这一语句。紧接着,我们看到了foreach
(这里的e
采用的是小写),它和Java 8中的forEach
(使用了大写的E
)也很类似。它是对一个区间进行操作的函数(这里你可以再次使用中缀表达式),它可以接受Lambda表达式做参数,对区间的每一个元素顺次执行操作。这里Lambda表达式的语法和Java 8也非常类似,区别是箭头的表示用=>
替换了->
2。前面的这段代码是函数式的:因为就像我们早期使用while
循环时示例的那样,你并未修改任何变量。
2注意,在Scala语言中,我们使用“匿名函数”或者“闭包”(可以互相替换)来指代Java 8中的Lambda表达式。
15.1.2 基础数据结构:List
、Set
、Map
、Tuple
、Stream
以及Option
几杯啤酒之后,你一定已经止住口渴,精神一振了吧?大多数的程序都需要操纵和存储数据,那么,就让我们一起看看如何在Scala中操作集合,以及它与Java 8中操作的不同。
1. 创建集合
在Scala中创建集合是非常简单的,这主要归功于它对简洁性的一贯坚持。比如,创建一个Map
,你可以用下面的方式:
val authorsToAge = Map("Raoul" -> 23, "Mario" -> 40, "Alan" -> 53)
这行代码中,有几件事情是我们首次碰到的。首先,你使用->
语法轻而易举地创建了一个Map
,并完成了键到值的映射,整个过程令人吃惊地简单。你不再需要像Java中那样手工添加每一个元素:
Map<String, Integer> authorsToAge = new HashMap<>;authorsToAge.put("Raoul", 23);authorsToAge.put("Mario", 40);authorsToAge.put("Alan", 53);
关于这一点,也有一些讨论,希望在未来的Java版本中添加类似的语法糖,不过在Java 83中暂时还没有这样的特性。第二件让人耳目一新的事是你可以选择不对变量authorsToAge
的类型进行注解。实际上,你可以编写val authorsToAge : Map[String, Int]
这样的代码,显式地声明变量类型,不过Scala可以替你推断变量的类型(请注意,即便如此,代码依旧是静态检查的!所有的变量在编译时都具有确定的类型)。我们会在本章后续部分继续讨论这一特性。第三,你可以使用val
关键字替换var
。这二者之间存在什么差别吗?关键字val
表明变量是只读的,并由此不能被赋值(就像Java中声明为final
的变量一样)。而关键字var
表明变量是可以读写的。
3参见http://openjdk.java.net/jeps/186。
听起来不错,那么其他的集合类型呢?你可以用同样的方式轻松地创建List
(一种单向链表)或者Set
(不带冗余数据的集合),如下所示:
val authors = List("Raoul", "Mario", "Alan")val numbers = Set(1, 1, 2, 3, 5, 8)
这里的变量authors
包含3个元素,而变量numbers
包含5个元素。
2. 不可变与可变的比较
Scala的集合有一个重要的特质我们应该牢记在心,那就是我们之前创建的集合在默认情况下都是只读的。这意味着它们从创建开始就不能修改。这是一种非常有用的特性,因为有了它,你知道任何时候访问程序中的集合都会返回包含相同元素的集合。
那么,你怎样才能更新Scala语言中不可变的集合呢?回到前面章节介绍的术语,Scala中的这些集合都是持久化的:更新一个Scala集合会生成一个新的集合,这个新的集合和之前版本的集合共享大部分的内容,最终的结果是数据尽可能地实现了持久化,避免了图14-3和图14-4中那样由于改变所引起的问题。由于具备这一属性,你代码的隐式数据依赖更少: 对你代码中集合变更的困惑(比如在何处更新了集合,什么时候做的更新)也会更少。
让我们看一个实际的例子,具体分析下这一思想是如何影响你的程序设计的。下面这段代码中,我们会为Set
添加一个元素:
val numbers = Set(2, 5, 3);val newNumbers = numbers + 8 ←─这里的操作符+会将8添加到Set中,创建并返回一个新的Set对象println(newNumbers) ←─(2, 5, 3, 8)println(numbers) ←─(2, 5, 3)
这个例子中,原始Set
对象中的数字没有发生变更。实际的效果是该操作创建了一个新的Set
,并向其中加入了一个新的元素。
注意,Scala语言并未强制你必须使用不可变集合,它只是让你能更轻松地在你的代码中应用不可变原则。scala.collection.mutable
包中也包含了集合的可变版本。
不可修改与不可变的比较
Java中提供了多种方法创建不可修改的(unmodifiable)集合。下面的代码中,变量
newNumbers
是集合Set
对象numbers
的一个只读视图:Set<Integer> numbers = new HashSet<>;Set<Integer> newNumbers = Collections.unmodifiableSet(numbers);
这意味着你无法通过操作变量
newNumbers
向其中加入新的元素。不过,不可修改集合仅仅是对可变集合进行了一层封装。通过直接访问numbers
变量,你还是能向其中加入元素。与此相反,不可变(immutable)集合确保了该集合在任何时候都不会发生变化,无论有多少个变量同时指向它。
我们在第14章介绍过如何创建一个持久化的数据结构:你需要创建一个不可变数据结构,该数据结构会保存它自身修改之前的版本。任何的修改都会创建一个更新的数据结构。
3. 使用集合
现在你已经了解了如何创建结合,你还需要了解如何使用这些集合开展工作。我们很快会看到Scala支持的集合操作和Stream API提供的操作极其类似。比如,在下面的代码片段中,你会发现熟悉的filter
和map
,图15-1对这段代码逻辑进行了阐释。
val fileLines = Source.fromFile("data.txt").getLines.toListval linesLongUpper = fileLines.filter(l => l.length > 10) .map(l => l.toUpperCase)
图 15-1 使用Scala的List
实现类Stream
操作
不用担心第一行的内容,它实现的基本功能是将文件中的所有行转换为一个字符串列表(类似Java 8提供的Files.readAllLines
)。第二行创建了一个由两个操作构成的流水线:
filter
操作会过滤出所有长度超过10的行map
操作会将这些长的字符串统一转换为大写字符
这段代码也可以用下面的方式实现:
val linesLongUpper = fileLines filter (_.length > 10) map(_.toUpperCase)
这段代码使用了中缀表达式和下划线(_
),下划线是一种占位符,它按照位置匹配对应的参数。这个例子中,你可以将_.length
解读为l =>l.length
。在传递给filter
和map
的函数中,下划线会被绑定到待处理的line
参数。
Scala的集合API提供了很多非常有用的操作。我们强烈建议你抽空浏览一下Scala的文档,对这些API有一个大致的了解4。注意,Scala的集合类提供的功能比Stream API提供的功能还丰富很多,比如,Scala的集合类支持压缩操作,你可以将两个列表中的元素整合到一个列表中。通过学习,一定能大大增强你的功力。这些编程技巧在将来的Java版本中也可能会被Stream API所引入。
4www.scala-lang.org/api/current/#package中既包含了著名的包,也包含一些不那么有名的包的介绍。
最后,还记得吗?Java 8中你可以对Stream
调用parallel
方法,将流水线转化为并行执行。Scala提供了类似的技巧;你只需要使用方法par
就能实现同样的效果:
val linesLongUpper = fileLines.par filter (_.length > 10) map(_.toUpperCase)
4. 元组
现在,让我们看看另一个特性,该特性使用起来通常异常繁琐,它就是元组。你可能希望使用元组将人的名字和电话号码组合起来,同时又不希望额外声明新的类,并对其进行实例化。你希望元组的结构就像:(“Raoul”,“+ 44 007007007”)、 (“Alan”,“+44 003133700”),诸如此类。
非常不幸,Java目前还不支持元组,所以你只能创建自己的数据结构。下面是一个简单的Pair
类定义:
public class Pair<X, Y> { public final X x; public final Y y; public Pair(X x, Y y){ this.x = x; this.y = y; }}
当然,你还需要显式地实例化Pair
对象:
Pair<String, String> raoul = new Pair<>("Raoul", "+ 44 007007007");Pair<String, String> alan = new Pair<>("Alan", "+44 003133700");
好了,看起来一切顺利,不过如果是三元组呢?如果是自定义大小的元组呢?这个问题就变得相当繁琐,最终会影响你代码的可读性和可维护性。
Scala提供了名为元组字面量的特性来解决这一问题,这意味着你可以通过简单的语法糖创建元组,就像普通的数学符号那样:
val raoul = ("Raoul", "+ 44 887007007")val alan = ("Alan", "+44 883133700")
Scala支持任意大小5的元组,所以下面的这些声明都是合法的:
5元组中元素的最大上限为23。
val book = (2014, "Java 8 in Action", "Manning") ←─元组类型为(Int, String, String)val numbers = (42, 1337, 0, 3, 14) ←─元组类型为(Int, Int, Int, Int, Int)
你可以依据它们的位置,通过存取器(accessor) _1
、_2
(从1开始的一个序列)访问元组中的元素,比如:
println(book._1) ←─打印输出2014println(numbers._4) ←─打印输出3
是不是比Java语言中现有的实现方法简单很多?好消息是关于将元组字面量引入到未来Java版本的讨论正在进行中(我们会在第16章围绕这一主题进行更深入的讨论)。
5. Stream
到目前为止,我们讨论的集合,包括List
、Set
、Map
和Tuple
都是即时计算的(即在第一时间立刻进行计算)。当然,你也已经了解Java 8中的Stream是按需计算的(即延迟计算)。通过第5章,你知道由于这一特性,Stream可以表示无限的序列,同时又不消耗太多的内存。
Scala也提供了对应的数据结构,它采用延迟方式计算数据结构,名称也叫Stream
!不过Scala中的Stream
提供了更加丰富的功能,让Java中的Stream有些黯然失色。Scala中的Stream
可以记录它曾经计算出的值,所以之前的元素可以随时进行访问。除此之外,Stream
还进行了索引,所以Stream
中的元素可以像List
那样通过索引访问。注意,这种抉择也附带着开销,由于需要存储这些额外的属性,和Java 8中的Stream比起来,Scala版本的Stream
内存的使用效率变低了,因为Scala中的Stream
需要能够回溯之前的元素,这意味着之前访问过的元素都需要在内存“记录下来”(即进行缓存)。
6. Option
另一个你熟悉的数据结构是Option
。我们在第10章中讨论过Java的Optional
,Option
是Java 8中Optional
类型的Scala版本。我们建议你在设计API时尽可能地使用Optional
,这种方式下,接口用户只需要阅读方法签名就能了解他们是否应该传递一个optional
值。我们应该尽量地用它替代null
,避免发生空指针异常。
第10章中,你了解了我们可以使用Optional
返回客户的保险公司名称——如果客户的年龄超过设置的最低值,就返回该客户对应的保险公司名称,具体代码如下:
public String getCarInsuranceName(Optional<Person> person, int minAge) { return person.filter(p -> p.getAge >= minAge) .flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown");}
在Scala语言中,你可以使用Option
使用Optional
类似的方法实现该函数:
def getCarInsuranceName(person: Option[Person], minAge: Int) = person.filter(_.getAge >= minAge) .flatMap(_.getCar) .flatMap(_.getInsurance) .map(_.getName).getOrElse("Unknown")
这段代码中除了getOrElse
方法,其他的结构和方法你一定都非常熟悉,getOrElse
是与Java 8中orElse
等价的方法。你看到了吗?在本书中学习的新概念能直接应用于其他语言!然而,不幸的是,为了保持同Java的兼容性,在Scala中依旧保持了null
,不过我们极度不推荐你使用它。
注意 在前面的代码中,你使用的是
_.getCar
(并未使用圆括号),而不是_.getCar
(带圆括号)。Scala语言中,执行方法调用时,如果不需要传递参数,那么函数的圆括号是可以省略的。
15.2 函数
Scala中的函数可以看成为了完成某个任务而组合在一起的指令序列。它们对于抽象行为非常有帮助,是函数式编程的基石。
对于Java语言中的方法,你已经非常熟悉了:它们是与类相关的函数。你也已经了解了Lambda表达式,它可以看成一种匿名函数。跟Java比较起来,Scala为函数提供的特性要丰富得多,我们在这一节中会逐一讲解。Scala提供了下面这些特性。
函数类型,它是一种语法糖,体现了Java语言中函数描述符的思想,即,它是一种符号,表示了在函数接口中声明的抽象方法的签名。这些内容我们在第3章中都介绍过。
能够读写非本地变量的匿名函数,而Java中的Lambda表达式无法对非本地变量进行写操作。
对科里化的支持,这意味着你可以将一个接受多个参数的函数拆分成一系列接受部分参数的函数。
15.2.1 Scala中的一等函数
函数在Scala语言中是一等值。这意味着它们可以像其他的值,比如Integer
或者String
那样,作为参数传递,可以作为结果值返回。正如我们在前面章节所介绍的那样,Java 8中的方法引用和Lambda表达式也可以看成一等函数。
让我们看一个例子,看看Scala中的一等函数是如何工作的。我们假设你现在有一个字符串列表,列表中的值是朋友们发送给你的消息(tweet)。你希望依据不同的筛选条件对该列表进行过滤,比如,你可能想要找出所有提及Java这个词或者短于某个长度的消息。你可以使用谓词(返回一个布尔型结果的函数)定义这两个筛选条件,代码如下:
def isJavaMentioned(tweet: String) : Boolean = tweet.contains("Java")def isShortTweet(tweet: String) : Boolean = tweet.length < 20
Scala语言中,你可以直接传递这两个方法给内嵌的filter
,如下所示(这和你在Java中使用方法引用将它们传递给某个函数大同小异):
val tweets = List( "I love the new features in Java 8", "How's it going?", "An SQL query walks into a bar, sees two tables and says 'Can I join you?'")tweets.filter(isJavaMentioned).foreach(println)tweets.filter(isShortTweet).foreach(println)
现在,让我们一起审视下内嵌方法filter
的函数签名:
def filter[T](p: (T) => Boolean): List[T]
你可能会疑惑参数p
到底代表的是什么类型(即(T) => Boolean
),因为在Java语言里你期望看到的是一个函数接口!这其实是一种新的语法,Java中暂时还不支持。它描述的是一个函数类型。这里它表示的是这样一个函数,它接受类型为T
的对象,返回一个布尔类型的值。Java语言中,它被编码为Predicate<T>
或者Function<T, Boolean>
。所以它实际上和isJavaMentioned
和isShortTweet
具有类似的函数签名,所以你可以将它们作为参数传递给filter
方法。Java 8语言的设计者们为了保持语言与之前版本的一致性,决定不引入类似的语法。对于一门语言的新版本,引入太多的新语法会增加它的学习成本,带来额外学习负担。
15.2.2 匿名函数和闭包
Scala也支持匿名函数。匿名函数和Lambda表达式的语法非常类似。下面的这个例子中,你将一个匿名函数赋值给了名为isLongTweet
的变量,该匿名函数的功能是检查给定的消息长度,判断它是否超长:
val isLongTweet : String => Boolean ←─这是一个函数类型的变量,它接受一个String参数,返回一个布尔类型的值 = (tweet : String) => tweet.length > 60 ←─一个匿名函数
在新版的Java中,你可以使用Lambda表达式创建函数式接口的实例。Scala也提供了类似的机制。前面的这段代码是Scala中声明匿名类的语法糖。Function1
(只带一个参数的函数)提供了apply
方法的实现:
val isLongTweet : String => Boolean = new Function1[String, Boolean] { def apply(tweet: String): Boolean = tweet.length > 60 }
由于变量isLongTweet
中保存了类型为Function1
的对象,你可以调用它的apply
方法,这看起来就像下面的方法调用:
isLongTweet.apply("A very short tweet") ←─返回false
如果用Java,你可以采用下面的方式:
Function<String, Boolean> isLongTweet = (String s) -> s.length > 60;boolean long = isLongTweet.apply("A very short tweet");
为了使用Lambda表达式,Java提供了几种内置的函数式接口,比如Predicate
、Function
、Consumer
。Scala提供了trait(你可以暂时将trait想象成接口,我们会在接下来的一节介绍它们)来实现同样的功能: 从Function0
(一个函数不接受任何参数,并返回一个结果)到Function22
(一个函数接受22个参数),它们都定义了apply
方法。
Scala还提供了另一个非常酷炫的特性,你可以使用语法糖调用apply
方法,效果就像一次函数调用:
isLongTweet("A very short tweet") ←─返回false
编译器会自动地将方法调用f(a)
转换为f.apply(a)
。更一般地说,如果f
是一个支持apply
方法的对象(注,apply
可以有任意数目的参数),对方法f(a1, ..., an)
的调用会被转换为f.apply(a1, ..., an)
。
闭包
第3章中我们曾经抛给大家一个问题:Java中的Lambda表达式是否是借由闭包组成的。温习一下,那么什么是闭包呢?闭包是一个函数实例,它可以不受限制地访问该函数的非本地变量。不过Java 8中的Lambda表达式自身带有一定的限制:它们不能修改定义Lambda表达式的函数中的本地变量值。这些变量必须隐式地声明为final
。这些背景知识有助于我们理解“Lambda避免了对变量值的修改,而不是对变量的访问”。
与此相反,Scala中的匿名函数可以取得自身的变量,但并非变量当前指向的变量值。比如,下面这段代码在Scala中是可能的:
def main(args: Array[String]) { var count = 0 val inc = => count+=1 ←─这是一个闭包,它捕获并递增count inc println(count) ←─打印输出1 inc println(count) ←─打印输出2}
不过在Java中,下面的这段代码会遭遇编译错误,因为count
隐式地被强制定义为final
:
public static void main(String args) { int count = 0; Runnable inc = -> count+=1; ←─错误:count必须为final或者在效果上为final inc.run; System.out.println(count); inc.run;}
我们在第7、13以及14章多次提到你应该尽量避免修改,这样你的代码更加易于维护和并发运行,所以请在绝对必要时才使用这一特性。
15.2.3 科里化
第14章中,我们描述了一种名为科里化的技术:带有两个参数(比如x
和y
)的函数f
可以看成一个仅接受一个参数的函数g
,函数g
的返回值也是一个仅带一个参数的函数。这一定义可以归纳为接受多个参数的函数可以转换为多个接受一个参数的函数。换句话说,你可以将一个接受多个参数的函数切分为一系列接受该参数列表子集的函数。Scala为此特别提供了一个构造器,帮助你更加轻松地科里化一个现存的方法。
为了理解Scala到底带来了哪些变化,让我们先回顾一个Java的示例。你定义了一个简单的函数对两个正整数做乘法运算:
static int multiply(int x, int y) { return x * y;}int r = multiply(2, 10);
不过这种定义方式要求向其传递所有的参数才能开始工作。你可以人工地对multiple
方法进行切分,让其返回另一个函数:
static Function<Integer, Integer> multiplyCurry(int x) { return (Integer y) -> x * y;}
由multiplyCurry
返回的函数会捕获x
的值,并将其与它的参数y
相乘,然后返回一个整型结果。这意味着你可以像下面这样在一个map
中使用multiplyCurry
,对每一个元素值乘以2:
Stream.of(1, 3, 5, 7) .map(multiplyCurry(2)) .forEach(System.out::println);
这样就能得到计算的结果2、6、10、14。这种方式工作的原因是map
期望的参数为一个函数,而multiplyCurry
的返回结果就是一个函数。
现在的Java语言中,为了构造科里化的形式需要你手工地切分函数(尤其是函数有非常多的参数时),这是极其枯燥的事情。Scala提供了一种特殊的语法可以自动完成这部分工作。比如,正常情况下,你定义的multiply
方法如下所示:
def multiply(x : Int, y: Int) = x * yval r = multiply(2, 10);
该函数的科里化版本如下:
def multiplyCurry(x :Int)(y : Int) = x * y ←─定义一个科里化函数val r = multiplyCurry(2)(10) ←─调用该科里化函数
使用语法(x: Int)(y: Int)
,方法multiplyCurry
接受两个由一个Int
参数构成的参数列表。与此相反,multiply
接受一个由两个Int
参数构成的参数列表。当你调用multiplyCurry
时会发生什么呢?multiplyCurry
的第一次调用使用了单一整型参数(参数x
),即multiplyCurry(2)
,返回另一个函数,该函数接受参数y
,并将其与它捕获的变量x
(这里的值为2)相乘。正如我们在14.1.2节介绍的,我们称这个函数是部分应用的,因为它并未提供所有的参数。第二次调用对x
和y
进行了乘法运算。这意味着你可以将对multiplyCurry
的第一次调用保存到一个变量中,进行复用:
val multiplyByTwo : Int => Int = multiplyCurry(2)val r = multiplyByTwo(10) ←─20
和Java比较起来,在Scala中你不再需要像这里这样手工地提供函数的科里化形式。Scala提供了一种方便的函数定义语法,能轻松地表示函数使用了多个科里化的参数列表。
15.3 类和trait
现在我们看看类与接口在Java和Scala中的不同。这两种结构在我们设计应用时都很常用。 你会看到相对于Java的类和接口,Scala的类和接口提供了更多的灵活性。
15.3.1 更加简洁的Scala类
由于Scala也是一门完全的面向对象语言,你可以创建类,并将其实例化生成对象。最基础的形态上,声明和实例化类的语法与Java非常类似。比如,下面是一个声明Hello
类的例子:
class Hello { def sayThankYou{ println("Thanks for reading our book") }}val h = new Helloh.sayThankYou
getter方法和setter方法
一旦你定义的类具有了字段,这件事情就变得有意思了。你碰到过单纯只定义字段列表的Java类吗?很明显,你还需要声明一长串的getter方法、setter方法,以及恰当的构造器。多麻烦啊!除此之外,你还需要为每一个方法编写测试。在企业Java应用中,大量的代码都消耗在了这样的类中。比如下面这个简单的Student
类:
public class Student { private String name; private int id; public Student(String name) { this.name = name; } public String getName { return name; } public void setName(String name) { this.name = name; } public int getId { return id; } public void setId(int id) { this.id = id; }}
你需要手工定义构造器对所有的字段进行初始化,还要实现2个getter方法、2个setter方法。一个非常简单的类现在需要超过20行的代码才能实现!有的集成开发环境或者工具能帮你自动生成这些代码,不过你的代码库中还是需要增加大量额外的代码,而这些代码与你实际的业务逻辑并没有太大的关系。
Scala语言中构造器、getter方法以及setter方法都能隐式地生成,从而大大降低你代码中的冗余:
class Student(var name: String, var id: Int)val s = new Student("Raoul", 1) ←─初始化Student对象println(s.name) ←─取得名称,打印输出Raouls.id = 1337 ←─设置id println(s.id) ←─打印输出1337
15.3.2 Scala的trait与Java 8的接口对比
Scala还提供了另一个非常有助于抽象对象的特性,名称叫trait。它是Scala为实现Java中的接口而设计的替代品。trait中既可以定义抽象方法,也可以定义带有默认实现的方法。trait同时还支持Java中接口那样的多继承,所以你可以将它们看成与Java 8中接口类似的特性,它们都支持默认方法。trait中还可以包含像抽象类这样的字段,而Java 8的接口不支持这样的特性。那么,trait就类似于抽象类吗?显然不是,因为trait支持多继承,而抽象类不支持多继承。Java支持类型的多继承,因为一个类可以实现多个接口。现在,Java 8通过默认方法又引入了对行为的多继承,不过它依旧不支持对状态的多继承,而这恰恰是trait支持的。
为了展示Scala中的trait到底是什么样,让我们看一个例子。我们定义了一个名为Sized
的trait,它包含一个名为size
的可变字段,以及一个带有默认实现的isEmpty
方法:
trait Sized{ var size : Int = 0 ←─名为size的字段 def isEmpty = size == 0 ←─带默认实现的isEmpty方法}
你现在可以使用一个类在声明时构造它,下面这个例子中Empty
类的size
恒定为0
:
class Empty extends Sized ←─一个继承自trait Sized的类println(new Empty.isEmpty) ←─打印输出true
有一件事非常有趣,trait和Java的接口类似,也是在对象实例化时被创建(不过这依旧是一个编译时的操作)。比如,你可以创建一个Box
类,动态地决定到底选择哪一个实例支持由trait Sized
定义的操作:
class Boxval b1 = new Box with Sized ←─在对象实例化时构建traitprintln(b1.isEmpty) ←─打印输出trueval b2 = new Boxb2.isEmpty ←─编译错误:因为Box类的声明并未继承Sized
如果一个类继承了多个trait,各trait中声明的方法又使用了相同的签名或者相同的字段,这时会发生什么情况?为了解决这些问题,Scala中定义了一系列限制,这些限制和我们之前在第9章介绍默认方法时的限制极其类似。
15.4 小结
下面是这一章中介绍的关键概念和你应该掌握的要点。
Java 8和Scala都是整合了面向对象编程和函数式编程特性的编程语言,它们都运行于JVM之上,在很多时候可以相互操作。
Scala支持对集合的抽象,支持处理的对象包括
List
、Set
、Map
、Stream
、Option
,这些和Java 8非常类似。不过,除此之外Scala还支持元组。Scala为函数提供了更加丰富的特性,这方面比Java 8做得好,Scala支持:函数类型、可以不受限制地访问本地变量的闭包,以及内置的科里化表单。
Scala中的类可以提供隐式的构造器、getter方法以及setter方法。
Scala还支持trait,它是一种同时包含了字段和默认方法的接口。