Scala

Scala简介

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

Scala的特点

静态类型

Scala的变量声明后不允许改变类型,换句话说所有变量和表达式的类型在编译时就已经完全确定。这一点和Java是一样的,反过来说像Python则是动态类型语言。
静态类型语言对IDE比较友好,IDE可以做到很好的代码感知能力,所以静态类型语言在开发大型复杂系统时更加效率。
静态类型语言可读性较好,多数静态类型语言要求在使用变量之前必须声明数据类型。

强类型

Scala是强类型语言,意味着Scala是类型安全的语言,这点和Java也相同,但Scala在声明变量时一般无需显示指定类型,Scala编译器会自动推断出类型。
Scala没有提供Java中的强制类型转换,取代方法有:
对象名.asInstanceOf[xxx]
对象名.toXXX方法
隐式转换(implicit关键字)

面向对象编程(oop)

Scala中一切值都是对象,可以说Scala比Java OOP更纯粹。比如,Java中类对象(包含静态字段和方法)便不是纯粹的对象,因为类是一个抽象的概念,对象是类的实例,是一个具体的概念。所以Scala中去除了static关键字,使用一中特殊的对象来模拟类对象(伴生对象),使得类的所有静态内容都可以放置在它的伴生对象中声明和调用。
另外,Scala去除了接口的概念。替换方案是对象的类型和行为是由类和特质来描述的。类可以由子类和一种灵活的、基于mixin(动态混入)的组合机制(它可作为多重继承的简单替换方案)来扩展。

函数式编程(FP)

Scala中一切函数都是值,所以可以将函数作为参数和返回值。正是因为函数,才使得Scala语法如此的简洁、优雅。函数在Scala中是一等公民,也是学习Scala的门槛。
函数式编程并不是一个新东西,其鼻祖Lisp比C语言还古老。长期以来函数式编程多用于学术研究中,并不是主流编程范式。随着计算机性能的提升与大数据的流行,函数式编程更容易编写多并发或多线程的应用,更易于编写利用多核的应用程序让大家认识到函数式编程固有的优势,使得函数式编程越来越主流。大数据技术中Spark、Kafka、Storm等都使用了支持函数式编程的语言来实现,甚至主流编程原因呢如C++、Java、Python都增加了对函数式编程的支持。

Scala初体验讲解要点

object HelloWorld {
	def main(args:Array[String]):Unit = {
		println("Hello,world!")
	}
}

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

Scala关键字

abstractcasecatchclass
defdoelseextends
falsefinalfinallyfor
forSomeifimplicitimport
lazymatchnewnull
objectoverridepackageprivate
protectedreturnsealedsuper
thisthrowtraittry
truetypevalvar
whilewithyield(空格)
-:==>
<-<:<%>:
#@

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

Scala变量与常量

变量定义

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

类型推断

Scala编译器可以推断出表达式的类型,因此不必显式的声明。我们可以省略变量的类型、函数返回类型、匿名函数形式参数的类型等。注意,减少自动类型推断,可提高代码可读性。
使用类型推断是必须指定变量初始值,否则无法进行类型推断。
var a // 错误定义方式
var a = 1 // 正确定义方式
var a:Int // 错误定义方式
如果不想赋初值可以使用下划线“_”。
var a:Int = _ // 正确定义方式
val a:Int = _ // 错误定义方式,因为val定义为常量

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

值类型与引用类型的区别

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

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

字面量

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

关于null

scala.Null是所有引用类型的子类,仅一个实例对象“null”,并且“null”是关键字,但在Java中null不是对象。既然在Scala中是对象,便可以有操作方法,但对null的任何操作(比较操作除外)都是非法的,容易引起java.lang.NullPointerException异常,应减少null的使用。

关于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

关于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程序控制

条件控制

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即"()"。

关于交互式解释器(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")

关于块表达式

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

scala> var x = 10
x: Int = 10

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

思考一下代码有什么问题

var x = 0
var y = 0
x = y = 10

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

循环控制

在Scala中极少使用while循环,重点掌握for循环和for推导式。

基本用法

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)
“变量 <- 表达式”也称为生成器

高级用法

1、条件过滤(守卫)
每个生成器都可以带一个或多个守卫。

val num = 10
for(i <- 1 to num;if(i%2==0))   // 其中";",if语句“()”可选
{
	println(i * 100)
}

简化格式:for(i <- 1 to num if i%20) println(i * 100)
更多守卫:for(i <- 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}")

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)
}

for推导式

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

val j = for (i <- 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会为用户选择不可变的数据结构。两者的差别在于:
不可变:初始化后内容不再发生变化
可变:初始化后内容可以发生变化

数组

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

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

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

元祖

元祖在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)

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

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

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

集合

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

def iterator:Iterator[A]

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

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

可以发现,第二次运行时it中的内容已经为空
Scala集合顶层层次结构图如下图所示。
在这里插入图片描述

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为主即可,其余的可以根据适用的场景灵活选择。

Set集合

Set操作与List类似,其主要特点如下:
集合中的元素不允许重复(常用于去重)
Set中的元素是无序的
Set提供并、差、交集运算

Map映射

Map类似于Java中的哈希表,存储键值对,是广泛使用的一种数据结构。默认情况下Scala使用不可变Map(scala.collection.immutable.Map)。注意如下代码:

var m = Map("a" -> 1)
m += ("j" -> 0)   // 新加一个键值对

执行上述代码后m为Map(“a” -> 1,“j” -> 0),是否与Map不可变冲突呢?实际上,“+=”操作后产生一个新的Map。如果这里将变量m改为val定义,即会报错。对于所有不可变集合,“+=”都是一个道理。

集合练习小结

对于集合的学习,理解上并无难度,只需多加练习操作即可。下图指出了常用的集合及创建方法,需要学员熟练掌握,红色部分是最低要求。
在这里插入图片描述
示例:

// Buffer
val buffer = scala.collection.mutable.ArrayBuffer[Int](10,20,30)
Buffer += (2,3)

// Array
val numbers = Array(1,2,3,4)
val first = numbers(0)   // read the first element
numbers(3) = 100   // replace the 4th array element with 100
val biggerNumbers = numbers.map(_*2)   // multiply all numbers by two 

// List
// Make a list via the companion object factory
val days = List("Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday")
// Make a list element-by-element
val when = "AM" :: "PM" :: List()

// Map
val m = Map("a" -> 123, "x" -> 789)
m("x")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值