scala初学笔记

作者 uunnfly 日期 2020-11-26
scala初学笔记

数据结构

Array 定长数组: 有序,可变类型,长度不可变。
ArrayBuffer 不定长数组:有序,可变类型,长度可以扩展。
List 列表:有序,不可变类型。
Set 集合:无序,不可变类型。如果你想使用可变集合,需要引用 scala.collection.mutable.Set 包。
Map 映射:无序,不可变类型。如果你需要使用可变集合,你需要显式的引入 import scala.collection.mutable.Map
Tuple 元组:有序,不可变类型,可以存放不同数据类型元素。
Option 选项:表示有可能包含值的容器,也可能不包含值。
Iterator 迭代器:不属于容器,但是提供了遍历容器的方法。

对不可变容器也可以进行添加/删除元素的操作,但是返回一个新的容器,旧的容器没变。

容器的基本操作:

  • head 返回集合第一个元素
  • tail 返回一个容器,包含除了第一元素之外的其他元素
  • isEmpty 在容器为空时返回true
  • minmax返回最大/最小元素

所有的集合类都可以在包scala.collectionscala.collection.mutablescala.collection.immutablescala.collection.generic中找到。客户端代码需要的大部分集合类都独立地存在于3种变体中,它们位于scala.collection, scala.collection.immutable, scala.collection.mutable包。每一种变体在可变性方面都有不同的特征。

如果你想要同时使用可变和不可变集合类,只导入collection.mutable包即可。

下图显示了scala.collection包中所有的集合类。这些都是高级抽象类或特质,它们通常具备和不可变实现一样的可变实现。

General collection hierarchy

下图显示了scala.collection.immutable中所有的集合类。

Immutable collection hierarchy

下图显示了scala.collection.mutable中所有的集合类。

Mutable collection hierarchy

图例

Graph legend

集合类共性

每一种集合都能用相同的语法创建,写法是集合类名紧跟着元素。

都通过相同的途径,用toString方法展示出来。

Traversable

  • 相加 ++
  • map:map, flatMap, collect
  • 转换器:toArray,toList,toIterable,toSeq,toIndexedSeq,toStream,toSet,和toMap
  • 拷贝:copyToBuffer和copyToArray
  • Size info:isEmpty,nonEmpty,size和hasDefiniteSize(是否无限)
  • 元素检索:head,last,headOption,lastOption和find
  • 子容器检索:tail,init,slice,take,drop,takeWhilte,dropWhile,filter,filteNot和withFilter:可以通过范围索引或一些论断的判断返回某些子容器。
  • 拆分:splitAt,span,partition和groupBy
  • 元素测试:exists,forall和count
  • 折叠(fold):foldLeft,foldRight,/:,:\,reduceLeft和reduceRight,用于对连续性元素的二进制操作。
  • 特殊折叠:sum, product, min, max
  • 字符串:mkString,addString和stringPrefix
  • 视图

Iterable

Seq

Seq trait用于表示序列。所谓序列,指的是一类具有一定长度的可迭代访问的对象,其中每个元素均带有一个从0开始计数的固定索引位置。

  • 索引和长度的操作:apply、isDefinedAt、length、indices,及lengthCompare。序列的apply操作用于索引访问;因此,Seq[T]类型的序列也是一个以单个Int(索引下标)为参数、返回值类型为T的偏函数。(Seq的isDefinedAt方法传入非法下标时会返回false)
  • 索引检索操作:indexOf、lastIndexOf、indexofSlice、lastIndexOfSlice、indexWhere、lastIndexWhere、segmentLength、prefixLength
  • 加法运算:+:,:+,padTo
  • 更新:updated,patch
  • 排序:sorted, sortWith, sortBy
  • 反转:reverse, reverseIterator, reverseMap
  • 比较:startsWith, endsWith, contains, containsSlice, corresponds
  • 多集操作:intersect, diff, union, distinct

特性(trait) Seq 具有两个子特征(subtrait) LinearSeqIndexedSeq。它们不添加任何新的操作,但都提供不同的性能特点:线性序列具有高效的 head 和 tail 操作,而索引序列具有高效的apply, length, 和 (如果可变) update操作。

常用线性序列有 scala.collection.immutable.Listscala.collection.immutable.Stream。常用索引序列有 scala.Array scala.collection.mutable.ArrayBuffer。Vector 类提供一个在索引访问和线性访问之间有趣的折中。它同时具有高效的恒定时间的索引开销,和恒定时间的线性访问开销。

缓冲器

不仅允许更新现有的元素,而且允许元素的插入、移除和在buffer尾部高效地添加新元素。buffer 支持的主要新方法有:用于在尾部添加元素的 +=++=;用于在前方添加元素的+=:++=: ;用于插入元素的 insertinsertAll;以及用于删除元素的remove-=

ListBuffer和ArrayBuffer是常用的buffer实现 。

集合

  • 测试型方法:containsapplysubsetOfcontains 方法用于判断集合是否包含某元素。set(elem)等同于set contains elem
  • 加法类型方法:+++。++是对于两个集合的加法
  • 减法类型方法: ---
  • 并、交、差集:每一种运算都存在两种书写形式:字母和符号形式。字母形式:intersect、union和diff,符号形式:&、|和&~。事实上,Set中继承自Traversable的++也能被看做union或|的另一个别名。区别是,++的参数为Traversable特质,而union和 | 的参数是集合。

mutable.Set

可变集合也提供了+++操作符来添加元素,---用来删除元素。但是这些操作在可变集合中通常很少使用,因为这些操作都要通过集合的拷贝来实现。可变集合提供了更有效率的更新方法,+=-=

还有从可遍历对象集合或迭代器集合中添加和删除所有元素的批量操作符++=--=

目前可变集合默认使用哈希表来存储集合元素,非可变集合则根据元素个数的不同,使用不同的方式来实现。空集用单例对象来表示。元素个数小于等于4的集合可以使用单例对象来表达,元素作为单例对象的字段来存储。 元素超过4个,非可变集合就用哈希前缀树(hash trie)来实现。

