【Spark】Spark学习笔记

本文意在收集整理网络上对Spark讲解比较好的博文,希望能发挥一个“Spark字典”的作用。

先列出一个比较好的入门级Spark教程:厦门大学数据库实验室的Spark教程。

厦门大学Spark入门教程(Scala版)

Scala菜鸟教程


第一部分:Scala基础

1. if语句

Scala中的if表达式的值可以直接赋值给变量。

val x = 6
val a = if (x>0) 1 else -1  // a的值为1

2. for循环

Scala中的for循环语句格式如下,其中,“变量<-表达式”被称为“生成器(generator)”。这里,i不需要提前进行变量声明,可以在for语句括号中的表达式中直接使用。

for (i <- 1 to 5) println(i)  // for (变量<-表达式) 语句块
for (i <- 1 to 5 by 2) println(i)  // 使用by来控制设置步长,这里i取值为1、3、5
for (i <- 1 to 5; j <- 1 to 3) println(i*j)  // for循环的多生成器,相当于双重for循环

for循环中的yield关键字会把当前的元素记下来,保存在集合中,循环结束后将返回该集合。Scala中的yield的主要作用是记住每次迭代中的有关值,并逐一存入到一个数组中。这种带有yield关键字的for循环,被称为“for推导式”。

scala> for (i <- 1 to 5) yield i
res: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5)

3. 数组Array

val intArray = new Array[Int](3)  // 声明一个长度为3的整型数组,每个数组元素初始化为0

在Scala中,对数组元素的应用,使用的是圆括号intArray(0),而不是方括号intArray[0],这一点和Java是不同的。

4. 列表List

val intList = List(1,2,3)  // 声明一个列表

:: 方法被称为cons,意为构造,具有右结合的特性,向队列的头部追加数据,创造一个新的列表。用法为 x::list,其中x为加入到头部的元素,无论x是列表与否,它都只将成为新生成列表的第一个元素,也就是说新生成的列表长度为list的长度+1。

x::list等价于list.::(x)

Scala中,列表中各个元素必须是相同类型,Nil表示空列表

Scala可以使用 ::: 方法对不同的列表进行连接得到一个新的列表。

List的遍历可以采用for循环的方式,也可以采用Java里面foreach的方式:

// for循环遍历列表
val list = List(1, 2, 3, 4, 5)
for (elem <- list) println(elem)

5. 元组Tuple

val tuple = ("scala", 2020, 2020.01)  // 声明一个元组,只需要用圆括号把多个元组的元素括起来就可以了

元组是不同类型的值的聚集也就是说元组可以包含不同类型的元素。 

当需要访问元组中的某个元素的值时,可以通过类似tuple._1、tuple._2、tuple._3这种方式实现。

6. 集合Set

var set = Set("Hadoop","Spark")  // 声明一个集合,默认是不可变集合;声明一个可变集,则需要引入scala.collection.mutable.Set包
set += "Scala"  // 向set中增加新的元素

集合Set是不重复元素的集合,集合中的元素是以hash的方式进行存储的,方便快速地找到某个元素。

集合分为可变集合和不可变集合。

想要在集合中进行插入删除等操作,若声明的是不可变集合,需使用var标识,此时的操作会产生一个新的集,原来的集并不会发生变化;若声明的是可变集合,使用val标识即可,此时改变的是该集合本身。

7. 映射Map

val events = Map("A" -> "click", "B" -> "view","C"->"skip")  // 声明一个Map
val value = events("A")  // 可以使用key来获取Map中这个key对应的value

在Scala中的Map是一系列键值对的集合,以key-value的形式进行存储。与Set类似,Scala中的Map包括可变和不可变两种,默认情况下创建的是不可变Map,如果需要创建可变Map,需要引入scala.collection.mutable.Map包。

Map可以直接使用key来进行一些操作,如增加元素、更新已有key的值等。

import scala.collection.mutable.Map
val events = Map("A" -> "click", "B" -> "view","C"->"skip")
events ("A") = "click again"  // 更新已有key的值
events ("D") = "like"  // 向map中添加新的元素
// 等效方法
events + = ("D"->"like")  // 添加一个新元素
events + = ("D"->"like","E"->"dislike",F->"share")  // 同时添加多个新元素

Map的遍历可以使用for循环的方式,也可以跟Java一样,使用迭代器的方式:

for (x <- map) 语句块  // 方式1,这里获取x的key/value可使用x._1/x._2
for ((k,v) <- map) 语句块  // 方式2
map.iterator.foreach(x => 语句块))  // 方式3

如Java一样,Scala中的Map也支持只去Map的key的集合或者Map的value的集合。

