Fork me on GitHub

Spark Core:Spark的算子

一.RDD基础

RDD

1.什么是RDD?

    RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。

2.RDD的属性(源码中的一段话)

(a)是一组分区

    理解:RDD是由分区组成的,每个分区运行在不同的Worker上,通过这种方式,实现分布式计算。

(b)split理解为分区

    在RDD中,有一系列函数,用于处理计算每个分区中的数据。这里把函数叫做算子。

(c )RDD之间存在依赖关系。窄依赖,宽依赖

    需要用依赖关系来划分Stage,任务是按照Stage来执行的。

(d)可以自动以分区规则来创建RDD

    创建RDD时,可以指定分区,也可以自定义分区规则。

(e)优先选择离文件位置近的节点来执行任务

3.RDD的创建方式

(1)通过外部的数据文件创建,如HDFS
1
val rdd1 = sc.textFile(“hdfs://192.168.1.121:9000/data/data.txt”)
(2)通过sc.parallelize进行创建
1
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))

4.RDD的类型:

    TransformationAction

5.RDD的基本原理

RDD的基本原理

二.Transformation

    RDD中的所有转换都是延迟加载的,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给Driver的动作时,这些转换才会真正运行。这种设计让Spark更加有效率地运行。

转换 含义
map(func) 返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
filter(func) 返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成
flatMap(func) 类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)
mapPartitions(func) 类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func) 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]
sample(withReplacement, fraction, seed) 根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子
union(otherDataset) 对源RDD和参数RDD求并集后返回一个新的RDD
intersection(otherDataset) 对源RDD和参数RDD求交集后返回一个新的RDD
distinct([numTasks])) 对源RDD进行去重后返回一个新的RDD
groupByKey([numTasks]) 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
reduceByKey(func, [numTasks]) 在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
aggregateByKey(zeroValue)(seqOp,combOp,[numTasks])
sortByKey([ascending], [numTasks]) 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks]) 与sortByKey类似,但是更灵活
join(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
cartesian(otherDataset) 笛卡尔积
pipe(command, [envVars])
coalesce(numPartitions)
repartition(numPartitions)
repartitionAndSortWithinPartitions(partitioner)

1.举例:

(1)创建一个RDD,使用List
1
2
3
4
5
6
7
8
9
10
11
12
13
val rdd1 = sc.parallelize(List(1,2,3,4,5,8,34,100,79))

val rdd2 = rdd1.map(_*2)

rdd2.collect

rdd2.sortBy(x=>x,true)

rdd2.sortBy(x=>x,true).collect

rdd2.sortBy(x=>x,false).collect

rdd2.filter(_>20).collect

(2)创建一个RDD,字符串
1
2
3
4
5
val rdd4 = sc.parallelize(Array("a b c","d e f","x y z"))

val rdd5 = rdd4.flatMap(_.split(" "))

rdd5.collect

(3)集合操作
1
2
3
4
5
6
7
8
9
10
11
12
13
val rdd6 = sc.parallelize(List(5,6,7,8,9,10))

val rdd7 = sc.parallelize(List(1,2,3,4,5,6))

val rdd8 = rdd6.union(rdd7)

rdd8.collect

rdd8.distinct.collect

val rdd9 = rdd6.intersection(rdd7)

rdd9.collect

(4)分组操作:reduceByKey groupByKey
1
2
3
4
5
6
7
8
9
10
11
12
13
val rdd1 = sc.parallelize(List(("Tom",1000),("Jerry",3000),("Mary",2000)))

val rdd2 = sc.parallelize(List(("Jerry",1000),("Tom",3000),("Mike",2000)))

val rdd3 = rdd1 union rdd2

rdd3.collect

val rdd4 = rdd3.groupByKey

rdd4.collect

rdd3.reduceByKey(_+_).collect

注意:使用分组函数时,不推荐使用groupByKey,因为性能不好,官方推荐reduceByKey

(5)cogroup操作
1
2
3
4
5
6
7
val rdd1 = sc.parallelize(List(("Tom",1),("Tom",2),("jerry",1),("Mike",2)))

val rdd2 = sc.parallelize(List(("jerry",2),("Tom",1),("Bob",2)))

val rdd3 = rdd1.cogroup(rdd2)

rdd3.collect

(6)reduce操作(reduce是一个Action)
1
2
3
val rdd1 = sc.parallelize(List(1,2,3,4,5))

val rdd2 = rdd1.reduce(_+_)

(7)需求:按value排序,SortByKey按照key排序。

做法:把Key Value交换位置,并且交换两次。
(a).第一步交换,把key value交换,然后调用sortByKey
(b).调换位置

1
2
3
4
5
6
7
8
9
10
11
12
13
val rdd1 = sc.parallelize(List(("tom",1),("jerry",1),("kitty",2),("bob",1)))

val rdd2 = sc.parallelize(List(("jerry",2),("tom",3),("kitty",5),("bob",2)))

val rdd3 = rdd1 union(rdd2)

val rdd4 = rdd3.reduceByKey(_+_)

rdd4.collect

val rdd5 = rdd4.map(t => (t._2,t._1)).sortByKey(false).map(t=>(t._2,t._1))

rdd5.collect

三.Action

动作 含义
reduce(func) 通过func函数聚集RDD中的所有元素,这个功能必须是课交换且可并联的
collect() 在驱动程序中,以数组的形式返回数据集的所有元素
count() 返回RDD的元素个数
first() 返回RDD的第一个元素(类似于take(1))
take(n) 返回一个由数据集的前n个元素组成的数组
takeSample(withReplacement,num, [seed]) 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子
takeOrdered(n, [ordering])
saveAsTextFile(path) 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path) 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统
saveAsObjectFile(path)
countByKey() 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数
foreach(func) 在数据集的每一个元素上,运行函数func进行更新

四.RDD的缓存机制

    RDD通过persist方法或cache方法可以将前面的计算结果缓存,但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

    通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。

    缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

(1)Demo示例:

(2)通过UI进行监控:

五.RDD的Checkpoint(检查点)机制:容错机制

    检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage(血统)做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。

    设置checkpoint的目录,可以是本地的文件夹、也可以是HDFS。一般是在具有容错能力,高可靠的文件系统上(比如HDFS, S3等)设置一个检查点路径,用于保存检查点数据。
    分别举例说明:

(1)本地目录

注意:这种模式,需要将spark-shell运行在本地模式

(2)HDFS的目录

注意:这种模式,需要将spark-shell运行在集群模式

(3)源码中的一段话

六.RDD的依赖关系和Spark任务中的Stage

1.RDD的依赖关系

    RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

(1)窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用

总结:窄依赖我们形象的比喻为独生子女

(2)宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition

总结:窄依赖我们形象的比喻为超生

2.Spark任务中的Stage

    DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据

七.RDD基础练习

(1)练习1:
1
2
3
4
5
6
7
8
//通过并行化生成rdd
val rdd1 = sc.parallelize(List(5, 6, 4, 7, 3, 8, 2, 9, 1, 10))
//对rdd1里的每一个元素乘2然后排序
val rdd2 = rdd1.map(_ * 2).sortBy(x => x, true)
//过滤出大于等于十的元素
val rdd3 = rdd2.filter(_ >= 10)
//将元素以数组的方式在客户端显示
rdd3.collect
(2)练习2:
1
2
3
4
val rdd1 = sc.parallelize(Array("a b c", "d e f", "h i j"))
//将rdd1里面的每一个元素先切分在压平
val rdd2 = rdd1.flatMap(_.split(' '))
rdd2.collect
(3)练习3:
1
2
3
4
5
6
7
8
9
val rdd1 = sc.parallelize(List(5, 6, 4, 3))
val rdd2 = sc.parallelize(List(1, 2, 3, 4))
//求并集
val rdd3 = rdd1.union(rdd2)
//求交集
val rdd4 = rdd1.intersection(rdd2)
//去重
rdd3.distinct.collect
rdd4.collect
(4)练习4:
1
2
3
4
5
6
7
8
9
10
11
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
//求jion
val rdd3 = rdd1.join(rdd2)
rdd3.collect
//求并集
val rdd4 = rdd1 union rdd2
//按key进行分组
rdd4.groupByKey
rdd4.collect

