热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

sparkpythonlist_曾经以为Python中的List用法足够灵活,直至我遇到了Scala…

导读:继续开工Scala系列专题,虽然对自己来说这是一个全新的方向和足够的挑战,阅读数也很是惨淡,但选择了方向就要坚持下去—

导读:继续开工Scala系列专题,虽然对自己来说这是一个全新的方向和足够的挑战,阅读数也很是惨淡,但选择了方向就要坚持下去——生活中的获得感不正是源于一个个挑战和抉择之间吗!

言归正传,前期分别完成了Scala中的变量标识符和运算符的分享,本文重点介绍Scala中的常用集合类数据结构(scala.collection),当完整了解这个包的构成以及各数据结构的常用方法后,你会再次认识到Scala语法的强大和奔放,以至于让我一度质疑“Python语法足够简洁”的论断。

开篇引题:程序=数据结构+算法,对于一个良好的编程实现来说二者缺一不可。而对于数据结构,除了特定框架的特有数据结构外(例如Spark框架的RDD、DataFrame,Pandas框架的DataFrame),其实更为通用的其实还是那些经典数据结构,例如数组、链表、集合、字典等等,这也是绝大多数编程语言的通用设计。当然,还有很多其他数据结构,例如栈、队列、树和图等,其底层大多可以基于这些基础的数据结构进行表示和实现。

具体而言,本文主要介绍Scala中的以下5种经典的集合类数据结构:Array

List

Set

Map

Tuple

01 Array

Array,原意即为数组,应该是所有编程语言中都有的数据结构,也是很多场景下常用的集合类型,有编程语言的应该都有了解。那么,Scala中的Array有什么特别之处吗?一句话概括Scala中的Array就是:同质、数据可变、长度不可变的集合。显然,这其中包含了3个关键词,也分别描述了Array的3个特点:同质:意味着Array中的所有元素类型(或者称之为泛型,字面意就是广泛存在的通用类型)都是相同的,例如都是Int整型、或者String字符串型。如果非要容纳不同的数据类型,例如既有Int又有Double还有String,那么这个Array的数据类型只能是这些数据类型的超类——Any型

数据可变:数据可变意味着Array中的每个元素可以发生变化,比如开始为0,后来可以变为1等等。这里之所以强调Array中的数据可变,是因为与之对应的是List数据不可变

长度不可变:这与C和Java中的数组有一定相似之处,即一旦初始指定了Array的数据个数(即Array的length),那么后续则不能再发生改变。那么如果一定想要发生改变怎么办呢,那就只能调用Array的兄弟,ArrayBuffer

了解了Array数据结构的这3大特点,就相当于get到了Array的价值观。那么接下来自然就是方法论层面的问题:即怎么创建和如何使用。

Array的创建有两种方式,一种是直接指定元素完成初始化,另一种是指定数据类型和长度,而不提供初始数据。

scala> val arr1 = Array(1, 2, "string")

val arr1: Array[Any] = Array(1, 2, string)

scala> val arr2 = Array[Int](3)

val arr2: Array[Int] = Array(3)

scala> val arr3 = new Array[Int](3)

val arr3: Array[Int] = Array(0, 0, 0)

如上述示例代码所示,arr1是一个直接指定初始元素的数组,由于此时未指定泛型且实际包含的初始数据既有整型也有字符串,所以相当于创建了一个泛型为Any、长度为3、初始元素为1、2、"string"的数组;arr2的初始化过程类似于arr1,但实际指定了泛型类型为Int型,且实际只有1个初始化数据3;arr3与arr2的唯一区别在于Array前多了一个new关键字,这将直接导致创建了一个长度为3、泛型为Int的数组,进一步地由于指定泛型为Int所以默认初始元素均为0。

这里,对比arr2和arr3的创建过程,可以发现当带有new关键字的初始化时采用的原原本本的由类创建对象的方式;而不带new关键字时,实际上是调用了Array类的伴生对象apply方法完成初始化,在这种方法中可以省略new关键字,从而简化由类创建对象的过程。这将在后续介绍类和对象时予以介绍,此处只需了解两种不同初始化方式的具体实现即可。

在创建一个Array数组后,还需了解基本的常用操作。这里,由于Array数组是数据可变长度不可变的集合,所以对该数组涉及的操作无非就是访问和修改值两类操作;但同时,虽然Array本身长度不可变,但却可以添加新的元素或者与其他Array连接构成新的Array,注意这里都是构成了新的Array,而不是改变原有Array,因为其长度不可变。

scala> arr1(0) // 用()+下标访问数组元素val res0: Any = 1

scala> arr1 :+ 3 // :+ 后面添加新元素val res1: Array[Any] = Array(1, 2, string, 3)

scala> 0 +: arr1 // +:前面添加新元素val res2: Array[Any] = Array(0, 1, 2, string)

scala> arr1 ++ Array(4, 5) // ++ 连接其他数组val res3: Array[Any] = Array(1, 2, string, 4, 5)

