Spark的大部分转化操作和一部分行动操作,都需要依赖用户传递的函数来计算。在我们支持的三种主要语言中,向Spark传递函数的方式略有区别。这里主要写Java
在Java中,函数需要作为实现了Spark的org.apache.spark.api.java.function包中的任一函数接口的对象来传递。根据不同的返回来行,我们定义了一些不懂的接口。我们把最基本的一些函数接口列在下表中,同时介绍了一些其他的函数接口,在需要返回特殊类型(比如键值对)的数据时使用。
函数名 | 实现方法 | 用途 |
Function |
R call(T) | 接收一个输入值并返回一个输出值,用于类似map()和filter()等操作中 |
Function2 |
R call(T1,T2) | 接收两个输入值并返回一个输出值,用于类似aggregate()和fold()等操作中 |
FlatMapFunction |
Iterable |
接收一个输入值并返回任意个输出,用于类似flatMap()这样的操作 |
可以把我们的函数类内联定义为使用匿名内部类,也可以创建一个具名类
在Java中使用匿名内部类进行函数传递
JavaRDD errorLog = log.filter(
(Function) s -> s.contains("error")
);
在Java中使用具名类进行函数传递
class ContainsError implements Function {
@Override
public Boolean call(String s) throws Exception {
return s.contains("error");
}
}
JavaRDD errorLog = log.filter(new ContainsError());
集体风格的选择取决于个人偏好。不过我们发现顶级具名类通常在组织大型程序时显得比较清晰。使用顶级函数的另一个好处在于你可以给他们的构造函数添加参数。
带参数的Java函数类
class ContainsError implements Function {
private String query;
public ContainsError(String query) {
this.query = query;
}
@Override
public Boolean call(String s) throws Exception {
return s.contains(query);
}
}
JavaRDD errorLog = log.filter(new ContainsError("error"));
在Java8中,也可以使用Lambda表达式来简洁地实现函数接口。当然啦 ,我之前写的都是这样一种简介表达式。这里不再赘述。
你很可能会用到的两个最常用的转化操作式map()和filter()。转化操作map()接收一个函数,把这个函数用于RDD中的每个元素,将函数的返回结果作为结果RDD中对应元素的值。而转化操作filter()则接收一个函数,并将RDD中满足该函数的元素放入新的RDD中返回。
我们可以使用map()来做各种各样的事情:可以把我们的URL集合中的每个URL对应的主机名提取出来,也可以简单到只对各个数字求平方值。map()的返回值类型不需要和输入类型一样。这样如果有一个字符串RDD,并且我们的map()函数是用来把字符串解析并返回一个Double值的,那么此时我们的输入RDD类型就是RDD[String],而输出类型RDD[Double]。
计算RDD中各值的平方
JavaSparkContext sc = new JavaSparkContext(conf);
JavaRDD rdd = sc.parallelize(Arrays.asList(1, 2, 3, 4));
JavaRDD result = rdd.map(
(Function) integer -> integer * integer
);
System.out.println(StringUtils.join(result.collect(), ","));
有时候,我们希望对每个输入元素生成多个输出元素。实现该功能的操作叫做flatMap()。和map()类似,我们提供给flatMap()的函数被分别应用到了输入RDD的每个元素上。不过返回的不是一个元素,而是一个返回值序列的迭代器。输出的RDD倒不是由迭代器组成的。我们得到的是一个包含各个迭代器可访问的所有元素的RDD。flatMap()的一个简单用途是把输入的字符串切分为单词。
JavaRDD lines = sc.parallelize(Arrays.asList("hello world", "hi"));
JavaRDD words = lines.flatMap(
(FlatMapFunction) s
-> (Iterator) Arrays.asList(s.split(" "))
);
System.out.println(words.first());
下图阐释了flatMap()和map()的区别。你可以把flatMap()看作将返回的迭代器”拍扁“,这样就得到了一个由各列表中的元素组成的RDD,而不是一个由列表组成的RDD。
尽管RDD本身不是严格意义上的集合,但它也支持许多数学上的集合操作,比如合并和相交操作,下图展示了四种操作。注意,这些操作都要求操作的RDD是相同数据类型。
我们的RDD中最常确实的集合属性是元素的唯一性,因为常常有重复的元素。如果要唯一各元素,我们可以使用RDD.distinct()转化操作来生成一个只包含不同元素的新RDD。不过需要注意,distinct()操作的开销很大,因为它需要将所有数据通过网络进行混洗(shuffle),以确保每个元素都只有一份。
最简单的集合操作时union(other),它会返回一个包含两个RDD中所有元素的RDD。这在很多用例下都是很有用,比如处理来自多个数据源的日志文件。与数学中的union()操作不同的是,如果输入的RDD中有重复数据,Spark的union()操作也会包含这些重复数据。
Spark还提供了intersection(other)方法,只返回两个RDD中都有的元素。intersection()在运行时也会去掉所有重复的元素。尽管intersection()与union()的概念相似,intersection()的性能却要差很多,因为它需要通过网络混洗数据来发现共有的元素。
有时我们需要移除一些数据。subtract(other)函数接收另一个RDD作为参数,返回一个由只存在于第一个RDD中而不存在于第二个RDD中的所有元素组成的RDD。和intersection()一样,它也需要数据混洗。
我们也可以计算两个RDD的笛卡尔积。cartesian(other)转化操作会返回所有可能的(a,b)对,其中a是源RDD中的元素,而b则来自另一个RDD。笛卡尔积在我们希望考虑所有可能的组合的相似度时比较有用,比如计算各自用户对各种产品的预期兴趣程度。我们也可以求一个RDD与其自身的笛卡尔积,这可以用于求用户相似度的应用中。不过要特别注意的是,求大规模RDD的笛卡尔积开销巨大。
函数名 | 目的 | 示例 | 结果 |
map() | 将函数应用于RDD中的每个元素,将返回值构成新的RDD | rdd.map(x => x + 1 | {2,3,4,4} |
flatMap() | 将函数应用于RDD中的每个元素,将返回的迭代器的所有内容构成新的RDD。通常用来切分单词 | rdd.flatMap(x => x.to(3)) | {1,2,3,2,3,3,3} |
filter() | 返回一个由通过传给filter()的函数的元素组成的RDD | rdd.filter(x => x!=1) | {2,3,3} |
distinct() | 去重 | rdd.distinct() | {1,2,3} |
sample(withReplacement, fraction, [seed]) | 对RDD采样,以及是否替换 | rdd.sample(false, 0.5) | {1,2,3}非确定 |
函数名 | 目的 | 示例 | 结果 |
union() | 生成一个包含两个RDD中所有元素的RDD | rdd.union(other) | {1,2,3,3,4,5} |
intersection() | 求两个RDD共同的元素的RDD | rdd.intersection(other) | {3} |
subtract() | 移除一个RDD中的内容(例如移除训练数据) | rdd.subtract(other) | {1,2} |
cartesian() | 与另一个RDD的笛卡尔积 | rdd.cartesian(other) | {(1,3),(1,4),...,(3,5)} |
你很有可能会用到基本RDD上最常见的行动操作reduce()。它接收一个函数作为参数,这个函数要操作两个RDD的元素类型的数据并返回一个同样类型的新元素。一个简单的例子就是函数+,可以用它来对我们的RDD进行累加。使用reduce(),可以很方便地计算出RDD中所有元素的总和、元素的个数,以及其他类型的聚合操作
JavaRDD counts = sc.textFile("README.md");
Integer sum = Integer.valueOf(counts.reduce(
(Function2) (s, s2)
-> String.valueOf(Integer.valueOf(s) + Integer.valueOf(s2))
));
fold()和reduce()类似,接收一个与reduce()接收的函数签名相同的函数,再加上一个”初始值“来作为每个分区第一次调用时的结果。你所提供的初始值应当是你提供的操作的单位元素;也就是说,使用你的函数对这个初始值进行多次计算不会改变结果(例如+对应的0,*对应的1,或拼接操作对应的空列表)。
fold()和reduce()都要求函数的返回值类型需要和我们所操作的RDD中的元素类型相同。这很符合像sum这种操作的情况。但有时我们确实需要返回一个不同类型的值。例如,在计算平均值时,需要记录遍历过程中的计数以及元素的数量,这就需要返回一个二元组。可以先对数据使用map()操作,来把元素转为元素和1的二元组,也就是我们所希望的返回类型。这样reducce()就可以以二元组的形式进行归约了。
aggregate()函数则把我们从返回值类型必须与所操作的RDD类型相同的限制中解放出来。与fold()类似,使用aggregate()时,需要提供我们期待返回的类型的初始值。然后通过一个函数把RDD中的元素合并起来放入累加器。考虑到每个节点是在本地进行累加的,最终,还需要提供第二个函数来将累加器两两合并。
我们可以用aggregate()来计算RDD的平均值,来代替map()后面接fold()的方式
/**
* @author DKing
* @description
* @date 2019/6/2
*/
public class CalculateSum {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setMaster("local").setAppName("calculateSum");
JavaSparkContext sc = new JavaSparkContext(conf);
JavaRDD counts = sc.parallelize(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
Function2 addAndCount =
(Function2) (avgCount, integer) -> {
avgCount.total += integer;
avgCount.num += 1;
return avgCount;
};
Function2 combine =
(Function2) (avgCount, avgCount2) -> {
avgCount.total += avgCount2.total;
avgCount.num += avgCount2.num;
return avgCount;
};
AvgCount initial = new AvgCount(0, 0);
AvgCount result = counts.aggregate(initial, addAndCount, combine);
System.out.println(result.avg());
}
static class AvgCount implements Serializable {
public int total;
public int num;
public AvgCount(int total, int num) {
this.total = total;
this.num = num;
}
public double avg() {
return total / (double) num;
}
}
}
函数名 | 目的 | 示例 | 结果 |
collect() | 返回RDD中的所有元素 | rdd.collect() | {1,2,3,3} |
count() | RDD中的元素个数 | rdd.count() | 4 |
countByValue() | 各元素在RDD中出现的次数 | rdd.countByValue() | {(1,1),(2,1),(3,2)} |
take(num) | 从RDD中返回num个元素 | rdd.take(2) | {1,2} |
takeOrdered(num)(ordering) | 从RDD中按照提供的顺序返回最前面的num个元素 | rdd.takeOrdered(2)(myOrdering) | {3,3} |
takeSample(withReplacement,num,[seed]) | 从RDD中返回任意一些元素 | rdd.takeSample(false,1) | 非确定的 |
reduce(func) | 并行整合RDD中所有数据 | rdd.reduce((x,y) => x+y) | 9 |
fold(zero)(func) | 和reduce()一样,但是需要提供初始值 | rdd.fold(0)((x,y) => x+y) | 9 |
aggregate(zeroValue)(seq0p,comb0p) | 和reduce()相似,但是通常返回不同类型的函数 | rdd.aggregate((0,0)) | (9,4) |
top(num) | 从RDD中返回最前面的num个元素 | rdd.top(2) | {3,3} |
foreach(func) | 对RDD中的每个元素使用给定的函数 | rdd.foreach(func) | 无 |
在Java中,各种RDD的特殊类型间的转换更为明确。Java中有个专门的类JavaDoubleRDD和JavaPairRDD,来处理特殊类型的RDD,这两个类还针对这些类型提供了额外的函数。这让你可以更加了解所发生的一切。
函数名 | 等价函数 | 用途 |
DoubleFlatMapFunction |
Function |
用于flatMapToDouble,以生成DoubleRDD |
DoubleFunction |
Function |
用于mapToDouble,以生成DoubleRDD |
PairFlatMapFunction |
Function |
用于flatMapToPair,以生成PairRDD |
PairFunction |
Function |
用于mapToPair,以生成PairRDD |
JavaRDD counts = sc.parallelize(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
JavaDoubleRDD doubleRDD = counts.mapToDouble(
(DoubleFunction) integer -> integer * integer
);
System.out.println(doubleRDD.mean());