(5)练习5:
1
2
3
4
5
6
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
//cogroup
val rdd3 = rdd1.cogroup(rdd2)
//注意cogroup与groupByKey的区别
rdd3.collect
(6)练习6:
1
2
3
4
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5))
//reduce聚合
val rdd2 = rdd1.reduce(_ + _)
rdd2.collect
(7)练习7:
1
2
3
4
5
6
7
8
9
val rdd1 = sc.parallelize(List(("tom", 1), ("jerry", 3), ("kitty", 2),  ("shuke", 1)))
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 3), ("shuke", 2), ("kitty", 5)))
val rdd3 = rdd1.union(rdd2)
//按key进行聚合
val rdd4 = rdd3.reduceByKey(_ + _)
rdd4.collect
//按value的降序排序
val rdd5 = rdd4.map(t => (t._2, t._1)).sortByKey(false).map(t => (t._2, t._1))
rdd5.collect

Spark Core:执行Spark任务的两个工具:spark-submit与spark-shell

1.spark-submit:用于提交Spark任务

(1)举例:spark 自带的实例程序。

/opt/module/spark-2.1.0-bin-hadoop2.7/examples/jars中有Spark自带的实例程序。

蒙特卡洛求PI(圆周率)

1
2
3
cd /opt/module/spark-2.1.0-bin-hadoop2.7

bin/spark-submit --master spark://hadoop1:7077 --class org.apache.spark.examples.SparkPi /opt/module/spark-2.1.0-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.1.0.jar 500

启动命令
执行结果

2.Spark-shell

(1)概念:相当于REPL工具,命令行工具,作为一个独立的Application运行
(2)两种运行模式:
(a)本地模式:不需要连接到Spark集群,在本地直接运行,用于测试

启动:

1
2
//后面不写任何参数,代表本地模式
bin/spark-shell

本地模式
local代表本地模式

(b)集群模式

命令:

1
bin/spark-shell --master spark://hadoop1:7077

集群模式

特殊说明:

  • Spark session(spark) : Spark2.0以后提供的,利用session可以访问所有spark组件(core sql..)
  • spark context(sc) 两个对象,可以直接使用

Spark HA的实现

分布式的HA

1.基于文件系统的单点恢复(注意:不能用于生产,主要用于开发和测试)

    主要用于开发或测试环境。当spark提供目录保存spark Application和worker的注册信息,并将他们的恢复状态写入该目录中,这时,一旦Master发生故障,就可以通过重新启动Master进程(sbin/start-master.sh),恢复已运行的spark Application和worker的注册信息。

    基于文件系统的单点恢复,主要是在spark-env.sh里对SPARK_DAEMON_JAVA_OPTS设置

配置参数 参考值
spark.deploy.recoveryMode 设置为FILESYSTEM开启单点恢复功能,默认值:NONE
spark.deploy.recoveryDirectory Spark 保存恢复状态的目录
(1)在spark-env.sh中添加
1
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/opt/module/spark-2.1.0-bin-hadoop2.7/recovery"
(2)测试:

a.在hadoop1上启动Spark集群
b.在hadoop2上启动spark shell

1
MASTER=spark://hadoop1:7077 spark-shell

c.在hadoop1上停止master

1
stop-master.sh

d.观察hadoop2上的输出:

e.在hadoop1上重启master

1
start-master.sh

2.基于Zookeeper的Standby Masters

    ZooKeeper提供了一个Leader Election机制,利用这个机制可以保证虽然集群存在多个Master,但是只有一个是Active的,其他的都是Standby。当Active的Master出现故障时,另外的一个Standby Master会被选举出来。由于集群的信息,包括Worker, Driver和Application的信息都已经持久化到ZooKeeper,因此在切换的过程中只会影响新Job的提交,对于正在进行的Job没有任何的影响。加入ZooKeeper的集群整体架构如下图所示。

配置参数 参考值
spark.deploy.recoveryMode 设置为ZOOKEEPER开启单点恢复功能,默认值:NONE
spark.deploy.zookeeper.url ZooKeeper集群的地址
spark.deploy.zookeeper.dir Spark信息在ZK中的保存目录,默认:/spark
(1)做法:

(a)在每个节点中的spark-env.sh文件中添加以下代码:

1
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop1:2181,hadoop2:2181,hadoop3:2181 -Dspark.deploy.zookeeper.dir=/spark"

(b)另外:每个节点上,需要将以下两行注释掉。

1
2
# export SPARK_MASTER_HOST=hadoop1
# export SPARK_MASTER_PORT=7077

(c )start-all启动spark集群
从任意Worker节点,再启动一个Master

(d)ZooKeeper中保存的信息

Spark-安装配置:伪分布式安装和全分布式安装

0.准备工作:安装JDK,配置主机名,免密登陆

一.伪分布模式搭建:

1.解压

1
tar -zxvf spark-2.1.0-bin-hadoop2.7.tgz -C /opt/module

2.修改配置文件:

(1)修改spark-env.sh

重命名(拷贝):

1
2
3
cd /opt/module/spark-2.1.0-bin-hadoop2.7/conf

cp spark-env.sh.template spark-env.sh

修改:

1
vi spark-env.sh

修改内容:

1
2
3
4
5
6
7
export JAVA_HOME=/opt/module/jdk1.8.0_144 
export SPARK_MASTER_HOST=hadoop1
export SPARK_MASTER_PORT=7077

//下面的可以不写,默认
export SPARK_WORKER_CORES=1
export SPARK_WORKER_MEMORY=1024m

(2)修改slaves
1
cp slaves.template slaves 

修改:

1
2
3
cd /opt/module/spark-2.1.0-bin-hadoop2.7/conf

vi slaves

修改内容:

1
hadoop1

二.全分布模式安装:

1.解压

1
tar -zxvf spark-2.1.0-bin-hadoop2.7.tgz -C /opt/module

2.修改配置文件:

(1)修改spark-env.sh

重命名(拷贝):

1
2
3
cd /opt/module/spark-2.1.0-bin-hadoop2.7/conf

cp spark-env.sh.template spark-env.sh

修改:

1
vi spark-env.sh

修改内容:

1
2
3
4
5
6
7
export JAVA_HOME=/opt/module/jdk1.8.0_144 
export SPARK_MASTER_HOST=hadoop1
export SPARK_MASTER_PORT=7077

//下面的可以不写,默认
export SPARK_WORKER_CORES=1
export SPARK_WORKER_MEMORY=1024m

(2)修改slaves
1
cp slaves.template slaves 

修改:

1
vi slaves

修改内容:

1
2
hadoop2
hadoop3

3.发送到另外两台服务器:

1
2
3
scp -r spark-2.1.0-bin-hadoop2.7/ root@hadoop2:/opt/module/

scp -r spark-2.1.0-bin-hadoop2.7/ root@hadoop3:/opt/module/

4.启动:

hadoop1:

1
2
3
cd /opt/module/spark-2.1.0-bin-hadoop2.7

sbin/start-all.sh

hadoop1
hadoop2
hadoop3

5.验证:

浏览器输入hadoop1IP地址:192.168.1.121:8080

Spark是什么