集合的两个特质是 SortedSetBitSet

有序集(SortedSet)

SortedSet 的默认表示是有序二叉树。 immutable.TreeSet 使用红黑树实现,它在维护元素顺序的同时,也会保证二叉树的平衡

创建一个空TreeSet,可以先定义排序规则

1
2
val myOrdering = Ordering.fromLessThan[String](_ > _)
TreeSet.empty(myOrdering)

也可以不指定排序规则参数,只需要给定一个元素类型或空集合。在这种情况下,将使用此元素类型默认的排序规则。

1
TreeSet.empty[String]

有序集合同样支持元素的范围操作: range, from

位集合(Bitset)

位集合是由单字或多字的紧凑位实现的非负整数的集合。其内部使用 Long 型数组来表示。第一个 Long 元素表示的范围为0到63,第二个范围为64到127,以此类推

位集合的大小取决于存储在该集合的最大整数的值的大小。假如N是为集合所要表示的最大整数,则集合的大小就是 N/64 个长整形字,或者 N/8 个字节,再加上少量额外的状态信息字节。

Map

1
2
3
4
5
6
7
8
9
10
11
12
13
// 空哈希表,键为字符串,值为整型
var A:Map[Char,Int] = Map()

// Map 键值对演示
val colors = Map("red" -> "#FF0000", "azure" -> "#F0FFFF")
//添加key-value
A += ('I' -> 1)
A += ('J' -> 5)

//三个基本操作
A.keys
A.values
A.isEMpty
  • 查询:apply、get、getOrElse、contains和DefinedAt。get返回 Option[Value],apply方法不进行Option封装,主键不存在会抛出异常
  • 添加及更新:+、++、updated
  • 删除类:-、–
  • 子集类: keys、keySet、keysIterator、values、valuesIterator,可以以不同形式返回映射的键和值。
  • filterKeys、mapValues等变换用于对现有映射中的绑定进行过滤和变换,进而生成新的映射。

mutable.Map

利用两种变形m(key) = value和m += (key -> value), 我们可以“原地”修改可变映射m。还有一种变形m put (key, value)

getOrElseUpdate特别适合用于访问用作缓存的映射(Map),getOrElseUpdate的第2个参数是“按名称(by-name)”传递的

同步集合(Set)和映射(Map)

无论什么样的Map实现,只需混入SychronizedMap trait,就可以得到对应的线程安全版的Map。

1
2
3
4
5
6
7
8
9
10
11
import scala.collection.mutable.{Map,
SynchronizedMap, HashMap}
object MapMaker {
def makeMap: Map[String, String] = {
new HashMap[String, String] with
SynchronizedMap[String, String] {
override def default(key: String) =
"Why do you want to know?"
}
}
}

上面的makeMap方法混入了SynchronizedMap,而且重写了default方法(当向某个Map查询给定的键所对应的值,而Map中不存在与该键相关联的值时,默认情况下会触发一个NoSuchElementException异常。不过,如果自定义一个Map类并覆写default方法,便可以针对不存在的键返回一个default方法返回的值。)

: 如有使用同步容器(synchronized collection)的需求,还可以考虑使用java.util.concurrent中提供的并发容器(concurrent collections)。

Array

遍历

1
2
3
for ( i <- 0 to (myList.length - 1)) {
total += myList(i);
}

多维数组

1
val myMatrix = Array.ofDim[Int](3, 4)

取值:myMatrix(i)(j)

区间数组:

Range 不包括上界

1
var myList1 = range(10, 20, 2)//10 ,12 ,14, 16,18,默认步长为1

数组方法: 需要使用 import Array._ 引入包

Scala 2.8中数组不再看作序列,因为本地数组的类型不是Seq的子类型。而是在数组和 scala.collection.mutable.WrappedArray这个类的实例之间隐式转换,后者则是Seq的子类。

字符串

像数组,字符串不是直接的序列,但是他们可以转换为序列,并且他们也支持所有的在字符串上的序列操作。

依赖于两种隐式转换。第一种,低优先级转换映射一个String到WrappedString,它是immutable.IndexedSeq的子类。用在一个string转换为一个Seq

另一种,高优先级转换映射一个string到StringOps 对象,从而在immutable 序列到strings上增加了所有的方法。插入在reverse,map,drop和slice的方法调用中

具体的不可变集实体类

List

1
2
3
4
5
6
7
8
9
// 空列表
val empty: List[Nothing] = List()
// 二维列表
val dim: List[List[Int]] =
List(
List(1, 0, 0),
List(0, 1, 0),
List(0, 0, 1)
)

构造列表的两个基本单位是 Nil::

Nil 也可以表示为一个空列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 整型列表
val nums = 1 :: (2 :: (3 :: (4 :: Nil)))

// 空列表
val empty = Nil

// 二维列表
val dim = (1 :: (0 :: (0 :: Nil))) ::
(0 :: (1 :: (0 :: Nil))) ::
(0 :: (0 :: (1 :: Nil))) :: Nil

//或者可以不用括号
val list1 = 1::2::3::Nil
val list2 = (1::2::Nil)::(1::3::4::Nil)::Nil

基本操作

  • 可以使用 ::: 运算符或 List.:::() 方法或 List.concat() 方法来连接两个或多个列表

    1
    2
    3
    list1 ::: list2
    list1.:::(list2)
    List.concat(list1, list2)
  • List.fill() 方法来创建一个指定重复数量的元素列表List.fill(3)("a")

  • List.tabulate() 方法是通过给定的函数来创建列表: 方法的第一个参数为元素的数量,可以是二维的,第二个参数为指定的函数,我们通过指定的函数计算结果并返回值插入到列表中,起始值为 0

    1
    2
    3
    4
    // 创建二维列表
    val mul = List.tabulate( 4,5 )( _ * _ )//值是 i * j
    println( "多维 : " + mul )
    //多维 : List(List(0, 0, 0, 0, 0), List(0, 1, 2, 3, 4), List(0, 2, 4, 6, 8), List(0, 3, 6, 9, 12))
  • List.reverse 用于将列表的顺序反转list1.reverse

  • take :It returns a list containing only the first n elements from the stated list or returns the whole list if n is more than the number of elements in the given list.