如上的示例代码中分别执行了Array元素的访问、前后向添加元素构成新的Array以及与其他Array拼接构成新的Array,基本上这几个操作也是最为常用的操作。除了以上访问和追加新的元素,当然Array也提供了很多常用的接口,例如:

scala> arr1.length // 返回数组长度val res4: Int = 3

scala> arr1.indices // 返回数组下标列表val res7: scala.collection.immutable.Range = Range 0 until 3

scala> arr1.foreach(print) // 调用foreach方法 12string

其中foreach方法应该称之为是所有集合类数据结构的通用方法。另外,除了length、indices等之外,如果是Array泛型为数值型,那么还有其他常用方法,例如max、min等。

最后,再补充关于Array的两个要点:创建多维数值。实际上,多维数组就是数组的多层嵌套,所以自然可以用前述的数组初始化方式嵌套完成多维数组的创建,当数组是一个整齐的维度例如m×n时,那么可直接调用Array.ofDim(m, n)创建即可;

前面提到,Array是一个长度不可变的数据集合,那么有时为了应用可变长度的数组,此时需要引用ArrayBuffer类来创建,其与Array的最大区别即在于它的长度是可以动态改变。

02 List

前述详细介绍了Array数据结构的特点、创建及常用方法,与Array表现极为相近但又有重要不同的数据结构就是List,即列表。List的特点可概括为:同质、数据不可变且长度不可变的集合。也就是说,相较于Array类型,List的最大区别在于数据不可变,即一旦初始化则其不可更改。更深层次的讲,Array的底层是一块连续申请的内存,而List则更符合链表的实现特性(实际上,在scala2.11之前的版本,确实存在一个LinkedList类)。

既然List与Array除了数据一旦完成初始即不再可变,所以其很多特性都有相通之处,当然也有很多不同。比如,由于数据不可变,所以其创建过程自然就不能仅指定长度而不提供初值,也就是创建时必须提供所有初值。

scala> val list = new List[Int](3)

^ error: class List is abstract; cannot be instantiated

scala> val list = List(1, 2, 3)

val list: List[Int] = List(1, 2, 3)

除了创建过程中的区别,Array中的数值访问、元素拼接、两个List拼接以及常用方法在List中也都适用。特别地,长度为0的List是一个特殊对象,表示为Nil。

scala> list(0)

val res9: Int = 1

scala> list :+ 4

val res10: List[Int] = List(1, 2, 3, 4)

scala> 0 +: list

val res11: List[Int] = List(0, 1, 2, 3)

scala> list ++ List(5, 6)

val res12: List[Int] = List(1, 2, 3, 5, 6)

scala> list.length

val res13: Int = 3

然而,如果List的常用方法也仅仅如此的话,那么其实好像是被Array完全碾压的节奏,所以实际上List还有更多的语法糖骚操作,即灵活运用::这个操作符,读作cons,即连接的意思。在前面介绍操作符一文时,有提到过在Scala中但凡以:结尾的操作符,那么都将以右操作数来调用,其实这里主要就是指的就是Array和List,而尤以List含有:方法居多。例如,同样拼接元素,下面的方法也可实现添加元素和拼接两个List:

scala> 0 :: list // list调用::方法,在前面添加元素0,构成新Listval res14: List[Int] = List(0, 1, 2, 3)

scala> List(0, 1) ::: list // list调用:::方法,与另一个List拼接成新Listval res15: List[Int] = List(0, 1, 1, 2, 3)

当然,将::写在两个操作数中间是将其看做是操作符来执行计算,而更为严谨的说其实质是调用的::方法,即上面两句代码其底层执行的是如下逻辑:

scala> list.::(0) // ::是List的方法接口val res16: List[Int] = List(0, 1, 2, 3)

scala> list.:::(List(0, 1)) // :::也是list的方法接口val res17: List[Int] = List(0, 1, 1, 2, 3)

怎么样,是不是感觉Scala中List可比Python中List的骚操作更多呢?

与此同时,List由于更贴近与链表的实现特性,所以具有更多的访问首尾方法,即head和tail,其中head为返回第一个元素,而tail则是返回第一个元素以外的所有元素。

scala> list.head // 返回第一个元素,结果是一个值val res18: Int = 1

scala> list.tail // 返回第一个以后的所有元素,结果仍然是一个listval res19: List[Int] = List(2, 3)

另外需要补充的是,与ArrayBuffer和Array的关系类似,List也有其兄弟ListBuffer,用于实现可变长的列表。

03 Set

与大多数语言中均提供了Set数据结构类似,Scala中的Set也扮演了这一角色。Set的最大特点是:同质、数据去重、长度不可变,其中数据去重是所有集合的特性,默认以哈希集实现。注:Scala中的可变Set和不可变Set是同名类,都叫做Set,这与Array和List区分是否带Buffer是完全不同的命名设计。

在了解Array和List的基础上,Set的创建也比较直观,同时也支持添加元素和拼接两个Set构成新的Set方法。另外,Set设计的定位是用于判断指定集合是否包含目标值,所以contains是其常用方法。Set常用操作示例如下:

scala> val set =Set(1, 2, 2, 3) // 创建Set,会自动去重val set: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> set + 4 // 添加新元素返回新的Setval res20: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

scala> set ++ Set(3, 4) // 拼接另一个Set,返回新的Setval res21: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

scala> set.contains(2)

val res22: Boolean = true

当然,既然是集合,所以也提供了数学意义上的交集、并集和补集操作。另外,如需使用可变长集合,则需引用scala.collection.mutable.Set类,其与不可变集合Set为同名类,按照就近原则引用。

04 Map

与Set类似,Map也是编程语言中的一种常用数据结构,用于表达映射关系,在Python中就是字典数据结构dict,通过提供键值对的访问方式,可以以O(1)的复杂度完成数据的访问和赋值。在Scala中,Map也区分可变和不可变映射,且为同名类,如果需要创建可变Map,则需在适当位置import相应类即可。

在Scala中,Map的元素类型实际上是一个二值的元组类型(Tuple2),两个值分别为key和value,而对于这个特殊的二值元组,实际上则又有两种具体表达形式,示例如下:

scala> val map1 = Map((1, 2), (3, 4)) // 创建map方式1:(key, value)val map1: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 4)

scala> val map2 = Map(2->3, 3->5) // 创建map方式2:key->valueval map2: scala.collection.immutable.Map[Int,Int] = Map(2 -> 3, 3 -> 5)

类似于Python中的dict,Scala中的Map也可通过keys和values获取所有的键和值,且keys实际上就是一个Set,因而不会存在重复值;而values则不受这一限制:

scala> map1.keys // 获取所有键,返回结果是一个Set类型val res23: Iterable[Int] = Set(1, 3)

scala> map1.values // 获取所有值,返回结果是一个迭代类型val res24: Iterable[Int] = Iterable(2, 4)

与Set类似,Map也支持类似的添加新键值对和与其他Map拼接的方法:

scala> map1 + (4->5)

val res27: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 4, 4 -> 5)

scala> map1 ++ map2

val res28: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 5, 2 -> 3)

05 Tuple

前面介绍的4种数据结构,实际上都有一个共性要求是所有元素必须是同质的,即使是存在形式上的不同类型(例如一个数组既有整型,又有字符串型),那么其实质上是定义了泛型为Any的数组。而Tuple元组则是一个实实在在的支持不同泛型的集合数据结构,比如可以是第一个元素是整型,第二个元素是字符串型等等。例如,Map的每个键值对实际上都是一个二值元组,而正因为二值元组可以支持两种不同的数据类型,才保证了Map定义的多样性。

Tuple的创建与前面4种类型也略有不同,其可以直接以圆括号进行初始化即可,括号内的数据即为初始化值,且一旦指定也不可改变。特别地,这里Tuple最多可以支持22个元素的初始化,分别对应Tuple1——Tuple22类型。

scala> val tuple1 = (1, "str") // 二值元组创建方式一:()val tuple1: (Int, String) = (1,str)

scala> val tuple2 = Tuple2("str", 3) // 二值元组创建方式二:Tuple2实例化val tuple2: (String, Int) = (str,3)

scala> val tuple3 = "str"->4 // 特殊地,二值元组还可通过->创建,如同Map中定义的那样val tuple3: (String, Int) = (str,4)

上面给出了三种二值元组的创建方式,其中前两种是所有Tuple类型共有的,且第一种更为简洁通用;第三种属于二值元组所特有的,毕竟二值元组是Map中的对象类型,具有一定的特殊性。

那么,既然Tuple最多仅支持22个元素,那貌似好像挺受局限,毕竟数量上较少,但实则不然。单从多样性的角度讲,由于元组的每个元素类型都可能不一样,例如上面示例中tuple1是一个(Int, String)型二值元组,而tuple2则是一个(String, Int)型二值元组,虽然仅是类型调换了顺序,却是实实在在的不同类型元组。照此计算,以Scala中9种基本的数据类型为例进行计算,那么Tuple中所有的元组类型总数应该是9+9^2+……+9^22,这其实是一个非常庞大的数字。

另外值得指出的是,得益于元组中支持不同类型的元素,所以函数中需要返回多个不同类型结果时即可以Tuple类型进行交换。

最后给出Scala中所有集合类数据结构的继承关系图,区分可变(mutable)和不可变(immutable)两种类型,仅做延伸了解,不具体展开。Scala中的不可变集合类数据结构继承关系Scala中的可变集合类数据结构继承关系



推荐阅读
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文详细解析了JavaScript中相称性推断的知识点,包括严厉相称和宽松相称的区别,以及范例转换的规则。针对不同类型的范例值,如差别范例值、统一类的原始范例值和统一类的复合范例值,都给出了具体的比较方法。对于宽松相称的情况,也解释了原始范例值和对象之间的比较规则。通过本文的学习,读者可以更好地理解JavaScript中相称性推断的概念和应用。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
author-avatar
奇国的雪儿
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有