《Java8实战》读书笔记13:Java8 与 Scala
Posted 笑虾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java8实战》读书笔记13:Java8 与 Scala相关的知识,希望对你有一定的参考价值。
《Java8实战》读书笔记13:Java8 与 Scala
第 15 章 面向对象和函数式编程的混合:Java 8和Scala的比较
本章内容
什么是Scala语言
Java 8与Scala是如何相生相承的
Scala中的函数与Java 8中的函数有哪些区别
类和trait
15.1.1 你好,啤酒
见识一下 scala
与 java8
做个对比 。
这是官方提供的在线环境用于新手了解Scala :https://scastie.scala-lang.org/
- 书上代码是 scala2 的,上面环境默认 scala3 到
BuildSettings
中设置一下即可。 - 注意这里用的是
=>
而不是 java8 的->
(我亲爱的js
有伴了) - 字符串前的
s
表示字符串插值
。(这里也有点像js
的模板字符串) - 喜欢折腾的可以配下 Idea 环境:GETTING STARTED WITH SCALA IN INTELLIJ
object Beer
def main(args: Array[String])
2 to 6 foreach n => println(s"你好 $n 杯啤酒")
scala3 如下:(莫非强制加返回类型?)
object HelloWorld
def main(args: Array[String]): Unit =
2 to 6 foreach n => println(s"你好 $n 杯啤酒")
直接看个截图吧:
15.1.2 基础数据结构:List、Set、Map、Tuple、Stream 以及 Option
1. 创建集合
// 创建 Map
val authorsToAge = Map("Raoul" -> 23, "Mario" -> 40, "Alan" -> 53)
// 创建 List
val authors = List("Raoul", "Mario", "Alan")
// 创建 Set
val numbers = Set(1, 1, 2, 3, 5, 8)
->
用于完成键
到值
的映射
。var
声明变量
,可读写。val
声明常量
,是只读的。val authorsToAge : Map[String, Int]
可显示声明变量类型。不过Scala可以替你推断变量的类型。
2. 不可变与可变的比较
Scala
的集合默认都是
只读`的。(Scala并未强制你必须使用不可变集合,只是各种暗示你接受这种风格)- 可见其推荐的
修改
就是将变化部分创建副本,并引用无变化的部分。得到一个新的集合。
val numbers = Set(2, 5, 3); // 这里的操作符+会将8添加到Set 中,创建并返回一个新的Set对象
val newNumbers = numbers + 8
println(newNumbers) // (2, 5, 3, 8)
println(numbers) // (2, 5, 3)
从例子中也能看出,拼接操作返回的是一个副本。
不可修改与不可变的比较
Java中提供了多种方法创建不可修改的(unmodifiable)集合。下面的代码中,变量newNumbers是集合Set对象numbers的一个只读视图:
Set numbers = new HashSet<>();
Set newNumbers = Collections.unmodifiableSet(numbers);
这意味着你无法通过操作变量newNumbers向其中加入新的元素。不过,不可修改集合仅仅是对可变集合进行了一层封装。通过直接访问numbers变量,你还是能向其中加入元素。
与此相反,不可变(immutable)集合确保了该集合在任何时候都不会发生变化,无论有多少个变量同时指向它。
我们在第14章介绍过如何创建一个持久化的数据结构:你需要创建一个不可变数据结构,该数据结构会保存它自身修改之前的版本。任何的修改都会创建一个更新的数据结构。
3. 使用集合
- 因为用的在线环境,没法读本地文件,只好改下代码:(分号不是必需的)
val fileLines = List("Java-123456789", "Scala-123456789", "JavaScrip-t123456789");
val linesLongUpper = fileLines.filter(l => l.length() > 10)
.map(l => l.toUpperCase())
.foreach(str => println(s"输出:$str"));
中缀表达式
和下划线
写法:_.length()
解读为l =>l.length()
val fileLines = List("Java-123456789", "Scala-123456789", "JavaScrip-t123456789");
fileLines filter (_.length() > 10) map (_.toUpperCase()) foreach (str => println(s"输出:$str"));
- 对应
Java 8
中Stream
调用parallel
方法。scala
可以使用par
方法。
val linesLongUpper = fileLines.par filter (_.length() > 10) map(_.toUpperCase())
4. 元组
声明元组
val book = (2014, "Java 8 in Action", "Manning") // 元组类型为(Int, String, String)
val numbers = (42, 1337, 0, 3, 14) // 元组类型为(Int, Int, Int, Int, Int)
读取元组(_1、_2 这东西叫 存取器accessor
,元素从 1
开始算 )
println(book._1) // 2014
println(numbers._4) // 3
- Scala支持任意大小的元组。
- 但是元组中元素的最大上限为
23
。(Scala3规则已经变了,不止23,具体多少没试。)
5. Stream
Scala
也提供了Stream
实现延迟计算
,而且功能更加丰富。Scala
中的Stream
可以记录它曾经计算出的值,所以之前的元素可以随时进行访问。(看来都做了缓存,是它的风格)Stream
还进行了索引,所以Stream
中的元素可以像List
那样通过索引访问。- 可能唯一的缺陷就是,你得多买点内存。
6. Option
我们建议你在设计API时尽可能地使用Optional
,这种方式下,接口用户只需要阅读方法签名就能了解他们是否应该传递一个optional
值。我们应该尽量地用它替代null
,避免发生空指针
异常。
- Java 版:
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 版:
def getCarInsuranceName(person: Option[Person], minAge: Int) =
person.filter(_.getAge() >= minAge)
.flatMap(_.getCar)
.flatMap(_.getInsurance)
.map(_.getName)
.getOrElse("Unknown")
注意 在前面的代码中,你使用的是_.getCar(并未使用圆括号),而不是_.getCar() (带圆括号)。Scala语言中,执行方法调用时,如果
不需要传参
,那么函数的圆括号
是可省
的。
15.2 函数
Scala提供了下面这些特性
- 函数类型,它是一种语法糖,体现了Java语言中函数描述符的思想,即,它是一种符号,表示了在函数接口中声明的抽象方法的签名。这些内容我们在第3章中都介绍过。
- 能够读写非本地变量的匿名函数,而Java中的Lambda表达式无法对非本地变量进行写操作。
- 对科里化的支持,这意味着你可以将一个接受多个参数的函数拆分成一系列接受部分参数的函数。
15.2.1 Scala 中的一等函数
函数
在Scala
语言中是一等值
。这意味着它们可以像 Integer
或者 String
那样,作为参数传递
,可以作为结果值返回
。正如我们在前面章节所介绍的那样,Java 8中的方法引用和Lambda表达式也可以看成一等函数。
- 使用谓词(返回一个布尔型结果的函数)定义这两个筛选条件:
def isJavaMentioned(tweet: String) : Boolean = tweet.contains("Java")
def isShortTweet(tweet: String) : Boolean = tweet.length() < 20
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) // I love the new features in Java 8
tweets.filter(isShortTweet).foreach(println) // How's it going?
filter
的函数签名:def filter[T](p: (T) => Boolean): List[T]
表示参数p
是一个函数,它接受参数类型T
返回结果类型Boolean
。函数类型
可以简写为:() => Boolean
或(T) => Boolean
- 对应
Java8
的Predicate<T>
或 者Function<T, Boolean>
。
15.2.2 匿名函数和闭包
1. 匿名函数
- 定义一个匿名函数,它的功能是检查给定的消息长度,判断它是否超长。
对应 Java8
中函数式接口的声明方式。先看个 Java8
原版的
Function<String, Boolean> isLongTweet = (String s) -> s.length() > 60;
boolean b = isLongTweet.apply("A very short tweet");
再看 Scala
版本
val isLongTweet : String => Boolean = new Function1[String, Boolean]
def apply(tweet: String): Boolean = tweet.length() > 60
最后看 Scala
语法糖版本
val isLongTweet : String => Boolean = (tweet : String) => tweet.length() > 60
- 这是一个函数类型的变量,它接受一个
String
参数,返回一个布尔
类型的值。 - 匿名函数很眼熟,没什么好说的。
- 这是
Scala
中声明匿名类的语法糖。
Scala
中从Function0
(一个函数不接受任何参数,并返回一个结果)到Function22
(一个函数接受22
个参数),它们都定义了apply
方法。
Scala
还提供了另一个非常酷炫的特性,你可以使用语法糖
调用apply
方法,效果就像一次函数调用:
var a = isLongTweet.apply("A very short tweet");
var b = isLongTweet("A very short tweet");
print(a); // false
print(b); // false
2. 闭包
Java8中Lambda
表达式自身带有一定的限制:它们不能修改
定义Lambda表达式的函数中的
本地变量
值。这些变量必须隐式地声明为fina
l。
public static void main(String[] args)
int count = 0;
Runnable inc = () -> count+=1; // 错误:count 必须为 final 或者在效果上为 final
inc.run();
System.out.println(count);
inc.run();
与此相反,Scala中的匿名函数可以取得自身的变量,但并非变量当前指向的变量值。比如,下面这段代码在Scala中是可能的:
object HelloWorld
def main(args: Array[String]): Unit =
var count = 0
val inc = () => count+=1 // 这是一个闭包,它捕获并递增count
inc()
println(count) // 打印输出1
inc()
println(count) // 打印输出2
书上说:虽然可以是可以,但是你这么玩了,你的并发运行
,还想不想要了?
15.2.3 科里化
Scala
特别提供了一个构造器,帮助我们更加轻松地科里化
一个现存的方法
。
- 先回顾一个
Java
的示例
// 原函数
static int multiply(int x, int y)
return x * y;
int r = multiply(2, 10);
// 科里化版本:
static Function<Integer, Integer> multiplyCurry(int x)
return (Integer y) -> x * y;
// 应用:利用 map 将 1、3、5、7 分别 *2
Stream.of(1, 3, 5, 7)
.map(multiplyCurry(2))
.forEach(System.out::println); // 2, 6, 10, 14
Scala
版本
// 原函数
def multiply(x : Int, y: Int) = x * y
val r = multiply(2, 10);
println(r);
// 直接声明:科里化版本
def multiplyCurry(x :Int)(y : Int) = x * y
val r = multiplyCurry(2)(10)
println(r); // 20
把前一半拆分出来用:
def multiplyCurry(x :Int)(y : Int) = x * y
val multiplyByTwo : Int => Int = multiplyCurry(2)
val r = multiplyByTwo(10)
println(r); // 20
15.3 类和 trait
15.3.1 更加简洁的 Scala 类
1. 声明类
class Hello
def sayThankYou() : Unit =
println("Thanks for reading our book")
val h = new Hello()
h.sayThankYou()
2. getter方法和setter方法
Scala
语言中构造器、getter、setter
方法都能隐式地生成:
class Student(var name: String, var id: Int);
val s = new Student("笨笨", 1); // 初始化 Student 对象
println(s.name); // 取得名称,打印输出 笨笨
s.id = 9527; // 设置id
println(s.id); // 打印输出9527
15.3.2 Scala 的 trait 与 Java 8 的接口对比
trait
对应Java中的接口
。trait
支持默认方法。trait
支持多继承。
定义一个名为Sized
的trait
,它包含一个名为size
的可变字段,以及一个带有默认实现的isEmpty
方法:
trait Sized
var size : Int = 0; // 名为size的字段
def isEmpty() = size == 0; // 带默认实现的 isEmpty 方法
声明 Empty
继承 Sized
class Empty extends Sized; // Empty 类继承自 Sized
println(new Empty().isEmpty()); // 打印输出true
有一件事非常有趣,trait
和Java
的接口类似,也是在对象实例化时被创建(不过这依旧是一个编译时的操作)。比如,你可以创建一个Box类,动态地决定到底选择哪一个实例支持由trait Sized定义的操作:
- 在对象实例化时构建
trait
class Box
val b1 = new Box() with Sized
println(b1.isEmpty()) // 打印输出true
- 没有构建
trait
class Box
val b2 = new Box()
b2.isEmpty() // 编译错误:因为Box类的声明并未继承Sized
15.4 小结
下面是这一章中介绍的关键概念和你应该掌握的要点。
Java 8
和Scala
都是整合了面向对象编程和函数式编程特性的编程语言,它们都运行于JVM
之上,在很多时候可以相互操作。Scala
支持对集合的抽象,支持处理的对象包括List、Set、Map、Stream、Option
,这些和Java 8
非常类似。不过,除此之外Scala
还支持元组
。Scala
为函数提供了更加丰富的特性,这方面比Java 8
做得好,Scala
支持:函数类型
、可以不受限制地访问本地变量
的闭包,以及内置的科里化
表单。Scala
中的类可以提供隐式
的构造器、getter、setter
方法。Scala
还支持trait
,它是一种同时包含
了字段
和默认方法
的接口
。
读书总结
对 Scala
有个了简单的了解。
以上是关于《Java8实战》读书笔记13:Java8 与 Scala的主要内容,如果未能解决你的问题,请参考以下文章
《Java8实战》 - 读书笔记 - Parallel Stream并行流知识
《Java8实战》 - 读书笔记 - Lambda 表达式的组合用法