Stream

鉴于List通常使用 ::运算符来进行构造,stream使用外观上很相像的#::

stream被特别定义为懒惰计算,并且stream的toString方法很谨慎的设计为不去做任何额外的计算。(用toString方法只打印第一个元素)

例: 定义一个斐波那契数列

1
def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b)

Vector

相对于访问、添加和删除List头结点只需要固定时间,访问和修改头结点之后元素所需要的时间则是与List深度线性相关的。

Vector结构能够在“更高效”的固定时间内访问到列表中的任意元素。

Vector结构通常被表示成具有高分支因子的(树或者图的分支因子是指数据结构中每个节点的子节点数目)。每一个树节点包含最多32个vector元素或者至多32个子树节点。

对于一般大小的vector数据结构,一般经过至多5次数组访问就可以访问到指定的元素。

Vectors结构是不可变的,不能通过修改vector中元素的方法来返回一个新的vector。但是可以通过update方法从一个单独的元素中创建出区别于给定数据结构的新vector结构。

对vector中的某一元素进行update操作可以通过从树的根节点开始拷贝该节点以及每一个指向该节点的节点中的元素来实现。这就意味着一次update操作能够创建1到5个包含至多32个元素或者子树的树节点。

原理如下:如果只改了一个元素,updated后的vector根节点是新的,然后往下的子节点中只有要修改的节点的祖先节点是新的,其他都是复用旧的,也就是说新的根节点与旧的跟节点不同之处只有一个指向子节点指针的差别

1
vec updated (2, 4)//把index为2的元素改成4

由于vector在快速随机选择和快速随机更新的性能方面做到很好的平衡,所以它目前正被用作不可变索引序列(IndexedSeq)的默认实现方式。

不可变栈 Immutable stacks

使用push向栈中压入一个元素,用pop从栈中弹出一个元素,用top查看栈顶元素而不用删除它。

不可变stack一般很少用在Scala编程中,因为List结构已经能够覆盖到它的功能:push操作同List中的::基本相同,pop则对应着tail。

Immutable Queues(不可变队列)

使用enqueue方法在不可变Queue中加入一个/多个元素。

可以使用dequeue方法从queue的头部删除一个元素,dequeue方法将会返回两个值,包括被删除掉的元素和queue中剩下的部分。

Range(等差数列)

在Scala中创建一个Range类,需要用到两个预定义的方法to和by。

Hash Tries

从表现形式上看,Hash Try和Vector比较相似,都是树结构,且每个节点包含32个元素或32个子树,差别只是用不同的hash code替换了指向各个节点的向量值。

Hash Try对于快速查找和函数式的高效添加和删除操作上取得了很好的平衡,这也是Scala中不可变映射和集合采用Hash Try作为默认实现方式的原因。

Red-Black Trees(红黑树)

红黑树在Scala中被作为SortedSet的标准实现,因为它提供了一个高效的迭代器,可以用来按照排好的序列返回所有的元素。

Immutable BitSets(不可变位集合)

BitSet内部的使用了一个64位long型的数组。数组中的第一个long表示整数0到63,第二个表示64到27,以此类推。所以只要集合中最大的整数在千以内BitSet的压缩率都是相当高的。

List Maps

ListMap被用来表示一个保存键-值映射的链表。一般情况下,ListMap操作都需要遍历整个列表

实际上ListMap在Scala中很少使用,因为标准的不可变映射通常速度会更快。唯一的例外是,在构造映射时由于某种原因,链表中靠前的元素被访问的频率大大高于其他的元素。

具体的可变容器类

Array Buffers

List Buffers

ListBuffer 类似于Array Buffer。区别在于ListBuffer内部实现是链表, 而非数组

StringBuilders

StringBuilder 用来构造字符串。

链表

支持类是LinkedList。在大多数的编程语言中,null可以表示一个空链表,但是在Scalable集合中不是这样。因为就算是空的序列,也必须支持所有的序列方法。空链表用一种特殊的方式编译:它们的 next 字段指向它自身。

双向链表

支持类是DoubleLinkedList

可变列表

MutableList 由一个单向链表和一个指向该链表终端空节点的指针构成。因为避免了贯穿整个列表去遍历搜索它的终端节点,这就使得列表压缩了操作所用的时间。MutableList 目前是Scala中mutable.LinearSeq 的标准实现。

队列

你可以像使用一个不可变队列一样地使用一个可变队列,但你需要使用+= 和++=操作符进行添加的方式来替代排队方法。

数组序列

Array Sequences 是具有固定大小的可变序列。在它的内部,用一个 Array[Object]来存储元素。在Scala 中,ArraySeq 是它的实现类。

可变栈支持类是mutable.Stack

数组栈

ArrayStack 是另一种可变栈的实现,用一个可根据需要改变大小的数组做为支持。它提供了快速索引,使其通常在大多数的操作中会比普通的可变堆栈更高效一点。

hash table

Hash Table 用一个底层数组来存储元素。在Scala中默认的可变map和set都是基于Hash Table的。你也可以直接用mutable.HashSetmutable.HashMap 来访问它们。

Weak Hash Maps

一种特殊的Hash Map,垃圾回收器会忽略从Map到存储在其内部的Key值的链接。这也就是说,当一个key不再被引用的时候,这个键和对应的值会从map中消失。

Weak Hash Map 可以用来处理缓存,比如当一个方法被同一个键值重新调用时,你想重用这个大开销的方法返回值。如果Key值和方法返回值存储在一个常规的Hash Map里,Map会无限制的扩展,Key值也永远不会被垃圾回收器回收。用Weak Hash Map会避免这个问题。