events.keys
events.values

或者,Map的遍历也可以采用foreach的方式:

// 等价操作
events foreach {case(k,v) => println(k+":"+v)}
events.foreach({case (k,v) => println(k+":"+v)})

8. 类和对象

Scala中的类和对象的使用与Java中的类和对象并没有太多的本质区别,只是有些特别的地方需要注意一些。

Scala类的成员如果没有使用访问修饰符修饰,默认是public访问类型,外部可以访问该字段。

Scala中调用方法可以使用类似Java的对象调用的方式,Scala在调用无参方法时,可以省略方法名后面的圆括号,只给出方法名即可。

object.无参方法名() 等价于 object.无参方法名

Scala并没有提供static这个关键字,Scala通过使用关键字object来实现类的单例模式。Scala中使用单例模式时,除了定义的类之外,还要定义一个同名的object 对象,它和类的区别是:object对象不能带参数。

单例对象用关键字object定义,在Scala中,单例对象分为两种,一种是并未自动关联到特定类上的单例对象,称为独立对象另一种是关联到一个类上的单例对象,该单例对象与该类共有相同名字,则这种单例对象称为伴生对象

当单例对象与某个类共享同一个名称时,这个单例对象被称作是这个类的伴生对象:companion object,类被称为是这个单例对象的伴生类:companion class。类和它的伴生对象必须定义在同一个源文件里,类和它的伴生对象可以互相访问其私有成员。

9. 方法与函数

Scala 使用 def 语句定义方法。方法是类的一部分,这一点跟 Java 的类似。

Scala方法的声明格式:

def functionName ([参数列表]) : [return type]

Scala方法的定义格式:

def functionName ([参数列表]) : [return type] = {
   function body
   return [expr]
}

 

Scala中,如果方法没有返回值,可以返回Unit,这个类似于Java的void同时,方法中的参数列表如果是多个参数,需要使用逗号进行分隔,并且每个参数都要指定数据类型。方法的返回值,不需要靠return语句,方法里面的最后一个表达式的值就是该方法的返回值。比如,上面max()方法里面x或者y就是该方法的返回值。

方法返回类型后面的等号和大括号后面,是该方法的方法体,包含了该方法要执行的具体操作语句。如果大括号里面只有一行语句,那么也可以直接去掉大括号。

def sum(x:Int, y:Int):Int = x+y

在Scala中允许对“字面量”直接执行方法,下面的两种用法是等价的,前者是后者的简写形式。

a 方法 b
a.方法(b)

Scala中方法与函数在语义上的区别很小。Scala方法是类的一部分,而函数是一个对象可以赋值给一个变量。换句话来说在类中定义的函数即是方法。

在函数式编程中,函数的“类型”和“值”也成为两个分开的概念,函数的“值”,就是“函数字面量”。

def counter(value: Int): Int = { value += 1}  // 定义一个函数

 这个函数的“类型”为:

(Int) => Int  // 函数类型,当参数只有一个时,圆括号可以省略,等价于:Int => Int

只要把函数定义中的类型声明部分去除,剩下的就是函数的“值”

(value) => {value += 1}  // 只有一条语句时,大括号可以省略

 采用类似定义变量的方式定义一个函数:

// val 变量名:变量类型 = 变量的值
val num: Int = 5 
// val 函数名:函数的类型 = 函数的值
val counter: Int => Int = { (value) => value += 1 }

10. 特质trait

Scala中没有接口的概念,而是通过特质(trait)来实现了接口的功能,如与接口不同的是,trait还可以定义属性和方法的实现。。Scala中,一个类只能继承自一个超类,却可以实现多个特质,通过重用特质中的方法和字段,进而实现多重继承。

trait的定义方式与class类似,使用的关键字trait进行定义,如下所示:

trait Equal {
  def isEqual(x: Any): Boolean
  def isNotEqual(x: Any): Boolean = !isEqual(x)
}

trait中没有方法体的方法,默认是抽象方法。上面的代码中,isEqual()方法没有定义方法的实现,isNotEqual()定义了方法的实现。子类继承trait可以实现未被实现的方法。所以其实Scala的trait更像是Java的抽象类。

11. 模式匹配

一个模式匹配包含了一系列备选项,每个都开始于关键字case。每个备选项都包含了一个模式及一到多个表达式。箭头符号 => 隔开了模式和表达式。

Scala的模式匹配最常用于match语句中,match对应Java里的switch,但是写在选择器表达式之后。match表达式通过以代码编写的先后次序尝试每个模式来完成计算,只要发现有一个匹配的case,剩下的case不会继续匹配。

  • 选择器 match {备选项}
  • 备选项以关键字case开始
  • 每个备选项都包含了一个模式及一到多个表达式用箭头符号=>分隔