1.什么是Spark?

    Spark是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学伯克利分校AMPLab,2010年开源,2013年6月成为Apache孵化项目,2014年2月成为Apache顶级项目。目前,Spark生态系统已经发展成为一个包含多个子项目的集合,其中包含SparkSQL、Spark Streaming、GraphX、MLlib等子项目,Spark是基于内存计算的大数据并行计算框架。Spark基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量廉价硬件之上,形成集群。Spark得到了众多大数据公司的支持,这些公司包括Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。当前百度的Spark已应用于凤巢、大搜索、直达号、百度大数据等业务;阿里利用GraphX构建了大规模的图计算和图挖掘系统,实现了很多生产系统的推荐算法;腾讯Spark集群达到8000台的规模,是当前已知的世界上最大的Spark集群。

2.为什么要使用Spark?

(1)Hadoop的MapReduce计算模型存在的问题:

    MapReduce的核心是Shuffle(洗牌)。在整个Shuffle的过程中,至少会产生6次的I/O。下图是MapReduce的Shuffle的过程。
    中间结果输出:基于MapReduce的计算引擎通常会将中间结果输出到磁盘上,进行存储和容错。另外,当一些查询(如:Hive)翻译到MapReduce任务时,往往会产生多个Stage(阶段),而这些串联的Stage又依赖于底层文件系统(如HDFS)来存储每一个Stage的输出结果,而I/O的效率往往较低,从而影响了MapReduce的运行速度。

MapReduce的Shuffle

(2)Spark的最大特点:基于内存

    Spark是MapReduce的替代方案,而且兼容HDFS、Hive,可融入Hadoop的生态系统,以弥补MapReduce的不足。

3.Spark的特点:快、易用、通用、兼容性

(1)快

    与Hadoop的MapReduce相比,Spark基于内存的运算速度要快100倍以上,即使,Spark基于硬盘的运算也要快10倍。Spark实现了高效的DAG执行引擎,从而可以通过内存来高效处理数据流。

(2)易用

    Spark支持Java、Python和Scala的API,还支持超过80种高级算法,使用户可以快速构建不同的应用。而且Spark支持交互式的Python和Scala的shell,可以非常方便地在这些shell中使用Spark集群来验证解决问题的方法。

(3)通用

    Spark提供了统一的解决方案。Spark可以用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应用中无缝使用。Spark统一的解决方案非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。
    另外Spark还可以很好的融入Hadoop的体系结构中可以直接操作HDFS,并提供Hive on Spark、Pig on Spark的框架集成Hadoop。

(4)兼容性

    Spark可以非常方便地与其他的开源产品进行融合。比如,Spark可以使用Hadoop的YARN和Apache Mesos作为它的资源管理和调度器,器,并且可以处理所有Hadoop支持的数据,包括HDFS、HBase和Cassandra等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。Spark也可以不依赖于第三方的资源管理和调度器,它实现了Standalone作为其内置的资源管理和调度框架,这样进一步降低了Spark的使用门槛,使得所有人都可以非常容易地部署和使用Spark。此外,Spark还提供了在EC2上部署Standalone的Spark集群的工具。

4.Spark集群的体系结构

Spark集群的体系架构1

Spark集群的体系架构2

Spark集群的体系架构3

(1)结构
(a)Master:接收任务请求,分发任务
(b)Worker:本节点任务调度,资源管理。默认占用该节点所有资源
  • Spark对内存没有很好管理,容易出现OOM。Spark把内存管理交给应用程序。
  • Spark架构出现单点故障问题,通过HA解决。
(c )Driver:客户端
(2)启动方式:
(a)Spark-submit:

    用于提交Spark 任务。每个任务是一个jar。

(b)spark-shell:

    类似于 REPL

5.Spark运行机制及原理分析

(1).WordCount执行的流程分析

WordCount执行的流程

(2).Spark提交任务的流程

Spark提交任务的流程

Scala高级特性

一.范型

1.什么是泛型类

    和Java或者C++一样,类和特质可以带类型参数。在Scala中,使用方括号来定义类型参数

测试程序:

2.什么是泛型函数

    函数和方法也可以带类型参数。和泛型类一样,我们需要把类型参数放在方法名之后。

注意:这里的ClassTag是必须的,表示运行时的一些信息,比如类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import scala.reflect.ClassTag

def mkIntArray(elems:Int*) = Array[Int](elems:_*)

mkIntArray(1,2,3,100)

def mkStringArray(elems:String*) = Array[String](elems:_*)

mkStringArray("Mike","Tom","Mary")

def mkArray[T:ClassTag](elems:T*) = Array[T](elems:_*)

mkArray(1,2,3,5,8)

mkArray("Tom","Marry")

3.上界和下界:Upper Bounds 与 Lower Bounds

(1)作用:规定泛型的取值范围:

举例:定义一个范型:T

1
2
3
4
5
类的继承关系   A---->B----->C----->D  箭头指向子类
可以规定T的取值范围 D <: T <: B
T 的取值范围只能是 B C D

<: 就是上下界表示方法
(2)定义:
  • 上界:s <: T ,规定了S的类型必须是T的子类或本身

  • 下界:u >: T ,规定了U的类型必须是T的父类或本身

(3)一个简单的例子:

(4)一个复杂一点的例子(上界):

(5)再来看一个例子:
1
2
3
4
5
def addTwoString[T<:String](x:T,y:T) = x + " **** " + y

addTwoString("Hello","123")

addTwoString(1,2)

报错原因:Int不是String类型。

解决:1和2转换成字符串再调用

1
addTwoString(1.toString,2.toString)

另外:可以使用 视图界定 来解决这个问题。

4.视图界定(View bounds)

    它比 <: 适用的范围更广,除了所有的子类型,还允许隐式转换过去的类型。用 <% 表示。尽量使用视图界定,来取代泛型的上界,因为适用的范围更加广泛

示例

(1)上面写过的一个列子。这里由于T的上界是String,当我们传递100和200的时候,就会出现类型不匹配。

(2)但是100和200是可以转成字符串的,所以我们可以使用视图界定让addTwoString方法接收更广泛的数据类型,即:字符串及其子类、可以转换成字符串的类型。

注意:使用的是 <%

(3)但实际运行的时候,会出现错误:

    这是因为:Scala并没有定义如何将Int转换成String的规则,所以要使用视图界定,我们就必须创建转换的规则。

(4)创建转换规则

(5)运行成功

5.协变和逆变

(1)协变:

    Scala的类或特征的范型定义中,如果在类型参数前面加入+符号,就可以使类或特征变为协变了。

(2)逆变:

    在类或特征的定义中,在类型参数之前加上一个-符号,就可定义逆变范型类和特征了。

总结一下:
Scala的协变:泛型变量的值可以是本身类型或者其子类的类型

Scala的逆变:泛型变量的值可以是本身类型或者其父类的类型

二.隐式转换

1.隐式转换函数

    所谓隐式转换函数指的是以implicit关键字申明的带有单个参数的函数。

(1)前面讲视图界定时候的一个例子:
1
implicit def int2String(n:Int):String = {n.toString}

(2)再举一个例子:把Fruit对象转换成了Monkey对象

2.隐式参数

    使用implicit申明的函数参数叫做隐式参数。我们也可以使用隐式参数实现隐式的转换

(1)区别:
  • 参数:定义函数的时候,会接收参数
  • 隐式参数:使用implicit修饰的函数参数
(2)作用:当你去调用函数的时候,如果没有给函数传递参数值,就会使用隐式参数
(3)举例:定义一个带有隐式参数的函数
1
2
3
4
5
6
7
8
9
def testParam(name:String) = println("The value is " + name)

def testParam(implicit name:String) = println("The value is " + name)

implicit val name:String = "AAAAAA"

testParam("dfsfdsdf")

testParam

3.隐式类

所谓隐式类: 就是对类增加implicit 限定的类,其作用主要是对类的功能加强

三.Actor并发模型

1.Java并发与Scala并发区别:

(1)Java中的并发编程是基于 共享数据 和 加锁 的一种机制。synchronized关键字,锁共享数据
(2)Scala中的并发:Scala中的Actor是一种 不共享数据 ,依赖于 消息传递 的一种并发编程模式。

    避免了死锁、资源争夺等情况。

2.如果 Actor A 和 Actor B 要相互沟通,步骤如下:

(1)A是要给B传递一个信息,B会有一个收件箱,然后B会不断地循环查询自己的收件箱。
(2)若B看见A发来的消息,B就会解析A的消息,并执行。

    使用 case class 来区分 消息类型

(3)处理完之后,有可能将处理的结果发送给A。

    在学习Actor时,一定要区分,发消息 与回复消息。这两个在代码中实现是不同的。

AKKA 负责来回传递消息

3.示例代码:

添加配置:
pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<properties>
<actor.version>2.5.4</actor.version>
</properties>

<dependencies>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.11</artifactId>
<version>${actor.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-remote_2.11</artifactId>
<version>${actor.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http_2.11</artifactId>
<version>10.0.9</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-protobuf_2.11</artifactId>
<version>${actor.version}</version>
</dependency>
</dependencies>
(1)示例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package Test01

import akka.actor.{Actor, ActorSystem, Props}

class HelloActor extends Actor{
def receive = {
/**
* 使用 case 来处理不同类型的信息
*/
case "Hello" => println("你好!")
case _ => println("你是?")
}
}
object DemoActor {

def main(args: Array[String]): Unit = {
//新建一个ActorSystem
val system = ActorSystem("HelloSystem")

val helloActor = system.actorOf(Props( new HelloActor),name="helloactor")

//给 helloactor 发送消息

helloActor ! "Hello123123123"
}
}

结果

(2)示例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import akka.actor._
/**
*
* 两个Actor 相互发消息
*
* Ping Pong
*/
//定义消息类型
case object PongMessage
case object PingMessage
case object StopMessage
case object StartMessage

class Pong(ping:ActorRef) extends Actor{
var count = 0
def incrementAndPrint { count += 1; println("count + 1 and pong")}
def receive = {
case PingMessage =>
if (count > 9){
sender ! StopMessage
println("count > 9")
context.stop(self)
} else {
println("Receive PingMessage")
incrementAndPrint
sender ! PongMessage

}

case StartMessage =>
println("Receive StartMessage")
//发送一个消息
ping ! PongMessage
}
}


class Ping extends Actor{
def receive = {
case PongMessage =>
println("Receive PongMessage")
//回复一个消息
sender ! PingMessage
case StopMessage =>
println("Stop Message")
context.stop(self)
//context.system.finalize()
}
}
object Demo2 {
def main(args: Array[String]): Unit = {
//pong ! StartMessage
val system = ActorSystem("PingPongSystem")
val ping = system.actorOf(Props[Ping],name="ping")
val pong = system.actorOf(Props(new Pong(ping)),name="pong")
pong ! StartMessage
}
}

结果

Scala集合

1.可变集合和不可变集合

(1)可变集合:
(2)不可变集合:
  • 集合从不改变,因此可以安全地共享其引用。
  • 甚至是在一个多线程的应用程序当中也没问题。
1
2
3
4
5
6
//不可变的集合
val math = scala.collection.immutable.Map("Alice"->80,"Bob"->90,"Mary"->85)

//可变的集合
val english = scala.collection.mutable.Map("Alice"->80,"Bob"->90,"Mary"->85)
val chinese = scala.collection.mutable.Map("Alice"->80,"Bob"->90,"Mary"->85)

(3)集合的操作:


a.获取集合中的值

1
2
3
4
5
6
7
8
9
chinese.get("Bob")

chinese.get("Tomdfsdfd")

chinese("Bob")

chinese.getOrElse("Tomsdlkfjlskdjfls",-1)

chinese("Tomdfsdfd")

获取集合中的值

b.更新集合中的值:注意:必须是一个可变集合

1
2
3
4
5
6
7
8
9
chinese

chinese("Bob")=0

chinese

math

math("Bob")=0

更新集合元素

c.添加新元素

1
chinese += "Tom"->85

添加新元素
d.移除元素

1
chinese -= "Bob"

移除元素

2.列表

(1)不可变列表(List)
1
2
3
4
5
6
7
8
9
//不可变列表:List
//字符串列表
val namelist = List("Bob","Mary","Mike")
//整数列表
val intList = List(1,2,3,4,5)
//空列表
val nullList:List[Nothing] = List()
//二维列表
val dim:List[List[Int]] = List(List(1,2,3),List(4,5,6))

不可变列表

不可变列表的相关操作
a.head:列表中的第一个元素

1
2
//输出列表中的值:head,tail,isEmpty
println("第一个人的名字:"+namelist.head)

b.tail:不是返回最后一个元素,而是返回除去第一个元素后,剩下的元素列表

1
println(namelist.tail)

c.isEmpty:判断列表是否为空

1
println("列表是否为空:"+namelist.isEmpty)

不可变列表的相关操作

(2)可变列表(LinkedList):scala.collection.mutable.LinkedList

LinkedList和不可变列表List类似,只不过我们可以修改列表中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//可变列表:
val myList = scala.collection.mutable.LinkedList(1,2,3,4,5)
//操作:将上面可变列表中的每个值乘以2
//列名 elem
//定义一个指针指向列表的开始
var cur = myList

//Nil:代表Scala中的null
while(cur != Nil){
cur.elem = cur.elem*2
//将指针指向下一个元素
cur = cur.next
}
//查看结果
println(myList)

1
2
3
myList

myList.map(_*2)


移除元素

1
2
3
myList.drop(2)

myList.dropWhile(_%2!=0)

dropWhile 去除当前数组中,符合条件的元素,碰到第一个不满足条件的元素,就结束。

3.序列

常用的序列有:Vector和Range

(1)Vector是ArrayBuffer的不可变版本,是一个带下标的序列
1
2
3
4
5
6
7
//Vector:为了提高list列表随机存取的效率而引入的新的集合类型
//支持快速的查找和更新
val v= Vector(1,2,3,4,5,6)

//返回的是第一个满足条件的元素
v.find(_>3)
v.updated(2,1000)

Vector

(2)Range表示一个整数序列
1
2
3
4
5
6
7
8
9
10
11
12
13
//Range:有序的通过空格分割的Int序列
//以下几个例子Range是一样

Range(0,5)
println("第一种写法:"+Range(0,5))
println("第二种写法:"+(0 until 5))
println("第三种写法:"+(0 to 4))

//两个range可以相加
('0' to '9')++('A' to 'Z')

//可以将Range转换为List
1 to 5 toList

Range

4. 集(Set)和集的操作

  • 集Set是不重复元素的集合
  • 和列表不同,集并不保留元素插入的顺序。默认是HashSet
(1)示例1:创建集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//a.**创建一个Set**
var s1 = Set(2,0,1)

//往s1中添加一个重复元素
s1+1

//往s1中添加一个不重复元素
s1+100

//创建一个LinkedHashSet,一定要先import下面这个,否则会保错
import collection.mutable
var weeksday = mutable.LinkedHashSet("星期一","星期二","星期三","星期四")

//创建一个排序的集
var s2 = mutable.SortedSet(1,2,3,10,4)

(2)示例2:集的操作

a.添加

1
weeksday+"星期五"

b.判断元素是否存在

1
weeksday contains("星期二")

c.判断一个集是否是另一个集的子集

1
Set("星期二","星期四","星期日") subsetOf(weeksday)

d.集的运算,union并集,intersect交集,diff差集

1
2
3
4
5
6
7
8
9
var set1=Set(1,2,3,4,5)

var set2=Set(5,6,7,8,9,10)
//并集:集合相加,去掉重复元素
set1 union set2
//intersect交集
set1 intersect set2
//差集
set1 diff set2

集的操作

5.模式匹配

(1)Scala有一个强大的模式匹配机制,可以应用在很多场合:
  • switch语句
  • 类型检查
(2)Scala还提供了样本类(case class),对模式匹配进行了优化
(3)模式匹配示例:
  • 更好的switch
1
2
3
4
5
6
7
8
9
10
11
//更好的switch
var sign =0

var ch1 = '-'

ch1 match{
case '+' => sign = 1
case '-' =>sign = -1
case _ =>sign = 0
}
println(sign)

(4)Scala的守卫:匹配某种类型的所有值
1
2
3
4
5
6
7
8
9
10
11
//scala的守卫:匹配某种类型的所有值
var ch2='6'
var digit:Int = -1
ch2 match{
case '+' => println("这是一个+")
case '-' => println("这是一个-")
case _ if Character.isDigit(ch2) => digit = Character.digit(ch2,10)
case _ => println("其他类型")
}

println("Digit:"+digit)

(5)模式匹配中的变量
1
2
3
4
5
6
7
8
//模式匹配的变量
var str3 = "Hello World"

str3(7) match{
case '+' => println("这是一个+")
case '-' => println("这是一个+")
case ch => println("这是字符:"+ch)
}

(6)类型模式
1
2
3
4
5
6
7
//类型模式
var v4:Any =100
v4 match{
case x:Int => println("这是一个整数:"+x)
case s:String => println("这是一个字符串:"+s)
case _ => println("这是其他类型")
}

(7)匹配数组和列表
1
2
3
4
5
6
7
8
//匹配数组和列表
var myArray = Array(1,2,3)
myArray match{
case Array(0) => println("0")
case Array(x,y) =>println("这是数组包含的两个元素,和是:"+(x+y))
case Array(x,y,z) => println("这是数组包含的三个元素,乘积是:"+(x*y*z))
case Array(x,_*) => println("这是一个数组")
}

1
2
3
4
5
6
7
var myList = List(1,2,3)
myList match{
case List(0) => println("0")
case List(x,y) =>println("这是列表包含的两个元素,和是:"+(x+y))
case List(x,y,z) =>println("这是列表包含的三个元素,乘积是:"+(x*y*z))
case List(x,_*) => println("这个列表包含多个元素")
}

6.样本类(CaseClass)

    简单的来说,Scala的case class就是在普通的类定义前加case这个关键字,然后你可以对这些类来模式匹配。

case class带来的最大的好处是它们支持模式识别。

    首先,回顾一下前面的模式匹配:

普通的模式匹配

    其次,如果我们想判断一个对象是否是某个类的对象,跟Java一样可以使用isInstanceOf

下面这个好像有点问题

最后,在Scala中有一种更简单的方式来判断,就是case class

注意:需要在class前面使用case关键字

Scala函数式编程

1.Scala中的函数

    在Scala中,函数是“头等公民”,就和数字一样。可以在变量中存放函数,即:将函数作为变量的值(值函数)

举例:使用Spark来执行WordCount

1
var result = sc.textFile("hdfs://....").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect

2.匿名函数:没有名字的函数

1
2
3
4
5
//普通函数
def fun1(x:Int):Int = x*3

//匿名函数
(x:Int)=>x*3

注:匿名函数:一个普通的函数,把返回值去掉,再把函数名和“def”去掉,再在”=”后面的加一个”>”

调用匿名函数作为函数参数

3.带函数参数的函数,即:高阶函数

(1)示例1:
  • (a)首先,定义一个最普通的函数

  • (b)再定义一个高阶函数

  • (c )分析这个高阶函数调用的过程

(2)示例2:

    在这个例子中,首先定义了一个普通的函数mytest,然后定义了一个高阶函数myFunction;myFunction接收三个参数:第一个f是一个函数参数,第二个是x,第三个是y。而f是一个函数参数,本身接收两个Int的参数,返回一个Int的值。

4.高阶函数示例

(1)map:相当于一个循环,对某个集合中的每个元素进行操作(接收一个函数),返回一个新的集合
1
2
3
4
5
6
7
8
9
10
//map
//在列表中的每个元素上计算一个函数,并且返回一个包含相同数目元素的列表
val numbers = List(1,2,3,4,5,6,7,8,9,10)

numbers.map((i:Int)=>i*2)

numbers.map(_*2)

//map函数不改变numbers值
numbers

1
2
3
_ 相当于循环变量 i
_*2 与 (i:Int)=>i*2 功能相同
_+_ 与 (i:Int,j:Int)=>i+j
(2)foreach:相当于一个循环,对某个集合中的每个元素进行操作(接收一个函数),不返回结果。
1
2
3
4
5
6
7
8
9
10
11
//foreach
//foreach和map相似,只不过它没有返回值,foreach主要是为了对参数进行作用
numbers.foreach((i:Int) => i * 2)

numbers.foreach(_*2)

numbers

numbers.foreach(println(_))

numbers.map(_*2).foreach(println)

(3)filter:过滤,选择满足的数据

举例:查询能够被2整除的数字

1
2
3
4
//filter
//移除任何使得传入的函数返回false的元素

numbers.filter((i:Int) =>i%2==0)

说明:(i:Int)=>i%2==0 如果是true 就返回

(4)zip:合并两个集合
1
2
3
4
5
6
7
//zip
//zip把两个列表的原色合成一个由元素对组成的列表
List(1,2,3).zip(List(4,5,6))

List(1,2,3).zip(List(4,5))

List(3).zip(List(4,5))

(5)partition:根据断言(就是某个条件,可以通过一个匿名函数来实现)的结果,进行分区

举例:把能够被2整除的分成一个区,不能整除的分成另一个区

1
2
3
//partition
//partition根据断言函数的返回值对列表进行拆分
numbers.partition((i:Int)=>i%2==0)

在这个例子中,可以被2整除的被分到一个分区;不能被2整除的被分到另一个分区。

(6)find:查找第一个满足条件(断言)的元素
1
2
3
//find
//find返回集合里第一个匹配断言函数的元素
numbers.find(_ % 3==0)

(7)flatten:把嵌套的结果展开,合并成为一个集合
1
2
3
//flatten
//flatten可以把嵌套的结构展开
List(List(1,2,3),List(4,5,6)).flatten

(8)flatmap:相当于map+flatten
1
2
3
4
5
//flatMap
//flatMap是一个常用的combinator,它结合了map和flatten的功能
val myList = List(List(2,4,6,8,10),List(1,3,5,7,9))

myList.flatMap(x=>x.map(_*2))

执行过程:

1
2
3
4
5
1.将List(2,4,6,8,10)和List(1,3,5,7,9)调用了x=>x.map(_*2)  这里x代表某个List
List(4, 8, 12, 16, 20)List(2, 6, 10, 14, 18)

2.合并成一个List
List(4, 8, 12, 16, 20, 2, 6, 10, 14, 18)

5.闭包

    就是函数的嵌套,即:在一个函数定义中,包含另外一个函数的定义;并且在内函数中可以访问外函数中的变量

测试上面的函数:

6.柯里化:Currying

    柯里化函数(Curried Function)是把具有多个参数的函数转换为一条函数链,每个节点上是单一参数。


一个简单的例子:

Scala面向对象

1.面向对象的基本概念

把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象

面向对象的三大特征:

  • 封装: 类 class。把属性和操作属性的方法,放在一起
  • 继承
  • 多态

2.类的定义

简单类和无参方法:

1
2
3
4
5
6
7
class Counter{
private var value =0;

def increment() { value +=1};
def current() = value;

}

案例:注意没有class前面没有public关键字修饰。

1
2
3
4
5
6
7
8
9
10
11
//scala中类的定义
class Student1{
//定义属性
private var stuName:String = "Tom"
private var stuAge:Int = 20
//成员方法
def getStuName():String = stuName
def setStuName(newName:String) = this.stuName=newName
def getStuAge():Int = stuAge
def setStuAge(newAge:Int) = this.stuAge=newAge
}

如果要开发main方法,需要将main方法定义在该类的伴生对象中,即:object对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 测试student1类,创建main函数
*
* 注意:object和class名字可以不一样的
* 如果oject和class的名字相同,object成为该class的 伴生对象
*
*/
//创建Student1的伴生对象
object Student1{

def main(args:Array[String]){

//创建一个学生对象
var s1 = new Student1

//访问属性并输出
println(s1.getStuName() + "\t" + s1.getStuAge())

//访问set方法
s1.setStuName("Mary")
s1.setStuAge(22)
println(s1.getStuName() + "\t" + s1.getStuAge())

/**
* 直接访问私有属性
*
* 上面定义中,属性都是private的
* 如果属性是private的,在类的外部,是不能访问的
*
*/
println("------------直接访问私有属性-----------")
println(s1.stuId +"\t"+ s1.stuName +"\t"+ s1.age)

/**
* 注意:我们可以直接访问类的私有成员,为什么?
* s1.stuId +"\t"+ s1.stuName +"\t"+ s1.age
* 使用 . 方式访问的
*
* scala中属性的get set 方法:
*
* 1、当一个属性是private的时候,scala会自动为其生成对应的get set 方法
* s1.stuId 访问的是scala中,自动为stuId生成的get方法,并且该方法的名字为stuId
*
* 2、如果值希望scala生成get方法,不生成set方法,可以将其定义为常量
* 因为常量的值不可改变
*
* 3、如果希望属性,不能被外部访问(希望scala不要生成set get 方法),使用 private [this] 关键字
*
*/
}
}

3.属性的getter和setter方法

(1)当定义属性是private时候,scala会自动为其生成对应的get和set方法
1
private var stuName:String = "Tom"
  • get方法: stuName —-> s2.stuName() 由于stuName是方法的名字,所以可以加上一个括号
  • set方法: stuName_= —-> stuName_= 是方法的名字
(2)定义属性:

private var money:Int = 1000 希望money只有get方法,没有set方法??

解决办法:将其定义为常量private val money:Int = 1000

(3) private[this]的用法:

该属性只属于该对象私有,就不会生成对应的set和get方法。如果这样,就不能直接调用,例如:s1.stuName —> 错误

4、内部类(嵌套类)

    我们可以在一个类的内部在定义一个类,如下:我们在Student类中,再定义了一个Course类用于保存学生选修的课程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import scala.collection.mutable.ArrayBuffer
//嵌套类,内部类
class Student3{
//定义一个内部类,记录学生选修的课程信息
class Course(val courseName:String,val credit:Int){
//定义方法
}

//属性
private var stuName:String = "Tom"
private var stuAge:Int = 20
//定义一个ArrayBuffer记录该学生选修的所有课程
private var courseList = new ArrayBuffer[Course]()

//定义方法往学生信息中添加新的课程
def addNewCourse(cname:String,credit:Int){
//创建新的课程
var c = new Course(cname,credit)

//将课程加入list
courseList +=c
}
}

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object Student3{
def main(args:Array[String]){
//创建学生对象
var s3 = new Student3

//给该学生添加新的课程
s3.addNewCourse("Chinese",2)
s3.addNewCourse("English",3)
s3.addNewCourse("Math",4)

//输出
println(s3.stuName+"\t"+s3.stuAge)
println("********选修的课程********")

for(s <- s3.courseList) println(s.courseName+"\t"+s.credit)
}
}

5.类的构造器

类的构造器分为:主构造器、辅助构造器

(1)主构造器:和类的声明结合在一起,只能有一个主构造器

Student4(val stuName:String,val stuAge:Int)

  • (a) 定义类的主构造器:两个参数
  • (b) 声明了两个属性:stuName和stuAge 和对应的get和set方法
1
2
3
4
5
6
7
8
9
10
11
class Student4(val stuName:String,val stuAge:Int){
}

object Student4{
def main(args:Array[String]){
//创建Student4的一个对象
var s4 = new Student4("Tom",20)

println(s4.stuName+"\t"+s4.stuAge) //调用了主构造器
}
}
(2)辅助构造器:可以有多个辅助构造器,通过关键字this来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Student4(val stuName:String,val stuAge:Int){
//定义辅助构造器
def this(age:Int){
//调用主构造器
this("no name",age)
}
}
object Student4{
def main(args:Array[String]){

//创建一个新的Student4对象,并调用辅助构造器
var s42 = new Student4(25)

println(s42.stuName+"\t"+s42.stuAge)
}
}

主构造器和辅助构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Student3(var stuName : String , var age:Int) {
//属性
private var gender:Int = 1

//定义一个辅助构造器,辅助构造器可以有多个
//辅助构造器就是一个函数,只不过这个函数的名字叫 this

def this (age : Int){
this("Mike",age) // new Student3("Mike",age)
println("这是辅助构造器")
}

def this (){
this(10)// new Student3("Mike",10)
println("这是辅助构造器2")
}

}

object Student3{
def main(args: Array[String]): Unit = {
//使用主构造器 创建学生对象
var s1 = new Student3("Tom",20)
println(s1.stuName+"\t"+s1.age)

//使用辅助构造器
var s2 = new Student3(20)
s2.gender = 0
s2.stuName = "have name"
println(s2.stuName+"\t"+s2.age+"\t"+s2.gender)
}
}

6.Scala中的Object对象

(1)什么是object对象?
  • Object中的内容都是静态的
  • scala中,没有static关键字,所有static都定义在object中
  • 如果class的名字,跟object的名字一样,就把这个object叫做类的伴生对象。

注意:main函数需要写在object中,但是不一定必须写在伴生对象中。

下面是Java中的静态块的例子。在这个例子中,就对JDBC进行了初始化。
java:

1
2
3
4
5
6
7
static{
try{
Class.forName(driver);
} catch(ClassNotFoundException e){
throw new ExceptionInInitializerError(e);
}
}

而Scala中的Object就相当于Java中静态块

Object对象的应用

(1)单例对象:使用object来实现单例模式:一个类只有一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//利用object对象实现单例模式
object CreditCard{

//变量保存信用卡号
private [this] var creditCardNumber:Long = 0

//产生新的卡号
def generateNewCCNumber():Long = {
creditCardNumber += 1
creditCardNumber
}

//测试程序
def main(args:Array[String]){
//产生新的卡号
println(CreditCard.generateNewCCNumber())
println(CreditCard.generateNewCCNumber())
println(CreditCard.generateNewCCNumber())
println(CreditCard.generateNewCCNumber())
}
}
(2)使用APP对象:可以省略main方法;需要从父类App继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//使用应用程序对象:可以省略main方法
object HelloWorld extends App{

// def main(args:Array[String]){
//
// }
//这里的main可以不写,相当于下面的代码是在main方法中执行的

println("Hello World")

//如何取得命令行的参数
if(args.length>0){
println(args(0))
}else{
println("no argument")
}
}

7. Scala中的apply方法

遇到如下形式的表达式时,apply方法就会被调用:

1
Object(参数1,参数2,......,参数N)

通常,这样一个apply方法返回的是伴生类的对象;其作用是为了省略new关键字

1
2
var marry = Array(1,2,3)
//这里没有用new关键字,其实就是用了apply方法

(1)Object的apply方法举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//object的apply方法
class Student5(val stuName:String){

}

object Student5{
def apply(stuName:String) = {
println("******Apply in Object***********")
}

def main(args:Array[String]){
//创建一个Student5的对象
var s51 = new Student5("Tom")
println(s51.stuName)

//创建一个Student5的对象
var s52 = new Student5("Tom")
println(s52.stuName)
}
}

  • 使用apply方法,让程序更加简洁。
  • apply方法必须写在伴生对象中。

8.Scala中的继承

Scala和Java一样,使用extends关键字扩展类。

(1)案例一:Employee类继承Person类

(2)案例二:在子类中重写父类的方法

(3)案例三:使用匿名子类

(4)案例四:使用抽象类。抽象类中包含抽象方法,抽象类只能用来继承。

(5)案例五:使用抽象字段。抽象字段就是一个没有初始值的字段

9.Scala中的trait(特质)

trait就是抽象类。trait跟抽象类最大的区别:trait支持多重继承

10.包的使用

    Scala的包和Java中的包或者C++中的命名空间的目的是相同的,管理大型程序中的名称

Scala中包的定义和使用:

(1)包的定义

(2)包的引入:Scala中依然使用import作为引用包的关键字,例如
1
2
3
import com.my.io.XXX     //可以不写XXX的全路径
import com.my.io._ //引用import com.my.io下所有的类型
import com.my.io.XXX._ //引用import com.my.io.XXX的所有成员
(3)而且Scala中的import可以写在任意地方
1
2
3
4
def method(fruit :Fruit){
import fruit._
println(name)
}

11.包对象

    包可以包含类、对象和特质,但不能包含函数或者变量的定义。很不幸,这是Java虚拟机的局限。
    把工具函数或者常量添加到包而不是某个Utils对象,这是更加合理的做法。Scala中,包对象的出现正是为了解决这个局限。
    Scala中的包对象:常量,变量,方法,类,对象,trait(特质)

Scala语言基础

1、Scala简介

    Scala是一种多范式的编程语言,其设计的初衷是要集成面向对象编程和函数式编程的各种特性。Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序。它也能运行于CLDC配置的Java ME中。目前还有另一.NET平台的实现,不过该版本更新有些滞后。Scala的编译模型(独立编译,动态类加载)与Java和C#一样,所以Scala代码可以调用Java类库(对于.NET实现则可调用.NET类库)。Scala包括编译器和类库,以及BSD许可证发布。

2、Scala的常用数据类型

注意:在Scala中,任何数据都是对象。

任何数据都是对象

(1)数值类型:Byte,Short,Int,Long,Float,Double
  • Byte: 8位有符号数字,从-128 到 127
  • Short: 16位有符号数据,从-32768 到 32767
  • Int: 32位有符号数据
  • Long: 64位有符号数据

例如:

1
2
3
4
5
6
7
scala> val a:Byte = 10

scala> a+10

scala> val b:Short = 20

scala> a+b

注意:在Scala中,定义变量可以不指定类型,因为Scala会进行类型的自动推导。

(2)字符类型和字符串类型:Char和String

对于字符串,在Scala中可以进行插值操作。

1
2
3
4
5
scala> val s1="Hello Movle"

scala> "My Name is ${s1}"

scala> s"My Name is ${s1}"

注意:前面有个s;相当于执行:”My Name is “ + s1

(3)Unit类型:相当于Java中的void类型
1
2
3
scala> val f = ()

scala>val f = {}

(4)Nothing类型:一般表示在执行过程中,产生了Exception

例如,我们定义一个函数如下:

1
def myfunction = throw new Exception("a lot error")

3.Scala变量的申明和使用

(1)使用val和var申明变量

例如:

1
scala> val answer = 8 * 3 + 2

可以在后续表达式中使用这些名称

(2)val:定义的值实际是一个常量,var:申明其值是可变的变量

注意:可以不用显式指定变量的类型,Scala会进行自动的类型推导

4. Scala的函数和方法的使用

(1)可以使用Scala的预定义函数

例如:求两个值的最大值

1
2
3
scala> import scala.math._

scala> max(1,2)

(2)也可以使用def关键字自定义函数

语法:

1
2
3
def 函数名称(参数列表:参数类型):返回类型={
//函数体
}

示例1:

1
2
3
4
// 求两个数字的和
def sum(x:Int, y:Int):Int= x+y

sum(1,2)

示例1

示例2:

1
2
3
4
5
6
7
8
9
//求某个数字的阶乘
def myFactor(x:Int):Int={
if(x<1)
1
else
x*myFactor(x-1)
}

myFactor(5)

示例2

5.Scala的条件表达式

    Scala的if/else语法结构和Java或C++一样。
    不过,在Scala中,if/else是表达式,有值,这个值就是跟在if或else之后的表达式的值。

6.Scala的循环

    Scala拥有与Java和C++相同的while和do循环
    Scala中,可以使用for和foreach进行迭代

(1)for的四种写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var list = List("LiMing","May","Mike")

println("for循环的第一种写法")
for(s<-list) println(s)

println("for循环的第二种写法")
for{
s <-list
if(s.length >3)
}println(s)

println("for循环的第三种写法")
for(s <- list if s.length < 3) println(s)

println("for循环的第四种写法")
var newList = for{
s <- list
s1=s.toUpperCase()
}yield(s1)
for(s1<-newList) println(s1)

结果

注意:

  • <- 表示Scala中的generator,即:提取符
  • 第三种写法是第二种写法的简写
  • 在for循环中,还可以使用yield关键字来产生一个新的集合,for循环的第四种写法

在上面的案例,for循环的第四种写法中,将list集合中的每个元素转换成了大写,并且使用yield关键字生成了一个新的集合。

(2)使用while循环:注意使用小括号,不是中括号
1
2
3
4
5
6
7
8
9
var list = List("LiMing","May","Mike")

println("******while循环******")
var i=0
println("while循环")
while(i<list.length){
println(list(i))
i+=1
}
(3)使用do … while循环
1
2
3
4
5
6
println("******dowhile循环******")
var j=0
do{
println(list(i))
j+=1
}while(j<list.length)

while循环与do while循环

(4)使用foreach进行迭代
1
2
3
val list = List("LiMing","XiaoHong","PengPeng")

list.foreach(println) //相当于for(s<-list) println(s )

foreach
注意:在上面的例子中,foreach接收了另一个函数(println)作为值
还有一个循环map

foreach和map的区别:

  • foreach没有返回值
  • map有返回值

7.Scala函数的参数

(1)Scala中,有两种函数参数的求值策略
  • Call By Value:对函数实参求值,且仅求一次
  • Call By Name:函数实参每次在函数体内被用到时都会求值 所用符号:=>


执行过程对比

1
2
3
test1  -->	test1(3+4,8)	-->	test1(7,8)	-->	7+7	-->	14

test2 --> test2(3+4,8) --> (3+4)+(3+4) --> 7+7 --> 14

举例
定义bar函数,其中x 是 call by value , y 是 call by name

1
def bar(x:Int,y: => Int) : Int = 1

定义一个死循环函数

1
def loop() : Int = loop

调用 bar 函数:

1
2
3
bar(1,loop)    //第一种

bar(loop,1) //第二种

哪种方式会产生死循环?
答案是:第二种方式
解析:

1
2
y 每次在函数中用到时,才会被求值,bar函数没有用到y,所以不会调用loop。
x call by value ,对函数参数求值,并且只求一次,所以会产生死循环。
(2)Scala中的函数参数的类型
  • 默认参数:当你没有给参数赋值的时候,就使用默认值。
1
2
3
4
5
6
//默认参数
def func1(name:String="Tom"):String="Hello"+name

func1()

func1("Marry")

  • 代名参数:当有多个默认参数的时候,通过代名参数可以确定给哪个参数赋值。
1
2
3
4
5
6
7
//代名参数
def func2(str:String="good morning",name:String="Tome",age:Int=20)
=str+name+"and the age of "+name+"is"+age

func2()

func2(age=22)

  • 可变参数:类似于java中的可变参数,即 参数数量不固定
1
2
3
4
5
6
//变长参数:求多个数字的和
def sum(args:Int*){
var result=0
for(arg<-args) result+=arg
result
}

8.Scala的Lazy值(懒值)

定义:如果常量是lazy的,他的初始化会被延迟,推迟到第一次使用该常量的时候

(1)当val被申明为lazy时,它的初始化将被推迟,直到我们首次对它取值。
1
2
3
4
5
val x:Int = 10

lazy val y:Int=x+1

y

举例
(a)读一个存在的文件

1
2
3
lazy val words = scala.io.Source.fromFile("/users/macbook/TestInfo/student.txt").mkString

words

(b)读一个不存在的文件

1
val words = scala.io.Source.fromFile("/users/macbook/TestInfo/studen1231312312t.txt").mkString

产生异常

1
lazy val words = scala.io.Source.fromFile("/users/macbook/TestInfo/studen1231312312t.txt").mkString

不会产生异常

9.异常的处理

    Scala异常的工作机制和Java或者C++一样。直接使用throw关键字抛出异常。

1
throw new Exception("a lot error happened!")

10.Scala中的数组

(1)Scala数组的类型:
  • 定长数组:使用关键字Array
1
2
3
4
5
6
//定长数组
val a = new Array[Int](10)

val b = new Array[String](5)

val c = Array("Tom","Marry","Mike")

定长数组

  • 变长数组:使用关键字ArrayBuffer
1
2
3
4
5
6
7
8
9
10
11
12
//一定要先导包
import scala.collection.mutable.ArrayBuffer
val d = ArrayBuffer[Int]()
//在变长数组种添加元素
d+=1
d+=2
d+=3
//在变长数组中添加多个元素
d+=(10,12,13)

//将ArrayBuffer转换为Array
d.toArray

变长数组

(2)遍历数组
1
2
3
4
5
6
7
8
9
10
11
12
13
//遍历数组
var a =Array("aa","bb","cc")
//使用for循环进行遍历
for(s <-a) println(s)

//对数组进行转换,新生成一个数组yield
val b = for{
s<-a
s1=s.toUpperCase
}yield(s1)

//使用foreach进行循环输出
a.foreach(println)

遍历数组

(3)Scala数组的常用操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scala.collection.mutable.ArrayBuffer

val myArray = Array(1,10,2,3,5,4)

//最大值
myArray.max
//最小值
myArray.min
//求和
myArray.sum
//定义一个变长数组
var myArray1= ArrayBuffer(1,10,2,3,5,4)

//排序
myArray1.sortWith(_>_)
//升序
myArray1.sortWith(_<_)

数组的常用操作

(4)Scala的多维数组
  • 和Java一样,多维数组是通过数组的数组来实现的。
  • 也可以创建不规则的数组,每一行的长度各不相同。
1
2
3
4
5
6
7
//定义一个固定长度的二维数组

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

matrix(1)(2)=10

matrix

固定长度的二维数组

1
2
3
4
5
6
7
//定义一个二维数组,其中每个元素是一个一维数组,其长度不固定
val triangle = new Array[Array[Int]](10)

for(i<-0 until triangle.length)
triangle(i)=new Array[Int](i+1)

triangle

长度不固定的二维数组

11.映射

(1).映射就是Map集合,由一个(key,value)组成。

-> 操作符用来创建

例如:

1
val scores = Map("Alice" -> 10,"Bob" -> 3,"Cindy" -> 8)

(2)映射的类型分为:不可变Map和可变Map
1
2
3
4
5
6
7
//不可变的Map
val math = scala.collection.immutable.Map("Alice"->80,"Bob"->95,"Mary"->70)

//可变的Map
val english = scala.collection.mutable.Map("Alice"->80,"Bob"->95,"Mary"->70)

val chinese = scala.collection.mutable.Map(("Alice",80),("Bob",95),("Mary",70))

不可不map和可变map

(3)映射的操作
  • 获取映射中的值
1
2
3
4
5
6
7
8
9
10
//获取Map中的值
chinese("Bob")


//需要判定是否存在才能够获得该映射的值
if(chinese.contains("Alice")){
chinese("Alice")
}else{
-1
}

获取映射中的值

1
2
3
4
5
chineses.getOrElse("To123123m",-1)

chineses.get("dlfsjldkfjlsk")

chineses.get("Tom")

使用get方法不会报错

  • 更新映射中的值(必须是可变Map)
1
2
3
4
5
6
7
8
9
10
//更新Map中的值
chinese("Bob")=100

chinese

//往Map中添加新的元素
chinese +="Tom"->85

//移除Map中的元素
chinese -="Bob"

更新映射中的值

  • 迭代映射

可以使用 for foreach

1
2
3
4
//迭代Map:使用for或者foreach
for(s <-chinese) println(s)

chinese.foreach(println)

迭代映射

12.元组(Tuple)

(1)元组是不同类型的值的聚集。

例如:

1
val t = (1, 3.14, "Fred") // 类型为Tuple3[Int, Double, java.lang.String]

这里:Tuple是类型,3是表示元组中有三个元素。

(2)Tuple的声明:
1
val t1 = Tuple3(1,0.3,"Hello")

(3)元组的访问和遍历:
1
2
3
4
5
6
7
8
9
10
11
12
13
//定义tuple
val t1 = (1,2,"Tom")

val t2 = new Tuple4("Marry",3.14,100,"Hello")

//访问tuple中的组员
t2._1
t2._2
t2._3
t2._4

//遍历Tuple:foreach
t2.productIterator.foreach(println)

元组

遍历Tuple中的元素
注意:Tuple中没有提供foreach函数,我们需要使用 productIterator

遍历分成2个步骤:

  • 使用productIterator 生成迭代器
  • 遍历

注意:要遍历Tuple中的元素,需要首先生成对应的迭代器。不能直接使用for或者foreach

13.Scala中的文件操作

类似于java中的io操作
举例:

(1)读取文件
1
2
3
4
5
6
7
8
9
10
def main(args: Array[String]): Unit = {
//读取行
val source = fromFile("/users/macbook/TestInfo/student.txt")

/**
* 1、将整个文件作为字符串输出
*/
println("--------mkString---------")
println(source.mkString)
}

1
2
3
4
5
6
7
8
9
10
11
def main(args: Array[String]): Unit = {
//读取行
val source = fromFile("/users/macbook/TestInfo/student.txt")

/**
* 2、将文件的每一行读入 java BufferedReader readLine方法类似
*/
println("--------lines---------")
val lines = source.getLines()
lines.foreach(println)
}

(2)读取二进制文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def main(args: Array[String]): Unit = {
/**
* 读取二进制文件:
* scala中并不支持直接读取二进制
* 可以通过调用java的InputStream来进行读入
*/
println("--------Read Bytes---------")
var file = new File("/users/macbook/TestInfo/hudson.war")
//构建一个inputstream
var in = new FileInputStream(file)
//构造buffer
var buffer = new Array[Byte](file.length().toInt)
//读取
in.read(buffer)
println(buffer.length)
//关闭
in.close()
}

(3)从url中获取信息
1
2
3
4
5
def main(args: Array[String]): Unit = {
println("--------fromURL---------")
var source2 = fromURL("https://www.baidu.com","UTF-8")
println(source2.mkString)
}

(4)写入文件
1
2
3
4
5
6
7
8
9
def main(args: Array[String]): Unit = {
/**
* 写入文件
*/
println("--------Write File---------")
var out = new PrintWriter("/users/macbook/TestInfo/insert_0601.txt")
for (i <- 0 until 10) out.println(i)
out.close()
}

  • Copyrights © 2015-2021 Movle
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信