在Scala中,WeakHashMap类是Weak Hash Map的实现类,封装了底层的Java实现类java.util.WeakHashMap

Concurrent Maps

实现类只有Java的java.util.concurrent.ConcurrentMap

Mutable Bitsets

Mutable bit sets在更新的操作上比不可变bit set 效率稍高,因为它不必复制没有发生变化的 Long值。

元组

元组不可变,但是可以包含不同类型的元素

1
2
val t = (1, 3.14, "Fred")  
val t = new Tuple3(1, 3.14, "Fred")

访问元组:使用 t._1 访问第一个元素, t._2 访问第二个元素

迭代:使用 Tuple.productIterator()

元素交换:使用 Tuple.swap 方法

迭代器

1
val it = Iterator(1,2,3)

迭代器 it 的两个基本操作是 nexthasNext

使用 it.minit.max 方法从迭代器中查找最大与最小元素

使用 it.sizeit.length 方法来查看迭代器中的元素个数

可以使用foreach以便在迭代器返回的每个元素上执行指定的程序

1
2
3
it foreach println
//或者用for
for (elem <- it) println(elem)

注:foreach后面跟println(不带参数),foreach不用括号

在迭代器或traversable容器中调用foreach方法的最大区别是:当在迭代器中完成调用foreach方法后会将迭代器保留在最后一个元素的位置。所以在这个迭代器上再次调用next方法时会抛出NoSuchElementException异常。

dropWhile方法:用来在迭代器中找到第一个具有某些属性的元素。

只有一个标准操作允许重用同一个迭代器, 每个都相当于迭代器it的完全拷贝。这两个iterator相互独立;一个发生变化不会影响到另外一个。

1
val (it1, it2) = it.duplicate

总的来说,如果调用完迭代器的方法后就不再访问它,那么迭代器的行为方式与容器是比较相像的。

带缓冲的迭代器

既可以看到下一个待返回的元素,又不会令迭代器跨过这个元素。[BufferedIterator]类是[Iterator]的子类,提供了一个附加的方法,head。在BufferedIterator中调用head 会返回它指向的第一个元素,但是不会令迭代器前进。

通过调用buffered方法,所有迭代器都可以转换成BufferedIterator

1
2
val it = Iterator(1, 2, 3, 4)
val bit = it.buffered

视图

转换器: 各种容器类自带一些用于开发新容器的方法,例如map、filter和++。

两个途径实现转换器。其一是紧凑法(strict),立即生成一个新容器。其二是惰性法(lazy),也就是懒加载,用到时再运算。

除了Stream的转换器是惰性实现的外,Scala的其他容器默认都是用紧凑法实现它们的转换器。
然而,通常基于容器视图,可将容器转换成惰性容器,反之亦可。

如果xs是个容器,那么xs.view就是同一个容器,不过所有的转换器都是惰性的。若要从视图转换回紧凑型容器,可以使用强制性方法。

1
2
3
val v = Vector(1 to 10: _*) //Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
v map (_ + 1) map (_ * 2) //会构造一个中间结果浪费资源
(v.view map (_ + 1) map (_ * 2)).force //先转成view再强制转回来。

过程如下:1. 用view方法将其转成SeqView对象

  1. 使用一次map方法,变成SeqViewM(…)。实质是记录函数map (_ + 1)应用在vector v数组上的封装。除非视图被强制转换,否则map不会被执行。然后,SeqView后面的 ‘’M‘’表示这个视图包含一个map操作。其他字母表示其他延迟操作。比如‘’S‘’表示一个延迟的slice操作,而‘’R‘’表示reverse操作。
  2. 使用第二次map方法,变成SeqViewMM(…)
  3. 最后强制转换结果

Option

Option[T] 是一个类型为 T 的可选值的容器: 如果值存在, Option[T] 就是一个 Some[T] ,如果不存在, Option[T] 就是对象 None 。

使用 getOrElse() 方法来获取元组中存在的元素或者使用其默认的值

isEmpty() 方法来检测元组中的元素是否为 None

从头定义新容器

1
2
3
4
5
6
7
8
Traversable() // 一个空的Traversable对象
List() // 空列表
List(1.0, 2.0) // 一个以1.0、2.0为元素的列表
Vector(1.0, 2.0) // 一个以1.0、2.0为元素的Vector
Iterator(1, 2, 3) // 一个迭代器,可返回三个整数
Set(dog, cat, bird) // 一个包含三个动物的集合
HashSet(dog, cat, bird) // 一个包含三个同样动物的HashSet
Map('a' -> 7, 'b' -> 0) // 一个将字符映射到整数的Map

上述每个例子都被“暗地里”转换成了对伴生对象apply方法的调用。在Scala标准库中,无论是List、Stream、Vector等具体的实现类还是Seq、Set、Traversable等抽象基类,每个容器类都伴一个带apply方法的伴生对象。针对后者,调用apply方法将得到对应抽象基类的某个默认实现

除了apply方法,每个容器类的伴生对象还定义了一个名为empty的成员方法,该方法返回一个空容器。也就是说,List.empty可以代替List()Map.empty可以代替Map(),等等。

Seq的子类同样在它们伴生对象中提供了工厂方法,总结如下表。简而言之,有这么一些:

  • concat,将任意多个Traversable容器串联起来
  • fill 和 tabulate,用于生成一维或者多维序列,并用给定的初值或打表函数来初始化。
  • range,用于生成步长为step的整型序列,并且iterate,将某个函数反复应用于某个初始元素,从而产生一个序列。

java和scala容器转换

1
2
3
4
5
6
7
8
Iterator               <=>     java.util.Iterator
Iterator <=> java.util.Enumeration
Iterable <=> java.lang.Iterable
Iterable <=> java.util.Collection
mutable.Buffer <=> java.util.List
mutable.Set <=> java.util.Set
mutable.Map <=> java.util.Map
mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap

需从JavaConverters对象中import这些转换 ,import之后,通过扩展方法 asScala 和 asJava 就可以在Scala容器和与之对应的Java容器之间进行隐式转换了

