2020.12.28课堂笔记(《Scala 入门》学习指导)

《Scala 入门》学习指导

一、Scala 简介

Scala 是一种基于 JVM 的多范式编程语言,这里的范式可以理解为一种编程风格,比如面向对象编程就是一种范式。常见范式包括:面向过程、面向对象、泛型以及函数式编程。

1.1 Scala 的特点

1.1.1 静态类型
Scala 的变量声明后不允许改变类型,换句话说所有变量和表达式的类型在编译时就已经完全确定。这一点和 Java 是一样的,反过来说像 Python 则是动态类型语言。
静态类型语言对 IDE 比较友好 ,IDE 可以做到很好的代码感知能力,所以静态类型语言在开发大型复杂系统时更加效率。
静态类型语言可读性较好,多数静态类型语言要求在使用变量之前必须声明数据类型。
1.1.2 强类型
Scala 是强类型语言,意味着 Scala 是类型安全的语言,这点和 Java 也相同, 但 Scala 在声明变量时一般无需显示指定类型,Scala 编译器会自动推断出类型。
Scala 没有提供 Java 中的强制类型转换,取代方法有:
对象名.asInstanceOf[XXX]
对象名.toXXX 方法
隐式转换(implicit 关键字)
1.1.3 面向对象编程(OOP)
Scala 中一切值都是对象,可以说 Scala 比 Java OOP 更纯粹。比如,Java 中类对象(包含静态字段和方法)便不是纯粹的对象,因为类是一个抽象的概念,对象是类的实例,是一个具体的概念。所以 Scala 中去除了 static 关键字,使用一种特殊的对象来模拟类对象(伴生对象),使得类的所有静态内容都可以放置在它的伴生对象中声明和调用。
另外,Scala 去除了接口的概念。替换方案是对象的类型和行为是由类和特质来描述的。类可以由子类和一种灵活的、基于 mixin(动态混入)的组合机制
(它可作为多重继承的简单替代方案)来扩展。
1.1.4 函数式编程(FP)
Scala 中一切函数都是值,所以可以将函数作为参数和返回值。正是因为函数,才使得 Scala 语法如此的简洁、优雅。函数在 Scala 中是一等公民,也是学习 Scala 的门槛。
函数式编程并不是一个新东西,其鼻祖 Lisp 比 C 语言还古老。长期以来函数式编程多用于学术研究中,并不是主流编程范式。随着计算机性能的提升与大数 据的流行,函数式编程更容易编写多并发或多线程的应用,更易于编写利用多 核的应用程序让大家认识到函数式编程固有的优势,使得函数式编程越来越主流。大数据技术中 Spark、Kafka、Storm 等都使用了支持函数式编程的语言来实现, 甚至主流编程语言如 C++、Java、Python 都增加了对函数式编程的支持。

1.2 Scala 初体验讲解要点

在这里插入图片描述
1.Scala 入口函数 main()的结构,对比 Java main()方法的异同。
2.Scala 源文件命名方式。
3.Scala 源文件编译方法。
4.Scala 源文件编译结果解读,本质仍然是 class 文件。
5.Scala 编译结果运行,可以思考如何使用 Java 命令运行?
参考:java -classpath [scala lib jar path] HelloWorld

二、Scala 关键字

Scala 关键字列表:

Java 与 Scala 关键字对比:
在这里插入图片描述
和 Java 一样,通常掌握常用关键字即可,这里先了解,具体在后面各个知识点中这些常用关键字都会陆续用到,建议与 Java 的关键字进行对比讲解。

三、Scala 变量与常量

3.1 变量定义

在 Scala 中变量和常量定义的关键字有所不同,分别用 var 与 val 表示,可记为 Variable 和 Value 的简写。
与 Java 中的变量和常量类似,变量可以重复赋值,而常量一旦初始化将不可以被修改。但如果常量是引用类型,则是可以更改属性的。
一般不严格说明时,变量和常量也统称为变量。变量是可变变量,而常量是不可变变量。