def matchTest(x: Any): Any = x match {
      case 1 => "one"
      case "two" => 2
      case y: Int => "scala.Int"
      case _ => "many"  // 这个case表示默认的全匹配备选项,即没有找到其他匹配时的匹配项,类似switch中的default
   }

“case _”表示默认的全匹配备选项,即没有找到其他匹配时的匹配项,类似switch中的default。 

12. 样例类case class

使用了case关键字的类定义就是就是样例类(case class),样例类是种特殊的类,经过优化以用于模式匹配。

object Test {
   def main(args: Array[String]) {
    val alice = new Person("Alice", 25)
    val bob = new Person("Bob", 32)
    val charlie = new Person("Charlie", 32)
   
    for (person <- List(alice, bob, charlie)) {
        person match {
            case Person("Alice", 25) => println("Hi Alice!")
            case Person("Bob", 32) => println("Hi Bob!")
            case Person(name, age) =>
               println("Age: " + age + " year, name: " + name + "?")
         }
      }
   }
   // 样例类
   case class Person(name: String, age: Int)
}

// 输出结果
Hi Alice!
Hi Bob!
Age: 32 year, name: Charlie?

Scala中的class类似于Java中的class,而case class被称为样例类,是一种特殊的类,常被用于模式匹配。

case class的定义:

case class ClassName 参数列表(可为空)

在Scala中的case class其实就是一个普通的class,但是它又和普通的class略有区别:

  • case class初始化的时候可以不用new,也可以加上,但是class必须加new;
  • 默认实现了equals、hashCode方法;
  • 默认是可以序列化的,也就是实现了Serializable;
  • 自动从scala.Product中继承一些函数;
  • case class构造函数的参数是public的,可以直接访问;
  • case class默认情况下不能修改属性值;
  • case class最重要的功能是支持模式匹配,这也是定义case class的重要原因。

当一个类被声名为case class的时候,scala会帮助我们做下面几件事情:

  1. 构造器中的参数如果不被声明为var的话,它默认的话是val类型的,但一般不推荐将构造器中的参数声明为var;
  2. 自动创建伴生对象,同时在里面给我们实现子apply方法,使得我们在使用的时候可以不直接显示地new对象(也就是说,在实例化样例类时,不需要使用关键字new,这是因为案例类有一个默认的apply方法来负责对象的创建);
  3. 伴生对象中同样会帮我们实现unapply方法,从而可以将case class应用于模式匹配,关
  4. 实现自己的toString、hashCode、copy、equals方法

除此之此,case class与其它普通的scala类没有区别。

补充一点:如果有一个class,还有一个与class同名的object,那么就称这个object是这个class的伴生对象,这个class是这个object的伴生类。

13. lambda表达式

lambda是可以理解为一种匿名函数,可以采用匿名函数的定义形式,lambda表达式的形式:

(参数) => 表达式  // 如果参数只有一个,参数的圆括号可以省略

第二部分:Spark基础 

1. map与flatMap

(1)一些比较好的讲解

Scala入门:map操作和flatMap操作

spark RDD 的map与flatmap区别说明

(2)map操作

map(func):将每个元素传递到函数func中,并将结果返回为一个新的数据集。

map操作是针对集合的典型变换操作,它将某个函数应用到集合中的每个元素,并产生一个结果集合。

换句话说,map是将集合中的每一个元素输入映射为一个新对象。{苹果,梨子}.map(去皮)= {去皮苹果,去皮梨子} ,其中: “去皮”函数的类型为:A => B,是一种lamda表达式的形式。

(3)flatMap操作

flatMap(func):与map()相似,但每个输入元素都可以映射到0或多个输出结果。

flatMap是map的一种扩展。在flatMap中,我们会传入一个函数,该函数对每个输入都会返回一个集合(而不是一个元素),然后,flatMap把生成的多个集合“拍扁”成为一个集合。

flatMap其实包含着两个操作:首先会将每一个输入对象输入映射为一个新集合,然后把这些新集合连成一个大集合。 {苹果,梨子}.flatMap(切碎) = {苹果碎片1,苹果碎片2,梨子碎片1,梨子碎片2} ,其中: “切碎”函数的类型为: A => List<B>,这个函数返回的必须是一个List。

简单理解,flatMap与map唯一不一样的地方就是传入的函数在处理完后返回值必须是List,其实这也不难理解,既然是flatMap,那除了map以外必然还有flat的操作,所以需要返回值是List才能执行flat这一步。

这里再次强调一下函数的等价用法:

val books = List("Hadoop","Hive","HDFS")
// 下面的操作是等价的
books flatMap (s => s.toList)
books.flatMap(s => s.toList)

2. reduceByKey与groupByKey

(1)一些比较好的讲解

【Spark系列2】reduceByKey和groupByKey区别与用法

(2)reduceByKey

reduceByKey(func)的功能是,使用func函数合并具有相同键的值。将reduceByKey应用于(K,V)键值对的数据集时,返回一个新的(K, V)形式的数据集,其中的每个值是将每个key传递到函数func中进行聚合。

比如,reduceByKey((a,b) => a+b),有四个键值对(“spark”,1)、(“spark”,2)、(“hadoop”,3)和(“hadoop”,5),对具有相同key的键值对进行合并后的结果就是:(“spark”,3)、(“hadoop”,8)。可以看出,(a,b) => a+b这个Lamda表达式中,a和b都是指value。

(3)groupByKey

groupByKey()的功能是,对具有相同键的值进行分组,应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集。比如,对四个键值对(“spark”,1)、(“spark”,2)、(“hadoop”,3)和(“hadoop”,5),采用groupByKey()后得到的结果是:(“spark”,(1,2))和(“hadoop”,(3,5))。

3. filter

 filter(func):筛选出满足函数func的元素,并返回一个新的数据集。

Scala中可以通过filter操作来实现从一个集合中获取满足指定条件的元素组成一个新的集合。通俗地讲,filter操作是在当前集合中“筛选”出符合指定条件的元素。

4. reduce

Scala使用reduce这种二元操作对集合中的元素进行归约,reduce实现的是对集合当中的元素进行归约操作。
reduce包含reduceLeft和reduceRight两种操作,其中reduceLeft从集合的头部开始操作,reduceRight从集合的尾部开始操作。

scala> val list = List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala> list.reduceLeft(_ + _)
res: Int = 15
scala> list.reduceRight(_ + _)
res: Int = 15

reduceLeft和reduceRight都是针对两两元素进行操作,在上面代码中,reduceLeft(_ + _)表示从列表头部开始,对两两元素进行求和操作,下划线是占位符,用来表示当前获取的两个元素,两个下划线之间的是操作符,表示对两个元素进行的操作,这里是加法操作(当然也可以使用其他操作)。 reduceRight(_ + _)表示从列表尾部开始,对两两元素进行求和操作。

如果直接使用reduce,而不用reduceLeft和reduceRigh,默认采用的是reduceLeft。

5. sortBy和sortByKey

(1)一些比较好的讲解

sortBy是对标准的RDD进行排序,而sortByKey函数是对PairRDD进行排序,也就是有Key和Value的RDD。

Spark: sortBy和sortByKey函数详解

(2)sortBy

sortBy可以指定对键还是value进行排序,返回根据提供的参数进行排序的RDD。该函数最多可以传三个参数:
  第一个参数是一个函数,该函数的也有一个带T泛型的参数,返回类型和RDD中元素的类型是一致的;
  第二个参数是ascending,这个参数决定排序后RDD中的元素是升序还是降序,默认是true,也就是说sortBy默认升序排序
  第三个参数是numPartitions,该参数决定排序后的RDD的分区个数,默认排序后的分区个数和排序之前的个数相等,即为this.partitions.size。

(3)sortByKey

sortByKey函数作用于Key-Value形式的RDD,并对Key进行排序。返回一个根据键排序的RDD。

6. class与object

在scala中没有静态方法和静态字段,所以在scala中可以用object来实现这些功能。object相当于class的单个实例,类似于Java中的static,通常在里面放一些静态的field和method。

第一次调用object中的方法时,会执行object的constructor,也就是object内部不在method或者代码块中的所有代码,但是object不能定义接受参数的constructor 。object的构造函数只会在第一次被调用时被执行一次,类似于Java类中static的初始化。

Scala中如果一个Class和一个Object同名,则称Class是Object的伴生类。Scala没有Java的Static修饰符,Object下的成员和方法都是静态的,类似于Java里面加了Static修饰符的成员和方法。Class和Object都可以定义自己的Apply()方法,类名()调用Object下的Apply()方法,变量名()调用Class下的Apply()方法。在object中一般可以为伴生类做一些初始化等操作。

类名()->调用了对应object下的apply方法
对象名()->调用了对应class的apply方法

还有一种解释是,class是定义是描述。它依照方法、或者其他组成元素定义一个类型。object相当于class的单例,它是你所定义的类的唯一实例。每个代码中的object都将创建一个匿名类,这个匿名类继承了你声明本object想要实现的类。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章