1
2
3
4
import collection.JavaConverters._
import collection.mutable._
val jul: java.util.List[Int] = ArrayBuffer(1, 2, 3).asJava
val buf: Seq[Int] = jul.asScala

在Scala内部,这些转换是通过一系列“包装”对象完成的,这些对象会将相应的方法调用转发至底层的容器对象。所以容器不会在Java和Scala之间拷贝来拷贝去。

还有一些Scala容器类型可以转换成对应的Java类型,但是并没有将相应的Java类型转换成Scala类型的能力,它们是:

1
2
3
4
Seq           =>    java.util.List
mutable.Seq => java.util.List
Set => java.util.Set
Map => java.util.Map

因为Java并未区分可变容器不可变容器类型,所以,虽然能将scala.immutable.List转换成java.util.List,但所有的修改操作都会抛出“UnsupportedOperationException”

下划线的用途

细数Scala下划线“_”的用法

Scala基础 - 下划线使用指南

循环

to: 1 to 10 : 生成1到10的数字

until: 1 until 10 1到10,不包括10

by: 1 to 10 by 2: 步长

yield: for 循环中的 yield 会把当前的元素记下来,保存在集合中,循环结束后将返回该集合。Scala 中 for 循环是有返回值的。如果被循环的是 Map,返回的就是 Map,被循环的是 List,返回的就是 List,以此类推。

函数与方法

使用 val 语句可以定义函数,def 语句定义方法

传名调用:将未计算的参数表达式直接应用到函数内部。在变量名和变量类型使用 => 符号来设置传名调用

1
def addByName(a: Int, b: => Int) = a + b

可变参数:在参数的类型之后放一个星号来设置可变参数(可重复的参数)

1
2
def printStrings( args:String* ) = {
}

默认参数值:

1
2
def addInt( a:Int=5, b:Int=7 ) : Int = {
}

高阶函数:高阶函数可以使用其他函数作为参数,或者使用函数作为输出结果。以函数入参时要多加一个 =>

1
2
3
4
5
   // 函数 f 和 值 v 作为参数,而函数 f 又调用了参数 v
def apply(f: Int => String, v: Int) = f(v)

//[A]表示这是一种泛型
def layout[A](x: A) = "[" + x.toString() + "]"

函数嵌套:定义在函数内的函数称之为局部函数

匿名函数:箭头左边是参数列表,右边是函数体。

1
var mul = (x: Int, y: Int) => x*y

偏应用函数: 绑定第一个 date 参数,第二个参数使用下划线(_)替换缺失的参数列表

1
val logWithDateBound = log(date, _ : String)

柯里化:将原来接受两个参数的函数变成新的接受一个参数的函数的过程

1
2
3
4
5
 def strcat(s1: String)(s2: String) = {
s1 + s2
}

println( "str1 + str2 = " + strcat(str1)(str2) )

偏函数

Scala之偏函数Partial Function

在Scala的scala包里,有一系列Function trait,它们实际上就是函数字面量作为“对象”存在时对应的类型。Function类型有多个版本,Function0表示无参数函数,Function1表示只有一个参数的函数,以此类推。

偏函数区别于普通函数的唯一特征就是:偏函数会自主地告诉调用方它的处理参数的范围,范围既可是值也可以是类型。

{case 2 => “OK”} 就是一个偏函数

省略括号和点号

在没有副作用的前提下,省略调用方法时候的空括号

只接收一个参数时,就可以使用花括号 代替 圆括号。通常人们还是会认为小括号是面向单行的,花括号面向多行的。

闭包

闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。

1
2
var factor = 3  
val multiplier = (i:Int) => i * factor

有一个自由变量factor,这个函数把自由变量成功捕获

img

类与对象

class

在scala中,类名可以和对象名为同一个名字,该对象称为该类的伴生对象,类和伴生对象可以相互访问他们的私有属性,但是他们必须在同一个源文件内。类只会被编译,不能直接被执行,类的申明和主构造器在一起被申明,在一个类中,主构造器只有一个所有必须在内部申明主构造器或者是其他申明主构造器的辅构造器,主构造器会执行类定义中的所有语句。scala对每个字段都会提供getter和setter方法,同时也可以显示的申明,但是针对val类型,只提供getter方法,默认情况下,字段为公有类型,可以在setter方法中增加限制条件来限定变量的变化范围,在scala中方法可以访问该类所有对象的私有字段。

object

单例对象

在scala中没有静态方法和静态字段,所以在scala中可以用object来实现这些功能,直接用对象名调用的方法都是采用这种实现方式,例如Array.toString。对象的构造器在第一次使用的时候会被调用,如果一个对象从未被使用,那么他的构造器也不会被执行;对象本质上拥有类(scala中)的所有特性,除此之外,object还可以一扩展类以及一个或者多个特质:例如,
abstract class ClassName(val parameter){}
object Test extends ClassName(val parameter){}

注意:object不能提供构造器参数,也就是说object必须是无参的

trait

在java中可以通过interface实现多重继承,在Scala中可以通过特征(trait)实现多重继承,不过与java不同的是,它可以定义自己的属性和实现方法体,在没有自己的实现方法体时可以认为它时java interface是等价的,在Scala中也是一般只能继承一个父类,可以通过多个with进行多重继承。

1
2
3
4
trait TraitA{}
trait TraitB{}
trait TraitC{}
object Test1 extends TraitA with TraitB with TraitC{}

继承

Scala 只允许继承一个父类。

  • 1、重写一个非抽象方法必须使用override修饰符。
  • 2、只有主构造函数才可以往基类的构造函数里写参数。
  • 3、在子类中重写超类的抽象方法时,你不需要使用override关键字。

构造器

辅助构造器

  1. 辅助构造器的名称为this
  2. 每个辅助构造器都必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始

主构造器

1. 主构造器的参数直接放在类名之后

2. 主构造器会执行类定义中的所有语句