3.2 类型推断(重点)

Scala 编译器可以推断出表达式的类型,因此不必显式地声明。我们可以省略变量的类型、函数返回类型、匿名函数形式参数的类型等。注意,减少自动类型推断,可提高代码可读性。

//使用类型推断是必须指定变量初始值,否则无法进行类型推断。
var a	  //错误定义方式
var a=1   //正确定义方式
var a:Int //错误定义方式
//如果不想赋初值可以使用下划线“_”。
var a:Int=_ //正确定义方式
val a:Int=_ //错误定义方式,因为val 定义为常量

四、Scala 数据类型(重点)

下图是 Scala 数据类型的层次结构图。
图 Scala 类型层次图
总体上分为两类:值类型(AnyVal)和引用类型(AnyRef),根类为 Any。其中值类型与 Java 八大基本类型一致(byte、short、int、long、float、double、char、boolean),注意在 Scala 中首字母均为大写,另外加上一个 Unit 类型。除了 scala.Unit 和 scala.Boolean 外其余都是数字类型。
所有非值类型都是引用类型,如果 Scala 运行中 Java 环境中,AnyRef 相当于java.lang.Object。特别地,Scala 中的 String 类型等同于 java.lang.String,源码如下。
type String = java.lang.String

4.1 值类型与引用类型的区别

值类型:直接存储值,在栈上存储其值。
引用类型:存储对其值的引用,在栈上存储地址,在堆上存储值。
在 Scala 中两个对象进行“==”操作,是比较值是否相等,而非地址,与 Java刚好相反。

case class Po(id:Int)    //使用了后面的内容,理解为 POJO 对象即可
val p1=Po(1)
val p2=Po(1)
p1==p2        //true  比较值
p1.eq(p2)     //false 比较地址

4.2 字面量

