在Spark里做维度关联你会用什么在Flink里做实时聚合你会用什么在ETL里做配置映射你会用什么答案都是Map。但很多人用了几年Map对它的认知还停留在key-value对。我在做一个用户画像项目的时候遇到过一个很隐蔽的bug一个用Map缓存用户信息的函数偶尔会抛出NoSuchElementException。一开始以为是数据问题后来查了半天发现是Map的apply方法在key不存在的时候会直接抛异常——而我写代码的时候图省事直接用了map(userId)而不是map.get(userId)。这个bug让我意识到一个问题很多人用Map就只会map(key)、map (key - value)这两个操作。但Map真正强大的地方——它的不可变性、它的集合运算、它和函数式编程的配合——大多数人都没用上。这篇文章不罗列Map有多少个方法——那些官方文档都有。我想聊几个真正值得思考的问题Scala的Map底层是怎么实现的为什么getOrElse比apply安全为什么大数据开发离不开Map什么时候你该用Map而不是其他数据结构先从一个真实的bug说起假设你要写一个函数从用户信息Map里取用户名称如果不存在就返回未知用户。很多人第一反应是这么写// 错误写法key不存在会直接抛异常defgetUserName(userInfo:Map[String,String]):String{userInfo(userName)}看起来很简洁对吧但当userInfo里没有userName这个key的时候这行代码会直接抛出NoSuchElementException。为什么因为Map的apply方法的设计就是这样的——它假设你知道key一定存在如果不存在就是程序错误直接抛异常。正确的写法应该是用getOrElse// 正确写法key不存在有默认值defgetUserName(userInfo:Map[String,String]):String{userInfo.getOrElse(userName,未知用户)}现在不管key存不存在函数都能正常返回——存在就返回对应的值不存在就返回默认值。这就是Map最容易踩的坑apply方法是不安全的它假设key一定存在。很多Java老程序员刚转Scala的时候特别容易踩这个坑——因为Java的HashMap.get(key)在key不存在的时候返回null不会抛异常。Scala的设计哲学不一样它不喜欢null所以它用Option来表示可能不存在。但很多人图省事直接用apply方法取key结果就是线上各种NoSuchElementException。Map不是带key的列表它是函数式的映射很多人对Map的理解停留在key-value对的集合。这个理解太浅了。List是什么List是有序的序列。Set是什么Set是数学上的集合。Map是什么Map是函数——它就是一个从key到value的映射函数。这不是什么玄学说法这是Scala的设计。在Scala里Map本身就是一个函数valuserMapMap(name-张三,age-25)// Map本身就是一个函数userMap(name)// 张三userMap(age)// 25看出来了吗Map就是一个函数——你给它一个key它返回对应的value。这就是为什么Map的apply方法直接抛异常——因为函数就应该是完整的你传了一个不在定义域里的参数当然是程序错误。这个理解很重要因为它决定了你该怎么用Map如果你把Map当成带key的列表你就会到处用apply方法取key到处抛异常如果你把Map当成函数你就会用get方法用Option来处理可能不存在的情况Scala Map的底层实现和Set是一套体系很多人不知道Scala的Map和Set其实是一套东西——Set就是Map的特例。你仔细想想Set存的是元素Map存的是key-value对。Set的元素就是Map的key。所以Scala的Set底层其实就是Map——Set的元素对应的value都是()Unit类型。// Set底层就是Map// Set(1, 2, 3) 等价于 Map(1 - (), 2 - (), 3 - ())这也是为什么Map和Set的实现这么像——都有HashMap、TreeMap、LinkedHashMap。因为它们本来就是一套东西。HashMap最常用的MapvalmapHashMap(name-张三,age-25)map(name)// 张三O(1)HashMap是最常用的Map实现底层基于哈希表——你调用get(key)的时候它会算key的hashCode然后去对应的桶里找。所以是O(1)。TreeMap有序的Mapimportscala.collection.immutable.TreeMapvalmapTreeMap(3-c,1-a,2-b)// TreeMap(1 - a, 2 - b, 3 - c) —— key自动排序如果你需要Map的key是有序的——比如你要按从小到大的顺序输出所有key——那就用TreeMap。它底层是红黑树查找是O(log n)比HashMap的O(1)慢一点但key是有序的。经验法则场景用什么Map只需要通过key快速查找valueHashMap需要key按大小排序TreeMap需要保持插入顺序LinkedHashMap数据量很小十几个key随便用哪个差别不大不可变Map vs 可变Map老传统又来了和List、Set一样Scala的Map也分不可变和可变两套。// 不可变Mapvalmap1Map(a-1,b-2)valmap2map1(c-3)// 返回新Mapmap1不变// 可变Mapimportscala.collection.mutablevalbufmutable.Map(a-1)buf(b-2)// 原地修改这个设计哲学和List、Set是一样的对外暴露不可变Map内部用可变Map构建。但Map有个和List不一样的地方——不可变Map的性能其实没那么差。为什么因为Map的核心操作是get是基于哈希的O(1)操作。即使你每次add都返回一个新Mapadd操作本身也是O(1)平均情况。所以在Map这个场景下可变和不可变的性能差距没有List那么大。很多时候你直接用不可变Map就够了不需要特意搞个可变Map来构建。// 直接用不可变Map就行不用搞个可变MapvalmapMap(a-1)(b-2)(c-3)这也是为什么很多人觉得Scala的Map比List好用——因为不可变Map的性能足够好你不需要像List那样纠结我到底该不该用ListBuffer。Map的核心操作不只是get和put很多人用Map就只会get、put、containsKey这三个操作。其实Map真正强大的地方是它和函数式编程的配合。map对所有value做转换valscoresMap(张三-80,李四-90,王五-75)// 给每个人加5分valboostedscores.map{case(name,score)(name,score5)}// Map(张三 - 85, 李四 - 95, 王五 - 80)这个操作在大数据处理里太常用了——你有一个Map存着用户的原始数据你要对所有value做转换一行代码搞定。filter按条件过滤valscoresMap(张三-80,李四-90,王五-75)// 只保留及格的valpassedscores.filter{case(name,score)score60}getOrElse安全地取值valuserMapMap(name-张三,age-25)// 安全取值key不存在有默认值valnameuserMap.getOrElse(name,未知用户)valemailuserMap.getOrElse(email,未填写)这是最常用的Map操作——安全地取值key不存在的时候有默认值不会抛异常。与Java Map对比Java也有HashMap也有TreeMap。那Scala的Map和Java的Map到底差在哪默认是不可变的Java里new的HashMap默认就是可变的。Scala里默认的Map就是不可变的。支持函数式操作Java 8以后虽然也有stream但写起来很啰嗦。Scala的map/filter直接就是Map的方法写起来很自然。和集合体系无缝互转Scala的Map和List、Set之间互转都是一行代码的事。什么时候该用Map这是最常见的困惑。我给一个简单的判断标准你要通过一个key快速找到对应的value吗是的话用Map。不是的话想想是不是该用其他数据结构。用Map的场景配置映射key是配置项名称value是配置值维度关联key是维度idvalue是维度信息缓存索引key是用户idvalue是用户信息计数统计key是事件类型value是出现次数分组聚合key是分组字段value是该组的数据不用Map的场景你只关心元素在不在里面用Set你要按顺序遍历元素用List你要按位置访问元素用Array写在最后我写这篇文章的时候特意没罗列Map有多少个方法——因为那些东西官方文档都有查一下就行。真正值得花时间理解的是Map到底解决了什么问题它不是带key的列表它是一个从key到value的映射函数。它用哈希表的O(1)查找换来了通过key快速找到value的能力。在大数据开发里Map无处不在——维度关联、配置映射、计数统计、分组聚合、缓存索引。理解了Map你就理解了大数据处理里最核心的几个操作查找、映射、聚合。很多人用了几年Map还停留在new一个HashMap然后put put put的阶段。这不是Map的正确打开方式。Map的真正威力在于它和函数式编程的配合——map、filter、getOrElse这些操作让你可以用声明式的方式处理key-value数据而不是命令式地遍历、判断、取值。当你下次要写一个key-value查找的时候先想想我是在解决通过key找value的问题还是在解决其他问题如果是前者用Map。用getOrElse安全取值用map做转换用filter做过滤。
阅读完成 · 觉得有帮助?