3. 在主构造器中使用默认参数,可防止辅助构造器使用过多

4. 如果不带val或var的参数至少被一个方法使用,它将升格为字段。这种字段是私有的,只能类内部访问。如果带val或var那么本身就是字段,而且可以被外界访问

构造器执行顺序

  • 调用超类的构造器;
  • 特征构造器在超类构造器之后、类构造器之前执行;
  • 特征由左到右被构造;
  • 每个特征当中,父特征先被构造;
  • 如果多个特征共有一个父特征,父特征不会被重复构造
  • 所有特征被构造完毕,子类被构造。

单例对象

object

当一个单例对象和某个类共享一个名称时,这个单例对象称为 伴生对象。 同理,这个类被称为是这个单例对象的伴生类。类和它的伴生对象可以互相访问其私有成员。使用伴生对象来定义那些在伴生类中不依赖于实例化对象而存在的成员变量或者方法。

案例类

实例化案例类时不需要使用关键字new,这是因为案例类有一个默认的apply方法来负责对象的创建。

当你创建包含参数的案例类时,这些参数是公开(public)的val

1
2
3
4
case class Message(sender: String, recipient: String, body: String)
val message1 = Message("guillaume@quebec.ca", "jorge@catalonia.es", "Ça va ?")

println(message1.sender)

案例类在比较的时候是按值比较而非按引用比较

可以通过copy方法创建一个案例类实例的浅拷贝,同时可以指定构造参数来做一些改变。

1
2
3
4
5
6
case class Message(sender: String, recipient: String, body: String)
val message4 = Message("julien@bretagne.fr", "travis@washington.us", "Me zo o komz gant ma amezeg")
val message5 = message4.copy(sender = message4.recipient, recipient = "claire@bourgogne.fr")
message5.sender // travis@washington.us
message5.recipient // claire@bourgogne.fr
message5.body // "Me zo o komz gant ma amezeg"

模式匹配

1
2
3
4
5
6
7
8
9
10
import scala.util.Random

val x: Int = Random.nextInt(10)

x match {
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "other"
}

math…case

案例类的匹配

1
2
3
4
5
6
7
8
9
10
def showNotification(notification: Notification): String = {
notification match {
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
case VoiceRecording(name, link) =>
s"you received a Voice Recording from $name! Click the link to hear it: $link"
}
}

模式守卫

在模式后面加上if <boolean expression>

仅匹配类型

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Device
case class Phone(model: String) extends Device {
def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
def screenSaverOn = "Turning screen saver on..."
}

def goIdle(device: Device) = device match {
case p: Phone => p.screenOff
case c: Computer => c.screenSaverOn
}

密封类

特质(trait)和类(class)可以用sealed标记为密封的,这意味着其所有子类都必须与之定义在相同文件中,从而保证所有子类型都是已知的。

这对于模式匹配很有用,因为我们不再需要一个匹配其他任意情况的case

正则表达式

.r方法可使任意字符串变成一个正则表达式。

1
2
3
4
5
6
7
8
import scala.util.matching.Regex

val numberPattern: Regex = "[0-9]".r

numberPattern.findFirstMatchIn("awesomepassword") match {
case Some(_) => println("Password OK")
case None => println("Password must contain a number")
}

findFirstMatchIn, findAllMatchIn

提取器对象

提取器对象是一个包含有 unapply 方法的单例对象。

apply 方法就像一个构造器,接受参数然后创建一个实例对象,反之 unapply 方法接受一个实例对象然后返回最初创建它所用的参数。提取器常用在模式匹配和偏函数中。

模式匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scala.util.Random

object CustomerID {

def apply(name: String) = s"$name--${Random.nextLong}"

def unapply(customerID: String): Option[String] = {
val stringArray: Array[String] = customerID.split("--")
if (stringArray.tail.nonEmpty) Some(stringArray.head) else None
}
}

val customer1ID = CustomerID("Sukyoung") // Sukyoung--23098234908
customer1ID match {
case CustomerID(name) => println(name) // prints Sukyoung
case _ => println("Could not extract a CustomerID")
}

当我们调用 CustomerID("Sukyoung") ,其实是调用了 CustomerID.apply("Sukyoung") 的简化语法。当我们调用 case CustomerID(name) => println(name),就是在调用提取器方法

注: 1. case _ 就相当于default

  1. 上面的例子中apply返回的就是一个string
  2. case CustomerID(name)实际上还定义并初始化了一个新变量name

因为变量定义可以使用模式引入变量,提取器可以用来初始化这个变量,使用 unapply 方法来生成值。

1
2
3
val customer2ID = CustomerID("Nico")//Nico---8940217181973413871
val CustomerID(name) = customer2ID
println(name) // prints Nico

上面的代码等价于 val name = CustomerID.unapply(customer2ID).get

如果没有匹配的值,会抛出 scala.MatchError

注:1. unapply方法上面定义时返回一个Option

  1. 虽然unapply返回的是Option,但是用CustomerID(name) = customer2ID的语法得到的name是Option里面的值,而且如果是None会直接报错,相当于编译器自动执行了get方法
  2. 这可以看作是一种语法糖

unapply 方法的返回值应当符合下面的某一条:

  • 如果只是用来判断真假,可以返回一个 Boolean 类型的值。例如 case even()
  • 如果只是用来提取单个 T 类型的值,可以返回 Option[T]
  • 如果你想要提取多个值,类型分别为 T1,...,Tn,可以把它们放在一个可选的元组中 Option[(T1,...,Tn)]

有时,要提取的值的数量不是固定的,因此我们想根据输入来返回随机数量的值。这种情况下,你可以用 unapplySeq 方法来定义提取器,此方法返回 Option[Seq[T]]

泛型

泛型类使用方括号 [] 来接受类型参数。一个惯例是使用字母 A 作为参数标识符

定义:

1
2
3
4
5
6
7
8
9
10
class Stack[A] {
private var elements: List[A] = Nil
def push(x: A) { elements = x :: elements }
def peek: A = elements.head
def pop(): A = {
val currentTop = peek
elements = elements.tail
currentTop
}
}