字面量就是直接在代码中写常量值(constant value)的方式。包括:
1.型字面量:0、0L、0xFF
2.浮点字面量:0.0、3.14f、.1
3.字符字面量:’a’、’\u0041’、’\n’
4.字符串字面量:”Hello \n world”,多行字符串用三个双引号来表示分隔符,格式为:""" … “”"
5.符号字面量:’<标识符> ,这里 <标识符> 可以是任何字母或数字的标识(注意:不能以数字开头)。对应“scala.Symbol”类型。
6.布尔字面量:true、false

4.3 关于Null,Nothing,Unit

1.关于 Null
scala.Null 是所有引用类型的子类,仅一个实例对象“null”,并且“null”是关键字,但在 Java 中 null 不是对象。既然在 Scala 中是对象,便可以有操作方法,但对 null 的任何操作(比较操作除外)都是非法的,容易引起 java.lang.NullPointerException 异常,应减少 null 的使用。
2.关于 Nothing
Nothing 是所有类型的子类,也是 Null 的子类,含义便是“啥都没有”,所以没有任何实例。Nothing 有什么用?
1.用于类型参数的自动推断
List(1)-> 类 型 为 List[Int] List(“a”,1)->类型可为 List[Any]
那么:List()是什么?答案就是 List[Nothing]。
2.可表示非正常退出
def e = throw new Exception(“error message”)
调用“e”将抛出异常,也是啥值都没有,所以就是 Nothing。
3.关于 Unit
Unit 代表没有任何意义的值类型,他是 AnyVal 的子类型,仅有一个实例对象“()”,叫做无用占位符,类似于 Java 中的 void 类型。
不同的是,在 Java 中 void 表示无返回值,但在 Scala 中,Unit 表示有返回值,便是“()”,因为 Scala 认为一切表达式都有值。

五、Scala 字符串插值

Scala 为我们提供了三种字符串插值的方式,分别是 s, f 和 raw。其本质都是定义在 StringContext 中的方法。在 2.11 以前的版本中,字符串插值对模式匹配语句不适用。

s"Hello, $name"等价于 StringContext("Hello, ", "").s(name) 
s" $name Hello,"等价于 StringContext( "","Hello, ").s(name)
s"hi $name Hello,"等价于 StringContext(" hi"," Hello,").s(name)

可以发现,所谓字符串插值便是使用了隐式转换功能实现的,这一点可以在后面学到隐式转换相关知识点时再进行回顾。

六、Scala 程序控制(重点)

6.1 条件控制

6.1.1 if 表达式的类型推断
任意表达式都有值,if 语句也不例外。那么 if 语句返回值是什么类型呢?虽然 Scala 可以自动类型推断出单个字面量的类型,但是 if…else…可能返回多个不同类型的值,又该如何判断?

val x=10
val y=if(x==10) x+1 else "我是字符串"

此时 y 的类型为 Int 和 String 的公共父类型,Int 的父类为 AnyVal,String 的父类是 AnyRef,再往上找到 AnyVal 和 AnyRef 的父类 Any。所以 y 是 Any 类型。根据此结论,请试一试下面 y 是什么类型:

val y=if(x==10) x+1 else true

另外,如果 if 后面没有跟 else,则默认 else 部分的值是 Unit 即“()”。
6.1.2 关于交互式解释器(REPL)
REPL 默认只能解释一行语句,但是 if 表达式通常是多行组成,所以通常在
REPL 中使用“:paste”结合“Ctrl+D”完成。如下代码:

val x=10 if(x>10){
	print("x 大于 10")
}
else{
	print("x 小于等于 10")
}

上面代码直接复制到 REPL 是运行失败的。
正确的方式:
在这里插入图片描述
注意:这里只针对 REPL 环境,其他环境无需考虑多行代码问题。上面代码也可以写在一行中解决:

val x=10;if(x>10){print("x 大于 10")}else{print("x 小于等于 10")}

默认情况下,scala 不需要语句终结符“;”,默认将每一行作为一个语句。如果语句块只有一行语句,省略“{}”即可。所以下面写法也是可以的。

val x=10
if(x>10)print("x 大于 10") else print("x 小于等于 10")

6.1.3 关于块表达式
在 Scala 中,“ {} ”块包含一系列值表达式,其结果也是表达式。块中最后一个表达式的值就是块的值,即 “{} ”块的值取决于最后一个表达式。
1.赋值语句问题
赋值语句动作没有值,准确来说,赋值语句的值为 Unit 类型,即“()”。

scala> var x=10
x: Int = 10
scala> val y={x=x+1}
y: Unit = ()

思考下面代码有什么问题。(Scala 中不允许连续赋值)

scala> var x=0
x: Int = 0
scala> var y=0 
y: Int = 0
scala> x=y=10
<console>:13: error: type mismatch;
 found   : Unit
 required: Int
       x=y=10

2.作用域问题
创建代码块时即创建了一个作用域,在代码块内可以访问代码块外的实体(变量、函数、类),反之不成立。

6.2 循环控制

在 Scala 中极少使用 while 循环,重点掌握 for 循环和 for 推导式。
6.2.1 基本用法
for 循环基本结构:

for (i<- 表达式) {
	statements
}

首先,变量 i 不需要声明。严格来说是 i 前面不允许有 val 或 var。i 可以是作用域内的现有变量。
其次“<-”是关键字,非操作符,类似 Java 中的“:”。

最后表达式可以是数组、元组、集合。在 Scala 中 Range 是常用的集合类型, 表示范围区间。
1 to n: 返回 1 到 n 的区间,包含 n
1 until n: 返回 1 到 n 的区间,不包含 n 1 to 10 by 2:返回 Range(1, 3, 5, 7, 9)
1 until 10 by 2:返回 Range(1, 3, 5, 7, 9)
“变量<-表达式”也称为生成器。
6.2.2 高级用法
1.条件过滤(守卫)
每个生成器都可以带一个或多个守卫。

val num:Int = 10;
for ( i: Int <- 1 to num ; if (i%2==0) )	//其中“;”,if 语句“()”可选
{
	println( i * 100 );
}
简化格式:for ( i: Int <- 1 to num if i%2==0) println( i * 100 )
更多守卫:for ( i: Int <- 1 to num if i%2==0; if i>2) println( i * 100 )

2.多生成器
可以提供多个生成器完成嵌套循环,多个表达式使用“;”分隔。多个表达式对应多重循环效果。

for(i<-1 to 5 if i%2==0;j<-1 to 5 if j%2!=0) println(s"$i+$j=${i+j}")

Scala 中有这样一个编程风格, 如果 “()” 中有多个表达式的话, 可以使用“{}” 代替 “()”来写一个块表达式。上面的示例等价于下面代码。

for {i <- 1 to 5 if i % 2 == 0
     j <- 1 to 5 if j % 2 != 0}
  println(s"$i+$j=${i + j}")

或者

for{ i<-1 to 5 if i%2==0; j<-1 to 5 if j%2!=0 } println(s"$i+$j=${i+j}")

3.中断(难点)
在 Java 中,break 是关键字,而在 Scala 中没有提供它,取而代之的是函数式风格实现。关键是 scala.util.control.Breaks 中 breakable()和 break()函数,源码如下 :

def breakable(op: => Unit) {
  try {
    op
  } catch {
    case ex: BreakControl =>
      if (ex ne breakException) throw ex
  }
}
def break(): Nothing = {
  throw breakException
}

可以发现,breakable()实际上是对代码块进行 try-catch,而 break()则是直接抛出异常达到中断目的。实际上我们是进行了上面两个函数的调用而已,看下面

示例:

import scala.util.control.Breaks._
//注意先导入
breakable {
  //调用 breakable()函数,op:=>Unit 表示无参函数
  for (i <- 1 to 10) {
    if (i == 5) {
      break () //调用 break()函数
    }
    println (i)
  }
}

同时,Breaks 的伴生对象(详见 Scala OOP 中的解释)继承了自身:
object Breaks extends Breaks
使得 Breaks 类中的所有函数变成静态函数,故上述代码可以写为:

import scala.util.control.Breaks._
for (i <- 1 to 10) {
  if (i == 5) {
    break //调用 break()函数,无参数可省略“()”
  }
  println(i)
}

6.2.3 for 推导式(难点)
如果 for 循环的循环体以 yield 开始, 则该循环会构造出一个集合, 每次迭代生成集合中的一个, 这类循环叫做 for 推导式。

val j=for ( i: Int <- 1 to num; if i%2==0 ) yield i

for 推导式返回的类型与表达式“1 to num”兼容。如果有多个生成器,则与第一个生成器兼容。
注意循环体必须以 yield 开头,多行语句使用 yield{…}。

七、Scala 数据结构(重点)

在 Scala 中,对象一般都有两种方式创建,一种使用类创建,一种使用其对应的伴生对象创建,区别在于是否使用 new 关键字。本章暂不涉及 OOP 的内容, 了解伴生对象创建的方式即可。
Scala 数据结构是非常重要的知识点,并且Scala 中的数据结构远比Java 丰富、复杂。但是基本原理都是相同的,比如说数组、栈、队列、链表、树、散列表。这里主要关注线性结构,包括 Array、Seq、List、Map 及 Set 等。
对比 Java , Scala 中的数据结构都强调了是可变还是不可变, 即mutable/immutable,默认情况下,Scala 会为用户选择不可变的数据结构。两者的差别在于:
不可变:初始化后内容不再发生变化。
可变:初始化后内容可以发生变化。

7.1 数组(scala.Array)

数组是可变的,但长度是固定的。当数组初始化后,我们可以改变每个元素值,但无法再往数组中追加新的元素。

var a1:Array[String] = new Array[String](3)

初始化时,默认值视类型参数而定,String 为 null,Int 则为 0,未指定类型参数时数组元素类型为 Nothing,默认值为 null。

a1(0)="Jason"

访问方式仍然是函数式风格。如果需要变长的数组可使用 ArrayBuffer,如下所示:

var ab=new scala.collection.mutable.ArrayBuffer[Int]()
ab.append(1)
ab.append(2)
ab+=3

对于可变长数组,这些操作均在原数组上进行。
小结:数组内容都是可变的,但区分为可变长与不可变长数组。

7.2 元组

元组在 Scala 中是非常重要一种数据结构,允许用户最多存储 22 个不同类型的元素,本质上分别对应 Tuple1~Tuple22 共 22 个类。除了 Tuple1 外其他的都可以使用“()”快速创建相应元组。

var tp1=new Tuple1("xxx")
print(tp1._1)
var tp2=(1,2)
print(tp2._2)

元组访问使用“_”,下标由 1 开始。运行如下语句将会发生错误:

for(i<-tp2)println(i)

原因是元组不支持遍历( Traversable ), 所以在需要遍历是使用元组的productIterator()函数产生 Iterator 对象。正确写法如下:

for(i<-tp2.productIterator)println(i)

正是因为有了元组这种结构,用户可以非常方便地为函数扩展参数与返回值, 可以说 Scala 代码如此优雅、简洁,元组功不可没。在后面课程中会大量使用键值对的结构,使用元组来表示再合适不过了。
元组是不可变的。意味着元组一旦初始化,用户无法改变元组的内容。如下操作便是非法的:tp2._1=2

7.3 集合

Scala 集合分三大类:Seq、Set 及 Map,顶层为 Traversable,这是一个特质。主要提供如下通用集合方法的实现:
Iterable 新增的功能不多,主要的有获得迭代器函数 iterator(),源码如下:

def iterator: Iterator[A]

而迭代器的特点是数据只能迭代一次。如下代码所示:

val it=Iterable(1,2,3,4,5).iterator
while(it.hasNext) print(it.next) //重复运行此行代码

可以发现,第二次运行时 it 中的内容已经为空。
7.3.1 Seq 序列
Seq 同样分为可变和不可变两大类, 此外还派生出 IndexedSeq 和
LinearSeq 两个重要的子特质。
 IndexedSeq :代表索引序列,对于基于索引的操作来说效率较高,一般底层依赖于数组实现。如 Vector、String、Range、ArrayBuffer。
 LinearSeq :代表线性序列,对于 head、tail,以及 isEmpty 一类的方法效率较高,一般底层依赖于链表实现。如 List、Stream、Stack、Queue、LinkedList。
LinearSeq 和子类 List,计算 length 的时间复杂度为 O(n),而 IndexedSeq 花费O(1)。O(n)就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。再比如时间复杂度 O(n^2),就代表数据量增大 n 倍时,耗时增大 n 的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的 O(n^2)的算法,对 n 个数排序,需要扫描 n×n 次。

Vector 类目前可以被认为是这么一个通用的不可变数据结构。Vector 是一个带索引的不可变序列容器,如果用户更倾向于使用一个链式不可变集合容器, 那么就可以选择 List。
序列练习以 List 为主即可,其余的可以根据适用的场景灵活去选择。
7.3.2 Set 集合
Set 操作与 List 类似,其主要特点如下:
 集合中的元素不允许重复(常用于去重)。
 Set 中的元素是无序的。
 Set 提供并、差、交集运算。
7.3.3 Map 映射
Map 类似 Java 中的哈希表,存储键值对,是广泛使用的一种数据结构。默认情况下 Scala 使用不可变 Map(scala.collection.immutable.Map)。注意如下代码:

执行上述代码后 m 为 Map(“a” -> 1, “j” -> 0),是否与 Map 不可变冲突呢?实际上,“+=”操作后产生了一个新 Map。如果这里将变量 m 改为 val 定义,即会报错。对于所有不可变集合,“+=”都是一个道理。
7.3.3 集合练习小结
对于集合的学习,理解上并无难度,只需多加练习操作即可。下图指出了常用的集合及创建方法,需要学员熟练掌握,红色部分是最低要求。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值