使用:

1
2
val stack = new Stack[Int]
stack.push(1)

只有当类型 B = A 时, Stack[A]Stack[B] 的子类型才成立。

型变

型变是复杂类型的子类型关系与其组件类型的子类型关系的相关性。

函数的参数类型是变的,而返回类型是变的。

理解:参数类型需要符合里氏替换,B extends A,如果参数类型声明是A,则B(子类)传入也是可以的,这就是逆变(向下)。而返回类型如果是B,那把其当作A(父类)也是可以的,因为A有的方法B都有。但是返回类型如果是A,不能当作B来用。这就是协变(向上)

协变

使用注释 +A,可以使一个泛型类的类型参数 A 成为协变。 对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 AB,如果 AB 的子类型,那么 List[A] 就是 List[B] 的子类型。

逆变

使用注释 -A。对于某个类 class Writer[-A] ,使 A 逆变意味着对于两种类型 AB,如果 AB 的子类型,那么 Writer[B]Writer[A] 的子类型。注意:A和B的位置互换了

不变

允许一个可变的泛型类成为协变并不安全

类型上界与下界

T <: A这样声明的类型上界表示类型变量T应该是类型A的子类。相当于java里? extends A

术语 B >: A 表示类型参数 B 或抽象类型 B 是类型 A 的超类型。 相当于java里? super A

1
2
3
class G[+A]{def fun(x: A){}}
Error: covariant type A occurs in contravariant position in type A of value x
class G[+A]{def fun(x: A){}}

函数参数是逆变点,但此处声明为+A(协变的)

解决办法:

1
2
3
class G[+A]{def fun[B >: A](x: B){}}
//或
class G[-A]{def fun(x: A){}}

G[+A]类似一个生产者,提供数据。(大部分情况下称G为容器类型)
G[-A] 是一个消费者,主要用来消费数据。

PECS 原则 (Producer-extends, consumer-super) 或者也叫 Get and Put 原则

https://hongjiang.info/java-generics/

使用了 <? extends E> 这样的通配符,test方法的参数list变成了只能get不能set(除了null) 或者不严谨的说它变成了只读参数了, 有些类似一个生产者,提供数据。

使用了<? super E> 这种通配符,test方法的参数list的get受到了很大的制约,只能最宽泛的方式来获取list中的数据,相当于get只提供了数据最小级别的访问权限(想想,你可能原本是放进去了一个Book,却只能当作Object来访问)。它更多适合于set的使用场景,像是一个消费者,主要用来消费数据。

内部类

在一些类似 Java 的语言中,内部类是外部类的成员,而 Scala 正好相反,内部类是绑定到外部对象的。

同一个类的不同对象的内部类对象类型是不一样的,除非把入参类型写成 outerClass#innerClass的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Graph {
class Node {
var connectedNodes: List[Graph#Node] = Nil
def connectTo(node: Graph#Node) {
if (!connectedNodes.exists(node.equals)) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}

抽象类型

特质和抽象类可以包含一个抽象类型成员,意味着实际类型可由具体实现来确定。

1
2
3
4
trait Buffer {
type T
val element: T
}

通过抽象类来扩展这个特质后,就可以添加一个类型上边界来让抽象类型T变得更加具体。这个SeqBuffer类就限定了缓冲区中存储的元素类型只能是序列。

1
2
3
4
5
abstract class SeqBuffer extends Buffer {
type U
type T <: Seq[U]
def length = element.length
}

含有抽象类型成员的特质或类(classes)经常和匿名类的初始化一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//工厂方法newIntSeqBuf使用了IntSeqBuf的匿名类实现方式,其类型T被设置成了List[Int]。
abstract class IntSeqBuffer extends SeqBuffer {
type U = Int
}


def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
new IntSeqBuffer {
type T = List[U]
val element = List(elem1, elem2)
}
val buf = newIntSeqBuf(7, 8)
println("length = " + buf.length)
println("content = " + buf.element)

把抽象类型成员转成类的类型参数或者反过来,也是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//这里的变量定义少了type T,T直接写在泛型里了
abstract class Buffer[+T] {
val element: T
}
//+T <: Seq[U]必不可少,相当于type T <: Seq[U]
abstract class SeqBuffer[U, +T <: Seq[U]] extends Buffer[T] {
def length = element.length
}

def newIntSeqBuf(e1: Int, e2: Int): SeqBuffer[Int, Seq[Int]] =
new SeqBuffer[Int, List[Int]] {
//这里也不用显示声明type T = List[U],写在类型参数里了
val element = List(e1, e2)
}

val buf = newIntSeqBuf(7, 8)
println("length = " + buf.length)
println("content = " + buf.element)

复合类型

可以将 obj 的类型同时指定为 CloneableResetable。 这种复合类型在 Scala 中写成:Cloneable with Resetable

复合类型可以由多个对象类型构成,这些对象类型可以有单个细化,用于缩短已有对象成员的签名。 格式为:A with B with C ... { refinement }

自类型

自类型用于声明一个特质必须混入其他特质,尽管该特质没有直接扩展其他特质。 这使得所依赖的成员(就是想用别的trait里的成员)可以在没有导入的情况下使用。

要在特质中使用自类型,写一个标识符,跟上要混入的另一个特质,以及 =>(例如 someIdentifier: SomeOtherTrait =>)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait User {
def username: String
}

trait Tweeter {
this: User => // 重新赋予 this 的类型
def tweet(tweetText: String) = println(s"$username: $tweetText")
}

class VerifiedTweeter(val username_ : String) extends Tweeter with User { // 我们混入特质 User 因为 Tweeter 需要
def username = s"real $username_"
}

val realBeyoncé = new VerifiedTweeter("Beyoncé")
realBeyoncé.tweet("Just spilled my glass of lemonade") // 打印出 "real Beyoncé: Just spilled my glass of lemonade"

因为我们在特质 trait Tweeter 中定义了 this: User =>,现在变量 username 可以在 tweet 方法内使用。 这也意味着,由于 VerifiedTweeter 继承了 Tweeter,它还必须混入 User(使用 with User)。

子类型强制A trait必须和B trait一起使用(如果A中自类型B)

隐式参数

方法可以具有 隐式 参数列表,由参数列表开头的 implicit 关键字标记。当方法需要多个参数的时候,可以定义一些隐式参数,这些隐式参数可以被自动加到方法填充的参数里,而不必手动填充。

1
2
3
4
5
6
7
8
9
10
11
12
def implicitParamFunc(name: String)(implicit tiger: Tiger, lion: Lion): Unit = {
println(name + " have a tiget and a lion, their names are: " + tiger.name + ", " + lion.name)
}

object Zoo {
implicit val tiger = Tiger("tiger1")
implicit val lion = Lion("lion1")
}

import Zoo._

implicitParamFunc("format")

implicitParamFunc方法只调用了一个参数,其他两个参数是隐式的,需要注意的是不仅仅方法中的参数需要被定义成隐式参数,对应的隐式参数的变量也需要被定义成隐式变量

隐式转换

Scala 隐式转换和隐式参数

隐式转换在两种情况下会用到:

  • 如果一个表达式 e 的类型为 S, 并且类型 S 不符合表达式的期望类型 T
  • 在一个类型为 S 的实例对象 e 中调用 e.m, 如果被调用的 m 并没有在类型 S 中声明。

在第一种情况下,搜索一个转换 c,它能适用于 e,并且结果类型为 T。 在第二种情况下,搜索一个转换 c,它能适用于 e,其结果包含名为 m 的成员。

scala.Predef.intWrapper 已经自动提供了一个隐式方法 Int => Ordered[Int]。下面提供了一个隐式方法 List[A] => Ordered[List[A]] 的例子。

1
2
3
4
5
6
7
8
import scala.language.implicitConversions

implicit def list2ordered[A](x: List[A])
(implicit elem2ordered: A => Ordered[A]): Ordered[List[A]] =
new Ordered[List[A]] {
//replace with a more useful implementation
def compare(that: List[A]): Int = 1
}

注意有implicit关键字。这样就可以用下面这个

1
2
List(1, 2, 3) <= List(4, 5)
//<= 是ordered的方法,这里list隐式转成了ordered

多态方法

Scala 中的方法可以按类型和值进行参数化。 语法和泛型类类似。 类型参数括在方括号中,而值参数括在圆括号中。

1
2
3
4
5
6
7
8
def listOfDuplicates[A](x: A, length: Int): List[A] = {
if (length < 1)
Nil
else
x :: listOfDuplicates(x, length - 1)
}
println(listOfDuplicates[Int](3, 4)) // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8)) // List(La, La, La, La, La, La, La, La)

运算符

在Scala中,运算符即是方法。 任何具有单个参数的方法都可以用作 中缀运算符

当一个表达式使用多个运算符时,将根据运算符的第一个字符来评估优先级:

1
2
3
4
5
6
7
8
9
10
(characters not shown below)
* / %
+ -
:
= !
< >
&
^
|
(all letters)

三元表达式

用if/else代替

1
val j = if (i>5) i else 5

注解

在编写与 Java 互操作的 Scala 代码时,注解语法中存在一些差异需要注意。 注意: 确保你在开启 -target:jvm-1.8 选项时使用 Java 注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface SourceURL {
public String value();
public String mail() default "";
}

//java只能显示地写出value
@SourceURL(value = "https://coders.com/",
mail = "support@coders.com")
public class MyClass extends HisClass ...

//scala更灵活
@SourceURL("https://coders.com/",
mail = "support@coders.com")
class MyScalaClass ...

包和导入

包:可以在文件头部声明package users 或者用大括号

导入

1
2
3
4
import users._  // 导入包 users 中的所有成员
import users.User // 导入类 User
import users.{User, UserPreferences} // 仅导入选择的成员
import users.{UserPreferences => UPrefs} // 导入类并且设置别名

Scala 不同于 Java 的一点是 Scala 可以在任何地方使用导入:比如在方法内部

如果存在命名冲突并且你需要从项目的根目录导入,请在包名称前加上 _root_

1
2
3
package accounts

import _root_.users._

注意:包 scalajava.lang 以及 object Predef 是默认导入的。

包对象

Scala 提供包对象作为在整个包中方便的共享使用的容器

按照惯例,包对象的代码通常放在名为 package.scala 的源文件中。

每个包都允许有一个包对象。 在包对象中的任何定义都被认为是包自身的成员。

举例: 假设有一个类 Fruit 和三个 Fruit 对象在包 gardening.fruits 中;

1
2
3
4
5
6
7
// in file gardening/fruits/Fruit.scala
package gardening.fruits

case class Fruit(name: String, color: String)
object Apple extends Fruit("Apple", "green")
object Plum extends Fruit("Plum", "blue")
object Banana extends Fruit("Banana", "yellow")

下面是包对象, 里面有变量 planted 和方法 showFruit

1
2
3
4
5
6
7
8
// in file gardening/fruits/package.scala
package gardening
package object fruits {
val planted = List(Apple, Plum, Banana)
def showFruit(fruit: Fruit): Unit = {
println(s"${fruit.name}s are ${fruit.color}")
}
}

使用:

1
2
3
4
5
6
7
8
9
// in file PrintPlanted.scala
import gardening.fruits._
object PrintPlanted {
def main(args: Array[String]): Unit = {
for (fruit <- planted) {
showFruit(fruit) //showFruit方法就是包对象中的
}
}
}

包对象与其他对象类似,这意味着你可以使用继承来构建它们

参考链接

https://docs.scala-lang.org/zh-cn/overviews/collections/overview.html

https://docs.scala-lang.org/zh-cn/tour/tour-of-scala.html

Scala 新手眼中的十种有趣用法